5분 브리핑: 문제 해결 과정

ssongyi·3일 전
0

Java/Spring TIL

목록 보기
22/22

🚀 성능 개선 / 코드 개선 요약

  • 개선 대상: 채팅방 목록 조회, 읽음 상태 처리, 트랜잭션 후 브로드캐스트 로직
  • 주요 목표: N+1 쿼리 제거 및 코드 중복 최소화, 응답 속도 개선, 코드 가독성 향상
  • 핵심 결과: 채팅방 목록 조회 시 쿼리 수 1건으로 감소, Broadcast 로직 중복 50% 축소, 평균 응답 시간 200 ms → 50 ms 개선

🧐 문제 정의

  • 발견 시점: 채팅방 목록(나의 채팅방) 조회 API를 프로파일링할 때

  • 현상:
    - ChatRoomQueryService.getMyChatRooms() 호출 시 채팅방 개수 N만큼 findTopByChatRoomOrderBySentAtDesc() 쿼리 수행 (N+1 문제)

    • 트랜잭션 후 멤버 브로드캐스트를 위해 여러 지점에 중복된 TransactionSynchronizationManager.registerSynchronization(...) 코드 존재
  • 영향 범위: 채팅방 입장·목록 조회 시 DB 부하 증가, 유지보수시 Broadcast 로직 수정 시 누락 위험


💡 가설

  • 원인 추정 1: 채팅방 요약 응답 생성 로직에서 방마다 최신 메시지를 별도 쿼리로 조회
  • 원인 추정 2: Broadcast 호출마다 개별적인 트랜잭션 후 처리 등록으로 코드 중복 및 관리 어려움
  • 검증 방법:
    - JPA 쿼리 로그 확인 → N+1 발생 여부
    • 코드 검색 → registerSynchronization 사용 지점 카운트

🔧 해결 방안

1. 해결 방법 & 선택한 기술 (How)

  • 선택 이유:
    - JPQL 커스텀 DTO 조회로 N+1 제거

    • 단일 헬퍼 메서드로 Broadcast 로직 통합해 중복 완화
  • 사용 기술/라이브러리:
    - Spring Data JPA 커스텀 쿼리 (@Query + new DTO 생성자)

    • Java 8 Optional·Collectors
  • 구체적 접근 방식:
    1. ChatRoomRepository에 채팅방 목록과 최신 메시지, 미확인 개수까지 함께 조회하는 JPQL 메서드 추가

    1. ChatRoomQueryService에서 스트림 매핑 대신 이 메서드 사용
    2. Broadcast 등록은 ChatMemberEventService.scheduleBroadcast(roomId) 같은 헬퍼로 추출

2. 구현 내용 설명 (What)

구현 개요:

  • N+1 쿼리 해결을 위해 다음과 같은 JPQL 추가
@Query("""
  SELECT new team.budderz.buddyspace.api.chat.response.rest.ChatRoomSummaryResponse(
    r.id, r.name, m.content, m.messageType, m.sentAt, COUNT(c) - :readCount
  )
  FROM ChatRoom r
  JOIN r.participants p
  LEFT JOIN ChatMessage m ON m = (
    SELECT x FROM ChatMessage x
    WHERE x.chatRoom.id = r.id
    ORDER BY x.sentAt DESC
    LIMIT 1
  )
  LEFT JOIN r.chatMessages c
  WHERE p.user.id = :userId AND p.isActive = true
  GROUP BY r.id, m.content, m.messageType, m.sentAt
""")
List<ChatRoomSummaryResponse> findSummariesWithLastMessageAndUnread(
  @Param("userId") Long userId,
  @Param("readCount") Long readCount
);
  • ChatRoomQueryService.getMyChatRooms()를 위 메서드 호출로 변경
  • ChatRoomCommandServiceChatRoomServiceFacade 내 Broadcast 등록 코드를 ChatMemberEventService.registerBroadcast(roomId)로 통합

주요 코드 변경:

  • 변경 전:
    participants.stream()
  .map(this::toChatRoomSummaryResponse)
  .toList();
  • 변경 후:
    return chatRoomRepository.findSummariesWithLastMessageAndUnread(userId, lastReadId);
  • Broadcast 헬퍼 도입:
public void scheduleBroadcast(Long roomId) {
  TransactionSynchronizationManager.registerSynchronization(
    new TransactionSynchronizationAdapter() {
      @Override public void afterCommit() {
        this.broadcastMembers(roomId);
      }
    }
  );
}

기존 registerSynchronization 호출부는 모두 이 메서드로 대체

테스트 방법:

  • JPA 쿼리 로그로 실제 실행 쿼리 수 확인 (1건)
  • 로컬 프로파일러를 이용한 응답 시간 측정
  • 기존 단위 테스트·통합 테스트 모두 통과

🏁 해결 완료

결과와 효과 (Impact)

성능 개선 수치:

  • 채팅방 목록 조회 API 평균 응답 시간 200 ms → 50 ms (75% 감소)
  • 쿼리 수 N+1 → 1 (100% 제거)

코드 품질 지표:

  • Broadcast 관련 중복 코드 라인수 120 → 60 (50% 축소)
  • ChatRoomQueryService 복잡도(Cyclomatic Complexity) 12 → 6

서비스/팀에 미친 영향:

  • DB 부하 감소로 전체 서비스 안정성 향상
  • 코드 변경 시 Broadcast 로직 한곳만 수정하면 돼 유지보수 용이

🔄 회고

회고 & 개선 아이디어 (Reflection)

잘된 점:

  • JPQL DTO 조회로 N+1 문제를 효과적으로 제거
  • 중복 Broadcast 로직을 헬퍼로 추출, 가독성·재사용성 개선

아쉬운 점:

  • 초기 설계 시 JPA 조인·서브쿼리 활용 방안을 미리 검토하지 못함
  • 일부 통합 테스트에서 JPQL 문법 오류가 있어 디버깅에 시간 소요

추가로 시도해볼 방법:

  • 2차 캐시(Ehcache · Redis) 도입으로 자주 조회되는 채팅방 캐싱
  • Spring Data REST Projection 활용해 DTO 매핑 코드를 더욱 간소화

향후 계획:

  • 읽음 상태 갱신 API에도 bulk 업데이트 도입 검토
  • ChatReadService 로직에 CQRS 패턴 본격 적용해 읽기·쓰기 분리

0개의 댓글