JPA를 공부하다가 1:1 관계에서는 FetchType.LAZY 를 사용해도 지연 로딩이 되지 않고 바로 객체를 불러온다고 한다. 책에 있는 내용을 통해 해당 링크를 찾아봤지만 어떠한 원리로 일어나고 해결방법은 어떤 것이 있는지 알고 있어야 실무에 실제로 사용할 때 주의할 수 있을 것 같다.
발생 원인
class A {
private Set bees;
public Set getBees() {
return bees;
}
public void setBees(Set bees) {
this.bees = bees;
}
}
class B {
// Not important really
}
보통 하이버네이트에서 프록시객체를 생성할 때 위와 같은 클래스 구조가 있고 하이버네이트가 Class A를 호출하게 되면 일단 초기화 되지 않은 Set wrapper로 감싸고 해당 a 객체에 "bess" a.setBess(wrapper) 형태로 있다가 a.getBess()로 호출하게 되면 이객체를 반환 합니다. 이 객체는 null이 아니라 데이터베이스에서 아직 로드되지 않은 형태입니다. a.getBess().size() 같은 의미있는 메소드가 호출 될 때 데이터베이스에서 해당 객체를 로딩하고 초기화 합니다.
이제 일대일 관계로 매핑된 객체를 보면
class B {
private C cee;
public C getCee() {
return cee;
}
public void setCee(C cee) {
this.cee = cee;
}
}
class C {
// Not important really
}
우리는 B class로 생성 된 객체를 로드한 후에 getCee()를 호출한다면 하이버네이트는 바로 Cee라는 객체를 바로 리턴해야합니다. 래퍼로 따로 감쌀 수 없는 형태이기 때문에 cee를 호출한 순간 리턴할 수 있도록 적절한 값을 넣어야 합니다.
class B {
@OneToOne(fetch=FetchType.LAZY)
private C cee;
public C getCee() {
return cee;
}
public void setCee(C cee) {
this.cee = cee;
}
}
그래서 하이버네이트는 이처럼 LAZY를 설정하여 지연로딩을 설정한다면 프록시 객체를 통해 사용할 수 있습니다. 위에 Set처럼 래퍼로 감싸고 get이나 set같은 메소드를 호출했을 때 데이터를 불러오는 방식으로 호출할 수 있다. 이렇게 사용했을 때 문제는 외래키를 가지고 있지 않은 테이블과 매핑된 Entity에서 호출했을 때 문제가 발생한다.
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne(mappedB="member")
private Locker locker
}
public class Locker {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne
@JoinColumn("MEMBER_ID")
private Member member;
}
이와같은 구조로 Locker클래스가 연관관계 주인인 형태로 Member.locker라는 객체를 get을 통해 객체를 호출 했을 때 어떤 FK에 매핑된 데이터인지 알 수 없습니다. 결국 SELECT 문을 통해 모든 데이터를 확인해봐야하는데 이것은 너무 비효율적으로 동작하기 때문에 하이버네이트는 LAZY가 아닌 EAGER형태로 동작하게 되는 것 입니다.
해결 방법
@ManyToOne 관계로 풀어낸다.
아래와 같이 풀어냈을 경우는 lokers에 직접 주입해서 사용해야 되기 때문에 즉시로딩으로 생길 수 있는 문제점을 해결할 수 있다.
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany
private List<Locker> lockers
}
public class Locker {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn("MEMBER_ID")
private Member member;
}
@OneToOne옵션을 optional=false로 설정한다.
연관 관계가 선택 사항인 경우 Hibernate는 쿼리를 실행시키지 않고는 해당 객체에 대한 주소가 존재하는지 알 수 있는 방법이 없습니다. 따라서 객체 참조하는 주소가 없을 수 있고 있을 수 있기 때문에 주소 필드를 프록시로 채울 수 없습니다.
하지만 null을 연결을 필수(예: optional=false)로 만들면 연결이 필수이므로 객체를 신뢰하고 주소가 존재한다고 가정합니다. 따라서 사람을 참조하는 주소가 있다는 것을 알고 주소 필드를 프록시로 직접 채웁니다. 사실상 EAGER와 같은 형태로 동작해야한다.
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne(optional=false)
private Locker lockers
}
public class Locker {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne
@JoinColumn("MEMBER_ID")
private Member member;
}
@MapsId로 FK를 PK로 설정한다.
MapsId어노테이션은 FK를 PK로 설정하게 도와주는 어노테이션이다. LOCKER 테이블의 PK를 MEMBER.ID로 사용하게 되면서 연결된 FK값을 알고 있기 때문에 문제를 해결할 수 있다. 단점은 1 : N 구조로 변경되었을 때 데이터베이스를 마이그레이션하는 작업을 해야되기 때문에 신중하게 설계를 해야한다.
public class Member {
@Id
private Long id;
private String name;
@OneToOne
private Locker locker
}
public class Locker {
@Id
private Long id;
private String name;
@MapId
@OneToOne
private Member member;
}
참조
https://stackoverflow.com/questions/17987638/hibernate-one-to-one-lazy-loading-optional-false
https://developer.jboss.org/docs/DOC-13960
https://thorben-janssen.com/hibernate-tip-lazy-loading-one-to-one/