나르지지

DTO 프로젝션을 활용한 일정 서비스 성능 개선

changha. 2025. 8. 15. 13:47

문제 상황

일정 서비스 로직은 단순한 조회 기능인데, 사용자가 체감할 정도의 응답 지연(약 750ms)이 발생하고 있었습니다. 

 

접근

스프링단 코드는 크게 이상이 없어 DEBUG 레벨 로그로 설정해보았습니다.

API 조회 콜 1개당 수많은 쿼리문이 발생하였습니다. 바로 N+1문제였습니다. 

 

원인

ONE TO ONE 양방향 매핑 N+1 문제

연관관계 주인 - game_player_stat

반대 - game_participant

 

  • GameParticipant 조인 시 GamePlayerStat도 EAGER 방식으로 가져오게 되어 문제 발생 
  • 자식 쪽에서는 부모 존재여부를 모르기 때문에 체크하게 되어 LAZY방식 작동안하게 됨

해결책

  1. @OneToOne(optional = false) (DTO 프로젝션과 비교분석함)
  2. JOIN FETCH (stat 필드가 많아서 비효율적)
  3. DTO 프로젝션 (이 방식으로 선택함)

 

1. OneToOne(optional = false)

명시적으로 false를 설정하면 상대 존재를 확신하게 되므로 존재여부 체크를 안해도됩니다. 

따라서 N+1문제가 해결됩니다.

GameParticipant 엔티티에서

 

GameParticipant 특성상 1개의 Game에 10개 GameParticipant가 중복되므로 DISTINCT를 해주어야 합니다.

DISTINCT 특성으로 인해 DB단에서는 임시 테이블을 만들게 됩니다.

이때 JPQL과 SQL DISTINCT차이로 인해 

DB단에서는 제대로 중복 판별이 안되는 것을 확인하였습니다. 

 

2. JOIN FETCH 

game_player_stat 테이블을 JOIN FETCH하여 N+1문제를 방지합니다. 

다만 이럴경우 game_player_stat의 필드도 모두 가져오게 되는데 

약 110개의 필드를 가진 game_player_stat테이블이라 불필요한 오버헤드가 생기게 됩니다. 

또한 본질적인 해결책이 아니라 제외하였습니다. 

3. DTO 프로젝션 

일정 서비스에서 필요한 필드(6개)들만 뽑아서 제공합니다. 

따라서 객체 로딩 필요없이 테이블에서 필요한 필드들만 사용할 수 있습니다.

이로인해 불필요한 오버헤드를 제거할 수 있고 N+1문제도 해결됩니다. (엔티티 설계의 정합성을 위해 optional=false 설정은 유지했습니다.)

 

아래와 같이 필요한 필드들을 정리하여 조인 관계를 설정하였습니다.

테이블 필요한 칼럼 사용하는 곳
games game_id matchId 생성을 위해
  scheduled_game_start_time scheduledTime 표시를 위해
leagues league_name leagueInfo 표시를 위해
  season_split leagueInfo 표시를 위해
game_participants is_win 팀별 승리 횟수(score) 계산을 위해
teams team_name teamName 표시 및 승리팀 구분을 위해

 

DTO 프로젝션 쿼리문

성능 비교 분석 

1번(OneToOne(optional=false))과 3번(DTO 프로젝션)을 비교 분석해보았습니다.

10초에 걸쳐 100명의 동시 요청 결과

기존 코드(1번 - OneToOne(optional=false)) 

생각보다 N+1문제만 해결하니 준수한 응답시간을 보였습니다. 

평균 25ms

 

최대 30%까지 CPU 사용률 올라감

개선 코드(3번 - DTO 프로젝션)

확실히 MAX 응답시간에서도 두자릿수 ms로 보장되었고 평균 ms도 미세하지만 더 낮게 측정되었습니다. 

MAX 부분에서도 50ms 이내 성능, 평균 22ms
최대 20%까지만 올라감

DB Fetch Time 

응답시간은 오히려 약간 더 높게 나올 때도 있었지만 Fetch Time에서 확실한 차이가 있었습니다.

(Duration / Fetch Time) 상 : 기존 코드, 하 : 개선 코드

DOT 프로젝션으로 요청 보내는 필드 개수가 6개 밖에 없다보니 대략 10배정도 차이가 나는걸 확인하였습니다. 

 

향 후 계획 

LCK 경기 데이터는 한 번 생성되면 거의 변하지 않는 불변성을 가집니다.

이 특성에 주목하여, DB 조회 없이 메모리에서 직접 데이터를 반환하는 캐싱 전략을 도입하여

응답 속도와 리소스 사용을 최적화하려고 합니다.