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
|