문제 상황
일정 서비스 로직은 단순한 조회 기능인데, 사용자가 체감할 정도의 응답 지연(약 750ms)이 발생하고 있었습니다.
접근
스프링단 코드는 크게 이상이 없어 DEBUG 레벨 로그로 설정해보았습니다.
API 조회 콜 1개당 수많은 쿼리문이 발생하였습니다. 바로 N+1문제였습니다.
원인
ONE TO ONE 양방향 매핑 N+1 문제
연관관계 주인 - game_player_stat
반대 - game_participant
- GameParticipant 조인 시 GamePlayerStat도 EAGER 방식으로 가져오게 되어 문제 발생
- 자식 쪽에서는 부모 존재여부를 모르기 때문에 체크하게 되어 LAZY방식 작동안하게 됨
해결책
- @OneToOne(optional = false) (DTO 프로젝션과 비교분석함)
- JOIN FETCH (stat 필드가 많아서 비효율적)
- DTO 프로젝션 (이 방식으로 선택함)
1. OneToOne(optional = false)
명시적으로 false를 설정하면 상대 존재를 확신하게 되므로 존재여부 체크를 안해도됩니다.
따라서 N+1문제가 해결됩니다.


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 표시 및 승리팀 구분을 위해 |

성능 비교 분석
1번(OneToOne(optional=false))과 3번(DTO 프로젝션)을 비교 분석해보았습니다.
10초에 걸쳐 100명의 동시 요청 결과
기존 코드(1번 - OneToOne(optional=false))
생각보다 N+1문제만 해결하니 준수한 응답시간을 보였습니다.


개선 코드(3번 - DTO 프로젝션)
확실히 MAX 응답시간에서도 두자릿수 ms로 보장되었고 평균 ms도 미세하지만 더 낮게 측정되었습니다.


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

DOT 프로젝션으로 요청 보내는 필드 개수가 6개 밖에 없다보니 대략 10배정도 차이가 나는걸 확인하였습니다.
향 후 계획
LCK 경기 데이터는 한 번 생성되면 거의 변하지 않는 불변성을 가집니다.
이 특성에 주목하여, DB 조회 없이 메모리에서 직접 데이터를 반환하는 캐싱 전략을 도입하여
응답 속도와 리소스 사용을 최적화하려고 합니다.
'나르지지' 카테고리의 다른 글
| SQLite와 MySQL 동시쓰기 비교해보기 (0) | 2025.12.05 |
|---|---|
| MySQL에서 SQLite로 마이그레이션 한 이유 (1) | 2025.11.15 |
| 반복되는 일정 서비스 API콜에 대한 캐시 적용 및 전략 (3) | 2025.08.15 |
| 8만 건 데이터 DB 마이그레이션 자동화 구축 (4) | 2025.08.06 |
| OOM 원인 API, DB 최적화로 해결하기 (4) | 2025.07.31 |