본문 바로가기
Projects/청하-청년을 위한 커뮤니티 서비스

[청하] 밸런스 게임/토론 기능 - (4) 댓글 조회 기능 개선 (feat. N+1 문제 방지를 위한 설계와 구현)

by Lpromotion 2025. 4. 30.
2025.01.13

 

이전 글에서는 밸런스 게임 조회 기능을 구현하면서 페이징 처리, 날짜 포맷팅, 참여 가능 여부 계산, 이전/다음 게임 이동 기능을 설명했다.

이번 글에서는 밸런스 게임 조회 시 함께 제공되는 댓글 목록댓글 작성자의 선택 정보를 효율적으로 조회하기 위한 방법을 다룬다.

단순한 JPA 기본 조회 방식만 사용하면, 밸런스 게임 조회 시 댓글 작성자 정보와 사용자 선택 정보를 각각 조회하면서 N+1 문제가 발생할 수 있다. 이를 해결하기 위해 다음의 2가지 최적화 전략을 적용했다.

  1. @EntityGraph를 사용해 댓글과 작성자 정보를 한 번의 쿼리로 조회
  2. 댓글 작성자의 선택 정보를 사전에 조회하여 Map을 사용한 임시 저장 방식으로 반복 쿼리 방지

 

1. 최적화 내용

@EntityGraph로 댓글 작성자 정보 조회 최적화

댓글 작성자의 게임 선택 정보를 조회할 때, 각 댓글마다 DB를 조회하게 되면 역시 N+1 문제가 발생한다. 이를 방지하기 위해 모든 선택 정보를 미리 조회하여 Map에 임시 저장했다.

CommentRepository

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {

    @EntityGraph(attributePaths = {"writer"})
    @Query("SELECT c FROM Comment c WHERE c.game = :game")
    List<Comment> findCommentsWithWriterByBalanceGame(BalanceGame game);
}
  • 하나의 쿼리로 모든 게임의 사용자 선택 정보를 조회한다.
  • Map<게임ID, Map<작성자ID, 선택값>> 형태로 구성하여, 댓글 작성자의 선택 정보를 반복 쿼리 없이 O(1)로 조회 가능하다.
  • 이후 댓글 DTO 변환 시 Map에서 선택 정보를 조회하여 별도의 DB 접근 없이 O(1) 시간 복잡도로 처리한다.

 

사용자 선택 정보 Map 임시 저장

댓글 작성자의 게임 선택 정보를 조회할 때, 작성자마다 DB를 조회하게 되면 역시 N+1 문제가 발생한다. 이를 방지하기 위해 모든 선택 정보를 미리 조회하여 Map에 캐싱했다.

getBalanceGame의 사용자 선택 정보 조회 로직

// 사용자 선택 정보 조회
List<BalanceGameChoice> choices = balanceGameChoiceRepository.findByGameIds(gameIds);
Map<Long, Map<Long, EChoice>> allChoicesByGame = choices.stream()
        .collect(Collectors.groupingBy(
                choice -> choice.getGame().getId(),
                Collectors.toMap(
                        choice -> choice.getUser().getId(),
                        BalanceGameChoice::getChoice
                )
        ));
  • 하나의 쿼리로 모든 게임의 사용자 선택 정보를 조회한다.
  • Map<게임ID, Map<작성자ID, 선택값>> 형태로 구성하여, 댓글 작성자의 선택 정보를 반복 쿼리 없이 O(1)로 조회 가능하다.
  • 이후 댓글 DTO 변환 시 Map에서 선택 정보를 조회하여 별도의 DB 접근 없이 O(1) 시간복잡도로 처리한다.

 

2. 개선 전과 개선 후

구분 개선 전 개선 후
댓글 작성자 조회 댓글마다 개별 쿼리 발생 (N+1) @EntityGraph로 한 번에 조회
사용자 선택 정보 댓글마다 선택 정보 개별 조회 선택 정보 전체 조회 후 Map으로 임시 저장
전체 쿼리 수 댓글 수 x 2~3배 게임 수 + 댓글 수 정도로 감소

 

개선 후 댓글 조회 흐름

  • 게임 리스트 조회 (Page<BalanceGame>)
    • 게임 ID 리스트 추출
      • 사용자 선택 정보 일괄 조회 → Map<게임 ID, Map<작성자 ID, 선택값>> 생성
      • 각 게임에 대해
        • 댓글 + 작성자 정보 조회 (@EntityGraph)
        • 댓글 작성자 선택값 Map에서 조회
        • BalanceGameChoiceListResponseDto로 변환

 

결론 및 인사이트

최적화 작업을 통해 댓글, 작성자 정보, 그리고 선택 정보까지 연관된 모든 데이터를 중복 쿼리 없이 효율적으로 조회할 수 있었다. 댓글 조회 기능은 단순히 댓글만 불러오는 것이 아니라, 연관된 작성자나 선택 정보까지 함께 조회되기 때문에 데이터 로딩을 전략적으로 구현하는 것이 필요했다.

  • @EntityGraph는 단순한 연관 로딩 최적화에 유용하다.
  • 선택 정보와 같이 사용자-게임 간 교차되는 정보는 사전에 조회하여 Map 임시 저장하는 것이 효율적이다.
  • 이를 통해 실제 댓글 수만큼 발생할 수 있었던 N+1 쿼리를 방지했다.
반응형

댓글