디스코드 프로젝트 실습: JPA N+1 문제 해결하기

2026. 3. 11. 13:53·코드잇 스프린트/실습

특정 사용자의 채널목록 조회

 

현재 "곽인성"이라는 특정 회원의 채널목록을 조회하면 2개의 결과가 나온다.

 

ChannelController

@GetMapping
    public ResponseEntity<List<ChannelDto>> getChannel(@RequestParam("userId") UUID userId) {
        List<ChannelDto> channels = channelService.findAllByUserId(userId);
        return ResponseEntity.ok(channels);
    }

 

ChannelService 내 findAllByUserId()

@Override
    @Transactional(readOnly = true)
    public List<ChannelDto> findAllByUserId(UUID userId) {

        List<Channel> mySubscribedChannels = readStatusRepository.findAllByUserId(userId).stream()
                .map(ReadStatus::getChannel)
                .toList();

        return channelRepository.findAll().stream()
                .filter(channel ->
                        channel.getType().equals(ChannelType.PUBLIC)
                                || mySubscribedChannels.contains(channel)
                )
                .map(this::toDto)
                .toList();
    }

현재 코드는 해당 userId를 통해 해당 userId를 가진 ReadStaus를 조회하고 그 ReadStatus내에 channel 정보를 가져와서

"mySubscribedChannels" 리스트에 저장한다.

 

PUBLIC 채널은 모든 사용자가 다 참여하고있다는 비즈니스 로직이 있기때문에

PUBLIC 채널 || 모든 채널중 내가 구독한 채널을 필터링하고, 이걸 DTO 형식으로 바꿔서 리스트로 반환하도록 되어있다.

 

문제점

  • 채널이 N개 있으면 N개의 채널을 조회하고 filter에 들어간다.(10000개 채널 있으면 10000개 채널 조회해야됨)
  • 실제 필요한것은 PUBLIC 채널 + 내가 참여하고있는 PRIVATE 채널
  • 10000개의 채널이 있다고 치면, 9900개는 필터링되고 100개를 toDto() 호출해야된다.
  • toDto() 내에는 messageRepository.findAllByChannelId() / readStatus.findAllByChannelId() 가 존재하는데 이게 각각 100번씩 호출하게 된다.
  • 정리하자면, ChannelService.findAllByUserId() 내 channelRepository.findAll()로 Select문이 1회 실행
  • toDto()가 또 실행되면서 N회의 Select문이 추가로 실행 -> 결과적으로 N+1문제 발생

 

해결전략

channelRepository.findAll()   // 1 query
.map(this::toDto)             // channel 개수만큼 실행

toDto() 안에서는

messageRepository.findAllByChannelId(channel.getId())
readStatusRepository.findAllByChannelId(channel.getId())

이것들이 실행된다.

그래서 1 + N + N 쿼리가 발생한다.

  • 채널 목록 조회
  • 채널 ID 목록 추출
  • message / readStatus를 channelId IN()으로 한번에 조회하도록 한다.
  • Map으로 묶어서 DTO 생성

 

private ChannelDto toDto(Channel channel,Map<UUID, Instant> lastMessageMap,Map<UUID,List<UserDto>> participantsMap) {
        ChannelDto baseDto = channelMapper.toDto(channel);

        Instant lastMessageAt = lastMessageMap.getOrDefault(channel.getId(),null);
        List<UserDto> participants = participantsMap.getOrDefault(channel.getId(),List.of());

        return new ChannelDto(
                baseDto.id(),
                baseDto.type(),
                baseDto.name(),
                baseDto.description(),
                participants,
                lastMessageAt
        );
    }

toDto()내 조회하는 문을 전부 제외하였고,
다른 메서드에서 조회해서 toDto에서는 조합하는 방식으로 바꾸었다.

 

lastMessageAt값의 경우는 원래는 toDto() 내부에서 조회하고 값을 필터링해서 가져왔다면,

지금은 findAllUserId() 내에서 값을 조회한다음에 toDto로 보내주고, toDto에서는 조합만 하도록 바꾼것이다.

 

lastMessageMap을 예시로 보면

Map<UUID,Instant> 형식으로 되어있고

key   → channelId
value → lastMessageAt

해당 값은 이런 식으로 담겨져 있다.

MessageRepository내 

@Query("""
    SELECT m.channel.id, MAX(m.createdAt)
    FROM Message m
    WHERE m.channel.id IN :channelIds
    GROUP BY m.channel.id
    """)//JPQL은 테이블이 아닌 Entity 기준으로 작성된다.
        //채널별 마지막 메시지 시간만 조회(ChannelService내 toDto에 사용), lastMessageAt을 위한 메서드
    List<Object[]> findLastMessageAtByChannelIds(List<UUID> channelIds);

findLastMessageAtByChannelIds 라는 메서드를 만들고, JPQL을 사용해서 channelId의 가장 마지막 메시지의 시간을 조회하도록 구성하였다. 이를 그대로 Map<UUID,Instant> 형식으로 받아오는것이다.

 

lastMessageAt의 값은 getOrDefault()를 사용하였고,

lastMessageMap.getOrDefault(channel.getId(),null);

key가 Map에 있으면 해당 value를 반환 즉, channel.getId()가 Map에 있으면 해당 시간(마지막 메시지 시간)이 반환

key가 없으면 null값이 반환되도록 하였다.

 

@Override
@Transactional(readOnly = true)
public List<ChannelDto> findAllByUserId(UUID userId) {

    List<Channel> channels = channelRepository.findAll();

    List<UUID> channelIds = channels.stream()
            .map(Channel::getId)
            .toList();

    Map<UUID, Instant> lastMessageMap = messageRepository.findLastMessageAtByChannelIds(channelIds).stream()
            .collect(Collectors.toMap(
                    row -> (UUID) row[0],
                    row-> (Instant) row[1]
            ));


    Map<UUID, List<UserDto>> participantsMap =
            readStatusRepository.findAllByChannelIdIn(channelIds)
                    .stream()
                    .collect(Collectors.groupingBy(
                            ReadStatus -> ReadStatus.getChannel().getId(),
                            Collectors.mapping(
                                    ReadStatus -> userMapper.toDto(ReadStatus.getUser()),
                                    Collectors.toList()
                            )
                    ));

    return channels.stream()
            .map(channel -> toDto(channel, lastMessageMap, participantsMap))
            .toList();

}

lastMessageMap과 participantsMap을 만들어서 toDto에 보내주는식으로 하였다.

findAllByUserId()내 잘생되는 쿼리문은 3개. N + 1 문제 해결 되었다.

 

IN Query 방식 사용

  • 기존 100개가 필터링 돼있으면 201개의 쿼리가 출력됐으면,
  • IN Query 방식으로 findAllByChannelIdIn() 메서드를 통해

'코드잇 스프린트 > 실습' 카테고리의 다른 글

Findex 프로젝트: 지수정보 update가 이루어지지않는다 400에러  (0) 2026.03.17
Findex 프로젝트: 지수정보 목록 조회 Cursor기반 페이지네이션 적용  (0) 2026.03.16
디스코드 프로젝트 실습: JPA Public 채널생성시 ReadStatus에 채널id가 null값으로 들어간다.  (0) 2026.03.10
MapStruct는 <T> 제네릭 타입 메서드 자체는 구현코드 생성 못한다.  (0) 2026.03.10
BinaryContent 저장로직 고도화  (0) 2026.03.09
'코드잇 스프린트/실습' 카테고리의 다른 글
  • Findex 프로젝트: 지수정보 update가 이루어지지않는다 400에러
  • Findex 프로젝트: 지수정보 목록 조회 Cursor기반 페이지네이션 적용
  • 디스코드 프로젝트 실습: JPA Public 채널생성시 ReadStatus에 채널id가 null값으로 들어간다.
  • MapStruct는 <T> 제네릭 타입 메서드 자체는 구현코드 생성 못한다.
과컴
과컴
벡엔드 개발자 최소기준 맞추겠습니다.
  • 과컴
    곽의 프로그램
    과컴
  • 전체
    오늘
    어제
    • 분류 전체보기 (76)
      • 위클리페이퍼 (6)
      • 파이썬 (4)
      • 코드잇 스프린트 (48)
        • Spring 이론 (7)
        • Java이론 (11)
        • 실습 (23)
      • 백엔드 개발자 최소기준 (1)
      • 코딩테스트 (5)
        • 알고리즘 (0)
        • SQL (1)
      • Git (5)
      • 스프링부트 핵심가이드 (1)
      • 트러블 슈팅 (2)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    백준2576
    문자열
    파이썬입문
    백준브론즈
    백준1075번
    혼공파
    파이썬기초
    백준1152
    파이썬
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
과컴
디스코드 프로젝트 실습: JPA N+1 문제 해결하기
상단으로

티스토리툴바