작년에 시작한 프로젝트를 최근 리팩토링 하기 시작했다. 지금도 감자지만 더더욱 감자일 때 작성한 코드다 보니, 리팩토링 할 부분이 많았고 오늘은 찜 관련 기능의 리팩토링을 진행했다. 우리 프로젝트에선 모임과 운동을 찜할 수 있었고, 각각의 찜 목록 조회에서 N+1 문제가 있어서 이를 해결하는 게 목적이었다.
문제의 발견
N+1을 해결하기 위해 주로 fetch join을 사용해왔고, 찜과 관련한 눈에 보이는 연관 객체들을 fetch join으로 한 번에 가져오도록 수정했다. 그러고 쿼리 카운트 테스트를 돌렸는데 실패했다. N+1문제가 해결되지 않았다는 뜻이었다.
북마크 찜 목록 N+1 회귀 테스트 > 찜한 운동 목록 - 찜 개수와 무관하게 고정된 쿼리 수만 실행한다 FAILED
북마크 찜 목록 N+1 회귀 테스트 > 찜한 모임 목록 - 찜 개수와 무관하게 고정된 쿼리 수만 실행한다 FAILED
> Task :test FAILED
BUILD FAILED in 2m
문제를 파악해보고자 SQL 쿼리문을 살펴보았는데, Party 한 개당 partyImg와 chatRoom이 함께 조회되고 있었다. 이 도메인들이 원인이었던 거다.
4753: select
4754: cr1_0.id,
4755: cr1_0.created_at,
4756: cr1_0.party_id,
4757: cr1_0.type,
4758: cr1_0.updated_at
4759: from
4760: chat_room cr1_0
4761: where
4762: cr1_0.party_id=?
4764: select
4765: cr1_0.id,
4766: cr1_0.created_at,
4767: cr1_0.party_id,
4768: cr1_0.type,
4769: cr1_0.updated_at
4770: from
4771: chat_room cr1_0
4772: where
4773: cr1_0.party_id=?
4793: select
4794: cr1_0.id,
4795: cr1_0.created_at,
4796: cr1_0.party_id,
4797: cr1_0.type,
근데 나는 둘 다 LAZY로 설정해두었고, 찜 목록 조회 로직에서는 chatRoom과 partyImg에 접근하지 않아서 N+1이 발생하지 않아야 정상이었다. 필요하지도 않은 엔티티들을 매번 접근하다보니 발생했던 거였다.
@OneToOne(mappedBy = "party")
private PartyImg partyImg; // LAZY인데...?
원인을 찾아본 결과는 다음과 같았다.
역방향 nullable OneToOne은 LAZY가 먹지 않는다
강제 EAGER 행 된 엔티티들
결론부터 말하면 강제 EAGER로 적용되고 있었다. 이유는 무엇이었을까?
찾아보니 FK를 갖지 않는 쪽에 있는 엔티티는, 부모 엔티티를 만들 때 그 자리에 프록시를 넣을지 null을 넣을지 정하게 된다. nullable하지 않은 경우에는 어차피 null이 들어가지 않기 때문에 프록시 객체를 넣어버릴 수 있지만, nullable한 경우 null과 프록시 중 무엇을 넣을지 알려면, 상대 테이블을 조회해봐야 한다.
무작정 프록시를 넣게 되면, 실제로 존재하지 않는데도 not null이 반환되어 예외가 발생하게 되기 때문에, EAGER로 동작하게 되는 것이다.
batch 기능 또한 LAZY 로딩들을 모아 IN으로 묶는 기능이기 때문에, 위처럼 즉시로딩이 되는 경우 해결되지 못한다!
해결방법
해당 로직에서는 사용하지 않지 않지만 fetch join을 해서 해결했다. 보통은 사용하는 객체를 fetch join 하지만, 사용하지 않는 eager 객체까지 같이 fetch join 해서 해결했다!
'백엔드 > JPA' 카테고리의 다른 글
| [Spring] DB와 JPA Persistence Context 불일치 해결하기 - trouble shooting (0) | 2026.03.21 |
|---|---|
| [JPA] JPA Query Methods 활용 (0) | 2024.04.09 |
| [JPA] 강의 추가학습 기록(3) - 스프링부트와 JPA활용1 (1) | 2024.03.01 |
| [JPA] 강의 추가학습 기록(2) - 스프링부트와 JPA활용1 (0) | 2024.02.27 |
| [JPA] 강의 추가학습 기록(1) - 스프링부트와 JPA활용1 (0) | 2024.02.22 |