AppFuse는 인기있는 오픈소스 자바프레임워크 기반의 애플리케이션을 쉽게 개발할 수 있게 도와주는 개발 start-kit이다.

Spring, Hibernate, iBatis, Struts, WebWork등을 이용한 애플리케이션을 개발할 때 필요한 base application을 간단하게 만들 수 있다.

나는 Spring, Hibernate를 사용하기 시작하면서부터 AppFuse에 도움을 많이 받았다. 직접 AppFuse를 이용해서 애플리케이션을 만들지는 않았지만 Spring, Hibernate를 이용한 좋은 샘플코드의 하나로 분석해보면서 많은 팁과 좋은 기술을 알게 되었다. Spring이 OpenSessionInView를 지원한다는 것도 Spring레퍼런스가 아닌 AppFuse코드를 보면서 알았으니까.

특히 AppFuse에서 가장 관심이 갔던 부분은 Test코드이다. 대부분의 TDD나 Test관련서적에서는 전형적인 Layered J2EE App.의 테스트 코드를 어떻게 만들 수 있는지 보여주는 샘플이 없다. Money같은 샘플을 본다고 DAO, Service, Controller class들을 어떻게 테스트 할지 감을 잡기는 힘들다. Spring에 포함된 샘플에도 테스트와 관련되서 직접 얻을 수 있는 아이디어는 별로 없다. 그나마 최근에 나온 JUnit Recipe가 많은 테스트 샘플을 가지고 있지만 Hibernate나 Spring기반에 등장하는 DAO나 컨트롤러를 어떻게 테스트 해야하는지에 대해서는 예제를 찾을 수 없다. 그런면에서 AppFuse에 나온 간단하지만 제대로 된 테스트코드들을 내가 Spring을 활용한 테스트코드를 어떻게 작성해야할지 많은 아이디어를 줬다. AppFuse의 개발자인 Matt Raible은 Spring Forum내에서도 Test관련부분에 많은 글을 작성했다. 포럼 안에서 논의된 여러가지 기법들이 AppFuse의 샘플을 통해서 잘 드러나이다.

그런데 최근에 AppFuse 최신버전을 설치하고 소스를 살펴보던 중 이상한 점을 하나 발견했다. 그것은 DaoTest와 관련된 부분들이다.

AppFuse는 Hibernate와 iBatis를 persistent framework으로 사용할 수 있게 되어있다. 문제는 Hibernate Dao를 사용하는 경우이다.

GenericDaoTest클래스의 testCRUD메소드를 살펴보면

[java]
public void testCRUD() {
User user = new User();
// set required fields
user.setUsername(“foo”);
user.setPassword(“bar”);
user.setFirstName(“first”);
user.setLastName(“last”);
user.getAddress().setCity(“Denver”);
user.getAddress().setPostalCode(“80465″);
user.setEmail(“foo@bar.com”);

// create
dao.saveObject(user);
assertNotNull(user.getId());

// retrieve
user = (User) dao.getObject(User.class, user.getId());
assertNotNull(user);
assertEquals(user.getLastName(), “last”);

// update
user.getAddress().setCountry(“USA”);
dao.saveObject(user);
assertEquals(user.getAddress().getCountry(), “USA”);

// delete
dao.removeObject(User.class, user.getId());
try {
dao.getObject(User.class, user.getId());
fail(“User ‘foo’ found in database”);
} catch (ObjectRetrievalFailureException e) {
assertNotNull(e.getMessage());
}
[/java]

전형적인 CRUD테스트 코드로 보인다.

문제는 이 테스트는 Spring의 AbstractTransactionalDataSourceSpringContextTests를 기반으로 하고 있다는 점이다.

AbstractTransactionalDataSourceSpringContextTests는 각 테스트 메소드의 코드를 하나의 transaction안에서 돌아갈 수 있게 해준다. Hibernate라면 당연히 하나의 session안에서 돌아가게 된다.

그렇다면 위의 코드에서 처럼 transient object를 하나 만든 뒤 save하고 get, update, delete까지 하는 코드는 어떻게 동작할 것인가?

Hibernate옵션에 show_sql=true를 추가하고 Hibernate에 의해서 생성되는 SQL을 살펴보자.[java]
app1] INFO [main] GenericDaoTest.loadContextLocations(133) | Loading config for: classpath*:/**/dao/applicationContext-*.xml,classpath*:META-INF/applicationContext-*.xml
[app1] INFO [main] GenericDaoTest.startNewTransaction(268) | Began transaction (1): transaction manager [org.springframework.orm.hibernate3.HibernateTransactionManager@1722456]; default rollback = true
Hibernate: insert into app_user (version, username, password, first_name, last_name, address, city, province, country, postal_code, email, phone_number, website, password_hint, account_enabled, account_expired, account_locked, credentials_expired) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
[app1] INFO [main] GenericDaoTest.endTransaction(237) | Rolled back transaction after test execution
[/java]

실행한 결과이다. 기대했던 것 처럼 insert,select,update,delete가 모두 실행된 것이 아니고 insert만 달랑 한개만 보인다.

음… 묘한 결과이다.

일단 select, update, delete가 실행이 되지 않은 이유는 그리 어렵지 않게 알 수 있다. Hibernate의 first level cache때문이다. Hibernate는 최적화를 위해 session flush를 최대한 미룬다. Id를 사용하지 않은 Query가 필요하거나 직접 flush하기 전에는 flush되지 않는다. Flush는 메모리에 있는 object와 DB의 sync.를 처리하는 것을 말한다. 새로 추가된 persistent object라면 sync.에 의해서 SQL insert문이 생성이 되어 DB로 보내지는 것이다. Hibernate는 새로 추가된 entity를 일단 메모리의 first level cache(session)에 저장해 두고 flush하는 시점에는 이를 DB에 전송한다. 그런데 만약 이 상태에서 id로 오브젝트를 가져오는 get이나 load를 사용하면 Hibernate는 DB에 조회를 하기 전에 먼저 first level cache를 검색한다. 그래서 같은 오브젝트가 있다면 DB에 select를 하지 않고 cache에서 바로 엔티티를 읽어온다. 어짜피 같은 tx안에 있기 때문에 구지 DB에서 읽을 이유가 없다. 그 후에 update를 하면 역시 update할 준비만 한 채로 여전히 first level cache에 남아있다. 이 후에 delete를 하면 메모리에서 persistent object가 삭제된다. 정확히는 transient로 바뀐다.
일반적으로 기대할 수 있는 대로 Hibernate기능을 이용해서 insert, select, update, delete query가 생성이 되고 각각의 기능이 동작을 하는 것을 테스트 한 뒤 AbstractTransactionalDataSourceSpringContextTests의 특징대로 rollback까지 되서 완벽한 테스트가 수행될 것인가 하면 실제로는 그렇지 않다. AbstractTransactionalDataSourceSpringContextTests는 tx를 rollback시켜버린다. 결국 단 한번도 DB에 sync가 일어나지 않으니 SQL은 하나도 DB로 날아가지 않는다.

즉 위의 테스트코드를 실행해봐야 메모리에 persistent object를 만들었다 날릴 뿐이고 DB와 연동되는 것은 전혀 검증해볼 수 없다는 것이다. AbstractTransactionalDataSourceSpringContextTests를 사용할 때 발생할 수 있는 대표적인 실수 중의 하나이다.
재밌는 현상의 저 코드를 실행했을때 내 기대와는 달리 한개의 SQL(insert)이 수행된다는 것이다. 이유를 찾아보니 User.hbm.xml안에 id generator가 native로 되어있다. MySQL을 사용했기 때문에 auto_increment방식의 id생성을 이요하기 위해서 강제로 insert를 먼저 한 것이다.

그 후에는 내가 예상했던대로 하나의 SQL도 실행되지 않았다. 만약 MySQL처럼 insert를 해야지만 PK를 생성할 수 있는 DB가 아니라면 insert조차 실행되지 않았을 것이다.

따라서 위의 testCRUD메소드는 Hibernate를 기준으로 한다면 엉터리 테스트 코드에 불과하다. DAO테스트는 integration test임이 분명하고 Hibernate를 사용한다고 했을 때는 Mapping이 정확한지까지 검증하는 것이 DAO test의 책임이다. 그런데 DB와 연동하지 않고는 Model과 Mapping에 대한 테스트는 불가능한 것이다. 위의 CRUD테스트를 제대로 수행하게 하려면 강제적으로 flush를 해서 DB에 SQL이 전송되도록 해야한다. 따라서 코드는 다음과 같이 수정되야한다. 일단은 현재 돌아가는 session을 가져와야 하기 때문에 sessionFactory를 가져올 수 있도록 다음과 같이 코드를 추가한다.

[java]
public class GenericDaoTest extends BaseDaoTestCase {
protected Dao dao;
protected SessionFactory sessionFactory;

public void onSetUpBeforeTransaction() throws Exception {
dao = (Dao) applicationContext.getBean(“dao”);
sessionFactory = (SessionFactory)applicationContext.getBean(“sessionFactory”);
}
[/java]

그리고 SessionFactoryUtils를 이용해서 현재 Spring이 관리하고 있는 Hibernate session을 가져온다. 그것을 이용해서 매 단계 flush()를 수행해준다. 다음처럼 테스트 코드를 수정한다.

[java]
public void testCRUD() {
User user = new User();

user.setEmail(“foo@bar.com”);

Session s = SessionFactoryUtils.getSession(sessionFactory, true);

// create
dao.saveObject(user);
assertNotNull(user.getId());
s.flush();

// retrieve
user = (User) dao.getObject(User.class, user.getId());
assertNotNull(user);
assertEquals(user.getLastName(), “last”);

// update
user.getAddress().setCountry(“USA”);
dao.saveObject(user);
assertEquals(user.getAddress().getCountry(), “USA”);

// delete
dao.removeObject(User.class, user.getId());
try {
dao.getObject(User.class, user.getId());
fail(“User ‘foo’ found in database”);
} catch (ObjectRetrievalFailureException e) {
assertNotNull(e.getMessage());
}
s.flush();
}
[/java]
MySQL이기 때문에 insert는 자동으로 수행되지만 다른 DB를 사용하는 경우를 위해서 create후에도 flush를 한다.

이러게 수정한 뒤 다시 테스트를 수행하면

[java]
[app1] INFO [main] GenericDaoTest.loadContextLocations(133) | Loading config for: classpath*:/**/dao/applicationContext-*.xml,classpath*:META-INF/applicationContext-*.xml
[app1] INFO [main] GenericDaoTest.startNewTransaction(268) | Began transaction (1): transaction manager [org.springframework.orm.hibernate3.HibernateTransactionManager@1722456]; default rollback = true
Hibernate: insert into app_user (version, username, password, first_name, last_name, address, city, province, country, postal_code, email, phone_number, website, password_hint, account_enabled, account_expired, account_locked, credentials_expired) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: update app_user set version=?, username=?, password=?, first_name=?, last_name=?, address=?, city=?, province=?, country=?, postal_code=?, email=?, phone_number=?, website=?, password_hint=?, account_enabled=?, account_expired=?, account_locked=?, credentials_expired=? where id=? and version=?
Hibernate: delete from app_user where id=? and version=?
[app1] INFO [main] GenericDaoTest.endTransaction(237) | Rolled back transaction after test execution
[/java]

update와 delete까지 수행된 것을 확인할 수 있다.

하지만 여전히 get에 의해서 select가 정상적으로 되는지를 확인해볼 수 없다. select는 왜 수행되지 않는 것일까? 이유는 flush는 first level cache를 DB와 sync.해주기는 하지만 cache는 그대로 유지된다. 따라서 get을 하면 cache에서 바로 읽기 때문에 select가 수행되지 않는다. select까지 테스트 하고 싶다면 cache를 강제로 삭제해야 한다. 그래야지만 DB에서 직접조회하기 때문이다.

First level cache를 삭제하고 싶다면 session.clear()를 하거나 session.evict(entity)를 하도록 한다.

[java]
// create
dao.saveObject(user);
assertNotNull(user.getId());
s.flush();
s.clear();
[/java]

이렇게 clear를 해주면 get을 하기 전에 cache에 있던 persistent object는 사라지기 때문에 get에서 직접 DB에 select를 해오게된다.

[java]
[app1] INFO [main] GenericDaoTest.loadContextLocations(133) | Loading config for: classpath*:/**/dao/applicationContext-*.xml,classpath*:META-INF/applicationContext-*.xml
[app1] INFO [main] GenericDaoTest.startNewTransaction(268) | Began transaction (1): transaction manager [org.springframework.orm.hibernate3.HibernateTransactionManager@3ac93e]; default rollback = true
Hibernate: insert into app_user (version, username, password, first_name, last_name, address, city, province, country, postal_code, email, phone_number, website, password_hint, account_enabled, account_expired, account_locked, credentials_expired) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: select user0_.id as id1_0_, user0_.version as version1_0_, user0_.username as username1_0_, user0_.password as password1_0_, user0_.first_name as first5_1_0_, user0_.last_name as last6_1_0_, user0_.address as address1_0_, user0_.city as city1_0_, user0_.province as province1_0_, user0_.country as country1_0_, user0_.postal_code as postal11_1_0_, user0_.email as email1_0_, user0_.phone_number as phone13_1_0_, user0_.website as website1_0_, user0_.password_hint as password15_1_0_, user0_.account_enabled as account16_1_0_, user0_.account_expired as account17_1_0_, user0_.account_locked as account18_1_0_, user0_.credentials_expired as credent19_1_0_ from app_user user0_ where user0_.id=?
Hibernate: select roles0_.user_id as user1_0_, roles0_.role_id as role2_0_ from user_role roles0_ where roles0_.user_id=?
Hibernate: update app_user set version=?, username=?, password=?, first_name=?, last_name=?, address=?, city=?, province=?, country=?, postal_code=?, email=?, phone_number=?, website=?, password_hint=?, account_enabled=?, account_expired=?, account_locked=?, credentials_expired=? where id=? and version=?
Hibernate: delete from app_user where id=? and version=?
[app1] INFO [main] GenericDaoTest.endTransaction(237) | Rolled back transaction after test execution
[/java]

이렇게 해서 원하는 CRUD테스트를 모두 마쳤다.

AbstractTransactionalDataSourceSpringContextTests와 Hibernate의 특징을 모두 잘 알아야지만 정확히 기대되는 테스트를 수행할 수 있다.

또한가지 DAO CRUD테스트를 하는 방법은  AbstractDependencyInjectionSpringContextTests는 이용하는 것이다. AbstractDependencyInjectionSpringContextTests는  한 tx안에서 테스트가 수행되게 하지 않는다. 따라서 DAO를 호출한다면 매 DAO마다 독립적인 session/tx가 만들어지고 commit될 것이다. 따라서 강제적인 flush없이 CRUD테스트를 할 수 있다. 하지만  자동 rollback이 되지 않고  중간에 테스트가 실패하면  DB에 테스트되던 데이터가 남아있게 된다.  물론 기존 데이터와 충돌 할 가능성도 있다.

Hibernate와 같은 transparent persistent framework을 이용한 DAO테스트는 많은 주의가 필요하다.

물론 Hibernate로 만든 Entity의 CRUD테스트는 무의미하다고 생각된다. 어짜피 CRUD수행은 Hibernate가 알아서 해줄 것이고 테스트 할 것은 Model과 DB가 잘 연동이되는가 하는 Mapping에 대한 테스트면 충분하다. 따라서 insert만 한번 해보는 정도면 CRUD레벨의 테스트는 충분하다. 그 외에 relation등이 있다면 좀 더 테스트가 필요하겠지만.

테스트코드를 살펴보다 한가지 더 이상한 것을 발견했다. UserDaoHibernate에 보면 다음과 같은 코드가 있다.

[java]
public void saveUser(final User user) {
getHibernateTemplate().saveOrUpdate(user);
// necessary to throw a DataIntegrityViolation and catch it in UserManager
getHibernateTemplate().flush();
}
[/java]

DAO의 save코드에서 직접 flush()를 해주는 부분이 있다. Comment를 보면 DataIntegrityViolation체크를 하기 위함이라는데 FK constraint같은 것을 검증하기 위함이라는 뜻일 것이다. 이 코드가 들어가게 된 것도 위에서 얘기한 것과 마찬가지 상황이기 때문일 것이다. save를 수행한다고 다 insert문이 실행되지 않는다. 따라서 DB쪽에서 체크해줘야하는 것을 검증하지 못할 수 있다는 말이다. 문제는 그렇다고 해서 저렇게 save할때마다 flush를 하는 것은 바보같은 짓이다. Hibernate가 구지 flush를 최대한 미루는 것은 최적화 때문이다. Batch Update를 이용해서 최적화된 DB처리를 하게 하기 위한 것이다. 그것을 저렇게 강제로 매번 flush를 해준다면 Hibernate의 장점을 잃어버리는 것이다.

Flush의 컨트롤은 DAO안에서 사용할 경우에는 꼭 필요한 경우에만 해야한다.

Matt Raible같은 유명한 Spring,Hibernate전문가도 이런 실수를 하기도 하나보다. 문제는 AppFuse로 Spring/Hibernate를 공부하고 그 코드를 가져다가 사용하는 많은 개발자들이 이런 잘못된 테스트기법이나 코드를 배우게 될까 걱정이다. 조만간 AppFuse이슈트래커에 이슈를 올려야 할 듯 싶다.

Related posts:

  1. 다시 발견한 AppFuse
  2. Spring기반의 Hibernate DAO Unit Test 만들기
  3. 학습테스트(Learning Test)를 이용해서 공부하기
  4. Spring 3.0 (19) Test 모듈의 선택라이브러리 분석
  5. [토스3] 스프링 JDBC DAO에 lazy-loading 적용하기 (2)
  6. [토스3] 매핑 가능한 BeanPropertySqlParameterSource
  7. Spring ROO 대충대충 분석 (3) ROO의 Inter-type declaration
  8. Maven 재도전기 (1)
  9. Spring 3.0 (25) Spring 3.0 빌드, 배포, 모듈과 라이브러리의 의존관계 분석 그 이후
  10. Spring 3.0 @MVC 메소드에서 자동으로 리턴 모델에 추가되는 것들
  11. 하이버네이트에 대한 오해, 미신 그리고 무지
  12. Rod Johnson의 새로운 블로그
  13. Spring 공부 다시 시작
  14. Spring 3.0 (20) Transaction 모듈의 선택 라이브러리
  15. Maven의 default directory layout 변경하기

Facebook comments:

to “AppFuse DAO Test 코드의 문제점”

  1. we like to honor a lot of other web web pages around the web, even if they arent linked to us, by linking to them. Underneath are some webpages really worth checking out

  2. It as very easy to find out any matter on web as compared to books, as I found this post at this website.

  3. Thanks a lot. Numerous knowledge!
    Augmentin Patient Comment

  4. here are some links to web-sites that we link to mainly because we assume they’re worth visiting

  5. whoah this blog is fantastic i really like reading your posts. Stay up the great work! You recognize, lots of persons are searching around for this information, you can help them greatly. |

  6. I really liked your blog. Fantastic.

  7. Appreciate it! Ample content.
    Propranolol Tremors

  8. I really enjoy the article post.Really thank you! Will read on…

  9. Thank you a bunch for sharing this with all people you really recognize what you are talking about! Bookmarked. Kindly also consult with my site =). We can have a hyperlink trade agreement between us!

  10. Hey, thanks for the blog.Really thank you! Great.

  11. Muchos Gracias for your article.Thanks Again.

  12. Say, you got a nice blog.Really looking forward to read more. Will read on…

  13. Thanks a lot. Very good information. acyclovir cream

  14. Lqhndc pesawo mens ed pills over the counter erectile dysfunction pills

  15. Hey! Do you use Twitter? I’d like to follow you if that would be ok. I’m undoubtedly enjoying your blog and look forward to new updates.|

Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

© 2017 Toby's Epril Suffusion theme by Sayontan Sinha