카테고리 없음

hibernate OneToOne lazy 구현하기

머룽 2023. 4. 22. 14:39
오늘이 시간에는 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 생략
}
Content와 ContentDetail는 양방향 관계이며 둘다 모두 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 상속받아 구현만 해주면 된다. 구현할 것은 별로 없다. setFieldHandler 와 getFieldHandler만 구현해주면 된다. 그리고 나서 OneToOne에 해당하는 필드의 getter, setter를 위와 같이 구현해주면 된다. FieldHandled는 javassist를 이용해서 바이트 코트를 조작한다고 한다. 관심있으면 소스를 까보면 될 것 같다. 그리고 나서 아까 만든 테스트를 다시 돌려보자.
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 로딩을 가능케 해봤다.