[번역] Batch Fetching - 객체 그래프 로딩을 최적화 하기 (JPA/ECLIPSELINK) - 1 기술

자주 들여다 보는 문서라 아예 번역해 봤습니다. 

(이글루스가 긴글을 지원 못해 잘랐습니다)


출처: http://java-persistence-performance.blogspot.kr/search/label/batch-fetch

원문 2010.9.9 에 작성됨 

 

객체 지향적인 객체 모델과, 관계 DB 관계 모델간의 가장 큰 임피던스(impedance) 차이를 보이는 부분은 데이터에  접근하는 방식이다. 

 

관계 모델에서는, 특정 유스케이스나 서비스 요청에 적합한 데이터를 모든 데이터를 조인 걸어서 가져 오는 단일 한방 쿼리를 작성한다. 가져와야 하는 데이터가 복잡할수록 쿼리도 복잡해진다. 최적화는 불필요한 조인을 제거하고, 중복 데이터를 가져오는 것을 제거하는 것으로 수행된다. 

 

객체 모델에서는 객체 하나 또는 몇개의 세트를 얻어 온후, 객체의 관계를 추적(traverse)하여 데이터를 수집한다. 

 

객체-관계-매퍼(ORM/JPA)를 사용하면 일반적으로 여러번의 쿼리가 실행되게 되며, 이것은 "N queries"라고 알려진 문제로 발전하게 된다. 또는 결과 세트(Result set)안에 포함된 객체 마다 별개의 쿼리를 실행하게 된다. 

 

예제로써 EMPLOYEE, ADDRESS, PHONE 테이블을 가지고 생각해자. 

EMPLOYEE는 ADDRESS ADDRESS_ID로 향하는 외부키 ADDR_ID를 가진다. PHONE은 EMPLOYEE EMP_ID로 향하는 외부키 EMP_ID를 가진다. 

 

파트타임 직원 목록을 주소와 전화번호를 포함하여 표시하고자 할때 다음과 같은 SQL을 작성할것이다. 

 

Big database query(한방쿼리)

SELECT E.*, A.*, P.* FROM EMPLOYEE E, ADDRESS A, PHONE P WHERE E.ADDR_ID = A.ADDRESS_ID AND E.EMP_ID = P.OWNER_ID AND E.STATUS = 'Part-time'

 

이렇게 나온 데이터는 각 직원 마다 전화 번호를 출력하깅 위해 그루핑한후 포메팅해야 한다. 

 

...

ID

Name

Address

Phone

6578

Bob Jones

17 Mountainview Dr., Ottawa

519-456-1111, 613-798-2222

7890

Jill Betty

606 Hurdman Ave., Ottawa

613-711-4566, 613-223-5678

 

 

위 예에 대응하는 객체 모델은 Employy, Address, Phone 클래스를 정의한다. 

JPA에서 Employee는 Address와 OneToOne 관계이고, Phone과 OneToMany 관계이다. Phone은 Employee와 ManyToOne 관계이다.

 

 

파트타임 직원 목록을 주소와 전화번호를 포함하여 표시하고자 할때 다음과 같은 JPQL을 작성할것이다.  

 

Simple JPQL

Select e from Employee e where e.status = 'Part-time'

 

역자 주서: 

Criteria API로 작성하면  다음과 같이 된다. 

 

 

직원 정보를  출력하기 위해서는  Employee에서 Address를 가져오고, 마찬가지로 모든 Phone을 가져오면된다.  출력된 결과는 SQL 버전과 동일하지만, JPA를 통해 생성된  SQL은 매우 다르다. 

 

...

ID

Name

Address

Phone

6578

Bob Jones

17 Mountainview Dr., Ottawa

519-456-1111, 613-798-2222

7890

Jill Betty

606 Hurdman Ave., Ottawa

613-711-4566, 613-223-5678

 

 

N+1 queries problem

SELECT E.* FROM EMPLOYEE E WHERE E.STATUS = 'Part-time'

...  실행된 다음 N개(선택된 employee의 수)만큼의  Address를 select한다. 

SELECT A.* FROM ADDRESS A WHERE A.ADDRESS_ID = 123
SELECT A.* FROM ADDRESS A WHERE A.ADDRESS_ID = 456 

 

...

 

...  실행된 다음 N개(선택된 employee의 수)만큼의  Phone를 select한다. 

SELECT P.* FROM PHONE P WHERE P.OWNER_ID = 789
SELECT P.* FROM PHONE P WHERE P.OWNER_ID = 135 

...

 

이것은 상당히 끔찍한 성능을 보여줄 것이다.(모든 객체가 메모리에 이미 캐쉬 되어 있지 않는한). JPA에서 이 문제를 해결하는 방법이 여럿 있다. 가장 흔하게 사용되는 방식은 Join fetching 이다. join fetch는 객체,  그 객체와 연관된 객체를 단일 쿼리를 통해 fetch하는 것이다.  JPQL에서 이것은 꽤나 쉽게 할수 있고, Join을 정의 하는 것과 유사하다. 

 

JPQL with join fetch

Select e from Employee e join fetch e.address, join fetch e.phones where e.status = 'Part-time' 


 

 

역자 주석

Criteria API 버전

 

 

이 JPQL은  SQL 버전과 동일한 SQL을 생성해 낸다. 

 

SQL for JPQL with join fetch

SELECT E.*, A.*, P.* FROM EMPLOYEE E, ADDRESS A, PHONE P WHERE E.ADDR_ID = A.ADDRESS_ID AND E.EMP_ID = P.OWNER_ID AND E.STATUS = 'Part-time' 

 

이 코드는 동일한 결과를 보여 줄수 있으면서, 객체는 더욱 효율적으로 읽어오게 된다. 

 

JPA는 join fetch를 JPQL을 통해서만 지정할수 있으나, Eclipselink는 @JoinFetch  어노테이션을 이용해 특정 관계에 대해 항상 Join Fetch를  수행하게 할수 있다.  다른 JPA 구현체들 중에는  EAGER 관계의 객체를 항상 join fetch하는 것도 있는데, 이것은 일견 좋은 아이디어 인거 같지만, 실제로는 매우 나쁜 아이디어이다. EARGER는 관계를 읽어 와야 하는 가를 정의하지, 데이터베이스에 어떻게 접근하느냐를 정의하는 것이 아니다. 어떤 사용자가 객체의 모든 관계를 읽어 오게 하려고 할때, 만약 모든 관계를 join fetch를 이용해 가져온다면, 모든 ToMany 관계에 대한 거대한 조인(Outer 조인이 사용된다)을 걸게 되고, 거대한 중복 데이터를 읽어고게 된다. 또한 parent, owner, manager 같은 ManyToOne 관계는 원래 공유 객체여야 하는데(보통은 cache된), join fech는 모든 child에 대해 중복된 부모 객체를 생성하게 되며, 이런 동작은 별개의 독리된 쿼리를 던지는 것(또는 캐쉬된)보다 나쁜 성능을 낼수 있다. 

 

JPA는 join fetch에 대한 aliasing을 지원하지 않기때문에, 추가적으로 가져오고자 하는 관계에 대해 두번 조인해 주어야 한다. JPA 내부적으로 ToOne 관계에 대해서는 단일 조인으로 최적화되다. ToMany에 대해서는 진짜로 두번째 Join이 필요하며 이 조인은 연관된 객체를 필터링하기 위해 사용된다. 어떤 JPA 구현체는 join fetch 에 대한 alias를 지원하지만, JPA 표준에서는 허용하지 않고, eclipselink도 아직 지원하지 않는다. 

 

중첩된(Nested) join fetch는 JPQL에서 직접 지원하지 않는다. eclipselink는 중첩된 join fetch를 지원하는데, 쿼리 힌트인 "eclipselink.join-fetch"와 "eclipselink.left-join-fetch" 를 이용하면 된다. 

 

Nested join fetch query hint


query.setHint("eclipselink.join-fetch", "e.projects.milestones");