JPA를 다루다보면 까다로운게 한두군데가 아닌거 같다. 아직 JPA를 손쉽게 다루지 못해서 그러지만.. 하면 할 수록 어렵단 말이야..

그 중에서 오늘은 JPA에서 OneToOne 관계에 대해서 알아보려한다. JPA의 OneToOne 관계는 정말 까다롭다. OneToOne 관계로 설계할 때는 심히 많은 고민을 해보길 바란다. 정말로 OneToOne 관계를 해야 되나…말이다.

첫 번째 방법

아래의 코드는 OneToOne(양방향)의 첫 번째 방법이다.

@Entity
public class Content {

  @Id
  @GeneratedValue
  private Long id;

  private String title;

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

  //기타 생략
}
@Entity
public class ContentDetail {

  @Id
  @GeneratedValue
  private Long id;

  private String text;

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

  //기타 생략
}

우리는 대략 이런 모델링을 했다고 가정하자. Content 엔티티가 부모측이고 ContentDetail가 자식측 이다. 이렇게 작성을 했을 때 스키마는 어떻게 작성 되나 살펴보자.

create table content (
  id bigint generated by default as identity,
  title varchar(255),
  primary key (id)
)

create table content_detail (
  id bigint generated by default as identity,
  text varchar(255),
  content_id bigint,
  primary key (id)
)

alter table content_detail
  add constraint FKkxm1ajimhk153isl1cnun1r4i
foreign key (content_id)
references content

대충 위와 같이 스키마가 나왔다. content에 ContentDetail은 연관관계의 주인이 아니기에 mappedBy로 설정하였다.
그리고 아래와 같이 저장을 해보자.

@Transactional
public void save(){
  ContentDetail contentDetail = new ContentDetail("content");
  Content content = new Content("title", contentDetail);
  contentDetail.setContents(content);
  final Content save = contentRepository.save(content);
  //생략
}

잘들어 간다. content 테이블에 먼저 insert 후에 content_detail 테이블에도 insert를 실행한다. 그러고 나서 해당하는 seq로 조회를 해보자.

public Content findOne(Long id) {
  final Content one = contentRepository.findOne(id);
  //생략
}

그리고나서 한번 실행된 쿼리문을 살펴보자.

select
  content0_.id as id1_0_0_,
  content0_.title as title2_0_0_
from
  content content0_
where
  content0_.id=?

select
  contentdet0_.id as id1_1_0_,
  contentdet0_.content_id as content_3_1_0_,
  contentdet0_.text as text2_1_0_
from
  content_detail contentdet0_
where
  contentdet0_.content_id=?

읭? 뭔가 이상하다. 쿼리가 두번이나 날라갔다. 분명 content에 있는 ContentDetail은 Lazy로딩인데 Lazy로딩이 먹히지 않았다.
왜일까? 책에서는 프록시 때문에 그런다고 했는데 자세히 안나온거 같아서 (필자가 자세히 못봤나?) 찾아봤다.
http://kwonnam.pe.kr/wiki/java/jpa/one-to-one
권남님이 자세히 설명해놨으니 읽어 보면 좋겠다. 간단히 말하자면 null값이 가능한 OneToOne의 경우 프록시 객체로 감쌀 수 없기 때문이다. 만약 null 값에 프록시 객체를 넣는다면 이미 그것은 null이 아닌 프록시 객체를 리턴하는 상태가 되버리기 때문이라고 설명하였다.

두 번째 방법

두 번째 방법은 @PrimaryKeyJoinColumn 를 사용해서 OneToOne을 설정 해보자.


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

나머지는 생략하고 위와 같이 변경을 해보자. 부모측의 키를 자식측까지 공유하게 설정했다. 생성된 스키마는 다음과 같다.

create table content (
  id bigint not null auto_increment,
  title varchar(255),
  primary key (id)
)

create table content_detail (
  id bigint not null auto_increment,
  text varchar(255),
  primary key (id)
)

다시 한번 테스트를 해보자.

select
  content0_.id as id1_0_0_,
  content0_.title as title2_0_0_
from
  content content0_
where
  content0_.id=?

select
  contentdet0_.id as id1_1_0_,
  contentdet0_.text as text2_1_0_
from
  content_detail contentdet0_
where
  contentdet0_.id=?

그래도 결과는 똑같다. 이 또한 프록시 객체를 리턴하지 못해 즉시로딩을 실시한다.

세 번째 방법

이번에는 @MapsId 를 이용한 방법이다.

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


//ContentDetail.class
@OneToOne(fetch = FetchType.LAZY)
@MapsId
private Content content;

위와 같이 자식측에 @MapsId 를 선언해 주면 된다. 생성된 스키마를 살펴보자.

create table content (
  id bigint not null auto_increment,
  title varchar(255),
  primary key (id)
)

create table content_detail (
  text varchar(255),
  content_id bigint not null,
  primary key (content_id)
)

alter table content_detail
  add constraint FKkxm1ajimhk153isl1cnun1r4i
foreign key (content_id)
references content (id)

자식측에 있는 content_id는 주키이면서 동시에 외래키까지 갖고 있다. 이거 역시 즉시로딩이 실시 된다.

select
  content0_.id as id1_0_0_,
  content0_.title as title2_0_0_
from
  content content0_
where
  content0_.id=?

select
  contentdet0_.content_id as content_2_1_0_,
  contentdet0_.text as text1_1_0_
from
  content_detail contentdet0_
where
  contentdet0_.content_id=?

어쨋든 OneToOne 에서는 지연로딩이 잘 동작 하지 않는다. 물론 동작하게 만들 수는 있다. 여러 조건들이 붙는거 같은데 굳이 해보진 않았다.
조건은 이렇다. null을 허용하지 않아야 하며, PrimaryKeyJoin이 아니여야 하고, 단방향 관계가 되어야 한다.

아무튼 OneToOne 관계를 까다롭다. 대부분의 사람들이 OneToOne의 관계를 설계할 때는 심히 고민을 많이하고 되도록이면 피하는게 좋다고 한다. OneToOne은 @SecondaryTable이나 @Embeddable 등으로 대체 가능 할 수 있으니 그 와 같은 방법을 고려 해보는 것도 나쁘지 않은 생각이다.

오늘은 이렇게 까따로운 OneToOne 관계를 살펴봤다. JPA도 끝이 없구나..