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 |