[하이버네이트] OneToMany에 FetchType.EAGER 사용시 어떤 일이 생길까?

Plan -> PlanDetail 관계에서 Plan 쪽에서 PlanDetail로 OneToMany 관계를 설정하고, fetch 모드를 EAGER로 설정하면,, 엄청난 문제가 생길 수 있습니다. @_@

P 1 <– PD 1
P 1 <– PD 2
P 2 <– PD 3
P 3

이렇게 PD 두 개가 같은 P에 속해있을 경우, P 목록을 뿌리고자, createQuery(“from P”).list(); HQL로 이렇게 작성하면.. 쿼리는 다음과 같이 날아가게 됩니다.

(양방향 관계에서 mappedby 설정했다고 가정하면..)
~~ from P p left outer join PD pd on p.id = pd.p_id ~~

(양방향 관계에서 mappedby를 설정하지 않았다고 가정하면.. 이건 거의 최악)
~~ from P p left outer join P_PD p_pd on p.id = p_pd.p_id ~~

LEFT OUTER JOIN 인거죠…ㄷㄷㄷ..

결과는 아래와 같은 모습일 겁니다.

P 1 – PD 1
P 1 – PD 2
P 2 – PD 3
P 3 – null

그래서 DB에서는 레코드가 한 줄인데, 화면에는 두 줄이 나타납니다. 크하하하..  그런데.. 이게 .. 이상한 일일까요? 글쎄요. 그런 것 같진 않습니다. P가 가지고 있는 컬렉션을 EAGER 패치로 가져오란 얘기가 곧 DB 관점에서는 P를 왼쪽에 두고 LEFT OUTER JOIN해서 P와 연관 맺고 있는 PD도 가져오란 얘기가 될 테니.. 하이버는 그저 시킨대로 한 죄 밖에는 없습니다. 결국 자연스러운 일입니다.

그렇다면 애초에 원했던 결과는 무엇이었을까요? 바로 P 목록만 가져오는 것이었습니다. 그러려면 P를 가져올 때 PD는 내비두고 오로지 P만 가져오게 해야겠죠. 어차피 P 목록을 보려고 하는데 PD 까지 가져올 필요는 없자나요. 패치모드를 LAZY로 바꾸면 from Plan 같은 HQL을 보내면 아예 join을 하지 않습니다.

~~ from P ~~

아마도 이런 SQL을 보시게 될 겁니다.

논외로  하이버 HQL, Criteria로 발생하는 SQL 쿼리를 이해하는 개발자가 되는 길은 멀고도 험한듯 합니다.

예를 들어 이번 이슈(P->PD)에서 패치모드, 방향성, mappedby의 변화로 생기는 쿼리 형태를 조사하려면 몇 가지 경우의 수를 고려해야 할까요?

– 방향성: 총 2가지(P->PD, P<->PD)
– 패치모드: 총 2가지(P->PD Lazy, P->PD Eager)
– mappedby: 총 2가지(P의 pd에 붙인 @OneToMany에서 mappedby=”p”)

정답은 그렇다면 2 * 2 * 2 = 8 가지? 글쎄요.

몇 번 해보시면 MappedBy 설정은 거의 영향이 없다는 걸 아실 수 있습니다. mappedby를 하면 좋은 점은 연관 테이블 수를 줄일 수 있다는 것. 하지만 결과에 영향이 없는 이유는 연관 테이블(P_PD)과 PD의 row 수가 같기 때문이죠. P가 PD와 left outer join을 하나, P가 P_PD와 left outer join을 하나 결과는 같으니까요.

따라서 2 * 2 = 4 가지 일까요? 그런데 만약 전제로 했던 P -> PD로의 방향성이 PD -> P 방향성 이라면?? ManyToOne이 되는데, 이때는 어떤 변화가 있을까요? @OneToMany의 fetch 속성 기본값은 LAZY 입니다. 별다른 설정을 하지 않으면 위와 같이 원하지 않았던 결과는 발생하지 않겠죠. 하지만 @ManyToOne의 fetch 속성 기본값은 EAGER입니다. 어떻게 될까요? 무슨 일이라도 생길까요? 앞선 경우처럼 DB에 들어있는 PD의 갯수보다 더 많은 PD의 갯수가 출력될까요?

그렇진 않습니다. ManyToOne 관계니까 그럴리는 없습니다.

PD 1 <- P 1
PD 2 <- P 1
PD 3 <- P 2
            P 3

PD를 왼쪽에 두고 left outer join을 해봤자. 이런 관계라고 할 때 결과는 다음과 같겠죠.

PD 1 – P 1
PD 2 – P 1
PD 3 – P 2

결과 row과 PD row와 동일한 상태가 됩니다. 따라서 ManyToOne에서도 fetch 모드를 별도로 설정하지 않더라도 HQL로 from PD를 날리면 예상하던(?) 결과를 얻을 수 있습니다. ㅎㅎ 재밌지요.

하이버네이트 API : Persistence context 관리하기

Persistence Context 캐시 제어하기

  • Persistent 객체들의 스냅샷들을 캐시에 복사해둔다.
  • 이 캐시들을 사용하여 dirty checking을 하여 persistent 객체들의 변경 사항들을 찾아낸다.
  • 수천개의 객체들을 로딩하면, OutOfMemoryException을 내고 죽어버릴 수가 있다.
  • 캐시를 줄이거나 메모리 공간을 제약하려면 다음과 같이 해야 한다.
    1. 필요한 객체만 Persistent 상태로 유지하라. 전체 객체 그래프를 모두 끌어오다가는…
    2. session.evict(object)를 사용해서 persistent 상태의 객체를 명시적으로 detached 시킬 수 있다.
    3. session.clear()를 사용해서 Persistent Context에 있는 모든 객체를 detached로 만들 수 있다. 이 녀석들은 dirty checking하지 않는다.
    4. session.setReadOnly(object, true)를 사용해서 특정 객체에 대해서는 dirty checking을 하지 않도록 설정할 수 있다. false를 주면 다시 한다. 객체의 상태는 바꾸지 않는다.

Persistent Context flushing 하기

  • 하이버는 Persistent 객체들에 대한 변경 사항을 바로바로 DB에 반영하지 않는다.
    1. DB 요청을 최소화 할 수 있다.
    2. DB 롹킹 기간을 최소화 한다.
    3. JDBC batch API의 장점을 취할 수 있다.
  • flush(): DB와 Persistent Context의 동기화
    1. Transaction(하이버의 API)이 commit() 될 때.
    2. 쿼리를 실행하기 전에..
    3. session.flush()를 호출 할 때.
  • session.setFlushMode()를 사용할 수 있다. 기본값은 FlushMode.AUTO.
  • FlushMode.COMMIT으로 설정하면
    1. 쿼리를 실행하기 전에.. 동기화를 하지 않는다.
    2. 오직 Transaction.commit()과 session.flush()를 할때 만 동기화한다.
    3. 쿼리 -> 수정 -> 쿼리 -> 수정. 이런 경우에 FlushMode를 Commit으로 설정해주면 효율적이다.
  • FlushMode.MONUAL로 설정하면
    1. 오직 session.flush()를 호출 할 때만 동기화한다.
  • FlushMode 제어는 나중에 Persistent Context를 Conversation으로 확장할 때 사용할 것이다.
  • 중요한 것은 flush 처리의 성능은 persistent context의 크기에 따라 달라진다는 것이다.