[데브코스] Spring Boot REST API 실습 (22강) – 글 작성 API 구현 (POST + JSON 요청, record DTO, RsData 응답 정리)

zuno·2025년 12월 25일

이번 강의(22강)에서는 글 작성 API를 POST 요청으로 구현하고,
요청 본문을 JSON 형태로 전달하는 방식까지 다뤘다.

이전 강의에서 정리했던 RsData 응답 구조를 그대로 유지하면서,
요청 DTO를 record 형태로 컨트롤러 내부에 선언하는 방식도 새롭게 등장했다.


1️⃣ 왜 글 작성은 POST 요청인가?

HTTP 메서드는 역할이 명확하다.

  • GET: 조회
  • POST: 생성
  • PUT / PATCH: 수정
  • DELETE: 삭제

글 작성은 새로운 리소스를 생성하는 행위이므로
조회(GET)가 아닌 POST 요청이 적절하다.

POST /api/v1/posts

2️⃣ 요청은 JSON 형태로 전달한다

글 작성 시 제목과 내용은 요청 본문(body)에 담아 전달한다.

요청 예시 (Postman)

{
  "title": "제목",
  "content": "내용"
}


3️⃣ 글 작성 컨트롤러 코드 전체

@RestController
@RequestMapping("/api/v1/posts")
@RequiredArgsConstructor
public class ApiV1PostController {

    private final PostService postService;

    @GetMapping
    @Transactional(readOnly = true)
    public List<PostDto> getItems() {
        return postService.findAll()
                .stream()
                .map(PostDto::new)
                .toList();
    }

    @GetMapping("/{id}")
    @Transactional(readOnly = true)
    public PostDto getItem(@PathVariable int id) {
        Post post = postService.findById(id).get();
        return new PostDto(post);
    }

    @DeleteMapping("/{id}")
    @Transactional
    public RsData<Void> delete(@PathVariable int id) {
        Post post = postService.findById(id).get();
        postService.delete(post);

        return new RsData<>(
                "200-1",
                "%d번 글이 삭제되었습니다.".formatted(id)
        );
    }

    record PostWriteForm(
            @NotBlank
            @Size(min = 2, max = 100)
            String title,

            @NotBlank
            @Size(min = 2, max = 5000)
            String content
    ) {}

    @PostMapping
    @Transactional
    public RsData<PostDto> write(@Valid @RequestBody PostWriteForm form) {
        Post post = postService.write(form.title(), form.content());

        return new RsData<>(
                "200-1",
                "%d번 글이 생성되었습니다.".formatted(post.getId()),
                new PostDto(post)
        );
    }
}

4️⃣ record 요청 DTO를 컨트롤러 안에 둔 이유

기존 강의에서는 DTO를 별도 패키지로 분리했는데,
이번에는 컨트롤러 내부에 record를 선언했다.

이유는 명확하다.

✅ 이 DTO는 이 API에서만 사용된다

PostWriteForm글 작성 API 전용이다.
다른 컨트롤러나 서비스에서 재사용되지 않는다.

→ 굳이 패키지로 분리할 필요가 없다.


✅ record는 “요청 폼”에 매우 적합하다

  • 불변 객체
  • 생성자 자동 생성
  • getter 자동 생성
  • 검증 애노테이션과 궁합이 좋음
record PostWriteForm(String title, String content) {}

요청 데이터 전달용 DTO로 딱 적당


✅ 컨트롤러 전용 DTO라는 의도가 드러난다

컨트롤러 안에 선언함으로써
“이 record는 이 API 전용이다”라는 의미가 명확해진다.


5️⃣ @RequestBody + @Valid 흐름 정리

public RsData<PostDto> write(
    @Valid @RequestBody PostWriteForm form
)
  • @RequestBody
    • JSON 요청 본문을 Java 객체로 변환 (Jackson)
  • @Valid
    • record 필드에 붙은 검증 조건 실행
@NotBlank
@Size(min = 2, max = 100)
String title

→ 조건을 만족하지 않으면 컨트롤러 로직은 실행되지 않고 예외 발생


6️⃣ 응답은 RsData로 감싼다

글 작성은 조회가 아닌 액션이므로
성공 여부와 메시지가 중요하다.

{
  "resultCode": "200-1",
  "msg": "4번 글이 생성되었습니다.",
  "data": {
    "id": 4,
    "createDate": "...",
    "modifyDate": "...",
    "title": "제목",
    "content": "내용"
  }
}


7️⃣ 왜 조회(GET)는 RsData로 감싸지 않을까?

조회 API는 대부분 성공 자체가 목적이다.

@GetMapping("/{id}")
public PostDto getItem(...)
  • 조회 성공 → 반환 데이터가 핵심
  • 실패할 가능성이 상대적으로 낮음

→ 매번 resultCode, msg를 감싸면 오버스펙

반면에,

  • 생성
  • 수정
  • 삭제

같은 액션 API는
프론트에서 “무슨 일이 일어났는지”가 중요하므로 RsData가 적합하다.


8️⃣ 강의 핵심 요약

  • 글 작성은 POST + JSON 요청
  • @RequestBody로 JSON → 객체 변환
  • record는 요청 DTO로 매우 적합
  • 컨트롤러 내부 record는 전용 DTO 의도를 명확히 함
  • 조회 API는 RsData로 감싸지 않아도 된다
  • 액션 API는 RsData로 응답 구조 통일

✍️ 개인 정리

이번 강의를 통해 느낀 점은
“모든 걸 무조건 통일하는 게 정답은 아니다”라는 것이다.

  • 조회는 단순하게
  • 액션은 명확하게
  • DTO는 상황에 맞게

이런 판단 기준이 결국
읽기 좋은 코드, 유지보수하기 좋은 코드로 이어진다는 걸 체감했다.

0개의 댓글