[데브코스] Spring Boot REST API 실습 (10강) - 댓글 다건 조회 / 단건 조회 API 구현과 LAZY 로딩 이해

zuno·2025년 12월 21일

이번 글은 데브코스 Spring Boot REST API 실습 10강 내용을 정리한 글이다.
10강에서는 게시글에 달린 댓글 다건 조회 / 단건 조회 API를 구현하며,
@OneToMany 연관관계와 LAZY 로딩이 실제로 언제 쿼리를 발생시키는지를 함께 학습한다.


1️⃣ 10강에서 구현한 기능

10강의 목표는 다음과 같다.

  • 특정 게시글의 댓글 목록 조회
  • 특정 게시글의 댓글 단건 조회
  • 댓글 응답을 엔티티가 아닌 DTO로 반환
  • JPA 연관관계(@OneToMany)와 지연 로딩(LAZY) 동작 이해

2️⃣ API 구조 설계

댓글은 항상 게시글에 종속되므로
API URL도 게시글 하위 리소스 구조로 설계한다.

GET /api/v1/posts/{postId}/comments
GET /api/v1/posts/{postId}/comments/{id}

이는 REST 관점에서도 자연스러운 구조다.


3️⃣ 댓글 컨트롤러 구현

ApiV1PostCommentController

@RestController
@RequestMapping("/api/v1/posts/{postId}/comments")
@RequiredArgsConstructor
public class ApiV1PostCommentController {

    private final PostService postService;

    // 댓글 다건 조회
    @GetMapping
    public List<PostCommentDto> getItems(
            @PathVariable int postId
    ) {
        Post post = postService.findById(postId).get();

        return post
                .getComments()
                .stream()
                .map(PostCommentDto::new)
                .toList();
    }

    // 댓글 단건 조회
    @GetMapping("/{id}")
    public PostCommentDto getItem(
            @PathVariable int postId,
            @PathVariable int id
    ) {
        Post post = postService.findById(postId).get();

        PostComment postComment = post.findCommentById(id).get();

        return new PostCommentDto(postComment);
    }
}

4️⃣ 댓글 DTO 구현

댓글 역시 엔티티를 그대로 노출하지 않고 DTO로 변환한다.

public record PostCommentDto(
        int id,
        LocalDateTime createDate,
        LocalDateTime modifyDate,
        String content
) {
    public PostCommentDto(PostComment postComment) {
        this(
                postComment.getId(),
                postComment.getCreateDate(),
                postComment.getModifyDate(),
                postComment.getContent()
        );
    }
}

DTO를 사용함으로써:

  • 엔티티 구조 노출 방지
  • API 응답 스펙 안정성 확보
  • 프론트엔드 요구사항 변경에 유연하게 대응 가능

5️⃣ Post 엔티티의 연관관계 설정

@OneToMany(
    mappedBy = "post",
    fetch = LAZY,
    cascade = {PERSIST, REMOVE},
    orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();

이 한 줄에는 여러 JPA 개념이 동시에 들어 있다.


6️⃣ mappedBy = "post"

  • 연관관계의 주인은 PostComment
  • 외래키(post_id)는 PostComment.post가 관리
  • Post.comments읽기 전용(반대편) 역할

즉, 댓글이 어떤 게시글에 속하는지는
PostComment 쪽에서 결정된다.


7️⃣ fetch = LAZY — 지연 로딩의 핵심

fetch = LAZY
연관된 엔티티를 바로 가져오지 않고, 필요할 때 가져오는 방식이다.

동작 흐름

Post post = postService.findById(postId).get();

이 시점에서는:

  • post만 조회
  • 댓글 조회 쿼리는 실행되지 않음
post.getComments();

이 순간:

  • 댓글이 필요하다고 판단
  • 아래와 같은 쿼리가 실행됨
SELECT *
FROM post_comment
WHERE post_id = ?

👉 댓글 조회 쿼리는 getComments()를 처음 호출하는 순간 실행된다.


8️⃣ 댓글 다건 조회 시 쿼리 흐름

post.getComments()
    .stream()
    .map(PostCommentDto::new)
    .toList();
  • getComments() 호출
  • LAZY 로딩 트리거
  • 댓글 전체 조회 쿼리 1번 실행
  • 이후 스트림 처리

한 번 로딩된 댓글 컬렉션은
같은 트랜잭션 내에서 다시 쿼리가 나가지 않는다.


9️⃣ 댓글 단건 조회의 주의점

PostComment postComment = post.findCommentById(id).get();

이 코드의 실제 동작은 다음과 같다.

  1. post.getComments() 호출
  2. 댓글 전체를 먼저 조회
  3. Java 스트림으로 id 필터링

즉, 단건 조회처럼 보이지만 DB에서는 다건 조회가 먼저 수행된다.

이 방식은 학습 단계에서는 이해하기 좋지만,
댓글 수가 많아질 경우 성능 이슈로 이어질 수 있다.


1️⃣0️⃣ cascade와 orphanRemoval의 의미

cascade = PERSIST

  • 게시글 저장 시
  • 새로 추가된 댓글도 함께 저장

cascade = REMOVE

  • 게시글 삭제 시
  • 해당 게시글의 댓글도 함께 삭제

orphanRemoval = true

  • 댓글을 comments 리스트에서 제거하면
  • DB에서도 해당 댓글 row가 자동 삭제

댓글 삭제 기능 구현 시 매우 유용한 설정이다.


🔚 10강 정리

  • 게시글 하위 리소스로 댓글 API를 설계했다
  • 댓글 다건 조회 / 단건 조회 API를 구현했다
  • DTO를 사용해 엔티티 노출을 방지했다
  • fetch = LAZY의 실제 쿼리 실행 시점을 이해했다
  • JPA 연관관계 설정의 의미를 정리했다

💡 한 줄 요약

10강은 댓글 조회 API를 구현하면서
JPA 연관관계와 LAZY 로딩이 실제로 어떻게 동작하는지 체감하는 강의였다.

0개의 댓글