JPA N+1 및 One to One fetch

주성민·2022년 6월 11일
1
post-thumbnail

JPA를 사용하다 보면 한번씩 마주치게 되는 문제 N+1에 대해 알아보자

N+1 문제?
연관 관계에서 발생하는 이슈로 엔티티를 조회할 경우 조회된 데이터의 갯수(N) 만큼 추가로 조회 쿼리가 발생하는 현상

테스트를 위해 데이터베이스 설정 및 엔티티를 작성하도록 하자
나의 경우 member(회원)과 login_history(로그인 이력) 두가지의 엔티티를 가지고 테스트를 해보려고 한다.
member테이블에는 총 4명의 회원이 있다.
그리고 login_history 테이블에는 4명의 회원에 대한 8개의 로그인 이력이 존재한다.
우선 연관관계 설정을 FetchType.EAGER로 설정한다.

JPA에서 연관 관계의 로딩 설정
FetchType.EAGER : 즉시 로딩, 연관 엔티티 즉시 조회
FetchType.LAZY : 지연 로딩, 연관 엔티티 사용 시 조회

위의 EAGER 즉시 로딩 설정으로 findAll()을 수행한다.
즉시 로딩 설정이 되어 있기 때문에 loginHistory 테이블을 조회 시 연관 되어 있는 Member 회원 4명에 대한 추가 쿼리가 발생하게 된다. (N+1 발생)

그렇다면 LAZY의 경우는 어떨까?
우선 당장은 추가 쿼리는 발생하지 않는다.
하지만 지금 당장 수행하지 않았을 뿐 실제로 LoginHistory 객체에서 Member의 정보를 조회하고자 할 때 추가적으로 쿼리가 발생하게 된다. (N+1)

즉, LAZY는 N+1 문제의 직접적인 해결 방법은 아니다.
단지 당장 사용하지 않을 때 쿼리를 발생시키지 않는 정도에 그친다.
그렇다면 이 문제를 해결하기 위해서는 어떻게 해야 할까
직접 메서드를 생성하여 fetch 조인을 통해 수행하여야 한다.
결과는 다음과 같았다.
여기서 예상하지 못한 결과를 확인하게 되었다.
나의 예상대로면 단 하나의 쿼리만 발생하여야 하는데 여전히 N+1 쿼리가 발생하고 있었다.

추가적으로 발생된 쿼리는 MemberAuthMgt에서 발생하고 있었다.
나의 MemberAuthMgt는 다음과 같다.
그리고 MemberAuthMgt를 가지고 있는 Member 엔티티는 다음과 같다.나는 이미 Lazy 설정을 통해 Member 엔티티만 가져오고, 추가적인 MemberAuthMgt는 사용할 때 엔티티 정보를 가져온다고 생각했는데 나의 생각과는 다르게 동작했다.

몇번을 바꾸고 해도 결과는 바뀌지 않아 구글링을 통해 문제를 찾아보았더니 원인은 'OneToOne' 관계에 있었다.
OneToOne 연관 관계의 LAZY 로딩 전략은 특정 조건에서만 동작한다고 한다.

특정 조건은 다름 아닌 연관관계의 주인쪽 엔티티에서만 Lazy가 동작한다는 것이다.
나의 경우 연관 관계의 Key를 들고 있는 주인쪽 엔티티는 'MemberAuthMgt' 이고, 이로인해 LAZY 설정이 먹히지 않았다는 것이다.

OneToOne - LAZY의 문제 원인
LAZY로 엔티티를 조회가 가능하도록 하려면 JPA 구현체에서 프록시를 생성해야 한다.
문제는 프록시의 경우 Null을 감쌀 수 없다고 한다.
즉 OneToOne 관계에서 Null이 존재할 수 있는 경우가 있다면 JPA에서는 쿼리 하나를 날려서 조회를 해야하기 때문에 EAGER로 동작한다고 한다.

그렇다면 OneToMany는 왜 동작하는 것일까?
이유는 컬렉션의 경우 값이 비어있다는 표현(isEmpty)가 가능하지만 1:1에서는 Null로 표현되기 때문이라고 한다.

조금 찾아보니 OneToOne에서도 'optional = false'를 통해 Null이 존재하지 않는다는 것을 명시해준다면 Lazy가 가능하다고 하는데 나의 경우 작동하지 않았다 ..

대안으로는 Many연관으로 바꾸거나, 주인 테이블에서 Key를 관리하거나 등 방법이 여럿 있다고 하니 상황에 맞춰 사용해야 할 것 같다.

profile
Java Dev

0개의 댓글