Whiteship's Note

'모하니?/Coding'에 해당되는 글 299건

  1. 2008.03.24 Hibernate 에러 공유 1
  2. 2008.03.02 EasyMock - Using Stub Behavior for Methods
  3. 2008.03.02 EasyMock - Flexible Expectations with Argument Matchers
  4. 2008.03.02 EasyMock - Strict Mocks, Switching Order Checking On and Off
  5. 2008.03.01 EasyMock - Relaxing Call Counts
  6. 2008.03.01 EasyMock - Changing Behavior for the Same Method Call
  7. 2008.03.01 EasyMock - Creating Return Values or Exceptions
  8. 2008.03.01 EasyMock - Working with Exceptions
  9. 2008.03.01 EasyMock - Specifying Return Values
  10. 2008.03.01 EasyMock - Expecting an Explicit Number of Calls (2)
  11. 2008.02.22 HibernateExceptionToDataAccessExceptionApsect (2)
  12. 2008.02.22 HibernateExceptionToDataAccessExceptionApsect 구상 중 (2)
  13. 2008.02.06 EasyMock으로 클래스의 Mock 객체 만들기
  14. 2008.01.23 99% Line Coverage
  15. 2008.01.21 하이버 + 스프링 관련 XML Template (2)
  16. 2008.01.15 PropertyEditorSupport 살펴보기 3 (2)
  17. 2008.01.15 PropertyEditorSupport 살펴보기 2
  18. 2008.01.15 PropertyEditorSupport 살펴보기 1
  19. 2008.01.13 Many To One 관계를 폼을 통해서 세팅하는 방법 고민 중.
  20. 2007.12.27 엑셀 자바로 다루기
  21. 2007.12.13 테스트의 소중함 (2)
  22. 2007.10.25 Acegi 필터 등록할 때 발생할 수 있는 몹쓸 버그
  23. 2007.10.25 LocaleChangeInterceptor(국제화 지원 인터셉터)가 있었군요.
  24. 2007.10.24 WebUtils 사용하기 (2)
  25. 2007.10.15 프로퍼티파일 변경 후 재컴파일이 필요하지 않을까?
  26. 2007.10.11 중복되는 라이브러리가 발생시킬 수 있는 문제 (2)
  27. 2007.10.06 구글 캘린더 정보 가져오기
  28. 2007.09.28 Spring AOP 예제 코드
  29. 2007.09.27 commons.lang.builder 패키지 (2)
  30. 2007.09.23 테스트 할 때 추상 클래스 활용하기

Hibernate 에러 공유 1

모하니?/Coding : 2008.03.24 21:46


2008-03-24 12:35:49,674 WARN [org.hibernate.util.JDBCExceptionReporter] - <SQL Error: 0, SQLState: null>
2008-03-24 12:35:49,674 ERROR [org.hibernate.util.JDBCExceptionReporter] - <Batch entry 0 insert into Material (cdate, itemcnt, itemtypecnt, month, purprice, suppid, udate, materialid) values (2008-03-24 12:35:49.659000 +00:00:00, 0, 0, 200802, 0.0, 1, 2008-03-24 12:35:49.659000 +00:00:00, 89) was aborted.  Call getNextException to see the cause.>
2008-03-24 12:35:49,674 WARN [org.hibernate.util.JDBCExceptionReporter] - <SQL Error: 0, SQLState: 23505>
2008-03-24 12:35:49,674 ERROR [org.hibernate.util.JDBCExceptionReporter] - <ERROR: duplicate key violates unique constraint "=========">
2008-03-24 12:35:49,674 ERROR [org.hibernate.event.def.AbstractFlushingEventListener] - <Could not synchronize database state with session>
org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update
    at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:71)
    at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:43)
    at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:253)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:237)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:141)
    at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:298)
    at org.hibernate.event.def.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:41)
    at org.hibernate.impl.SessionImpl.autoFlushIfRequired(SessionImpl.java:969)
    at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1562)
    at org.hibernate.impl.CriteriaImpl.list(CriteriaImpl.java:283)
   
    at org.springframework.orm.hibernate3.HibernateTemplate.execute(HibernateTemplate.java:374)
    at org.springframework.orm.hibernate3.HibernateTemplate.executeFind(HibernateTemplate.java:343)
   
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)

Caused by: java.sql.BatchUpdateException: Batch entry 0 insert into ====  Call getNextException to see the cause.
    at org.postgresql.jdbc2.AbstractJdbc2Statement$BatchResultHandler.handleError(AbstractJdbc2Statement.java:2534)
    at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1328)
    at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:352)
    at org.postgresql.jdbc2.AbstractJdbc2Statement.executeBatch(AbstractJdbc2Statement.java:2596)
    at com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.executeBatch(NewProxyPreparedStatement.java:1723)
    at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:48)
    at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:246)


이것 때문에 이제 퇴근합니다. 더 늦게가면 운동 할 때 너무 추워서;; 삽질은 한 번으로 끝내자꾸나!!

위와 비슷한 에러를 만나시면, save()를 호출하기 전에 flushAndClear()를 호출해보세요.

외국인들을 위해 영어로도 한 마디..

If you meet this kind errors, just try sychnonize with DB(flushAndClear() or flush())before write(save(), saveOrUpdate()) any data to DB.
top


EasyMock - Using Stub Behavior for Methods

모하니?/Coding : 2008.03.02 00:51


EasyMock으로 생성한 Mock을 마치 Stub처럼 사용할 수 있습니다. 이 말이 무슨 말이냐면... Mock은 Stub과 달리 예측이라는 개념이 추가되어 있습니다. 따라서 예측 대로 Mock이 수행되지 않으면 AssertionError가 발생하는데, Stub은 그런 개념이 없고, 따라서 구현해둔 메소드가 호출 되든 말든 상관이 없습니다. 다시 돌아가서 Stub 처럼 사용한 다는 것은 Mock으로 레코딩 한 부분이 호출되지 않아도 테스트가 통과 되도록 작성하는 것입니다.

언제 이런 걸 사용할까요. 몇 번이나 호출될지 예상할 수 없을 때 사용할 수 있을 것 같습니다. 아니면 기본적인 규칙이 있다면, 굳이 매번 설정할 필요 없이 EasyMock 문서에 나온대로 사용할 수도 있겠습니다.

expect(mock.voteForRemoval("Document")).andReturn(42);
expect(mock.voteForRemoval(not(eq("Document")))).andStubReturn(-1);

인자가 "Document"일 때는 42를 반환하고, "Document"라는 인자가 아닐 때는 -1을 반환하도록 레코딩 되어 있습니다.

voteForRemoval이라는 메소드는 최소 한 번은 "Document"라는 인자를 받아서 42를 반환하도록 되어 있으며 그 뒤로는 몇 번 호출될지도 모르지만, "Document"라는 인자가 오지는 않을 것이며, 만약 이런 메소드가 호출되면, -1을 반환하도록 레코딩 한 것입니다.

    @Test
    public void arguments() throws Exception {
        expect(memberDao.bar(1)).andReturn(1);
        expect(memberDao.bar(not(eq(1)))).andStubReturn(2);
       
        replay(memberDao);
        int i1 = memberServiceImpl.bar(1);
//        int i2 = memberServiceImpl.bar(2);
//        int i3 = memberServiceImpl.bar(2);

        assertEquals(1, i1);
//        assertEquals(2, i2);
//        assertEquals(2, i3);
        verify(memberDao);
    }

따라서 위의 코드에서 주석을 풀어도 되고, 안 풀어도 테스트는 통과합니다.
andStubReturn(), andStubThrow(), andStubAnswer() 를 사용할 수 있습니다.
top


EasyMock - Flexible Expectations with Argument Matchers

모하니?/Coding : 2008.03.02 00:36


레코딩을 할 때, 테스트가 의존하는 메소드에 넘겨주는 아규먼트의 폭을 유동적으로 설정할 수 있습니다. 그때 사용하는 것들이 Argument Matcher입니다. 기본으로 제공해주는 것들을 사용할 수도 있고, 사용자가 새것을 정의해서 추가하여 사용할 수도 있습니다.

예를 들어.. memberService의 foo 메소드에 어떤 값을 넘겨주던 memberDao의 foo 메소드에는 integer 값이 넘어간다고 했을 때.. 어떻게 레코딩 해야할까요.

    @Test
    public void arguments() throws Exception {
        memberDao.foo(anyInt());
       
        replay(memberDao);
        memberServiceImpl.foo(1);
       
        verify(memberDao);
    }

anyInt() 라는 Argument Matcher를 사용해서 어떤 int값이 넘겨지도록 설정할 수 있습니다. 그러면 1이 넘겨지던 2가 넘겨지든 관계없이 테스트는 통과합니다.

EasyMock에서는 aryEq()을 소개하고 있는데.. 무슨 내용인지 잘 이해가 안되더군요;

    @Test
    public void arguments() throws Exception {
        String[] names = new String[]{"whiteship", "toby"};
        memberDao.foo(names);
       
        replay(memberDao);
        memberServiceImpl.foo(names);
       
        verify(memberDao);
    }

이렇게 해도 테스트는 통과 하던데.. 어떤 때에 aryEq()을 사용해야 한다는 것인지.. 훔.. 나중에 다시 살펴봐야겠습니다.
top


EasyMock - Strict Mocks, Switching Order Checking On and Off

모하니?/Coding : 2008.03.02 00:02


createMock()을 사용해서 얻어낸 Mock 객체는 기본적으로 메소드를 호출하는 순서를 확인하지는 않습니다. 그런데 경우에 떄라서는 메소드가 호출되는 순서가 중요할 수 있는데, 그럴 땐는 두 가지 방법이 있습니다.

1. Mock객체를 생성할 때 createMock()이 아니라, createStrickMock()을 호출하여 mock 객체를 생성하면, 순서까지 확인합니다.

MemberDaoMock = createStrickMocek(MemberDao.class);

2. Mock객체에 레코딩을 시작하기 전에, checkOrder(mock 객체, true); 를 사용해서 메소드의 호출 순서도 확인하도록 설정할 수 있습니다.

//테스트 코드

    @Test
    public void add() throws Exception {
        checkOrder(memberDao, true);
        Member member = new Member();
        memberDao.add(member);
        memberDao.get(member);
       
       
        replay(memberDao);
        memberServiceImpl.add(member);
       
        verify(memberDao);
    }

// MemberServiceImpl 코드

    public void add(Member member) {
        memberDao.get(member);
        // ToDo check email
        memberDao.add(member);
    }

get()으로 먼저 가져와서 확인한 다음에 추가해야 하기 때문에 위의 테스트 코드를 에러가 발생합니다.
top


EasyMock - Relaxing Call Counts

모하니?/Coding : 2008.03.01 23:52


테스트 대상이 호출하는 메소드의 호출 횟수를 예측할 때 횟수의 범위를 지정해서 어느정도 테스트에 여유를 줄 수 있습니다.

예를 들어, MemberServiceImpl의 add를 호출할 때, 이미 add하려는 Member 데이터의 email이 DB에 존재하는지 확인하기 위해서, DB에 findByEmail()을 한 번 이상 호출 한다고 가정하겠습니다. 이 메소드는 정확히 몇 번 호출이 될지는 모릅니다.(사실 테스트를 만드는 입장에서는 알고 있겠죠. 그냥 그렇다고 하겠습니다.)

그럴때 사용할 수 있는 것이 atLeastOnce() 입니다. 즉 최소한 한 번은 호출되어야 한다는 거죠. 한 번 호출되든 말든.. 상관없을 때는 anyTimes() 라는 메소드를 사용할 수 있고, 이런 메소드를 붙여주지 않으면 무조건 딱 한 번 호출되는 것으로 간주됩니다.

    @Test
    public void add() throws Exception {
        Member member = new Member();
        memberDao.get(member);
        expectLastCall().anyTimes();
       
        replay(memberDao);
        memberServiceImpl.add(member);
       
        verify(memberDao);
    }

time()로 횟수를 정확하게 명시하거나

top


EasyMock - Changing Behavior for the Same Method Call

모하니?/Coding : 2008.03.01 23:41


같은 메소드가 여러번 호출 될 때, 매번 예측되는 결과가 다르다면..

memberDao.add(member); // Member 객체가 무사히 저장된다.
memberDao.add(member); // 예외가 발생한다.

이런 경우에 그냥 fluent interface로 계속해서 andReturn() 이나 andThrow()를 호출하여 recording 할 수 있습니다.

memberDao.add(member)
    .andReturn(resultMember);
    .adndThrow(new DuplicatedEmailException());

테스트 클래스 내부에서 의존하는 객체를 여러번 호출할 때, 각각이 다른 행위를 해야 한다면 유용하겠습니다.
top


EasyMock - Creating Return Values or Exceptions

모하니?/Coding : 2008.03.01 12:36


리턴값이나 예외를 실제 메소드가 호출되는 시점에 만들고 싶을 수 있는데 그럴 때는 andAnswer()을 사용합니다.
expectLastCall().andAnswer(new IAnswer<Object>() {
public Object answer() throws Throwable {
((User) getCurrentArguments()[0]).setUserId(1);
return null;
}
});
이 전 글들에서 사용했던 andReturn()이나, andThrow()의 인자로는 미리 만들어 둔 객체들을 넘겨줬다면, andAnswer()에서는 IAnswer 인터페이스를 구현하여 특정 조건에 따라 각기 다른 결과를 반환하는 구현체를 만들어 둘 수 있습니다.

특정 조건이라고 한다면.. 어떤 데이터를 가지고 판단을 하겠다는 건데, 저 안에서 접근할 수 있는 데이터는  Obejct 타입의 배열을 반환하는 getCurrentAurguments()를 사용하여, 인지로 넘겨준 값에 접근할 수 있습니다.

반환하는 값이 없으면 위 처러 null을 리턴하면 되고, 반환하는 값이 있으면 그 값을 반환하고, 예외를 던지고 싶으면 던져주면 됩니다.
top


EasyMock - Working with Exceptions

모하니?/Coding : 2008.03.01 12:17


예외를 던지는 메소드가 있다면, andReturn()을 사용해서 정말 예외를 던지는 지 확인할 수 있습니다.

        memberDao.remove(member);
        expectLastCall().andThrow(new RuntimeException());
       
        replay(memberDao);
        memberServiceImpl.remove(member);
       
        verify(memberDao);

간단하지요.  인자로 왠지..class 타입을 넘겨줄 것 같았는데, andThrow(RuntimeException.class) 이렇게 말이죠. 근데;; 역시 객체를 넘겨야 되네요. 이런게 EasyMock 스타일인가 봅니다.
top


EasyMock - Specifying Return Values

모하니?/Coding : 2008.03.01 12:00


테스트의 대상이 사용하는 Mock의 메소드가 반환할 값이 필요한 경우에 다음과 같이 할 수 있습니다.

1. 예상되는 메소드 호출을 expect() 메소드로 감싸기.
2. andReturn(Object returnValue) 사용해서 예상되는 리턴값을 expect() 메소드 뒤에 .으로 이어서 호출하기
public void testVoteAgainstRemoval() {
mock.documentAdded("Document"); // expect document addition
// expect to be asked to vote for document removal, and vote against it
expect(mock.voteForRemoval("Document")).andReturn((byte) -42);
replay(mock);
classUnderTest.addDocument("Document", new byte[0]);
assertFalse(classUnderTest.removeDocument("Document"));
verify(mock);
}

저 위의 빨간 줄 한 줄을 expectLastCall()을 사용해서 두 줄로 나눌 수도 있습니다.

mock.voteForRemoval("Document");
expectLastCall().andReturn((byte) 42);

메소드가 심하게 길어지지 않는 이상 굳이 두 줄로 코드를 나눌 필요는 없겠습니다.


top


EasyMock - Expecting an Explicit Number of Calls

모하니?/Coding : 2008.03.01 11:53


같은 메소드 호출을 여러번 하는 경우 그런 행위를 녹화하기 위해서, 메소드를 여러번 호출할 수도 있지만.

    public void testAddAndChangeDocument() {
mock.documentAdded("Document");
mock.documentChanged("Document");
mock.documentChanged("Document");
mock.documentChanged("Document");
replay(mock);
classUnderTest.addDocument("Document", new byte[0]);
classUnderTest.addDocument("Document", new byte[0]);
classUnderTest.addDocument("Document", new byte[0]);
classUnderTest.addDocument("Document", new byte[0]);
verify(mock);
}

이런 코드를 expectLastCall().times(3); 를 사용해서 코드를 줄일 수 있다.

    public void testAddAndChangeDocument() {
mock.documentAdded("Document");
mock.documentChanged("Document");
expectLastCall().times(3);
        replay(mock);
classUnderTest.addDocument("Document", new byte[0]);
classUnderTest.addDocument("Document", new byte[0]);
classUnderTest.addDocument("Document", new byte[0]);
classUnderTest.addDocument("Document", new byte[0]);
verify(mock);
}

top


HibernateExceptionToDataAccessExceptionApsect

모하니?/Coding : 2008.02.22 13:52



테스트 코드를 작성하고, 만들기 시작했습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class MemberDaoTest {
   
    @Autowired
    private MemberDao memberDao;
   
    @Test(expected=DataAccessException.class)
    public void hibernateExceptoin() throws Exception {
        assertNotNull(memberDao);
       
        memberDao.crazy();
    }
   
}

XML 설정 파일에는 별 거 없습니다.

    <bean class="whiteship.dao.support.HibernateExceptionToDataAccessExceptionAspect" />
   
    <aop:aspectj-autoproxy/>

위에서 만든 Apsect를 bean으로 등록해주고, Auto Proxy를 사용하도록 설정했습니다.

crazy라는 메소드는 다음과 같이 구현되어있습니다.

    public void crazy() {
        sessionFactory.getCurrentSession().createQuery("Delete From IWantYou");
    }

IWantYou라는 테이블이 있을리 만무하기 때문에, 당연히 저런 테이블을 찾을 수 없다는 HibernateException이 발생할 것입니다. 그러나 그걸 HibernateExceptionToDataAccessExceptionAspect가 적용된 프록시가 가로채서 스프링의 DataAccessException으로 바꿔서 다시 던져줄 겁니다. 그래서 위에 있는 테스트는 통과하게 되어있죠.

사용자 삽입 이미지


처음에는 @Around를 사용해서 구현했는데, 그럴 필요가 없더군요. @AfterThrowing만 사용해도 충분히 제 역할을 할 수 있길래, 바꿨습니다. 소스 코드는 대략.10줄.

@Aspect
public class HibernateExceptionToDataAccessExceptionAspect {

    @Pointcut("execution(* *.*Dao.*(..))")
    private void anyHibernateDao() {
    }

    @AfterThrowing(pointcut="anyHibernateDao()", throwing="e")
    public void doTranslate(Throwable e) throws Throwable {
        if (e instanceof HibernateException) {
            throw SessionFactoryUtils.convertHibernateAccessException((HibernateException) e);
        }
        throw e;
    }
}



top


HibernateExceptionToDataAccessExceptionApsect 구상 중

모하니?/Coding : 2008.02.22 12:26


어제랑 오늘은 회사 이사를 하느라고 공부를 제대로 못하겠네요. 아 삭신이.. 쑤시네요. ㅠ.ㅠ 살이 조금이라도 빠지기를 바라며.. 각설하고 본론으로 들어갑니다.

스프링에서 Hibernate를 사용해서 DAO를 구현하는 방법은 세 가지가 있습니다. 각각은 다음과 같은 특징이 있습니다.

HibernateTemplate

  • JDBC Template과 비슷한 방식이다. 기존 스프링 사용자들에게 가장 익숙한 방법.
  • Session에 직접 접근할 필요가 있을 때는 콜백 메소드를 사용해야 한다.
  • 스프링 클래스를 사용해야만 한다.
  • HibernateDaoSupport를 사용하면 SessionFactory의 세터를 만들 필요가 없다.

Spring 기반 DAO

  • 콜백을 사용하지 않고도 Session에 직접 접근할 수 있다.
  • HibernateDaoSuppoer의 getSession(Boolean)을 사용한다.
  • unchecked exception은 물론이고 checked exception도 던질 수 있다.
  • convertHibernateAccessException()를 사용해서 하이버네이트의 예외를 스프링의 DAE 형태로 바꿀 수 있다. 하지만 try-catch를 해서 다시 던져야 한다.

Hibernate 3 API

  • 하이버 3이 "contextual Sessions"이라는 개념으로 하이버 자체에서 트랜잭션 당 Session 하나를 관리한다.
  • 스프링 코드를 하나도 사용하지 않는다.
  • 엊그제 저녁 토비형이 언급한 부분

Fortunately, Spring's LocalSessionFactoryBean supports Hibernate's SessionFactory.getCurrentSession() method for any Spring transaction strategy, returning the current Spring-managed transactional Session even with HibernateTransactionManager. Of course, the standard behavior of that method remains: returning the current Session associated with the ongoing JTA transaction, if any (no matter whether driven by Spring's JtaTransactionManager, by EJB CMT, or by JTA).


예전에 정리해둔 내용인데 다 까먹었서, 엊그제 토비형이 물어봤는데 이상한 대답을 했습니다. 머리에서 점점 사라져 가는 스프링.. 난 널 원해.. 내 머리에서 나가지 말아죠.

어쨋거나. 세 번째 경우의 DAO의 코드가 가장 심플한데 딱 하나의 단점(?)을 가지고 있습니다. 바로 예외와 관련된 부분인데, 하이버 3.0 부터 Unchecked Exception으로 바꾸긴 했지만, 스프링의 Data Access Exception을 꼭 사용하고 싶을 수도 있겠습니다.

예외 처리가 필요 없거나, 하이버네이트를 매우 많이 사용하는 애플리케이션이라면 그냥 사용하면 되겠지만, 스프링의 DAE를 사용하여 OptimisticLockingFailureException 이런 에러를 잡아 내고 싶다면, 이런 제약 사항이 별로 달갑지 않을 것입니다.

그래서.. 생각 해본 게 바로 HibernateException을 DataAccessException으로 바꿔주는 Aspect입니다. HibernateException이 발생하면, 해당 예외를 잡아서 DataAccessException으로 바꿔서 다시 던지는 역할을 하는 Aspect를 만들고 싶습니다. 재미로..

Apsect 구현은 간단하게 Spring의 @Aspect 를 사용하는 방법과 AspectJ를 사용하여 구현한 다음 Spring과 연동하는 방법이 있는데 전자를 먼저 해보고, 잘 되면 후자를 해보겠습니다.

ps:  설마 이미 누가 만들어 두진 않았겠죠. 간단한거라.. 그랬을 수도 있겠지만 재미삼아 ㄱㄱㅆ
top


EasyMock으로 클래스의 Mock 객체 만들기

모하니?/Coding : 2008.02.06 16:59


방명록에서 '제도관퐈터'님의 질문으로 인해 오랜만에 EasyMock을 아주 살짝 살펴봤습니다. 질문은 JFrame의 Mock객체가 안 만들어진다는 것이었습니다. 그래서 해봤습니다.

import javax.swing.JFrame;
import static org.easymock.EasyMock.*;
import static org.junit.Assert.*;

import org.junit.Test;

public class What {

    @Test
    public void createJFrameMock() throws Exception {
        JFrame mockFrame = createMock(JFrame.class);
        assertNotNull(mockFrame);
    }

}

사용자 삽입 이미지

안 되는 군요. 에러 메시지를 읽어보니까 원인도 보여줍니다. 인터페이스가 아니니까 못 만든다는 것입니다. 이걸 보셨으면 저한테 굳이 질문을 하셨을까 싶지만... 그래도 뭐 심심하니까 검색해봤습니다.

easymock createmock class 라고 입력하면, Toby'epril 블로그의 내용 중에 하나가 검색 됩니다. Hibernate Dao Test 코드 만드는 것과 관련된 Toby님의 글입니다.

거기에 보면 이지목 확장 클래스를 사용해서 HibernateTemplate의 mock객체를 만들었다는 이야기가 나오고 소스코드도 있습니다.

그래서 EasyMock 다운로드 사이트에 가봤더니 정말고 확장 클래스라는것이 있었습니다.
사용자 삽입 이미지

테스트 코드를 바꿨습니다.

import static org.junit.Assert.assertNotNull;
import static org.easymock.classextension.EasyMock.*;
import javax.swing.JFrame;

import org.junit.Test;

public class What {

    @Test
    public void createJFrameMock() throws Exception {
        JFrame mockFrame = createMock(JFrame.class);
        assertNotNull(mockFrame);
    }

}

테스트 코드는 실패합니다. 왜냐면 Cglib을 필요로 합니다. 인터페이스가 아니라 클래스의 프록시를 만들려면 필요한 라이브러리죠. Cglib은 spring 2.5 lib 폴더에 있는 것을 사용했습니다.


저걸 추가해주고 다시 테스트를 하면 테스트는 통과합니다.

사용자 삽입 이미지

이제 클래스의 Mock 객체를 만들어서 사용하실 수 있습니다.
top


99% Line Coverage

모하니?/Coding : 2008.01.23 16:55


몇일전 기록한 98% Line Coverage 기록을 깨습니다. 이제 남은 건 100% Line Coverage 뿐이군요.

사용자 삽입 이미지

볼링 게임을 구현한 것인데, 완성하는데 하루가 걸렸습니다. 아 어제 이맘때부터 계속 머릿속을 괴롭히던 녀석을 처리하니 피곤이 몰려오는 것 같습니다.

볼링 게임이 매우 간단한 것 같은데, Stike, Spare 점수 계산 그리고 마지막 프레임의 특이함 때문에 코드가 금방 지져분해집니다.

만들면서 느낌점을 요약했는데, 다음과 같습니다.

느낀점.
- 어떤 테스트를 통과Pass 하게 만들려다가 그 전에 먼저 작성해야 할 메소드가 생긴다.
    - 이 때는 지금 하던 걸 멈추고 사전에 먼저 작성해야 할 것 부터 만들자.
    - @Ignore
- 테스트를 잘못 작성하면 구멍이 생긴다.
    - 테스트를 작게 잘 작성하자. 하나의 Task가 5분을 넘기지 말아야 한다.
    - 어떻게 하면 구멍이 생기지 않는 테스트를 작성할 수 있을까?
- 객체 지향 기본 원칙 지키며 코딩하는게 정말 어렵다.
    - 코딩하다보면, 어느새 객체 지향 원칙은 안중에도 없어 진다. 사실 잘 발견을 못한다.
    - 변하는 부분과 변하지 않는 부분을 분리하라.
    - 어떻게 하면 자연스럽게 패턴이 튀어나올까?


top


하이버 + 스프링 관련 XML Template

모하니?/Coding : 2008.01.21 17:30



사용자 삽입 이미지
위 화면에서 import로 등록하시면 다음과 같은 template 코드를 XML에서 사용하실 수 있습니다.

1. Spring 설정파일 XML
name: springContext
code:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xmlns:lang="http://www.springframework.org/schema/lang"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
        http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <context:component-scan base-package="${cursor}" />
   
</beans>

2. Session Factory 설정파일
name: sessionFactory
code:
    <bean id="sessionFactory"
        class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.format_sql">true</prop>
                <prop key="hibernate.hbm2ddl.auto">update</prop>
                <prop key="hibernate.connection.autocommit">false</prop>
            </props>
        </property>
        <property name="annotatedClasses" ref="annotatedClasses" />
    </bean>

    <util:list id="annotatedClasses">
        <value>${cursor}</value>
    </util:list>

3. datasource
name: datasource
code:
    <bean id="dataSource"
        class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver" />
        <property name="url" value="jdbc:hsqldb:hsql://localhost" />
        <property name="username" value="sa" />
        <property name="password" value="" />
    </bean>

4. transactionManager
name: transactionManager
code:
    <bean id="transactionManager"
        class="org.springframework.orm.hibernate3.HibernateTransactionManager"
        p:dataSource-ref="dataSource" p:sessionFactory-ref="sessionFactory" />

    <tx:annotation-driven transaction-manager="transactionManager" />

5. 기타
name: web.log4j  <= web.xml 에 추가해줄 log4j 관련 설정
name: web.encoding <= web.xml에 추가해줄 UTF-8로 인코딩 설정
name: web.hibernateOSIV <= 하이버네이트 Open Session In View 필터 설정, 이 녀석은 handlerMapping에 interceptor를 등록해도 되는데, 일단은...
name: web.springMVC <= web.xml에 추가해줄 Dispatcher Servlet 설정.
name: web.springContext <= web.xml에 추가해줄 Spring Context Loader Listener 설정.
name: viewResolver <= 이녀석은 web.xml에 포함되지 않으므로 web. 을 안붙였는데 나중에 spring. 을 앞에 붙여줄까 합니다. spring.handlerMapping 을 만들면서 수정해야겠습니다.

맨 위의 파일을 import 하셨으면 다음과 같이 위의 템플릿이 추가된 것을 확인할 수 있습니다.

사용자 삽입 이미지

그럼 이제 XML 파일에서 사용하시면 됩니다. 가장 자주 사용하는 스프링 설정 파일의 경우 Spring IDE를 사용해서 만들어도 되지만, 저는 저렇게 탬플릿 등록해놓고 쓰는게 좀 더 빠르고 편한 것 같습니다.

1. springContxt는 tag가 아니라, new xml 타입으로 설정해 두었기 때문에 XML에서 아무것도 입력하지 않은 상태에서 Ctrl + Space를 클릭하면 다음과 같이 선택할 수 있는 창이 뜹니다.

사용자 삽입 이미지

2. 그밖에 Tag로 설정해둔 탬플릿은 name의 일부를 입력한 다음 Ctrl + Space를 클릭하면 볼 수 있습니다. 아.. Ctrl + Space를 먼저 입력한 다음에 name의 일부를 입력하셔도 됩니다. 어차피 자동완성이 발동 된 후에 검색을 시작하기 때문에 후자의 방법이 더 편할 수도 있습니다.

사용자 삽입 이미지

이 간단한 파일을 스프링 하이버 관련
1. Java Template
2. XML Template

위 두 개의 파일을 차차 업그레이드 해갈 계획입니다.
top


PropertyEditorSupport 살펴보기 3

모하니?/Coding : 2008.01.15 23:54


참조 : 자바빈 Property Editors

Petclinic의 petForm.jsp 파일을 보면 다음과 같은 코드가 있습니다.

    <tr>
      <th>
        Type: <form:errors path="type" cssClass="errors"/>
        <br/>
        <form:select path="type" items="${types}"/>
      </th>
    </tr>

여기서 굵은 글씨에 주목해야 합니다. type은 문자열 타입이 아닙니다. 스프링이 기본으로 제공해주는 PropertyEditor들이 primitive 타입들과 그것의 배열들은 알아서 바인딩 해주지만, 저 타입은 스프링이 알지 못합니다. 따라서 자동으로 바인딩이 되지 않을 겁니다. 따라서 저 것은 그냥 객체입니다.

그런데, 문자열이 올 자리에 객체가 와서 그런건지, toString()이 호출 됩니다. 그리고 PetType 클래스에는 toString을 다음과 같이 구현해 뒀습니다.

    public String toString() {
        return this.getName();
    }

오호.. 그럼 저 자리에 문자열이 채워지고 따라서 getAsString() 메소드를 구현할 필요가 없어졌습니다. 그래서 그런지 PetTypeEditor 클래스에는 정말 getAsString()이 없습니다.

public class PetTypeEditor extends PropertyEditorSupport {

    private final Clinic clinic;


    public PetTypeEditor(Clinic clinic) {
        this.clinic = clinic;
    }

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        for (PetType type : this.clinic.getPetTypes()) {
            if (type.getName().equals(text)) {
                setValue(type);
            }
        }
    }

}

하지만 어디까지나 저런 경우는 특수한 경우일테니까요.(PetType에 속성이 name밖에 없더군요.) 대부분의 경우에는 getAsText()를 구현해서 써야겠죠.

public String getAsText() {
  Calendar value = (Calendar) getValue();
  return (value != null ?
    this.dateFormat.format(value.getTime()) :
    “”
  );
}

위 코드는 여기서 참조했습니다. getValue()를 사용해서 value 객체를 가져온 다음 이 객체가 가지고 있는 속성을 사용해서 문자열(String)을 만들어서 반환하고 있습니다.

이렇게 만들어 둔 Propery Editor를 스프링이 사용하도록 등록하는 방법이 있습니다. 여러 군대에서 사용되는 Property Editor라면

    <bean name="customEditorConfigurer" class="org.springframework.beans.factory.config.CustomEditorConfigurer">
        <property name="customEditors">
            <map>
                <entry key="java.util.regex.Pattern">
                    <bean class="PatternPropertyEditor"/>
                </entry>
            </map>
        </property>
    </bean>

이렇게 스프링 설정 파일에 있는 <property>의 value 값에 적용됩니다.

컨트롤러에서 사용할 거라면... 폼 컨트롤러 안에 다음과 같은 코드를 추가합니다.

protected void initBinder(HttpServletRequest request,
    ServletRequestDataBinder binder)
    throws Exception {
    binder.registerCustomEditor(
      Calendar.class,
      new CustomCalendarEditor(
        new SimpleDateFormat(“dd/MM/yyyy”), true));
  }

Spring 2.5에서는 다음과 같이 추가합니다.

@InitBinder
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(
                dateFormat, false));
    }

음... 이제 JSP의 EL에서 member.department.departmentid 이렇게 길게 쓰지 않아도 되겠습니다. 그냥 member.department 라고 써도 커스텀 에디터가 ember.department.departmentid 이렇게 쓴거나 동일하게 처리해 줄 것 입니다.
top


PropertyEditorSupport 살펴보기 2

모하니?/Coding : 2008.01.15 23:33


이전 글에 이어서 먼저 setValue(Object) 메소드를 보겠습니다. 이 녀석이 뭐하는 녀석인지... API에 써있지만 별로 와닿지 않습니다.

그래서 그냥 소스코들 봤습니다. 즉 PropertyEditor 인터페이스의 구현체인 PropertyEditorSupport 클래스를 살펴봤습니다.

    public void setValue(Object value) {
    this.value = value;
    firePropertyChange();
    }

위와같이 구현되어 있습니다. value라는 이름으로 객체를 받아와서 '속성 바꿔치기'를 하고 있군요. 저 메소드도 보이지만, 먼 산으로 갈까봐 관 둡니다. 이런.. 소스를 봐도 별 소득이 없네요. 예제 코드를 봐야겠습니다. 스프링의 Petclinic을 뒤져보면 다음과 같은 코드가 나옵니다.

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        for (PetType type : this.clinic.getPetTypes()) {
            if (type.getName().equals(text)) {
                setValue(type);
            }
        }
    }

setAsText에서 setValue()를 호출하고 있군요. 이 때 넘겨주는 객체는 PetType이라는 객체인 걸 보니, 이 프로퍼티 에디터는 PetType을 다루기 위해 작성된 것 같습니다. 잠깐 딴 길로 새서.. setAsText()를 보겠습니다.

    public void setAsText(String text) throws java.lang.IllegalArgumentException {
    if (value instanceof String) {
        setValue(text);
        return;
    }
    throw new java.lang.IllegalArgumentException(text);
    }

이 메소드는 String을 받아온 다음에 객체를 만들어서 setValue() 메소드에 넘겨주고 있습니다.

이제 조금 감이 잡힙니다.
사용자 삽입 이미지
어디에선가 입력된 텍스트를 가지고 객체를 만들어서 setValue() 메소드에 그 객체를 넘겨주는 메소드인가 봅니다. 그리고 setValue()는 아마도 해당 객체를 어딘가에서 값으로 사용하도록 넘겨줄 겁니다.

이제 String getAsText() 하나만 남아있군요. 이 메소드는 setAsText(String)과 반대일 것 같다는 느낌이 팍팍 듭니다. 다음과 같이 구현되어 있습니다.

    public String getAsText() {
    if (value instanceof String) {
        return (String)value;
    }
    return ("" + value);
    }

흠.. value 객체를 이번에는 거꾸로 String 타입으로 변환해서 반환하는 메소드입니다. PetType으로 예를 들자면,

    @Override
    public String getAsText() {
        Object value = getValue();
        if(value instanceof PetType)
            return ((PetType)value).getName();
        else
            throw new RuntimeException();
    }

이런식으로 구현할 수도 있겠습니다. PetType 객체가 오면 이 객체가 가지고 있는 name 속성이 이 객체를 대변하도록 말이죠.
사용자 삽입 이미지

그럼 대체 객체의 어떤 값이 해당 객체를 대신하도록 하는것이 좋을까요? 당연히 유일한 값이 좋겠죠. 흠..

다음에는 이런 PropertyEditor를 사용한 PetClinic 예제를 살펴보겠습니다.
top


PropertyEditorSupport 살펴보기 1

모하니?/Coding : 2008.01.15 22:49


이 클래스는 JDK 1.5에 추가된 클래스 입니다. 이 클래스를 좀 살펴보겠습니다. 이 클래스는 PropertyEditor 인터페이스를 구현하고 있습니다. 아마도 PropertyEditor를 쌩으로 구현하기는 불편하니까 Custom Editor를 구현하기 편하게 만들어둔 클래스로 유추 됩니다.
사용자 삽입 이미지


PropertyEditor 인터페이스를 보겠습니다.

사용자 삽입 이미지

API 대강 번역

PeopertyEditor 클래스는 GUI에 사용자가 주어진 타입의 속성 값을 편집하고 싶을 때 이를 지원하기 위해 제공된다

PropertyEditor는 속성 값을 보여주거나 수정할 수 있는 다양한 방법을 제공한다. 대부분의 PropertyEditor는 본 API 문서에서 가용한 옵션 중에 일부만 사용해도 충분할 것이다.

간단한 PropertyEditor들은 getAsTest와 setAsText 메소드만 사용할 것이고 복잡한 타입일 경우에는 paintValue와 getCustomEditor를 사용할 것이다.

모든 PropertyEditor는 반드시 다음의 세 가지 방법 중에 한 가지 스타일로 속성을 보여주어야 한다.
1. isPaintable
2. getTags()에서 null이 아닌 String[]을 반환하고 getAsText()에서 null이 아닌 값을 반환한다.
3. getAsText()에서 null이 아닌 값을 반환한다.

모든 Property Editor들은 반드시 setValue메소드에 이 PropertyEditor를 적용할 객체를 넘겨주어야 한다. 또한 반드시 custom editor를 지원하거나 setAsText를 지원해야 한다.

각각의 PropertyEditor들은 기본 생성자를 가지고 있어야 한다.


복잡한것 같지만 대강 요약하면 다음과 같습니다.
1. 기본 생성자가 있어야 한다.
2. setValue(Object) 메소드에 넘겨줄 Object 객체는 해당 PropertyEditor가 처리할 객체여야 한다.
3. getAsText()를 구현하거나 isPaintable()을 구현해야 한다.
4. setAsText()를 구현해야 한다.

그럼 다음에는 저 많은 메소드들 중에서 딱 저 세 개의 메소드만(isPaintable()은 생략) 살펴보겠습니다.
top


Many To One 관계를 폼을 통해서 세팅하는 방법 고민 중.

모하니?/Coding : 2008.01.13 12:03


참조 무결성.
- Many 쪽의 객체를 생성할 때 반드시 One 쪽의 객체를 세팅해줘야 함.

1. Many To One 관계가 단방향일 경우.
- Many 쪽 객체가 One 쪽의 객체를 알고 있는 경우. 테이블에서도 실제 Many쪽의 릴레이션에 One쪽의 id를 가지고 있는 컬럼을 가지고 있다.

1-1. Many쪽 객체를 생성하는 페이지에서 해당 객체가 어디에 속할 지 알고 있는 경우.
- 예를 들어, 특정 게시판에서 글하나를 추가하는 경우 해당 글은 이미 자기가 어디에 속해야 할 지 알고 있다.

1-2. Many쪽 객체를 생성하는 페이지에서 해당 객체가 어디에 속할 지 모르고 있는 경우.
- 예를 들어, 티스토리 블로그처럼 글을 작성하는 시점에 해당 글이 포함될 카테고리를 선택할 수 있다.

2. Many To One 관계가 양방향일 경우.
- 위에서 언급한 관계를 설정한 뒤에 추가적으로 One 쪽의 객체에서 자기가 가지고 있는 모든 Many 쪽의 객체들을 알고 있어야 한다.

2-1. Many쪽 객체를 생성하는 페이지에서 해당 객체가 어디에 속할 지 알고 있는 경우.

2-2. Many쪽 객체를 생성하는 페이지에서 해당 객체가 어디에 속할 지 모르고 있는 경우.

폼에서 그럼 어떻게 세팅해야 할까?

1-1-1. Session에서 One 쪽 객체를 가지고 있을 경우
- 예를 들어, 현재 게시판 BoardID를 Session에서 가져와서, Post를 생성할 때, session에서 이 값을 가져와서 실제 Board 객체를 가져온 다음에 Post에 세팅하기.

int boardID = SessionUtil.getIntValue(req, "boardID"); //SessionUtil 이 있다고 치고..
Post.setBoard(boardDao.get(boardID));

1-1-2. 굳이 세션에 boardID를 담고 있어야 할까?
- 생각해보니, Sessino에 들도 다닐 필요가 없다. 게시판에서 "새글 작성하기" URL을 다음과 같이 만들면 되겠다.

addPost.do?boardID=${board.boardID}

물론 이렇게 한 다음 1-1-1. 같은 코드를 사용해서 Board 객체를 Post에 세팅한다.

1-2. Session에 모든 카테고리 목록을 들고 다닌다.
- 새 글을 작성할 때, 화면에서 카테고리 목록을 뿌려줘야 하고, 카테고리 이름 뿐만 아니라 실제 카테고리의 id도 있어야 한다. 사용자가 특정 카테고리를 선택하고 서브밋을 하면 어떻게 될까?

int categoryID = RequestUtil.getIntValue(req, "categoryID"); //RequestUtil이 있다고 치고..
Post.setBoard(categoryDao.get(categoryID);

- Session은 그럼 무거운 콜렉션을 들고 다녀야 하나? Session에서는 Category 객체 콜렉션이 아니라 Category의 ID와 Name만 들고 다니자. 하지만, 이것도 Session한테는 무리가 될 수 있을텐데..

2-1. URL을 통해서(1-1-1. 같은 방식으로) 어디에 속할지 알수 있기 때문에, 첨부터 세팅해서 폼을 채운다.(Spring의 Petclinic 예제가 사용한 방식)
- request의 속성으로 이미 새로 만들 Many 쪽의 객체가 포함 될 One 쪽 객체의 id를 알고 있기 때문에.. 폼을 채우기 전에 다음과 같이 세팅을 해서 채울 수 있음.

    @RequestMapping(method = RequestMethod.GET)
    public String setupForm(@RequestParam("ownerId") int ownerId, ModelMap model) {
        Owner owner = this.clinic.loadOwner(ownerId);
        Pet pet = new Pet();
        owner.addPet(pet);
        model.addAttribute("pet", pet);
        return "petForm";
    }

이때 위의 addPet() 메소드는 아래에 있는 Convenient Method 형태.

    public void addPet(Pet pet) {
        getPetsInternal().add(pet);
        pet.setOwner(this);
    }



2-2. 어디에 속하는지 객체인지 입력(Command 객체)을 통해서 알아낸다.
- 새로운 Many 객체를 추가할 입력 폼에서 One 쪽의 객체를 선택해야 한다.

${post.category.categoryID} 여기에 선택한 카테고리의 ID를 세팅해 주겠지. 이렇게 할 수 있으려면 new Post() 객체에 new Category()를 세팅해준 다음에 화면으로 보내줘야 돼. 안 그러면 NullPointerException이 발생할테니까...(getCategory() 호출할때..)

그럼 formBackingObject 사용해서 보내자. 그렇게 한 다음 command 객체를 받아오면 어떻게 코딩할까.

Post post = (Post)command;
CategoryDao.get(post.getCategory().getCategoryID()).addPost(post);

- 여전히 뭔가 어색하다. Post가 가지고 있는 Category는 id만 채워져있는 비어있는 카테고리이고, 이 id를 가져와서 실제 카테고리를 가져온 다음에 Post를 세팅하고 있다.

공부 할 것
- 세션, 쿠키, 컨텍스트
    Session에 무언가를 들고 다니는 것 자체가 위험할 수 있다.
- 스프링 PetClinit 예제코드

top


엑셀 자바로 다루기

모하니?/Coding : 2007.12.27 12:06


Handle Excel files

위 링크에 가시면 여러 가지 API들을 소개해주고 있습니다. 이 중에서 두 개만 확인해봤습니다.

public class ReadCell {

    @Test
    public void readByPOI() throws Exception {
        POIFSFileSystem fs = new POIFSFileSystem(new FileInputStream("Book1.xls"));
        HSSFWorkbook wb = new HSSFWorkbook(fs);
        HSSFSheet sheet = wb.getSheetAt(0);
        HSSFRow row = sheet.getRow(0);
        HSSFCell cell = row.getCell((short) 0);
        assertEquals("A1한글", cell.getRichStringCellValue().toString());
    }

    @Test
    public void readByJExel() throws Exception {
        Workbook workbook = Workbook.getWorkbook(new File("Book1.xls"));
        Sheet sheet = workbook.getSheet(0);
        Cell a1 = sheet.getCell(0, 0);
        assertEquals("A1한글", a1.getContents());
    }
}

첫 번째 테스트는 아파치의 POI. 두 번째 테스트는 JExel을 사용해봤습니다.

POI는 sheet에서 cell을 바로 구해낼 수 없고, row를 통해서 cell을 받아야 하는게 좀 불편합니다. 반면에 JExel은 sheet에서 cell에 바로 접근이 가능하죠. 일단 Exel에서 cell에 담긴 정보를 읽는 API는 JExel이 더 편하네요.
top

TAG Exel Java

테스트의 소중함

모하니?/Coding : 2007.12.13 20:01


테스트를 (먼저) 만들면 결과물이 훨씬 간결합니다.

테스트가 있어야 믿음직합니다.

테스트가 있어야 빨리 버그를 잡을 수 있습니다.

HTTP 요청에 메시지를 실어서 보내고 응답을 받는 간단한 클래스 작성이 목적입니다. 다음과 같은 인터페이스가 필요합니다.

public interface HttpClient{
    String request(String url, String message);
}

기존 코드(스크롤 주의)
인코딩과 로깅까지 고려한 코드입니다. 하지만 위의 코드는 제대로 동작하지 않습니다. 저렇게 긴 코드를 대체 어디서부터 어떻게 손을 댈지 고민하느니 새로 짜는게 좋을 것 같다는 생각이 들어서 새로 짰습니다.

새로 짠 코드
로깅와 인코딩은 전혀 신경쓰지 않고 짠 코드입니다. 소스 코드의 절반이 try-catch 코드지만 제대로 동작합니다. 물론 저 코드를 짠 순간은 저녀석이 제대로 동작하는지 알 길이 없습니다. 불안하죠. 그래서 테스트 코드를 작성합니다.

    public void testRequest() throws Exception {
        String url = "http://localhost:8080/tapsTest/merchantProcess.do?message=";
        String msg = "123456789012345678901234567890";
        String response = SimpleHttpClient.request(url, msg);
        assertEquals("CnbReqQ             1234567890", response);
    }

테스트 코드 네 줄까지 합쳐도 기존 코드보다 짧습니다. 그리고 위 테스트 코드를 통해서 요청과 응답을 제대로 받았음을 확인했습니다.(서버에 다녀와야 하기때문에 단위테스트는 아닙니다.)

덤으로 기존의 애플리케이션이 바로 위의 코드때문에 제대로 동작하지 않는다는 것도 확인할 수 있습니다. 위의 테스트케이스에서 클래스만 살짝 바꿔주면 되니까요.
top


Acegi 필터 등록할 때 발생할 수 있는 몹쓸 버그

모하니?/Coding : 2007.10.25 15:09


참조 : NoSuchBeanDefinitionException: No bean named '' is defined

사용자 삽입 이미지
위와 같이 필터 체인 프록시에서 필터를 등록할 때 보기 좋게 하려고 줄을 맞춰 주면 안 됩니다. 이렇게 하면 다음과 같은 에러 메시지를 보시게 될 것입니다.(항상 발생하는 버그는 아닙니다. 어떤 경우에는 줄바꿈을 하더라도 에러가 발생하지 않았습니다.)
사용자 삽입 이미지
해결책은?? 그냥 필터들의 이름을 쭈욱~ 이어줍니다.

사용자 삽입 이미지
이렇게 쉼표와 필터이름들 사이에 공백을 제거하면 에러가 없어집니다.

참조한 링크에 보시면 이 에러 때문에 하루를 날렸다는 외국인도 있군요. ㄷㄷㄷ.. 구글신과 스프링 포럼 유저들에게 무한 감사 날립니다. ㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳㄳ

top


LocaleChangeInterceptor(국제화 지원 인터셉터)가 있었군요.

모하니?/Coding : 2007.10.25 10:18


이 클래스 역시 Spring에서 제공하고 있는 기본 Interceptor입니다. 다음과 같이 bean으로 등록한 다음에 국제화를 지원하고 싶은 요청들의 인터셉터로 등록하면 되겠습니다.
사용자 삽입 이미지

위 설정 파일은 스프링 프레임웤에 포함되어 있는 샘플 애플리케이션 일부 입니다. LocaleChangeInterceptor 인터셉터의 핵심 코드는 다음과 같이 구현되어 있습니다.

사용자 삽입 이미지
request에서 "locale"이라는 파라미터 값을 가져와서 Locale을 적용해 줍니다.
top


WebUtils 사용하기

모하니?/Coding : 2007.10.24 12:37


Spring MVC를 다룰 때, 요청에 딸려오는 값들을 처리할 때 유용한 ServletRequestUtils 와 더불어 하나 더 유용한 클래스가 있는데, 바로 WebUtils 입니다.

WebUtils 클래스를 사용하면 Session에 담겨있는 객체들을 보다 짧은 코드로 넣고 빼고 할 수 있으며, 세션 객체나 쿠키 객체를 받아올 수 있습니다.

원래는 다음과 같이 Request 객체를 직접 통해서 Session 객체에 접근해야 했습니다.
UserSession userSession = (UserSession) request.getSession().getAttribute("userSession");

그러나 Spring의 WebUtils를 사용하면 . 을 두 개 사용하여 길게 가지 않아도 됩니다.
UserSession userSession = (UserSession) WebUtils.getSessionAttribute(request, "userSession");

이렇게 보다 . 한 방으로 원하는 객체를 얻을 수 있게 됩니다.
top

TAG WebUtils

프로퍼티파일 변경 후 재컴파일이 필요하지 않을까?

모하니?/Coding : 2007.10.15 09:38


몇일 전에 경험했던 삽질을 공개합니다.

xxx.properties 라는 파일이 있고 이 파일에 있는 내용을 yyy.xml 에서 사용하고 있었습니다. xxx.properties는 db connection 정보를 담고 있었고, yyy.xml은 spring에서 사용할 datasource를 정의할 때 xxx.properties에 있는 내용을 읽어다가 사용했습니다. 이때 xxx.properties의 내용을 변경했습니다.

저는 '음.. 그래 단순히 설정이 바뀐 것 뿐인데, Java 코드도 아니고 재컴파일 할 필요는 없겠지?'라고 생각했습니다.

그러나 왠 걸 서버를 실행해 보니, DB connection을 가져올 때 수정하기 전에 사용한 URL과 user, password를 사용하고 있는 것이었습니다. -_-;;

이런.. 컴파일 해야 되는구나.

왜?
사용자 삽입 이미지

이 녀석이 프로퍼티 파일을 읽어들인 다음 정보를 가지고 있는데, 이 녀석을 컴파일 한 class 파일로부터 정보를 가져오기 때문입니다. 프로퍼티 파일이 XML이었다면 어떨까요? 그때도 컴파일이 필요할까요? 흠.. 그건 아닐 겁니다.

여기서 생기는 의문점 하나..
사용자 삽입 이미지

이 녀석도 똑같이 프로퍼티파일을 읽어들여서 사용하긴 하지만, 국제화 지원을 위해서 또는 그냥 텍스트를 별도로 관리하기 위해 사용합니다. 앞서 본 프로퍼티 플레이스 홀더와 다른 점은 이 녀석은 프로퍼티파일을 변경한뒤 페이지 새로고침을 하면 view에 바로바로 적용이 된다는 것입니다.

흠... 어찌된 영문일까요?  ResourceBundleMessageSource API를 살펴보아도 번들은 VM이 살아있는 동안 영원히 캐쉬 된다고 합니다. 그럼 새로고침 한다고 해서 반영이 되면 안되고 이 역시 재컴파일이 필요할텐데 말이죠. 그래서 주기적으로 번들을 다시 읽어들이는 ReloadableResourceBundleMessageSource도 존재 하는데 말입니다. 흠.. 신기할 따름입니다. 프로퍼티파일을 변경하면, JSP가 변경 된 것으로 생각하고, JSP를 Servlet으로 바꾸고, 그걸 다시 컴파일 하면서 ResourceBundleMessageSource도 다시 컴파일 하게 되는 걸까요?? 그렇다면 프로퍼티파일을 변경한 것을 JSP가 어떻게 알았을까요? 흠;; 미궁속으로 빠져드는 느낌입니다.

XML과 프로퍼티파일 그리고 JSP의 특징으로 인해 빚어진 에피소드였습니다.
top


중복되는 라이브러리가 발생시킬 수 있는 문제

모하니?/Coding : 2007.10.11 14:17


javax.servlet.ServletException: org.eclipse.jdt.internal.compiler.CompilationResult.getProblems()[Lorg/eclipse/jdt/core/compiler/IProblem;
    org.apache.jasper.servlet.JspServlet.service(JspServlet.java:272)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
    org.springframework.web.servlet.view.InternalResourceView.renderMergedOutputModel(InternalResourceView.java:142)
    org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:243)
    org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1141)
    org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:878)
    org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:792)
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:461)
    org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:416)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:690)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:96)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:75)

위의 에러로그는 이클립스에서 컴파일 하여 WAR로 묶은 다음 서버로 배포한 다음에 발생한 에러 입니다.

위의 에러를 발생시킨 애플리케이션은 로컬 웹 서버, 즉 이클립스에서 돌렸을 때는 잘 동작했는데, 배포만 하면.. 저런 에러에 마주치게 됩니다. 영문을 몰라서 구글링을 했지만 어떤 분은 yum update tomcat을 했더니 에러가 사라졌다고 하는 분도 있고, 그 밖에 알쏭 달쏭한 글들이 많이 있었습니다.

곰곰히 저 에러가 발생하기 전과 후에 무슨 일을 했는지 생각했습니다. Ant로 배포 자동화 해보려고 빌드 파일을 추가하고, Ant에서 필요하다고 하는 라이브러리들을 web/WEB-INF/lib에 넣어줬습니다.

어떤 걸 넣었지.. 하고 보는 순간... 어라. 이것들 어디서 많이 본건데.. 헉.. 톰캣 라이브러리자나..-_-;;;

라이브러리가 중복되면 저런 메시지가 발생할 수도 있나봅니다. 특히나 톰캣에 포함되어 있는 라이브러리가 중복됐을 때 위의 메시지를 보실 수 있겠죠. 아마도 jasper API 또는 servlet, jsp API가 중복되서 발생한 문제 같습니다. 저 같은 실수를 하지 마시길...

top


구글 캘린더 정보 가져오기

모하니?/Coding : 2007.10.06 20:38


구글 캘린더 API를 사용하는 방법은 여러가지(Java, .Net, Protocol, Javascript)가 있는데 그 중에서 가장 간단한 것은 아마도 자바스크립트 입니다. 뷰에서 데이터를 읽어온 다음에 바로 보여주기 때문에 매우 간단합니다.

구글 캘린더 정보를 가져오는 방법을 크게 두 가지 방법으로 나눌 수 있는데, 나누는 기준은 인증입니다. 즉 로그인을 거친 다음에 정보를 가져오느냐, 아니면 인증 없이 공개된 일정만 가져오느냐 입니다. 당연히 인증을 거치는 방법이 쪼금 더 복잡하겠죠. 그래서 저는 인증을 사용하지 않고, 공개된 일정만 가져와서 뿌려주는 예제를 돌려봤습니다.

자바스크립트 코드는 여기서 참조하실 수 있습니다.

1. 구글 API 키 받기.
여타 다른 오픈 API를 제공하는 곳과 마찬가지로 구글도 여기서 key를 받아서 사용해야 합니다.

2. 외부 자바스크립트 파일 포함시키기.
<script type="text/javascript" src="http://www.google.com/jsapi?key=YOUR_KEY_HERE"></script>
요건 구글 API 키 인증작업에 해당하고, 실제 API를 가져오는 것은 다음과 같습니다.
google.load("gdata", "1");

3. 캘린더 정보 가져오기.
function loadCalendarByAddress(calendarAddress) {
  var calendarUrl = 'http://www.google.com/calendar/feeds/' +
                    calendarAddress +
                    '/public/full';
  loadCalendar(calendarUrl);
}

function loadCalendar(calendarUrl) {
  var service = new
      google.gdata.calendar.CalendarService('gdata-js-client-samples-simple');
  var query = new google.gdata.calendar.CalendarEventQuery(calendarUrl);
  query.setOrderBy('starttime');
  query.setSortOrder('ascending');
  query.setFutureEvents(true);
  query.setSingleEvents(true);
  query.setMaxResults(10);

  service.getEventsFeed(query, listEvents, handleGDError);
}

위의 코드는 전체 코드에서 캘린더 정보를 가져오는 부분에 해당합니다. 가장 핵심이 되는 부분은 맨 아래의 loadCalendar 함수 입니다. 이 함수에 calendarUrl을 넘겨주면 되는데 해당 URL 형식은 위에 있는 메소드에서 알 수 있습니다. calendarAddress에 자신의 구글 계정을 입력하면 자신의 캘린더 정보(자신이 가지고 있는 모든 캘린더 정보가 아니라 기본으로 만들어져 있는 한 개의 캘린더를 지칭합니다.)를 가져올 수 있습니다.

4. 화면에 보여주기

function listEvents(feedRoot) {
  var entries = feedRoot.feed.getEntries();
  var eventDiv = document.getElementById('events');
  if (eventDiv.childNodes.length > 0) {
    eventDiv.removeChild(eventDiv.childNodes[0]);
  }     
  /* create a new unordered list */
  var ul = document.createElement('ul');
  /* set the calendarTitle div with the name of the calendar */
  document.getElementById('calendarTitle').innerHTML =
    "Calendar: " + feedRoot.feed.title.$t;
  /* loop through each event in the feed */
  var len = entries.length;
  for (var i = 0; i < len; i++) {
    var entry = entries[i];
    var title = entry.getTitle().getText();
    var startDateTime = null;
    var startJSDate = null;
    var times = entry.getTimes();
    if (times.length > 0) {
      startDateTime = times[0].getStartTime();
      startJSDate = startDateTime.getDate();
    }
    var entryLinkHref = null;
    if (entry.getHtmlLink() != null) {
      entryLinkHref = entry.getHtmlLink().getHref();
    }
    var dateString = (startJSDate.getMonth() + 1) + "/" + startJSDate.getDate();
    if (!startDateTime.isDateOnly()) {
      dateString += " " + startJSDate.getHours() + ":" +
          padNumber(startJSDate.getMinutes());
    }
    var li = document.createElement('li');

    /* if we have a link to the event, create an 'a' element */
    if (entryLinkHref != null) {
      entryLink = document.createElement('a');
      entryLink.setAttribute('href', entryLinkHref);
      entryLink.appendChild(document.createTextNode(title));
      li.appendChild(entryLink);
      li.appendChild(document.createTextNode(' - ' + dateString));
    } else {
      li.appendChild(document.createTextNode(title + ' - ' + dateString));
    }       

    /* append the list item onto the unordered list */
    ul.appendChild(li);
  }
  eventDiv.appendChild(ul);
}

google.setOnLoadCallback(init);

<div id="calendarTitle"></div>
<div id="events"></div>

google.setOnLoadCallback(init); 까지는 <script> 태그로 묶여 있어야 하는 부분입니다. listEvents 함수 내부에서는 구글 캘린더 API와 DOM을 사용해서 특정 id를 가진 엘리먼트에 데이터를 채워넣고 있습니다.

이렇게 간단하게 구글 캘린더 정보를 가져와서 보여줄 수 있습니다.
사용자 삽입 이미지

top


Spring AOP 예제 코드

모하니?/Coding : 2007.09.28 13:40



사용자 삽입 이미지
패키지 구조
aopSchema 에는 aop 스키마 기반의 Spring AOP 예제 코드
aspectAnnotation 에는 @AspectJ 기반의 Spring AOP 예제 코드
aspectJ 에는 Spring에서 AspectJ를 사용하는 예제 코드
classicSpringAOP 에는 Spring 2.0 이전의 Spring AOP 예제 코드가 담겨있습니다.

주의사항
문자열 인코딩은 UTF-8 입니다.
AJDT 플러그인과, Spring IDE 2.0 플러그인을 설치 해주시면 유용합니다.
각 패키지 별로 ~~~Test 라는 클래스를 실행하시면 됩니다.

*위의 소스코드는 AJN에서 제가 진행하고 있는 Spring In Action 세 번째 스터디에서 사용 할 예정입니다.
따라서 자세한 설명은 온라인에서 하지 않습니다. 문의 사항이 있으시면 메일(블로그 바닦에 있습니다.)을 보내 주세요.


2007/09/05 - [Spring In Action/4. Advising beans] - 휴.. Spring AOP와 AspectJ 연동 성공
2007/09/10 - [Spring IDE] - Spring IDE 2.0 활용하기2
2007/07/02 - [Good Tools] - Eclipse 3.3 Europa 설치 뒤 할 일


top


commons.lang.builder 패키지

모하니?/Coding : 2007.09.27 19:24


http://wiki.javajigi.net/display/WEB20/4.+Ajax+Application+Examples
위 링크에 있는 포함되어 있는 소스코드를 살펴보다가 재밌는 코드를 발견했습니다.

import java.io.Serializable;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

/**
 * 주석을 넣어 주세요. 배가 고파요.
 *
 * @author 박재성(자바지기 - javajigi@gmail.com)
 */
public class BaseObject implements Serializable {
    public String toString() {
        return ToStringBuilder.reflectionToString(this,
                ToStringStyle.MULTI_LINE_STYLE);
    }

    public boolean equals(Object o) {
        return EqualsBuilder.reflectionEquals(this, o);
    }  

    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }
}

박재성님께서 작성하신 클래스 같은데요. Entity가 될 도메인 클래스에서 위의 클래스를 상속받으면 도메인 클래스에서 equals()와 hashCode() 그리고 toString()을 오버라이딩 할 필요가 없어집니다.

equals()와 hashCode()는 객체의 동일성을 검사할 때 사용하는 메소드로 Hash Code를 사용하는 콜렉션(HashSet 같은)을 사용할 때 꼭 구현해 줘야 하며, EJB 3.0의 JPA나 Hibernate 같은 ORM을 사용하여 객체를 유일한 단위로 식별해야할 필요가 있는 클래스는 반드시 오버라이딩하도록 권장하고 있는 메소드들이기도 합니다.

따라서 일일히 도메인 클래스마다 구현해야할 메소드를 리플랙션을 사용해서 구현해 놓은 것 같은 Commons Lang API를 사용하면 개발 시간이 엄청나게 단축됩니다. 특히나 이 jar파일은 spring.jar파일이 의존하고 있는 유일한 jar파일 이기도 하기 때문에 라이브러리 추가로 인한 추가 비용이 제로에 가깝다고 할 수 있습니다.

왜 이렇게 좋은 API를 이제야 알았는지 모르겠네요. 박재성님께서 쓰신 Spring 워크북에서도 얼핏 보고 그냥 지나친 것 같은데 그 때 좀 더 자세히 살펴볼 걸 하는 아쉬움이 생깁니다.
top


테스트 할 때 추상 클래스 활용하기

모하니?/Coding : 2007.09.23 19:24


테스트를 할 때 종종 같은 설정 파일을 필요로 하는 경우가 있습니다. 이럴 때 Abstract 클래스로 중복되는 설정파일을 읽어들이는 코드를 상위로 올리면 테스트 클래스를 작성할 때 매우 간결해 집니다.

public abstract class AbstractDaoTest extends AbstractTransactionalDataSourceSpringContextTests {

    @Override
    protected String[] getConfigLocations() {
        return new String[] { "file:web/WEB-INF/spring/hibernateContext.xml",
                "file:web/WEB-INF/spring/dataSourceContext.xml", "file:web/WEB-INF/spring/daoContext.xml", };
    }
}

이제 위 상위 클래스를 상속받아서 테스트 클래스를 작성하면 됩니다.
public class MemberDaoTest extends AbstractDaoTest{

    private MemberDao memberDao;

    public void setMemberDao(MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    public void testAdd() throws Exception {
        Member member = new Member();
        memberDao.add(member);
        memberDao.flush();
        assertEquals(1, memberDao.getAll().size());
        assertNotNull(member.getMemberId());
    }

}

이것이 바로 말도 많고 탈도 많은 상속의 묘미가 아닐런지 생각해 봅니다.
top




: 1 : ··· : 5 : 6 : 7 : 8 : 9 : 10 :