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

Join Fetch는 여러 경우에 최고의 솔류션이지만, 대단히 관계 DB 중심의 접근이기도 하다. 더 창의적이고 객체 지향적은 솔류션은 Batch Fetching 이다. Batch Fetching은 전통적인 관계중심의 사고방식으로는 더 어렵게 느껴지겠지만, 일단 이해하고 나면 훨씬 강력하다. 

JPA는 join fetch만 정의하고 있다. batch fetching 을 사용하기 위해서는 eclipselink의 쿼리힌트 "eclipselink.batch"를 사용한다. 

Batch fetch query hint

query.setHint("eclipselink.batch", "e.address");
query.setHint("eclipselink.batch", "e.phones");

batch fetch 를 실행해도 원래 쿼리가 실행된다. (단순히 employee만 가져오는). 차이점은 연관된 객체를 가져오는 방식에 있다.  일단 employee의 목록을 얻어온후 어느 한 employee의 address를 사용하기 위해 접근할때, 오직  앞서 SELECTED 된  모든 employee들의 모든 address를 가져온다.  batch fetching에는 여러 유형이 있으며,  JOIN 타입의 batch fetch의 SQL은 다음과 같이 생성된다. 

SQL for batch fetch (JOIN)

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


처음 볼때 join fetch를 사용한 1개의 sql 대신 3개의 sql이 실행된것을 볼수 있다. 3개의 쿼리가 실행 되었으니 더 느릴 것이라고 생각하기 쉽지만, 실제로는 대부분의 경우에 더 빠르다. 1번 과  3번의 select의 차이는  거의 없다 (Pretty minimal).  최적화 되지 않은 예전 경우의 가장 큰 이슈가 N 번의 쿼리가 실행되는 것이였고, 그때는 차이가 100초가 1000초가 될수도 있었다. 

 

batch fetching의 주요한 장점은 오직 필요한 정보만 select 된다는 것이다. join fetch의 경우, EMPLOYEE와 ADDRESS 의 데이터가 PHONE의 결과마다 중복 된다는 것이였다. (역자주: 별도의 group by  나 distinct 를 주어서 해결할수도 있지만, 쿼리가 느려진다. ) ToMany 관계에서  이런 현상이 발생하며, 만약 여러개의 또는 중첩된 ToMany 관계를 읽어오려고 하면  얼어붙은것 처럼 될것이다. 예를 들어  직원의 프로젝트 목록을 join fetch하고, 각 프로젝트의 milestone을 join fetch할때,  직원당 5개의 프로젝트가 있고 프로젝트당 10개의 마일스톤이 있다고 하면 직원정보는 50배 중복된다(프로젝트 정보는 10배 중복된다)  복잡한 객체 모델이라면 이런 현상은 큰 이슈가 된다. 

 

join fetching은 직원이 주소나 전화번호가 없는 경우를 처리 하기 위해 일반적으로  outer 조인을 사용해야 하다. outer 조인은 DB에서 일반적으로 비효율적으로 처리되며, 결과에 null인 row를 추가한다. 

batch fetching은 만약 직원이 주소나 전화번호가 없다면, 그냥 batch fetch 결과에 없을 뿐이고 훨씬 적은 데이터를 읽어 오게 된다.  Batch fetching을 이용하면 ManyToOne 관계에서 distinct를 허용한다. 예를 들어 직원의 관리자가 batch fetch 된다면, distinct 는 유니크한 관리자가 select 되게 하고, 중복된 데이터를 읽어 오지 않게 한다. 

 

JOIN batch fetch의 단점은 원래 쿼리가 여러번 실행 된다는 것이다. 만약 원래 쿼리가 비용이 비싼 쿼리라면 join fetch가 더 효율적일수 있다.  또다른 경우인, 단 하나의 결과만 select 한다면, batch fetch는 아무런 이익을 제공해주지 못하지만 join fetch는 쿼리를 단 한번만 실행하기 때문에 실행할 쿼리를 줄여줄수 있다. 

 

Batch Fetching은 여러 형태가 있는데 Eclipselink 2.1에서는 JOIN, EXISTS,IN 3가지 타입을 지원한다. (BatchFetchType  enum에 정의 되어 있다.)  batch fetch 타입의 지정은 쿼리 힌트 "eclipselink.batch.type"을 통해 지정할수 있다.  관계에 대해 상상 batch fetching을 사용하고자 한다면 @BatchFetch 어노테이션을 붙여 주면 된다. 

 

Batch fetch query hints and annotations

query.setHint("eclipselink.batch.type", "EXISTS"); 
 
@BatchFetch(type=BatchFetchType.EXISTS)  

 

EXISTS 옵션은 JOIN 옵션관 비슷하지만, Join 대신 exists와  sub-select 를 사용한다. 이 옵션의 장점은 lob이나 복잡한 쿼리를 에서 distinct 를 사용할 필요가 없다는 것이다. 

 SQL for batch fetch (EXISTS)

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

 

IN 옵션은 JOIN이나 EXISTS옵션과 많이 다르다. IN 옵션은 원래 select 쿼리를 사용하지 않고, 대신 객체의 ID를 IN 구절을 이용해 사용한다. IN 옵션의 장점은 원래 쿼리를 다시 실행할 필요가 없다는 것이고, 이것은 원래 쿼리가 복잡할수록 큰 이득을 준다. IN 옵션은 페이징 기능도 지원하며, 다른 옵션이 무조건 전체 객체를 읽어 와야 하는 것에 비해  커서의 사용도 잘 지원한다. Eclipselink에서 IN 옵션은 캐쉬도 지원하기 때문에 캐쉬에 없는 것만 IN에 넣어 가져 오기 때문에 더 적은 분량만 읽어 오게 된다. 

 

IN 옵션의 이슈는 이미 읽어온 세트가 너무 많으면 IN 구절이 너무 커져서 DB에서 처리하는데 비효율적이 될수 있다는 것이다.  그리고 복합키가 문제가 될수 있다.  Eclipselink는 in 구절에서 복합키를 지원하하며 , Oracle같은 DB는 SQL 의 in 구절에서 복합키를 지원하지만 다른 DB에서는 IN 구절에서 복합키를 지원하지 않는 경우도 있다.  IN 옵션은 또한 SQL의 IN  파트가 동적으로 생성되는것이 지원되는 DB여야 한다.  

SQL for batch fetch (IN)

SELECT E.* FROM EMPLOYEE E WHERE E.STATUS = 'Part-time'
  
SELECT A.* FROM ADDRESS A WHERE A.ADDRESS_ID IN (1, 2, 5, ...)
  
SELECT P.* FROM PHONE P WHERE P.OWNER_ID IN (12, 10, 30, ...) 

 

Batch fetching은 쿼리 힌트에 점(.) 표기를 이용해 중첩시킬수 있다.  

Nested batch fetch query hint

query.setHint("eclipselink.batch", "e.projects.milestones"); 


Batch fetching의 기능중 join fetch에서 제공되지  않는 것중에 하나가 바로 최적화된 트리 구조 읽기이다.  만약 트리구조에서 자식 관계에 @BatchFetch를 설정하면, 단일 쿼리가 각 레벨을 위해 실행된다.