서론

JPA를 사용하면서 일대일 연관관계를 맺었는데

예상치 못한 쿼리를 만나게 되었고 원인을 분석하면서 알게된 내용을 공유해보려고 한다.

 

One-to-One 관계에서는 Lazy로딩은 특정 조건에서만 동작한다.

 

그렇다면 언제 Lazy 로딩이 잘 동작하고,

언제는 동작하지 않으며 그 이유가 무엇인지 알아보자.


특정 조건이라는 게 그럼 무엇일까?
결론부터 말하자면 연관관계 주인쪽 엔티티 측에서는 Lazy 로딩이 정상적으로 동작한다.

 

다음과 같은 일대일 양방향 연관관계가 있다고 해보자

 

@Entity
public class Post {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String title;

  @OneToOne(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
  private PostDetails details;

  ..
  // getter, 생성자 및 기타 생략
}

 

@Entity
public class PostDetails {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String content;

  @OneToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "post_id")
  private Post post;

  ..
  // getter, 생성자 및 기타 생략
}

 

연관관계 주인 엔티티 조회

  @Test
  void select_query() {
    Post post = new Post("title");
    PostDetails postDetails = new PostDetails("content");
    post.updateDetails(postDetails);

    em.persist(post);

    em.flush();
    em.clear();

    PostDetails findPostDetail = em.find(PostDetails.class, postDetails.getId());
  }

 

쿼리문을 확인해보자.

만일 Lazy로딩이 정상적으로 동작했으면 PostDetail을 조회하는 쿼리문 하나만 발생했을 것이다.

Lazy 로딩이 정상적으로 작동해서 select 쿼리가 한 개만 발생한 걸 확인할 수 있다.

 

 

그렇다면 이번엔 연관관계의 주인 반대편인 Post를 조회하면 어떻게 될까?

다른 코드는 모두 동일하고 PostsDetail -> Post로 변경했다.

@Test
void select_query(){
  ..
  // 중략
  
  Post findPost = em.find(Post.class, post.getId());
}

 

쿼리문을 확인해보자.

Post를 조회했더니 아까와는 다르게 Eager 로딩이 되어서 select 쿼리문이 두 번 발생했다.

왜 이런일이 발생한 것일까?

 

원인을 파악해보자.

Thorben Janssen는 다음과 같이 말했다. (원문)

That’s because Hibernate needs to know if it shall initialize the manuscript attribute with null or a proxy class.
It can only find that out, by querying the manuscript table to find a record that references this Book entity.
The Hibernate team decided that if they have to query the manuscript table anyways, it’s best to fetch the associated entity eagerly.

 

해석해보면 이렇다.

자 Post 객체에서 postsDetail은 null 혹은 proxy로 와야한다. (컬렉션이 아니기 때문에 값이 없을 경우 null이 와야 한다)

그런데, DB관점에서 생각해보자 post 테이블에는 postdetail_id 컬럼이 존재하지 않는다.

 

그럼 Post엔티티를 불러올 때 어떻게 될까? postdetail_id 컬럼이 없으니 postdetail 테이블을 조회해서 확인해야 한다.

그렇다. 이미 DB에 쿼리를 날려서 조회를 해야하기 때문에 Lazy로딩이 의미가 없어지는 것이다.  그래서 eager 로딩이 동작하는 것이다.


그렇다면 OneToMany는 Lazy로딩 으로 동작하는 이유는?

OneToMany에서는 Lazy로딩이 기본 설정이다.  일대다 관계에서는 일 측에서는 컬럼을 갖고있지 않는다. 그렇다면 일대일과 같이 값을 모르는데,  어떻게 Lazy로딩이 동작할 수 있을까?

 

이유는 아주 간단하다. 컬렉션은 값이 비어있다고 표현이 가능하지만(isEmpty), 1:1관계에서는 값이 없다면 null로 표현을 해야하기 때문이다. 즉 그렇기 때문에 컬렉션은 proxy로 가져올 수 있기 때문에 Lazy 로딩을 사용할 수 있다.

 

그렇다면 해결 방법은 무엇일까?

크게 두 가지로 살펴볼 수 있습니다.

1. shared PK

2. 주인테이블에서 대상 테이블 id 관리하기. (즉 여기서는 Post에서 PostDetail의 id를 관리하는 방식)

 

2번 경우 대상 테이블에서 조회할 시 Lazy로딩이 동작하지 않기 때문에 fetch join을 활용해야 하며, 주인 테이블이 무거워지는 단점이 있다. 뭐가 더 좋을지는 상황에 따라 트레이드 오프를 고려해서 고르면 될 거 같다.

 

관련해서 좋은 의견이 있으면 댓글 남겨주시면 감사하겠습니다.

 

Reference

https://thorben-janssen.com/hibernate-tip-lazy-loading-one-to-one/