티스토리 뷰

300x250

GET/POST/PUT/PATCH/DELETE를 어떻게 구현할까?

REST API를 공부하다 보면 GET, POST, PUT, PATCH, DELETE는 자주 보는데, 막상 Spring Boot에서 실제 CRUD 코드로 옮기려고 하면 헷갈리는 경우가 많습니다.

특히 초보 단계에서는 아래 질문이 자주 나옵니다.

  • GETPOST는 알겠는데 PUTPATCH는 정확히 뭐가 다를까?
  • CRUD API를 만들 때 메서드를 어떻게 나눠야 할까?
  • 컨트롤러, 서비스, DTO는 어떤 식으로 연결해야 할까?

이번 글에서는 회원(User) 관리 예제를 기준으로, Spring Boot에서 GET/POST/PUT/PATCH/DELETE를 실제 CRUD로 어떻게 구현하는지 쉽게 정리해보겠습니다.


1. 먼저 CRUD와 HTTP 메서드를 연결해보자

CRUD는 데이터 처리의 4가지 기본 동작입니다.

  • Create: 생성
  • Read: 조회
  • Update: 수정
  • Delete: 삭제

이걸 HTTP 메서드에 연결하면 보통 이렇게 정리합니다.

CRUD와 HTTP 메서드 매핑
Spring Boot REST API에서 가장 자주 쓰는 기본 구조입니다
동작 메서드 URL 예시 설명
Create POST `POST /users` 새 사용자 생성
Read GET `GET /users`, `GET /users/{id}` 목록/단건 조회
Update PUT / PATCH `PUT /users/{id}`, `PATCH /users/{id}` 전체 수정 / 부분 수정
Delete DELETE `DELETE /users/{id}` 사용자 삭제

한 줄로 요약하면 아래처럼 기억하면 됩니다.

  • GET = 조회
  • POST = 생성
  • PUT = 전체 수정
  • PATCH = 부분 수정
  • DELETE = 삭제
728x90

2. 예제 시나리오: User CRUD API 만들기

이번 글에서는 아래처럼 단순한 회원 관리 API를 만든다고 가정하겠습니다.

  • 사용자 등록
  • 사용자 전체 조회
  • 사용자 단건 조회
  • 사용자 전체 수정
  • 사용자 일부 수정
  • 사용자 삭제

실전에서는 인증, 검증, 예외처리, 페이징, 감사 로그까지 붙겠지만, 이번 글은 HTTP 메서드와 CRUD 흐름 이해가 목적이므로 핵심 구조 위주로 보겠습니다.


3. User 엔티티 예시

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String email;
    private Integer age;

    protected User() {
    }

    public User(String name, String email, Integer age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }

    public void update(String name, String email, Integer age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }

    public void updateName(String name) {
        this.name = name;
    }

    public void updateEmail(String email) {
        this.email = email;
    }

    public void updateAge(Integer age) {
        this.age = age;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public Integer getAge() {
        return age;
    }
}

4. Repository 만들기

public interface UserRepository extends JpaRepository<User, Long> {
}

Spring Data JPA를 사용하면 기본 CRUD 메서드를 상당 부분 자동으로 사용할 수 있어서, 실무에서도 이 구조를 많이 씁니다.


5. Request / Response DTO 만들기

public class UserRequest {
    private String name;
    private String email;
    private Integer age;

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public Integer getAge() {
        return age;
    }
}
public class UserResponse {
    private Long id;
    private String name;
    private String email;
    private Integer age;

    public UserResponse(User user) {
        this.id = user.getId();
        this.name = user.getName();
        this.email = user.getEmail();
        this.age = user.getAge();
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public Integer getAge() {
        return age;
    }
}

6. Service에서 CRUD 로직 구현하기

@Service
@Transactional(readOnly = true)
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional
    public Long createUser(UserRequest request) {
        User user = new User(request.getName(), request.getEmail(), request.getAge());
        User savedUser = userRepository.save(user);
        return savedUser.getId();
    }

    public List<UserResponse> getUsers() {
        return userRepository.findAll()
                .stream()
                .map(UserResponse::new)
                .toList();
    }

    public UserResponse getUser(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("해당 사용자가 없습니다."));
        return new UserResponse(user);
    }

    @Transactional
    public void putUser(Long id, UserRequest request) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("해당 사용자가 없습니다."));

        user.update(request.getName(), request.getEmail(), request.getAge());
    }

    @Transactional
    public void patchUser(Long id, UserRequest request) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("해당 사용자가 없습니다."));

        if (request.getName() != null) {
            user.updateName(request.getName());
        }
        if (request.getEmail() != null) {
            user.updateEmail(request.getEmail());
        }
        if (request.getAge() != null) {
            user.updateAge(request.getAge());
        }
    }

    @Transactional
    public void deleteUser(Long id) {
        if (!userRepository.existsById(id)) {
            throw new IllegalArgumentException("해당 사용자가 없습니다.");
        }
        userRepository.deleteById(id);
    }
}

이 코드에서 중요한 포인트는 아래입니다.

  • createUser() → POST용 생성 로직
  • getUsers(), getUser() → GET용 조회 로직
  • putUser() → PUT용 전체 수정 로직
  • patchUser() → PATCH용 부분 수정 로직
  • deleteUser() → DELETE용 삭제 로직

7. Controller에서 HTTP 메서드 매핑하기

이제 컨트롤러에서 HTTP 메서드별로 연결해보겠습니다.

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public ResponseEntity<Void> createUser(@RequestBody UserRequest request) {
        Long userId = userService.createUser(request);
        URI location = URI.create("/users/" + userId);
        return ResponseEntity.created(location).build();
    }

    @GetMapping
    public ResponseEntity<List<UserResponse>> getUsers() {
        return ResponseEntity.ok(userService.getUsers());
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
        return ResponseEntity.ok(userService.getUser(id));
    }

    @PutMapping("/{id}")
    public ResponseEntity<Void> putUser(@PathVariable Long id,
                                        @RequestBody UserRequest request) {
        userService.putUser(id, request);
        return ResponseEntity.ok().build();
    }

    @PatchMapping("/{id}")
    public ResponseEntity<Void> patchUser(@PathVariable Long id,
                                          @RequestBody UserRequest request) {
        userService.patchUser(id, request);
        return ResponseEntity.ok().build();
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

이제 메서드별 의미가 명확해집니다.

  • POST /users → 사용자 생성
  • GET /users → 사용자 목록 조회
  • GET /users/{id} → 사용자 단건 조회
  • PUT /users/{id} → 사용자 전체 수정
  • PATCH /users/{id} → 사용자 일부 수정
  • DELETE /users/{id} → 사용자 삭제

8. PUT과 PATCH는 어떻게 다를까?

초보자가 가장 헷갈리는 부분이 바로 여기입니다.

PUT

PUT은 전체 수정에 가깝습니다. 즉, 자원의 전체 상태를 새 값으로 바꾼다고 이해하면 쉽습니다.

예를 들어:

{
  "name": "Kim",
  "email": "kim@example.com",
  "age": 30
}

이런 전체 데이터를 보내서 통째로 갱신하는 느낌입니다.

PATCH

PATCH는 부분 수정에 가깝습니다. 즉, 바꾸고 싶은 필드만 보내는 방식입니다.

예를 들어:

{
  "email": "new-email@example.com"
}

이렇게 일부 값만 전달해서 수정할 수 있습니다.

한 번에 비교하면

PUT vs PATCH
둘 다 수정이지만 수정 범위가 다릅니다
PUT
전체 수정
  • 자원 전체를 새 값으로 교체하는 느낌
  • 전체 데이터가 들어오는 경우에 적합
  • 필수값 누락이 없도록 주의해야 함
PATCH
부분 수정
  • 바꾸려는 필드만 전달해 수정
  • 프로필 일부 변경 같은 상황에 적합
  • null 여부를 보고 수정 대상을 판단하기 쉬움

9. Postman이나 프론트엔드에서는 어떻게 호출할까?

예를 들어 사용자 생성 요청은 이렇게 보낼 수 있습니다.

POST /users

{
  "name": "Kim",
  "email": "kim@example.com",
  "age": 30
}

PUT /users/1

{
  "name": "Kim",
  "email": "kim-new@example.com",
  "age": 31
}

PATCH /users/1

{
  "age": 31
}

이렇게 보면 Spring Boot 코드와 HTTP 요청이 어떻게 연결되는지 훨씬 잘 보입니다.


10. 실무에서 자주 보는 추가 포인트

10-1. 상태코드도 함께 맞춰야 한다

  • 조회/수정 성공 → 200 OK
  • 잘못된 요청 → 400 Bad Request
  • 대상 없음 → 404 Not Found

10-2. 예외 처리는 별도로 관리하는 편이 좋다

@RestControllerAdvice를 두고 공통 예외 응답을 정리하는 경우가 많습니다.

그래야 컨트롤러/서비스 코드가 훨씬 깔끔해집니다.

10-3. 엔티티를 그대로 요청/응답에 쓰지 않는 편이 안전하다

DTO를 분리하면 API 스펙이 흔들리는 걸 줄일 수 있고, 프론트엔드와 협업할 때도 유리합니다.


11. 전체 흐름을 한 번에 보면

Spring Boot CRUD API의 흐름은 보통 이렇게 이어집니다.

  1. 클라이언트가 HTTP 요청을 보낸다
  2. Controller가 요청을 받는다
  3. Service가 비즈니스 로직을 처리한다
  4. Repository가 DB와 통신한다
  5. 결과를 ResponseEntity로 응답한다

즉, 단순히 메서드 어노테이션만 외우는 게 아니라, HTTP 요청이 애플리케이션 계층을 어떻게 흐르는지 함께 이해하는 것이 중요합니다.


12. 한 번에 정리하는 핵심 요약

  • GET은 조회
  • POST는 생성
  • PUT은 전체 수정
  • PATCH는 부분 수정
  • DELETE는 삭제
  • Spring Boot에서는 @GetMapping, @PostMapping, @PutMapping, @PatchMapping, @DeleteMapping으로 연결한다
  • 이번 예제에서는 흐름 이해를 위해 Request DTO를 UserRequest 하나로 통일했다
  • 특히 PUT과 PATCH 차이, 상태코드 의미, Controller-Service-Repository 흐름은 꼭 함께 이해하는 것이 좋다

마무리

Spring Boot에서 CRUD API를 구현할 때 가장 중요한 건, HTTP 메서드 이름을 외우는 것이 아니라 각 메서드가 어떤 역할을 갖는지 정확히 이해하는 것입니다.

처음에는 GET/POST/PUT/PATCH/DELETE가 비슷해 보여도, 한 번 실제 CRUD 예제로 연결해보면 구조가 훨씬 선명해집니다.

728x90
댓글
반응형
최근에 올라온 글
글 보관함
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30