오늘이 시간에는 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 로딩을 가능케 해봤다.