Fetch Strategy

Lazy Loading 이란 Hibernate에서 기본적으로 객체가 실제로 필요하기 전까지 SQL을 실행하지 않고 Proxy 객체로 리턴하는 것을 말한다. 이러한 Lazy Loading을 통해 불필요한 DB 접근을 줄이고 Session 내에 존재하는 Persistence 객체의 개수를 감소시킬 수 있다. 하지만 이러한 Lazy Loading을 처리하기 위해 다음과 같은 N+1 SELECT 이슈가 발생하게 된다. 다음은 Lazy Loading으로 발생할 수 있는 N+1 SELECT 문제를 테스트할 수 있는 HibernateFetchWithDefaultLazyLoadingTest.java 파일의 일부이다.
hqlBuf.append("FROM Category category ");
hqlBuf.append("ORDER BY category.categoryName ASC");
Query query = session.createQuery(hqlBuf.toString());

/*1번의 쿼리 수행 :
select category0_.CATEGORY_ID as CATEGORY1_0_, 
category0_.CATEGORY_NAME as CATEGORY2_0_, category0_.CATEGORY_DESC as 
CATEGORY3_0_ from PUBLIC.CATEGORY category0_
 order by category0_.CATEGORY_NAME ASC */
List categoryList = query.list();

for (int i = 0; i < categoryList.size(); i++) {
	Category category = (Category) categoryList.get(i);

	if (i == 0) {
		assertEquals("fail to match a category name.", "Comedy",
				category.getCategoryName());
		
		Set movies = category.getMovies();
		
		/* n번의 쿼리 수행 :
		select movies0_.CATEGORY_ID as CATEGORY1_1_, movies0_.MOVIE_ID 
		as MOVIE2_1_, movie1_.MOVIE_ID as MOVIE1_3_0_, movie1_.COUNTRY_CODE 
		as COUNTRY2_3_0_, movie1_.TITLE as TITLE3_0_, movie1_.DIRECTOR 
		as DIRECTOR3_0_, movie1_.RELEASE_DATE as RELEASE5_3_0_ 
		from MOVIE_CATEGORY movies0_ left outer join PUBLIC.MOVIE movie1_ 
		on movies0_.MOVIE_ID=movie1_.MOVIE_ID
 where movies0_.CATEGORY_ID=? */
		assertEquals("fail to match the size of movie list.", 2, movies.size());
	} else if (i == 1) {
		assertEquals("fail to match a category name.", "Horror",
				category.getCategoryName());

		Set movies = category.getMovies();
		...
}
Category에 대한 조회 작업을 수행하며 특정 Category에 속한 Movie Set 조회시 Movie 정보 조회를 위한 SELECT문이 수행된다. 이를 해결하기 위해 Fetch 방식에 대한 제어가 필요하며 그 예는 다음과 같다.

Batch를 이용하여 데이터 조회

Hibernate Mapping XML 파일 내에 특정 객체에 대한 batch-size를 지정할 경우 지정한 개수만큼 해당 객체를 로딩하는 방식으로 쿼리 실행 회수가 n / batch size + 1로 감소한다. 다음은 batch-size 설정 예인 Country.hbm.xml 파일의 일부이다.
<hibernate-mapping>
    <class name="anyframe.sample.model.bidirection.Country" table="COUNTRY" 
        lazy="true" schema="PUBLIC">
        <id name="countryCode" type="string">
            ..
        </id>
        <property name="countryId" type="string">
            <column name="COUNTRY_ID" length="2" not-null="true" />
        </property>
        ..        
        <set name="movies" inverse="true" cascade="save-update" batch-size="2">
            <key>
                <column name="COUNTRY_CODE" length="12" />
            </key>
            <one-to-many class="anyframe.sample.model.bidirection.Movie" />
        </set>
    </class>
</hibernate-mapping>
위와 같이 정의할 경우 Country:Movie 관계에서 Movie Set에 대한 Fetch Strategy를 Batch Fetching한다.(여기서는 batch-size="2"로 정의함.) 특정 Country에 속한 Movie Set을 조회하고자 할 때 batch-size를 기반으로 SELECT문이 수행된다.
hqlBuf.append("FROM Country");
Query query = session.createQuery(hqlBuf.toString());
List countryList = query.list();

// 3. assert result - country
assertEquals("fail to match the size of movie list.", 3, countryList
		.size());

for (int i = 0; i < countryList.size(); i++) {
	Country country = (Country) countryList.get(i);

	if (i == 0) {
		assertEquals("fail to match a country name.", "Korea", country
				.getCountryName());

		Set movies = country.getMovies();
		
		/* batch-size가 2이므로 2개씩 조회
		select movies0_.COUNTRY_CODE as COUNTRY2_1_, movies0_.MOVIE_ID as MOVIE1_1_,
		movies0_.MOVIE_ID	as MOVIE1_3_0_, movies0_.COUNTRY_CODE as COUNTRY2_3_0_,
		movies0_.TITLE as TITLE3_0_, movies0_.DIRECTOR as DIRECTOR3_0_, 
		movies0_.RELEASE_DATE as RELEASE5_3_0_ from PUBLIC.MOVIE movies0_ 
		where movies0_.COUNTRY_CODE in ('COUNTRY-0001', 'COUNTRY-0003')'*/
		assertEquals("fail to match the size of movie list.", 2, movies
				.size());
	} else if (i == 1) {
		assertEquals("fail to match a country name.", "Japan", country
				.getCountryName());

		Set movies = country.getMovies();
		//쿼리 수행 안함.
		assertEquals("fail to match the size of movie list.", 1, movies
				.size());
위에 대한 테스트 코드는 HibernateFetchWithBatchSizeTest.java 를 참고한다.

Sub-Query를 이용하여 데이터 조회

또다른 fetch 전략으로 subselect 속성을 주는 방법이 있다. subselect 속성 정의 방법은 Mapping XML 파일인 Movie.hbm.xml 에서 다음과 같이 확인할 수 있다.
<hibernate-mapping>
    <class name="anyframe.sample.model.bidirection.Movie" table="MOVIE" lazy="true"..>
        <id name="movieId" type="string">
            <column name="MOVIE_ID" />
            <generator class="assigned" />
        </id>
        <property name="title" type="string">
            <column name="TITLE" length="100" not-null="true" />
        </property>
		...
        <set name="categories" inverse="false" table="MOVIE_CATEGORY" fetch="subselect">
            <key>
                <column name="MOVIE_ID" length="8" not-null="true" />
            </key>
            ..
        </set>
    </class>           
</hibernate-mapping>
위와 같이 Movie 클래스 내의 categories set에 대해 fetch 속성의 값을 subselect로 정의할 경우 해당 데이터를 불러올때 Sub Query 형태의 SELECT 문이 수행되며 한번에 모두 로딩하게 된다.
for (int i = 0; i < movieList.size(); i++) {
	Movie movie = (Movie) movieList.get(i);

	if (i == 0) {
		..
		
		Set categories = movie.getCategories();
		
		/* categories에 대한 Sub Query 형태의 SELECT문이 발생한다.
		 select categories0_.MOVIE_ID as MOVIE2_1_, categories0_.CATEGORY_ID 
		 as CATEGORY1_1_, category1_.CATEGORY_ID as CATEGORY1_0_0_, category1_.CATEGORY_NAME 
		 as CATEGORY2_0_0_, category1_.CATEGORY_DESC as CATEGORY3_0_0_ 
		 from MOVIE_CATEGORY categories0_ 
		 left outer join PUBLIC.CATEGORY category1_ 
		 on categories0_.CATEGORY_ID=category1_.CATEGORY_ID
		 where categories0_.MOVIE_ID 
		 in (select movie0_.MOVIE_ID from PUBLIC.MOVIE movie0_) */
		assertEquals("fail to match the size of category list.", 2, categories.size());
				
				
	} else if (i == 1) {
 		..
 		Set categories = movie.getCategories();
		//쿼리 수행 안함.
		assertEquals("fail to match the size of category list.", 2,categories.size());
	...
하지만 최초로 필요한 순간에 모든 데이터를 로딩하므로 동시에 많은 데이터 요청이 있을 경우 메모리 사용량이 급격히 증가할 수 있음에 유의한다. 위의 테스트 코드는 HibernateFetchWithSubselectTest.java 에서 확인할 수 있다.

join fetch를 이용하여 데이터 한꺼번에 조회

특정 HQL문에 "join fetch"절을 사용하게 되면 해당 Join 객체에 대해서 Lazy Loading과 다른 방식으로 한 번에 필요한 데이터를 모두 로딩하게 된다. 다음은 join fetch가 적용된 HibernateFetchWithoutLazyLoadingTest.java 파일의 일부이다.
StringBuffer hqlBuf = new StringBuffer();
hqlBuf.append("SELECT movie ");
hqlBuf.append("FROM Movie movie join fetch movie.categories category ");
hqlBuf.append("WHERE category.categoryName = ?");
Query query = session.createQuery(hqlBuf.toString());
query.setParameter(0, "Romantic");

/* fetch join된 categories의 데이터도 한꺼번에 모두 로드시킨다. (Lazy Loading이 아님)
select movie0_.MOVIE_ID as MOVIE1_3_0_, category2_.CATEGORY_ID as CATEGORY1_0_1_, 
movie0_.COUNTRY_CODE as COUNTRY2_3_0_, movie0_.TITLE as TITLE3_0_, 
movie0_.DIRECTOR as DIRECTOR3_0_, movie0_.RELEASE_DATE as RELEASE5_3_0_, 
category2_.CATEGORY_NAME as CATEGORY2_0_1_, category2_.CATEGORY_DESC as CATEGORY3_0_1_, 
categories1_.MOVIE_ID as MOVIE2_0__, categories1_.CATEGORY_ID as CATEGORY1_0__ 
from PUBLIC.MOVIE movie0_ inner join MOVIE_CATEGORY categories1_ 
on movie0_.MOVIE_ID=categories1_.MOVIE_ID inner join PUBLIC.CATEGORY category2_ 
on categories1_.CATEGORY_ID=category2_.CATEGORY_ID where category2_.CATEGORY_NAME='Romantic' */
List movieList = query.list();

// 3. assert result - movie
assertEquals("fail to match the size of movie list.", 2, movieList
		.size());

for (int i = 0; i < movieList.size(); i++) {
	Movie movie = (Movie) movieList.get(i);

	if (i == 0) {
		..
		Set categories = movie.getCategories();
		//쿼리 수행 안함.
		assertEquals("fail to match the size of category list.", 1,
					categories.size());
	} else if (i == 1) {
		..
		Set categories = movie.getCategories();
		//쿼리 수행 안함.
		assertEquals("fail to match the size of category list.", 1,
				categories.size());
	...
이는 categories에 대한 fetch 속성을 "join"으로 준것과 같이 동작하게 된다. 하지만 Mapping XML에 정의할 경우 Movie를 조회할 때마다(Category 목록이 필요하지 않은 경우에도) 모든 Category 목록도 함께 초기화되어 메모리에 올라 오게 되므로 위와 같이 HQL문에 join fetch를 사용하여 필요한 경우에만 적용되도록 하는 것이 효율적이다.

Resources

  • 다운로드
  • 샘플 테스트 코드를 포함하고 있는 anyframe-hibernatetest-src.zip 파일을 다운받은 후, 테스트 환경 설정 을 참조하여 위에서 제시한 예제 코드 (src/test/java 폴더의 anyframe.core.hibernate.fetch 패키지에 속한 *Test.java)를 실행해 볼 수 있다.
    Name
    Download
    anyframe-hibernatetest-src.zip
    Download