OSAF 2.0 준비중

최근에 계속 포스팅 했던 [회사일]을 하면서 다른 프로젝트에서도 사용할 수 있는 코드들을 별도 패키지에 모아뒀습니다. 

Generic 사용 극대화
이전과 마찬가지로 Generic 사용을 극대화해서 DAO, Service, Controller를 만들고 PersistentEnum을 활용하는 GenericPEFormatter도 만들어 뒀고, GenericExcelView까지 있습니다.
REST 스타일 URL 지원
스프링 3.0 최신 버전과 하이버네이트를 사용했으며 URL은 REST 스타일로 만들어 놨습니다. 따라서 GenericController만 상속받으면 기본적인 CRUD 기능을 RESTful URL로 제공해줍니다.
계층 구조
기본적인 계층 구조(DAO, Service, Controller) 형태기 때문에 적응하기 쉬울 겁니다. 스프링 Roo 스타일로 만들까 했지만. AsepctJ를 공부헀었는데도 잘 적응이 안되더라구요.ㅋ
no-iframe
참.. 화면이 이전처럼 iframe으로 나눠진게 아니라 통짜 구조입니다. 화면을 나누는건 JQuery layout 플러그인을 사용했구요.  iframe보다 편합니다.
그리드
그리드도 jqGrid를 사용해서 이전에 쓰던 그리드(뭔지도 기억안남)보다 기능도 많고 더 좋습니다.
상속
단점은.. Spring Roo 처럼 완전히 비침습적인 프레임워크가 아닌지라.. 상속을 사용해야 합니다. DAO, Service, Controller, Enum 들이 OSAF 코드를 상속 받아야 합니다.
코드 생성
코드 생성 기능은 없습니다. 만들라면 만들겠지만 귀찮기도 하고 손으로 직접 만들면서 사용법을 익히는게 좋을 것 같아서 만들지 않았습니다.
바로 시작
프로젝트를 다운받아서 라이브러리 형태로 넣을 필요없이 프로젝트를 받아서 바로 자신의 웹 프로젝트로 사용할 수 있게끔 배포할 계획입니다. 
기본 모델 제공
바로 실행할 수 있는 프로젝트에 Member, Right, Code, CodeCat 같은 기본 도메인을 제공합니다. 자주 쓰는 도메인 코드를 가진 상태에서 시작하세요. Generic류 코드를 어떻게 쓰는지, OSAF 태그파일을 사용해서 화면 코드를 작성하는 방법을 쉽게 볼 수 있습니다.

[OSAF 테스트 가이드 초안] 서비스 통합 테스트

서비스 통합 테스트

(테스트할 메서드 내에서 new를 이용하여 객체를 생성하는 등) 의존성을 목킹하기 까다로운 상황 또는,
사용하는 DAO의 기능을 충분히 테스트 했으며, DAO 실행 시간이 오래 걸리지 않을 경우
굳이 목킹하여 단위테스트를 작성할 필요 없이 통합 테스트를 작성 할 수 있습니다.

테스트 하지 않을 것

서비스 단위 테스트와 동일합니다.

테스트 할 것

  • 서비스 단위 테스트에서 하지 않은 테스트

테스트 코드 작성 방법

  • 스프링 @Test 설정 추가
  • 테스트 클래스 이름

서비스 단위 테스트와 겹치지 않도록 테스트할 클래스 이름 뒤에 Test를 붙여줍니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/testApplicationContext.xml")
@Transactional
public class PurYearPlanServiceImplTest {

@Autowired PurYearPlanServiceImpl service;

...
}
  • 테스트 작성하기
	@Test
public void update() {
PurYearPlan entity = new PurYearPlan();
service.add(entity);
assertThat(entity.getAmount(), is(new BigDecimal(0)));

entity.addDetail(makeNewDetailWithPriceAndQty(10, 7));
service.update(entity);
checkPriceQtyAndAmount(entity, 10, 7, 70);

entity.addDetail(makeNewDetailWithPriceAndQty(20, 3));
service.update(entity);
checkPriceQtyAndAmount(entity, 15, 10, 150);
}

private void checkPriceQtyAndAmount(PurYearPlan entity, int price, int qty, int amount) {
assertThat(entity.getPrice(), is(new BigDecimal(price)));
assertThat(entity.getQty(), is(new BigDecimal(qty)));
assertThat(entity.getAmount(), is(new BigDecimal(amount)));
}

private PurYearPlanDetail makeNewDetailWithPriceAndQty(int price, int qty) {
PurYearPlanDetail detail = new PurYearPlanDetail();
detail.setPrice(new BigDecimal(price));
detail.setQty(new BigDecimal(qty));
return detail;
}

생각해 볼 것

  • 서비스 단위 테스트와 통합 테스트 구분이 좀 애매 하다.

무조건 단위 테스트를 하기로 하고,
단위 테스트가 정 불편한 어떤 이유(그 이유를 정리해둬야겠다)를 가진 것들만 통합 테스트를 하는것이 좋치 않을까?

  • 서비스 단위 테스트와 통합 테스트의 성능 차이는 꽤 크다.

그런면에서도 통합테스트 보다는 단위 테스트가 더 좋아보인다.
쌓이고 쌓이고 쌓이다보면 빌드 하는데 엄청 오래 거릴 것이다.

  • 테스트 작성 및 이해가 비교적 쉽다

목킹을 하지 않기 때문인지, 목킹 코딩에 익숙치 않아서 인지,
목킹을 하면 역시 가독성도 떨어지고 테스트 작성이 완만하지 않다.
그럼 다시 반대로 통합 테스트를 기본으로 하고 복잡하고 오래 걸리는 DAO를 사용할 경우에만 단위 테스트로 옮길까?

[OSAF 테스트 가이드 초안] 서비스 단위 테스트

서비스 단위 테스트

DAO를 사용하지 않는 부분을 테스트 하거나,
테스트 하려는 부분에서 사요하는 DAO 기능이 충분한 테스트를 거치지 않았거나,
테스트에서 사용하는 DAO가 아직 제대로 구현되지 않았거나,
실행이 오래 걸리는 DAO를 사용할 경우에
DAO를 목킹(mocking)하여 서비스 계층의 코드만 단위 테스트 할 수 있습니다.

테스트 하지 않을 것

DAO쪽으로 단순 위임하는 코드는 테스트하지 않습니다.

	public PurYearPlanDetail getPurYearPlanDetailById(int detailid) {
return dao.getPurYearPlanDetailById(detailid);
}

테스트 할 것

  • 서비스 인터페이스에 새로 추가하고 구현한 메서드
  • OSAF의 GenericService에서 상속받은 것을 재정의한 메서드

OSAF의 GenericService에서 상속 받은 것 중에서 재정의한 메서드를 테스트 합니다.

	public void update(PurYearPlan entity) {
super.update(entity);
entity.calcAll();
}
  • 비즈니스 로직

서비스에 포함되어 있는 복잡한 비즈니스 로직을 테스트 합니다.

	public void addPurYearPlanDetail(int yearplanid, PurYearPlanDetail detail) {
detail.calcAmount();
get(yearplanid).addDetail(detail);
}

public String getAutoGeneratedCode(Date year) {
String currentCode = null;
SimpleDateFormat formatter = new SimpleDateFormat("yyyy");
String syear = formatter.format(year);

String previousCode = dao.getCurrentCode(year);
if (previousCode != null) {
Integer currentNo = Integer.valueOf(previousCode.substring(4)) + 1;
currentCode = (currentNo / 10) > 0 ? syear + currentNo.toString()
: syear + "0" + currentNo.toString();
} else {
currentCode = syear + "01";
}
return currentCode;
}

테스트 코드 작성 방법

  • Mockito 러너 추가

MockitoJUnitRunner를 추가합니다.

  • 테스트 클래스 이름

테스트 대상인 클래스 이름에 UnitTest를 붙여줍니다.

@RunWith(MockitoJUnitRunner.class)
public class PurYearPlanServiceImplUnitTest {

}
  • 의존성 Mocking

@Mock을 사용해서 의존하는 인터페이스에 대한 목 객체를 만들고, @Before를 붙인 setUp 메서드에서 테스트 객체에 주입해 줍니다.
@Mock을 이용하면 mock(PurYearPlanDao.class); 같은 코드를 호출하지 않아도 MockitoJUnitRunner에 의해 자동으로 목 객체가 생성됩니다.

@RunWith(MockitoJUnitRunner.class)
public class PurYearPlanServiceImplUnitTest {

PurYearPlanServiceImpl service;
@Mock PurYearPlanDao mockDao;

@Before
public void setUp() {
service = new PurYearPlanServiceImpl();
service.setDao(mockDao);
}
...
}
  • 테스트 작성

테스테에 필요한 객체들을 정의하고 테스트 이후에 변경되는 값의 현재 상태를 확인합니다.
Mockito의 stub()을 사용하여 목 객체가 어떻게 동작해야 하는지 정의해 줍니다.
그런 다음 테스트 하려는 메서드를 실행합니다.
마지막으로 비즈니스 로직이 제대로 수행되었는지 여러 객체의 상태들을 확입합니다.

	@Test
public void addPurYearPlanDetail() throws Exception {
PurYearPlan yearPlan = new PurYearPlan();
int yearplanid = 1;
yearPlan.setId(yearplanid);
PurYearPlanDetail detail = new PurYearPlanDetail();

when(mockDao.get(yearplanid)).thenReturn(yearPlan);

service.addPurYearPlanDetail(yearplanid, detail);
assertThat(yearPlan.getDetails().size(), is(1));
}

생각해 볼 것

  • 목킹을 사용하는 여러 가지 경우를 좀 더 구체적으로 작성해야겠다.

[OSAF 테스트 가이드 초안] DAO 통합 테스트

어렴풋이 생각은 해봤었는데, 이번 기회에 정리해 보네요. 매우 주관적이며 OSAF 프레임워크를 사용하는 경우에 해당하는 가이드이기 때문에 통용될리는 없다고 생각합니다. 하지만 적어도 저한테는 이렇게 하는 것이 타당해 보이며, 조금만 응용하면, 다른 프레임워크 또는 별도의 프레임워크가 없는 경우에도 어느정도 적절할 것으로 보입니다. 그럼.. 차근차근 정리해 보겠습니다.

DAO 통합 테스트

DAO 테스트는 항상 데이터베이스를 필요로 하기 때문에 통합 테스트로 볼 수 있습니다.

테스트하지 않을 것

  • 프레임워크 코드

OSAF의 GenericDao로부터 상속받은 코드와 하이버네이트 코드는 테스트하지 않습니다.

테스트 할 것

  • 하이버네이트 도메인 모델 검증

도메인 모델에 맵핑 정보가 제대로 설정되었는지 확인합니다.
OSAF GenericDao가 제공하는 save로 도메인 모델을 저장해보는 것으로 확인합니다.

	@Test
public void crud() throws Exception {
Code code = new Code();
code.setCodeCate(CodeCate.CAR_TYPE);
code.setName("BLK");

cd.save(code);
}
  • DAO에서 재정의하는(overriding)하는 addRestrictions 메서드

해당 메서드에서 정의하는 검색 조건과 정렬 옵션에 따른 쿼리가 제대로 동작하는지 테스트 합니다.

	protected Criteria addRestrictions(Criteria c, CodeParams params) {
CriteriaUtils.ilike(c, "name", params.getName(), MatchMode.ANYWHERE);
CriteriaUtils.conditionalEq(c, "codeCate", params.getCodeCate());
c.addOrder(Order.asc("codeCate"));
return c;
}
  • DAO 인터페이스에 별도로 추가한 메서드의 구현체

OSAF에서 상속받지 않은 코드로, DAO 인터페이스에 추가하고, DaoImpl 클래스에서 구현한 메서드도 테스트가 필요합니다.

//DAO 인터페이스
public interface CodeDao extends GenericDao<Code, CodeParams> {
public List<Code> findByCodeCate(CodeCate codeCate);
}

//DAO 인터페이스 구현체
@Repository
public class CodeDaoImpl extends GenericDaoImpl<Code, CodeParams> implements CodeDao{

...

@SuppressWarnings("unchecked")
public List<Code> findByCodeCate(CodeCate codeCate){
return super.getSession().createCriteria(Code.class).add(
Restrictions.eq("codeCate", codeCate)).list();
}

}

테스트 코드 작성 방법

  • 스프링 @Test 선언

애노테이션 추가: DAO 테스트에 스프링 테스트 러너, 설정 파일 위치, 트랜잭션을 설정합니다.

테스트 설정 파일 위치: 테스트용 스프링 설정파일은 test 폴터 밑에 위치한 testApplicationContext.xml을 사용합니다.

트랜잭션 설정: OSAF의 DAO는 트랜잭션 당 세션을 유지하기 위해 getCurrentSession()을 사용하고 있기 때문에 @Transactional을 반드시 추가해야 합니다.

테스트 클래스 이름: 테스트할 DAO 구현체 이름뒤에 Test를 붙여서 작명합니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/testApplicationContext.xml")
@Transactional()
public class CodeDaoImplTest {

}
  • DBUnitSupport 클래스 상속.

DBUnit을 편하게 사용할 수 있도록 DBUbnitSupport 클래스를 상속 받습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/testApplicationContext.xml")
@Transactional()
public class CodeDaoImplTest extends DBUnitSupport{

}
  • 테스트 데이터를 적성합니다.

XML 테스트 데이터가 필요한 경우 테스트 클래스와 동일한 패키지에 두어 접근성을 높여줍니다.
XML 테스트 데이터는 다음과 같이 작성합니다.

<dataset>
<code id="1" name="test" codeCate="10" />
<code id="2" name="test" codeCate="10" />
<code id="3" name="test" codeCate="20" />
<code id="4" name="test" codeCate="20" />
</dataset>
  • 테스트를 작성합니다.

테스트 데이터가 필요한 테스트는 DBUnitSupport에서 상속받은 insertXmlData();
또는 inserXlsData(); 메서드를 이용하여 테스트 데이터를 넣어줍니다.

	@Test
public void search() throws Exception {
insertXmlData("testData.xml");
CodeParams params = new CodeParams();
OrderPage orderPage = new OrderPage();

params.setName("te");
assertThat(cd.search(params, orderPage).size(), is(4));

params.setName("");
params.setCodeCate(PersistentEnumUtil.valueOf(CodeCate.class, 20));
assertEquals(2, cd.search(params, orderPage).size());
}

생각해볼 것

  • 테스트 데이터베이스와 실제 데이터페이스가 다르다

배포용으로는 MySQL을 사용하면서 테스트할 때는 HSQL을 사용한다. 과연 제대로 테스트 했다고 볼 수 있을까?

OpenSprout Nexus 이용하여 스프링 3.0 라이브러리 추가하기

참조:
Spring 3.0 (4) – Maven에서 Spring 3.0 최신버전 사용하기
Spring3.0 (5) – 스프링 모듈의 의존관계
Maven의 version range를 사용할 때 주의할 점

일단 사부님이 관리 중인 OpenSprout Nexus를 메이븐 settings.xml나 프로젝트의 pom.xml에 등록해 주세요.

<repository> 
    <id>spring-latest</id> 
    <name>Spring Latest by OpenSprout</name> 
    <url>http://www.opensprout.org/nexus/content/repositories/spring-latest</url> 
</repository> 

다음으로 스프링 번들 리파지토리도 등록해주세요.

<repository> 
    <id>com.springsource.repository.bundles.external</id>  
    <name>SpringSource Enterprise Bundle Repository – External Bundle Releases</name> 
    <url>http://repository.springsource.com/maven/bundles/external</url> 
</repository>

이제 준비는 끝났습니다. 본격적으로 스프링 3.0 라이브러리를 추가하면 됩니다. 추가하는 방법은 두 가지가 있을 수 있습니다. 모든 라이브러리를 직관적으로 명시해주는 방법과 추이적 종속성을 이용하여 반드시 명시해야 할 것만 명시하는 방법이 있습니다.

사용자 삽입 이미지원본 이미지: http://toby.epril.com/?p=598

사부님이 그린 그림을 보면 빨간색 박스로 표시한 라이브러리만 추가하면 파란색으로 칠한 모든 라이브러리를 추이적으로 가져올 거라는 것을 알 수 있습니다. 따라서.. pom.xml에 다음과 같이 설정하면 스프링 라이브러리 중에 필요한 것은 대부분 가져옵니다.

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.web.servlet</artifactId>
            <version>[3.0.0.BUILD-00000000000000,9.9.9.BUILD]</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.aspects</artifactId>
            <version>[3.0.0.BUILD-00000000000000,9.9.9.BUILD]</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.orm</artifactId>
            <version>[3.0.0.BUILD-00000000000000,9.9.9.BUILD]</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.test</artifactId>
            <version>[3.0.0.BUILD-00000000000000,9.9.9.BUILD]</version>
        </dependency>

버전 범위 표시를 저렇게 한 이유는 세 번째 참조 글에서 확인할 수 있습니다.

이렇게 해서 가져온 라이브러리들을 M2Eclipse 플러긴의 Dependency Graph 또는 Dependency Hierarchy로 확인할 수 있습니다.

사용자 삽입 이미지사용자 삽입 이미지
스프링 3.0이 어떤 제 3 라이브러리(어떤 버전을) 사용하는지 궁금하다면

http://spreadsheets.google.com/pub?key=ppDRa3Yit-05zS2cqWYFlNA

사부님이 작성하신 위 문서를 참조하시면 됩니다.

나이스!! 이제는 드디어 스프링 3.0 개발을 시작합니다~