오늘이 시간에는 JPA OneToOne 관계에서 lazy 로딩 구현을 해보자. 일반적으로 JPA에서 OneToOne 관계는 Lazy로딩이 잘 동작하지 않는다.
물론 동작하게 만들 수는 있다. 여러 조건을 만족해야 하며 테이블 구조도 조금 달라 질 수 있다. 또한 OneToOne을 OneToMany로 바꾸어서 사용하는 방법도 존재한다. 위와 같이 여러 방법이 있겠지만 오늘 우리는 하이버네이트의 API를 이용해 OneToOne관계를 Lazy로딩이 가능하게 하도록 해보자.

JPA의 구현체 중 하이버네이트는 OneToOne 관계에서도 Lazy로딩을 할 수 있다. 다른 여러 구현체들은 잘 쓰지 않아 모르기에 생략하도록 하자.
일단 아래와 같은 엔티티가 있다고 가정하자.

@Entity
public class Content {

  @Id
  @GeneratedValue
  private Long id;

  private String title;

  @OneToOne(mappedBy = "content", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
  private ContentDetail contentDetail;

  //etc 생략
}

위는 Content란 엔티티로 부모측에 해당하는 엔티티이고, 아래는 자식측에 해당하는 ContentDetail라는 엔티티가 있다고 가정해보자.

@Entity
public class ContentDetail {

  @Id
  @GeneratedValue
  private Long id;

  private String text;

  @OneToOne(fetch = FetchType.LAZY)
  private Content content;

  //etc 생략
}

ContentContentDetail는 양방향 관계이며 둘다 모두 Lazy 로딩을 할 수 있도록 설정 해놨다.
일단 설정을 끝났으니 테스트를 해보자.

@RunWith(SpringRunner.class)
@DataJpaTest
public class ContentTest {

  @Autowired
  private TestEntityManager entityManager;

  @Autowired
  private ContentRepository repository;

  @Test
  public void oneToOneTest() {
    Content content = new Content("title", new ContentDetail("content text"));
    final Content persist = entityManager.persist(content);
    entityManager.detach(persist);
    repository.findOne(persist.getId());
  }
}

Test는 다음과 같다. 먼저 content를 넣고 detach를 사용해 object을 준영속으로 상태로 만든다. 그리고 나서 해당 id로 조회를 해보자.

select
  content0_.id as id1_1_0_,
  content0_.title as title2_1_0_
from
  content content0_
where
  content0_.id=?

select
  contentdet0_.id as id1_2_0_,
  contentdet0_.content_id as content_3_2_0_,
  contentdet0_.text as text2_2_0_
from
  content_detail contentdet0_
where
  contentdet0_.content_id=?

그럼 역시나 두 번 퀴리를 날린다. 그럼 이제 하이버네이트의 LazyToOne을 사용해서 Lazy 로딩을 할 수 있게 만들어보자.
위의 Content엔티티를 조금 수정해야 된다.

@Entity
public class Content implements FieldHandled {

  @Id
  @GeneratedValue
  private Long id;

  private String title;

  @OneToOne(mappedBy = "content", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
  @LazyToOne(LazyToOneOption.NO_PROXY)
  private ContentDetail contentDetail;

  private FieldHandler fieldHandler;

  public ContentDetail getContentDetail() {
    if (fieldHandler != null) {
      return (ContentDetail) fieldHandler.readObject(this, "contentDetail", this.contentDetail);
    }
    return contentDetail;
  }

  public void setContentDetail(ContentDetail contentDetail) {
    if (fieldHandler != null) {
      this.contentDetail = (ContentDetail) fieldHandler.writeObject(this, "contentDetail", this.contentDetail, contentDetail);
    } else {
      this.contentDetail = contentDetail;
    }
    if (this.contentDetail != null) {
      this.contentDetail.setContent(this);
    }
  }

  @Override
  public void setFieldHandler(FieldHandler handler) {
    this.fieldHandler = handler;
  }

  @Override
  public FieldHandler getFieldHandler() {
    return fieldHandler;
  }

  //etc 생략
}

하이버네이트에서 지원해주는 @LazyToOne 어노테이션을 이용하면 된다. 그리고나서 FieldHandled 상속받아 구현만 해주면 된다. 구현할 것은 별로 없다. setFieldHandlergetFieldHandler만 구현해주면 된다. 그리고 나서 OneToOne에 해당하는 필드의 getter, setter를 위와 같이 구현해주면 된다. FieldHandledjavassist를 이용해서 바이트 코트를 조작한다고 한다. 관심있으면 소스를 까보면 될 것 같다. 그리고 나서 아까 만든 테스트를 다시 돌려보자.

select
  content0_.id as id1_1_0_,
  content0_.title as title2_1_0_
from
  content content0_
where
  content0_.id=?

드디어 쿼리가 한번만 날라갔다. 우리가 원하는 OneToOne 관계에서 Lazy로딩이 먹혔다. Lazy가 잘 동작하는지 테스트도 해보자

@Test
public void oneToOneTest() {
  Content content = new Content("title", new ContentDetail("content text"));
  final Content persist = entityManager.persist(content);
  entityManager.detach(persist);
  final Content result = repository.findOne(persist.getId());
  final ContentDetail contentDetail = result.getContentDetail();
}

음 잘동작한다. getContentDetail() 메서드를 호출할 때 쿼리가 잘 날라간다. 이렇게 하이버네이트의 @LazyToOne 이용해서 OneToOne 관계의 Lazy 로딩을 구현할 수 있다.

하지만 만약 이렇게 되면 어떻게 될까? 만약 Content 엔티티에 OneToOne 관계가 한개 더 있다고 가정해보자.

//생략

@OneToOne(mappedBy = "content", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@LazyToOne(LazyToOneOption.NO_PROXY)
private ContentSetting contentSetting;

//구현도 생략 

구현은 아까 위와 동일하게 구현해주면 되기에 생략하겠다. 그리고 나서 두 번째 테스트 했던 케이스를 돌려보자.

select
  content0_.id as id1_1_0_,
  content0_.title as title2_1_0_
from
  content content0_
where
  content0_.id=?

select
  contentdet0_.id as id1_2_0_,
  contentdet0_.content_id as content_3_2_0_,
  contentdet0_.text as text2_2_0_
from
  content_detail contentdet0_
where
  contentdet0_.content_id=?

select
  contentset0_.id as id1_3_0_,
  contentset0_.content_id as content_3_3_0_,
  contentset0_.setting as setting2_3_0_
from
  content_setting contentset0_
where
  contentset0_.content_id=?

그럼 이상하게 위와 같이 쿼리가 한번 더 나간다. 분명 테스트에는 contentDetail만 가져오는 코드만 존재하는데 다른 OneToOne 관계의 데이터도 가져온다. 필자가 잘못한건지 아니면 원래 그런거지는 잘 모르겠다. 아무튼 Lazy 로딩이라도 OneToOne 관계가 여러개 있을 때는 호출 여부와 상관 없이 특정 OneToOne을 호출하였을 때는 다른 OneToOne 관계의 데이터를 모두 가져오는 듯 하다. 이게 정확한지는 한번 테스트를 해보길 바란다.

어쨋든 우리는 JPA의 OneToOne관계를 하이버네이트를 API를 사용하여 Lazy 로딩을 구현해봤다. 일단 이 정도도 만족한다. 굳이 단점이 있다면 하이버네이트에 종속적이라는 것이다. 필자의 경우는 거의 대부분 하이버네이트를 이용하니 단점까지는 아닌 듯하다.

오늘은 이렇게 hibernate의 API를 이용하여 OneToOne 관계의 Lazy 로딩을 가능케 해봤다.