Easy_Test 오픈소스 프로젝트

토비의 스프링3에 들어있는 DispatcherServlet 학습 테스트용 클래스를 개조해서 스프링 MVC 기반 웹 애플리케이션을 쉽게 테스트 할 수 있는 라이브러리를 만들어봤습니다.

https://github.com/keesun/easy_test

지금은 FunctionalTest라는 기능밖에 없습니다.

https://github.com/keesun/easy_test/wiki/FunctionalTest

자세한 내용은 위키에 정리해두었습니다.

앞으로도 계속해서 스프링 기반 웹 애플리케이션에서 테스트를 작성할 때 쓸만한 라이브러리를 만들어 넣을까 합니다.

스프링 3.1 m2

http://blog.springsource.com/2011/06/09/spring-framework-3-1-m2-released/

스프링 3.1 M2가 나온지 조금 됐습니다. 저 글에서는 RC1을 7월에 내놓고 9월에 GA 릴리즈를 하겠다고 했었지만, 아시다시피 스프링은 배포 일정을 항상 굉장히 긍정적으로 말해주기 때문에 최소한 +2~3 달 정도를 하시면 얼추 비슷하게 나옵니다. 그래서 제 예상에는 RC1이 9월쯤 나올 것 같구요. GA는 빠르면 12월쯤이 되야 나올 것 같습니다.

스프링 3.1 M2는 스프링 3.1 M1에서 추가한 주요 기능의 완성도를 높였다고 합니다.

– Environment 추사화를 안정화 했으니 사용해 보시라.
– @Feature 말고 @Enable* 애노테이션으로 자바 기반 설정 기능 강화.

두 기능 모두 말 그대로 완성 버전이라고 생각됩니다. Environment 추상화와 가자 기반 설정 모두 3.1의 핵심 기능인 만큼 나중에 조금 더 자세히 소개드리겠습니다.

새로 추가한 기능도 있습니다.

– 서블릿 3.0 기반 initializer: 이걸로 이제 tomcat 7 같은 servlet 3.0을 지원하는 컨테이너에는 web.xml 없이 자바 파일만 가지고 스프링 웹 애플리케이션을 배포할 수 있습니다.

– JPA 사용할 때도 packagesToScan 지원. 그래!! 바로 이거에요. Hibernate SessionFactory를 설정할 때는 저 옵션이 있는데, EntityManagerFactory를 설정할 때는 저 옵션이 없어서 굉장히 불편했지요.

– 핸들러 메서드 기반의 RequestMappingHandlerAdapter. 이것도 꼭 필요한 것이죠. 핸들러 단위가 클래스에서 메서드로 바뀌었기 때문에 이런게 필요합니다.

이밖에…

– “c” 네임스페이스 추가.

– 테스트 콘텍스트에서 @Configuration과 Environment 프로파일 지원

– REST 지원 기능 강화

정말 하나같이 다 주옥같은 업데이트로군요.

하이버네이트, 패칭(fetching) 전략을 사용한 성능 최적화 가이드

하이버네이트 완벽 가이드(Java Persistence With Hibernate) 13장 2절 5단에 보시면 하이버네이트 패칭 전략을 사용한 성능 최적화 가이드가 나와있습니다. 읽어본지가 하두 오래되서 기억을 되새기며 짧게 요약합니다.

기본적으로 하이버네이트는 요청하지 않은 데이터는 가져오지 않는다.

=> 책에는 이렇게 적혀있지만, 그 당시와는 달리 요즘은 기본적으로 x2Many는 모두 lazy 패칭이고, x2One은 모두 eager 패칭

그러다보니 n+1 select 문제라는게 발생할 수 있다.

=> 그래서 eager 패칭, 배치 패칭, 서브 셀렉트 등을 사용해서 n+1 select에 대한 여러 해결책을 제공한다.

그렇다고, join을 사용해서 쿼리 수를 줄이면 다 해결 되느냐? 아니, 또 다른 문제가 생긴다. 바로, 카테시안 곱. 쿼리 수는 줄이겠지만, 가져오는 데이터가 너무 많다.

=> 그래서 균형을 잘 잡아야 돼. 글로벌 패칭 전략이 무엇인지 알아야 하고, 특정 쿼리에 적용되는 패칭 전략은 무엇인지 알아야 돼.

N+1 Select 문제

 

Item <-> Bid를 가지고 살펴보자.

List<Item> allItems = session.createQuery("from Item").list();
// List<Item> allItems = session.createCriteria(Item.class).list();
Map<Item, Bid> highestBids = new HashMap<Item, Bid>();
for (Item item : allItems) {
    Bid highestBid = null;
    for (Bid bid : item.getBids() ) { // Initialize the collection
         if (highestBid == null)
             highestBid = bid;
         if (bid.getAmount() > highestBid.getAmount())
             highestBid = bid;
    }
    highestBids.put(item, highestBid);
}

Criteria API를 사용하던, HQL을 사용하던 똑같이, Item 목록만 가져온다. 첫번째 줄에서 Item을 N개 가져오는 쿼리가 하나 날아갔다. 그 다음, 반복문 안에서 해당 Item과 연관 관계에 있는 Bid 목록을 가져오느라도 N개 만큼 쿼리가 날아간다. 그래서 N+1 select 문제라고 한다.

해결책1. 배치 패칭

<set name="bids"
     inverse="true"
     batch-size="10">
    <key column="ITEM_ID"/>
    <one-to-many class="Bid"/>
</set>

이렇게 배치 패칭을 사용하면, n+1 select에서 n+1/10 select로 쿼리 수를 줄일 수 있다. 이 방법은 Bid를 필요할 때 가져오는 방법을 계속 고수할 수 있으면서(Lazy Fetching)도, Bid 컬렉션을 하나 가져올 때 다른 Item의 Bid 컬렉션도 같이 몇 건 더 가져오는 방식으로 select 쿼리 수를 줄인다.

해결책2. Subselect 기반 선 패칭

<set name="bids"
     inverse="true"
     fetch="subselect">
    <key column="ITEM_ID"/>
    <one-to-many class="Bid"/>
</set>

이 방법은 select 쿼리 수를 n+1에서 2개로 줄이는 방법이다. 이 방법도 Bid를 필요할 때 가져오는 방법을 고수할 수 있으지만, 배치 패칭과의 차이가 있는데, 바로 첫번째 Bid 컬렉션을 가져올 때 나머지 모든 Item의 Bid 컬렉션도 다 같이 가져온다는 것이다.

해결책3. Eager 패칭 설정

<set name="bids"
     inverse="true"
     fetch="join">
    <key column="ITEM_ID"/>
    <one-to-many class="Bid"/>
</set>

이 방법은 select 수를 1번으로 줄이는 방법이다. Item 목록을 가져올 때 해당 Item의 Bid 목록도 반드시 필요한 경우라면 패칭 전략을 Eager 모드로 설정할 수 있다. 하지만 그런 경우가 드물 뿐더러, Cartesian product 문제가 발생하고, 메모리 소비가 크다는 문제가 있다.

해결책4. 필요한 쿼리만 Eager 패칭

List<Item> allItems =
    session.createQuery("from Item i left join fetch i.bids")
            .list();
List<Item> allItems =
    session.createCriteria(Item.class)
        .setFetchMode("bids", FetchMode.JOIN)
        .list();
// Iterate through the collections…

이 방법은 특정 쿼리에만 Eager 패칭을 적용한다. 사실 내용은 해결책3번과 같지만, 중요한 차이가 있는데, 해당 쿼리에만 적용된다는 것이다.

Cartesian product 문제

이 문제는 Eager 패치, 즉 FetchMode=Join, 즉 위에서 살펴봤던 해결책3과 4를 두 개 이상의 컬렉션에 적용했을 때 생기는 문제다.

Item <-> Bid와 Item <-> Image 이렇게 Item에 두 개의 컬렉션이 있다고 가정하고, 이 두 컬렉션에 모두 해결책3을 적용하면 이렇게 된다.

<class name="Item">
    …
    <set name="bids"
         inverse="true"
         fetch="join">
        <key column="ITEM_ID"/>
        <one-to-many class="Bid"/>
    </set>
    <set name="images"
         fetch="join">
        <key column="ITEM_ID"/>
        <composite-element class="Image">…
    </set>
</class>

이 상태에서 Item 목록을 가져오면 하이버네이트는 다음과 같은 쿼리를 보낸다.

select item.*, bid.*, image.*
  from ITEM item
    left outer join BID bid on item.ITEM_ID = bid.ITEM_ID
    left outer join ITEM_IMAGE image on item.ITEM_ID = image.ITEM_ID

만약에 Item이 3개 있고, 1번 Item에 Bid가 3개 Iamger가 2개, 2번 Item에는 Bid와 Image가 각각 1개식, 3번 Item에는 아무런 Bid나 Image가 없다고 가정했을 때 쿼리 결과 데이터수는 다음과 같다.

1 * 3 * 2

+ 1 * 1 * 1

+ 1 * 1 * 1

= 8

자, 그럼 숫자를 조금 더 키워서 Item이 1,000개, 각 Item에 Bid가 20개 그리고 Imager가 5개씩 있다면 어떻게 될까? 100,000건이 된다.

이게 문제다. 너무 크고, 결과 Result에 중복 데이터가 엄청 많다.

해결책. Subseleclt

커다른 쿼리 하나 보다는 오히려 작은 쿼리 셋으로 쪼개는 게 속도나 메모리 면에서 더 좋다. N+1 문제 해결책으로 살펴봤던, 해결책 2번과 같은 방식으로 풀 수 있다. 그러면 쿼리 수는 세번으로 늘어나지만, 데이터를 중복해서 읽어오지 않기 때문에 메모리를 덜 사용할 수 있고, 속도도 더 빠를 것이다.

최적화 스탭 바이 스탭

 

  1. 하이버네이트 쿼리를 로깅한다. hibernate.format_sql 과 hibernate.use_sql_comments를 사용해서 SQL문을 로깅하고 가독성을 높인다.
  2. 유즈케이스 별도 얼마나 많이 어떤 쿼리가 실행되는지 확인한다.
  3. 패치 모드를 변경 해본다.
    1. join을 사용한 SQL이 너무 복잡하고 느린 경우: join해서 가져오는 컬렉션이나 엔티티를 두번째 select(subselect)로 대체할 수는 없는지 고민해본다. hibernate.max_fetch_depth 설정을 1~5 사이로 유지한다.
    2. 너무 많은 select 쿼리: 특이한 상황에서 확실시 된다면, fetch=”join”을 사용해도 좋치만, 두 개 이상의 컬렉션에 적용하는 것은 Cartesian Product 문제가 있다는 걸 염두하자. 배치 패칭을 사용할 수도 있는데, 그 크기는 3~15 사이를 사용하자.