[스프링 로드맵] A-2. REST 확장 - POST/PUT/DELETE와 ResponseEntity

2026. 1. 19. 17:05·백엔드

A1에서 GET + @RequestParam까지 봤다면, 이제부터는 “진짜 API”에 필요한 것들을 붙인다.

  • POST/PUT/DELETE로 CRUD 흐름 만들기
  • 요청 JSON을 @RequestBody DTO로 받기
  • 성공/실패 상황에 맞게 HTTP 상태코드(201/204/400/404) 설계하기
  • 그걸 코드로 통제하는 도구가 ResponseEntity

1. @RequestBody란

  • @RequestParam
    -> URL 쿼리스트링(?page=1)이나 form parameter 같은 간단한 입력
  • @RequestBody
    -> 요청 바디(JSON)를 통째로 받아 객체(DTO)로 변환할 때 사용

REST API에서 “생성/수정”은 보통 JSON으로 많은 필드를 보내기 때문에 @RequestBody가 표준에 가깝다.


2. 메서드별 매핑: @PostMapping, @PutMapping, @DeleteMapping

스프링 MVC는 URL(path)뿐 아니라 HTTP 메서드로도 라우팅한다.

  • @PostMapping("/resources") : 생성
  • @PutMapping("/resources/{id}") : 전체 수정(또는 규칙에 따라 부분 수정도 가능)
  • @DeleteMapping("/resources/{id}") : 삭제

그리고 URL에 들어간 {id} 같은 값은 보통 @PathVariable로 받는다.

(A1에서 다루지 않았지만 CRUD에 필요해서 여기서 같이 쓴다.)


3. 요청 바디 DTO 패턴(왜 도메인 엔티티를 바로 받지 않나)

컨트롤러는 HTTP 입출력 경계다. 그래서 보통 요청/응답 전용 DTO를 둔다.

  • 보안: 엔티티 필드가 그대로 노출되는 걸 방지
  • 유연성: API 스펙 변경이 DB 구조 변경으로 번지지 않게 분리
  • 검증: @Valid로 입력 검증을 DTO에 걸기 쉬움

예시는 아주 단순한 “Todo API”로 간다.


4. 응답 설계: 상태코드 + 바디(또는 바디 없음)

REST에서 상태코드는 “서버가 상황을 어떻게 이해했는지”를 표현한다.

  • 201 Created: 생성 성공 (새 리소스가 생김)
  • 204 No Content: 삭제/수정 성공인데 응답 바디가 필요 없음
  • 400 Bad Request: 입력이 잘못됨(검증 실패/형식 오류)
  • 404 Not Found: 해당 리소스가 없음

이 상태코드를 깔끔하게 제어하는 도구가 ResponseEntity다.


5. 예제 코드(“REST 확장” 한 번에 체감하기)

실제 DB/JPA 없이도 흐름만 잡기 위해 메모리(Map)로 구현했다.

다음 파트(JPA)에서 Repository로 바꾸면 된다.

5-1) DTO

package com.example.restservice.todo;

import jakarta.validation.constraints.NotBlank;

public record TodoCreateRequest(
        @NotBlank String title
) {}

public record TodoUpdateRequest(
        @NotBlank String title,
        boolean done
) {}

public record TodoResponse(
        long id,
        String title,
        boolean done
) {}

  • @NotBlank는 Validation 파트에서 자세히 다루지만, 여기서는 400 예시를 만들기 위해 최소만 사용

5-2) Controller

package com.example.restservice.todo;

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

@RestController
@RequestMapping("/todos")
public class TodoController {

    private final AtomicLong idSeq = new AtomicLong(0);
    private final Map<Long, TodoResponse> store = new ConcurrentHashMap<>();

    // CREATE: POST /todos
    @PostMapping
    public ResponseEntity<TodoResponse> create(@Valid @RequestBody TodoCreateRequest req) {
        long id = idSeq.incrementAndGet();
        TodoResponse saved = new TodoResponse(id, req.title(), false);
        store.put(id, saved);

        // 201 Created + Location 헤더(생성된 리소스 위치)
        return ResponseEntity
                .created(URI.create("/todos/" + id))
                .body(saved);
    }

    // READ: GET /todos/{id}
    @GetMapping("/{id}")
    public ResponseEntity<TodoResponse> getOne(@PathVariable long id) {
        TodoResponse found = store.get(id);
        if (found == null) {
            return ResponseEntity.notFound().build(); // 404
        }
        return ResponseEntity.ok(found); // 200
    }

    // UPDATE: PUT /todos/{id}
    @PutMapping("/{id}")
    public ResponseEntity<TodoResponse> update(
            @PathVariable long id,
            @Valid @RequestBody TodoUpdateRequest req
    ) {
        TodoResponse existing = store.get(id);
        if (existing == null) {
            return ResponseEntity.notFound().build(); // 404
        }

        TodoResponse updated = new TodoResponse(id, req.title(), req.done());
        store.put(id, updated);

        // 수정은 보통 200(바디 포함) 또는 204(바디 없음) 중 택1
        return ResponseEntity.ok(updated);
    }

    // DELETE: DELETE /todos/{id}
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable long id) {
        TodoResponse removed = store.remove(id);
        if (removed == null) {
            return ResponseEntity.notFound().build(); // 404
        }

        // 삭제 성공: 204 No Content
        return ResponseEntity.noContent().build();
    }
}


6. 왜 ResponseEntity가 필요한가

컨트롤러에서 객체만 반환하면(예: return updated;) 스프링은 기본적으로 200 OK로 응답한다.

하지만 REST에서는 상황별로 아래가 필요하다.

  • 생성이면 201 + Location 헤더
  • 삭제면 204
  • 없으면 404
  • 바디가 필요 없으면 .build()로 종료

이걸 가장 명확하게 표현하는 방법이 ResponseEntity다.


7. 직접 호출해보는 테스트 시나리오

7-1) 생성

curl -i -X POST <http://localhost:8080/todos> `
  -H "Content-Type: application/json" `
  -d '{"title":"study spring"}'

기대:

  • HTTP/1.1 201
  • Location: /todos/1
  • body에 생성된 todo JSON

7-2) 조회

curl -i <http://localhost:8080/todos/1>

7-3) 수정

curl -i -X PUT <http://localhost:8080/todos/1> \\
  -H "Content-Type: application/json" \\
  -d '{"title":"study spring hard","done":true}'

7-4) 삭제

curl -i -X DELETE <http://localhost:8080/todos/1>

기대: 204 No Content

7-5) 400 확인(Validation)

curl -i -X POST <http://localhost:8080/todos> \\
  -H "Content-Type: application/json" \\
  -d '{"title":""}'

기대: 400 Bad Request


8. 결론

  • @RequestBody는 JSON 바디를 DTO로 받는 표준 방식이다.
  • CRUD는 HTTP 메서드 + URL 패턴으로 설계한다.
  • 상태코드(201/204/400/404)를 의도대로 내리려면 ResponseEntity가 가장 깔끔하다.
  • DTO는 “HTTP 스펙”과 “도메인/DB 구조”를 분리하기 위한 안전장치다. 

'백엔드' 카테고리의 다른 글

[스프링 로드맵] B-3. DI/Bean 실전 - @Autowired, @Qualifier, Profile  (0) 2026.01.19
[스프링 로드맵] B-2. DI/Bean 내부 - 빈 등록 방식과 라이프사이클  (1) 2026.01.19
[스프링 로드맵] B-1. DI/Bean 기본 - new 지옥에서 벗어나기  (1) 2026.01.19
[스프링 로드맵] A-1. REST 입문 - JSON이 응답되는 이유  (0) 2026.01.19
[스프링 로드맵] 0편. 전체 그림 – REST부터 JPA까지 한 번에 보기  (0) 2026.01.19
'백엔드' 카테고리의 다른 글
  • [스프링 로드맵] B-2. DI/Bean 내부 - 빈 등록 방식과 라이프사이클
  • [스프링 로드맵] B-1. DI/Bean 기본 - new 지옥에서 벗어나기
  • [스프링 로드맵] A-1. REST 입문 - JSON이 응답되는 이유
  • [스프링 로드맵] 0편. 전체 그림 – REST부터 JPA까지 한 번에 보기
samsam031
samsam031
samsam031 님의 블로그 입니다.
  • samsam031
    samsam031 님의 블로그
    samsam031
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 디지털포렌식
      • 드림핵 문제풀이
      • 대외활동
      • 개발 실습
      • 컴퓨터 보안
      • 클라우드
      • 자격증
      • 자연어처리
      • 백엔드
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
samsam031
[스프링 로드맵] A-2. REST 확장 - POST/PUT/DELETE와 ResponseEntity
상단으로

티스토리툴바