잊지 않겠습니다.

Hibernate에서의 양방향 @OneToOne의 이용

Domain Model을 만들때, @OneToOne은 많은 의미를 갖습니다. Master-Detail 구조에서 Detail을 의미하기도 하고, Query를 만들어서 처리할 때, 많은 데이터를 한번에 읽기보다는 작은 데이터를 먼저 읽어서 표시하는데에 사용하는 등의 어찌보면 역정규화를 이용할 때 주로 사용됩니다. (개인적으로는 하나의 Big Table로 처리하는 것이 개발 상에서는 가장 편할 지도 모른다. 그런데 이렇게 하는 경우, Query를 사용해서 처리할 때 Query양이 많아지고 Table이 너무나 커져버리는 단점이 있습니다.) 또한, @OneToOne Relation은 Table을 이용한 BL의 확장에 큰 영향을 주게 됩니다. 새로운 BL Process가 생성되었을 때, 기존 Table을 변경하지 않는 방향으로 BL을 확장할 수 있기 때문에 현업에서는 자주 사용되는 방법입니다.

그런데, 여기에 문제가 있는 것이, 기본적으로 JPA에서는 @OneToOne은 모두 Early Loading으로만 처리가 가능하다는 점입니다. 이를 Lazy Loading으로 전환하기 위해서는 단방향처리만이 가능하게 처리해야지 됩니다. Parent에서 Key를 갖는 형식으로 말이지요.

그런데, 이와 같은 방법 역시 문제를 가지고 있습니다. 단방향처리만이 가능하다는 것은 기본적으로 처리하는 parent entity에서 FK를 가져야지 된다는 점입니다. Table 추가를 통한 Domain Logic의 확장에 있어서, 이는 기존 Table을 변경해야지 되는 큰 약점을 가지게 됩니다.

따라서, @OneToOne을 양방향으로 사용할 수 있는 방법을 반드시 제시할 수 있어야지 Domain Logic을 생성하는데 ORM을  사용하는 것이 될 수 있을 것입니다. 양방향(bidirection)으로 @OneToOne을 Lazy Loading으로 사용하기 위한 방법은 다음 3가지가 제시됩니다.

@OneToMany, @ManyToOne을 이용하는 방법

객체를 @OneToOne으로 사용하지 않고, Lazy Loading을 사용하기 위해서 다른 Relation으로 정의하고, Code상에서는 @OneToOne 처럼 사용하는 것입니다. 개인적으로 가장 싫어하는 방법입니다. 객체간의 관계를 객체만으로 명확히 보이는 ORM의 장점을 모두 무시하는 방법이라고 생각합니다. 또한 QueryDSL이나 HQL의 작성시에 매우 어렵게 되며, 생성되는 Query 역시 효율성이 떨어지는 Query를 생성하게 됩니다.

byte code instrument를 이용하는 방법

: 이는 CTW(compile time weaver)를 이용해서 처리하는 것입니다. Hibernate에서 제공하는 build time instrument를 이용해서 compile 시에 처리하는 방법입니다. 이에 대한 설명은 다음 Hibernate의 문서를 참고하는 것이 좋습니다. (ant 등 build Task가 별도로 필요합니다. ) -http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html#performance-fetching-lazyproperties
: 이 부분에 대한 설명은 Hibernate 4.x 대에서 제거되었습니다. 4.x에서의 byte code instrument가 지원되는지는 추가 확인이 필요합니다.
: Proxy를 사용하지 않기 때문에 다음과 같은 NoProxy 설정이 필요합니다.

@OneToOne(fetch = FetchType.LAZY, optional = true) @LazyToOne(LazyToOneOption.NO_PROXY) @JoinColumn(name = "customerPetitId", nullable = true) private CustomerPetit customerPetit;
runtime byte code instrument를 이용하는 방법

: 이 방법은 LTW(load time weaver)를 이용해서 처리하는 방법입니다. 이 방법을 사용하기 위해서는 full-blown JEE environment에서 구성되어야지 됩니다. tomcat, jboss, jetty와 같은 JEE 환경에서만 사용이 가능하다는 단점을 갖습니다. (JUnit Test 환경에서는 동작하지 않습니다.)
: hibernate.ejb.use_class_enhancer 설정을 true로 해주는 것으로 설정이 가능합니다. 이 방법 역시 @LazyToOne(LazyToOneOption.NO_PROXY) annotation이 필요합니다.
: Spring 환경에서도 사용이 가능합니다. 단, Lazy Loading시의 performance 이슈는 해결되어야지 됩니다.

위 3가지 방법이 JBoss에서 문서화된 방법들입니다. 이러한 방법 이외에 다른 방법을 하나 더 소개하고자 합니다. 이 방법은 @OneToMany, @ManyToOne 방법과 같이 HQL의 변경을 가지고 오지도 않고, build 시에 새로운 task를 생성할 필요가 없으며, LTW와 같이 성능 저하나 환경을 따르지도 않습니다.

FieldHandler, FieldHandled를 이용한 Bi-Direction @OneToOne

이 방법은 Hibernate의 내부에 이미 구현된 FieldHandler를 사용하는 방법입니다. FieldHandler는 객체에 값을 주입할 때, Hibernate core에서 사용되는 객체입니다. 또한 Hibernate는 FieldHandled interface를 구현한 객체의 경우, Hibernate에서 load될 때, 자동으로 주입시켜주기 때문에 우리가 별도로 개발할 필요는 없습니다.

구현하기 위한 조건은 다음 2가지입니다.

  1. @LazyToOne(LazyToOneOption.NO_PROXY) annotation
  2. Entity 객체에 FieldHandled interface 구현

구현되는 코드는 다음과 같습니다. (양방향이기 때문에 Parent, Child Entity에 모두 구현해야지 됩니다.)

public class OperationResult extends AbstractInsertUpdateEntity implements Serializable, FieldHandled {
    @OneToOne(fetch = FetchType.LAZY, optional = true)
    @LazyToOne(LazyToOneOption.NO_PROXY)
    @JoinColumn(name = "operationId", nullable = true)
    private OperationPlan plan;

    public void setPlan(OperationPlan plan) {
        if(fieldHandler != null) {
            this.plan = (OperationPlan) fieldHandler.writeObject(this, "plan", this.plan, plan);
        } else {
            this.plan = plan;
        }
    }

    public OperationPlan getPlan() {
        if(fieldHandler != null) {
            this.plan = (OperationPlan) fieldHandler.readObject(this, "plan", plan);
        }
        return this.plan;
    }

    private FieldHandler fieldHandler;

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

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

이 구현자체는 byte instrument에서 compile/runtime 시에 동작하는 원리와 완전히 동일하게 동작합니다. byte code instrument를 이용한 compile/runtime 시의 동작이 위의 get/set method에 대한 AOP 동작을 주입하는 것이기 때문입니다.

개인적으로는 저는 이 방법을 추천합니다. 이 방법의 경우 다음과 같은 장점이 있습니다.

  1. @OneToMany, @ManyToOne과 같은 HQL을 변경시키는 Relation을 쓰지 않아도 됩니다.
  2. LTW와 같은 성능저하가 작습니다.
  3. CTW와 같이 build시에 따로 처리할 필요가 없습니다.

reference


Posted by Y2K
,