Whiteship's Note

'2009/12'에 해당되는 글 42건

  1. 2009.12.31 2장 JSF 기본 - 주요 구성 요소
  2. 2009.12.31 1장 JSF 소개 - Introducing JavaServer Faces
  3. 2009.12.28 [인텔리J IDEA] 스프링 XML 설정 지원 기능
  4. 2009.12.23 세상에서 제일 맛있는 쿠키 (8)
  5. 2009.12.23 스프링 3.0의 설정 간편화
  6. 2009.12.22 [결혼] 반년차 부부의 걱정 - 적자 (24)
  7. 2009.12.22 스프링 3.0의 MVC 간편화 (2)
  8. 2009.12.19 [웹 사이트 속도 향상 베스트 프랙티스 10] 자바스크립트 CSS 크기 줄이기 (2)
  9. 2009.12.18 [스프링 테스트 확장] static member class를 빈으로 등록하는 테스트 로더 1 (6)
  10. 2009.12.17 뱀이다~ 뱀이다~ 몸에 좋고 맛도 좋은 뱀이다~ (12)
  11. 2009.12.17 봄싹 Career Path (6)
  12. 2009.12.17 스프링 프레임워크 3.0이 GA 됐다. (4)
  13. 2009.12.16 ProxyFactoryBean을 이용한 초간단 AOP 구현 (2)
  14. 2009.12.16 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 4 - 일단 끝 (4)
  15. 2009.12.15 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 3
  16. 2009.12.15 2010 매쉬업 경진대회 시작
  17. 2009.12.15 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 2 (2)
  18. 2009.12.15 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 1
  19. 2009.12.11 [스프링 3.0] 스프링 bean과 일반 자바 객체가 호출하는 @Bean 메서드의 차이
  20. 2009.12.11 [빈둥 빈둥] 닌텐도Wii 4.2k V2 홈브류 설치 (30)
  21. 2009.12.10 [스프링 3.0] @Async 사용하기
  22. 2009.12.09 스프링이 컴포넌트 스캔을 할 때 클래스로딩을 하지 않는다는 증거 (2)
  23. 2009.12.08 스프링의 로깅 의존성 (2)
  24. 2009.12.08 [봄싹] 하이버네이트와 자바 퍼시스턴스 베타리딩 합니다. (8)
  25. 2009.12.04 조금 친절한 코드 생성기 7(일단 완성) - DAO 생성기 코딩
  26. 2009.12.04 조금 친절한 코드 생성기 6 - 테스트 코드 리팩토링
  27. 2009.12.04 조금 친절한 코드 생성기 5 - 리팩토링
  28. 2009.12.04 조금 친절한 코드 생성기 4 - 설계 변경 적용(인터페이스)
  29. 2009.12.04 조금 친절한 코드 생성기 3 - 설계 변경 적용(Map)
  30. 2009.12.04 조금 친절한 코드 생성기 2 - 설계 변경

2장 JSF 기본 - 주요 구성 요소

JSF/JSF in Action : 2009.12.31 17:06


UI 컴포넌트(컨트롤 또는 그냥 컴포넌트라고 부르기도 함) - 상태를 유지하는 객체다(stateful object). UI 컴포넌트는 자바빈으로 프로퍼티, 메서드, 이벤트를 가지고 있다. UI 컴포넌트는 뷰를 구성하며, 보통 컴포넌트 트리가 한 페이지를 보여준다.

랜더러(renderer) - UI 컴포넌트를 보여주며 사용자 입력값을 컴포넌트의 값으로 변환하는 책임을 가지고 있다. 랜더러는 한개 또는 여러개의 UI 컴포넌트를 다룰 수 있으며 한개의 UI 컴포넌트가 여러개의 다른 랜더러에 의해 처리될 수 있다.

검증기(validator) - 사용자가 입력한 값을 수용할 수 있는지 검사한다. 한 개의 UI 컴포넌트가 여러개의 검증기에 의해 처리될 수 있다.

백빈(backing bean) - UI 컴포넌트에서 값을 수집하며 이벤트 리스너 메소드를 구현하는 자바빈이다. UI 컴포넌트에 대한 레퍼런스도 가질 수 있다.

변환기(converter) - 컴포넌트의 값을 화변에 보여줄 문자열로 변환하고 그 반대로도 변환해준다. 한 개의 UI 컴포넌트에는 한 개의 컨버터만 적용된다.

이벤트와 리스너(events and listners) - JSF는 자바빈 이벤트/리스너 모델을 사용한다. UI 컴포넌트는 이벤트를 발생시키며 리스너를 등록할 수 있다.

메시지(message) - 사용자에게 다시 보여줄 정보. 백빈, 검증기, 컨버터 등에서 사용자에게 보여줄 메시지나 에러 메시지를 생서앟ㄹ 수 있다.

네비게이션(nvigation) - 어떤 페이지에서 다른 페이지로 이동하는 기능. JSF는 특화된 이벤트 리스너와 연동된 강력한 네비게이션 시스템을 가지고 있다.




top


1장 JSF 소개 - Introducing JavaServer Faces

JSF/JSF in Action : 2009.12.31 12:28


1.1 JSF는 무엇인가?

JSF는 RAD 툴의 4계층 중에 셋으로 컴포넌트 아키텍처, UI 위젯 표준 집합, 애플리케이션 인프라를 정의한 것이다. JSF 컴포넌트 아키텍처는 UI 위젯을 만드는 일반적인 방법을 정의한다.

1.2 기반 기술

HTTP - 텍스트 헤더 기반의 간단한 프로토콜로 클라이언트가 요청을 서버는 요청한 문서를 브라우저로 보내준다. HTTP는 "stateless" 프로토콜이다. 클라이언트의 여러 요청에 대한 정보를 기억하지 못한다.

Servlet - HTTP는 정적인 페이지를 보여줄 때 매우 좋다. 동적인 페이지를 만들려면 코딩이 필요하다. HTTP가 간단하긴 하지만 헤더를 파싱해서 뭘 원하는지 알아야 하고 다시 또 적절한 형식으로 헤더를 만들어야 하는 불편한 코딩을 객체 지향적으로 할 수 있게 서블릿 API를 제공한다. 즉 HTTP 요청과 응답을 객체로 캡슐화하고 Servlet에서 요청을 처리한다.

Portlet - 패스

JavaBean - 강력한 이벤트 모델을 가지고 있다.

1.4 컴포넌트

JSF는 UI 컴포넌트 또는 컨트롤을 제공한다.

1.5 Hello, world!


인텔리에서 해봤는데 JSF 개발하는데 아무 지장 없을 듯.
faces-config.xml 파일 작성할 때 자동완성 지원 귿.

top


[인텔리J IDEA] 스프링 XML 설정 지원 기능

Good Tools : 2009.12.28 18:08



녹색 빈이 있길래 마우스를 올려봤더니.. 저런 메시지가.. 오호!


클릭해봤더니.. 와우!
top


세상에서 제일 맛있는 쿠키




아내가 만들어준 쿠키. 냠냠냠.. 이미 다 먹고 두 개 남았다는... 그나저도 5분 뒤면 사라질...

top

TAG 쿠키

스프링 3.0의 설정 간편화

Spring/etc : 2009.12.23 11:15


원문: http://blog.springsource.com/2009/12/22/configuration-simplifications-in-spring-3-0/

케이스가 시작한 "스프링 3 간편화" 시리즈 두번째로 스프링의 새로운 @Configuration 애노테이션 및 그와 관련된 지원 기능을 매우 간략하게 소개하고자 한다.

스프링 JavaConfig 프로젝트에 관심을 가지고 있던 사람들이라면 @Configuration 애노테이션 클래스가 스프링 XML 파일과 매우 비슷한 역할을 한다는 것을 알 것이다. 메서드와 애노테이션을 사용하여 스프링 빈을 정의를 선언하는 코드-중심적인 방법을 제공한다. 이러한 것을 POC(Plain Old Configruation)이라고 부를 수도 있겠다. :) XML이 하나도 필요없는 간단한 상활을 일컷는 말이다.

시작해보자. @Configuration 기능을 설명하려고 새로운 스프링 샘플 SVN 저장소에 매우 간단한 프로젝트를 만들어두었다. 그것을 받아서 바로 빌드하고 싶을것이다. 서브버전 클라이언트와 최신 버전 메이븐이 필요하다.

svn co https://src.springframework.org/svn/spring-samples/configuration-basic/trunk configuration-basic
cd configuration-basic
mvn clean test
[...]
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ————————————————————————
[INFO] BUILD SUCCESSFUL
[INFO] ————————————————————————

이클립스 .classpath와 .project 메타데이터도 체크아웃에 포함되어 있기 때문에 프로젝트를 SpringSprout Tool Suite 또는 m2eclipse 플러그인을 설치한 최신버전 이클립스에서 import 할 수 있을 것이다. 두 경우 모두 File -> Import -> Existing Projects into Workspace를 하면 된다.

AppConfig.java 클래스부터 살펴보자. @Configuration 애노테이션을 추가한 클래스는 XML 설정 기반 스프링 애플리케이션의 app-config.xml과 같은 역할을 한다. 실행시점에 어떻게 객체 인스턴스들이 연관되어 있으며 관리되는지에 대한 "청사진"을 제공하기 때문에 프로젝트를 살펴보기 좋은 시작점이 된다.

@Configuration
public class AppConfig {

    public @Bean TransferService transferService() {
        return new TransferServiceImpl(accountRepository());
    }

    public @Bean AccountRepository accountRepository() {
        return new InMemoryAccountRepository();
    }

}

Granted, 이 예제는 진짜 JDBC Datasource를 사용하지도 않는 매우 간단한 애플리케이션이다. 하지만 이 글의 목적은 기본 개념을 제공하는 것이기 때문에 괜찮다. @Bean을 선언한 메서드는 실행 시점에 스프링 컨테이너에의해 호출되며 반환되는 객체를 스프링 컨테이너가 다른 빈들처럼 관리한다. 다른 빈과의 의존성은 간단하게 다른 빈 메서드를 호출하는 것으로 표현할 수 있다. TransferServiceImpl은 AccountRepository를 생성자 인자로 필요로 하기 때문에 간단하게 accountRepository() 메서드를 호출했다.

경험많은 스프링 사용자라면 이 예제를 보고 "빈 스코프는 어떻게 되는가?"라고 질문할 것이다. 좋은 질문이다. 아시다시피 모든 스프링 빈은 스코프를 가지고 있다. 기본으로 빈의 스코프는 '싱글톤'이며 이것은 스프링 컨테이너 당 하나의 빈 인스턴스만 사용된다는 것을 의미한다. 위의 코드를 보면 accountRepository()를 여러번 호출할 때마다 여러 인스턴스를 만들것 처럼 보이지만 실제로 그렇지는 않다! @Configuration 클래스가 실행 시점에 처리되면 동적으로 (CGLIB을 사용하여) 그 하위 클래스를 만들고, 그 하위 클래스의 @Bean 메서드들은 스코프 개념을 보장하도록 코드를 조작한다.

보시다시피, @Bean 메서드를 정의하는 것은 매우 간단하다. 이제 컨테이너를 동작시키고 저 객체들을 사용해보자.

TransferServiceTest JUnit 시스템 테스트와 transfer100Dollars() @Test 메서드를 보자. 가장 먼저 눈에띄는 것은 AnnotationConfigApplicationContext 사용일 것이다. 이것은 새로운 ApplicationContext 구현체로 @Configuration 클래스를 직접 사용하여 스프링 컨테이너 생성을 지원하도록 스프링 3에 추가되었다. 이 컨텍스트는 AppConfig.class를 생성자 매개변수로 사용하며, 그런다음 TransferService와 AccountRepository 타입의 빈을 getBaen(Class) 메서드를 이용하여 가져온다. 메서드의 남은 부분은 일반적인 JUnit 검증 과정으로 TransferService와 AccountRepository API를 확인하고 예상한대로 잘 동작하는지 확인한다.

public class TransferServiceTest {

    @Test
    public void transfer100Dollars() {
        // create the spring container using the AppConfig @Configuration class
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);

        // retrieve the beans we'll use during testing
        AccountRepository accountRepository = ctx.getBean(AccountRepository.class);
        TransferService transferService = ctx.getBean(TransferService.class);

        // create accounts to test against
        accountRepository.add(new Account("A123", 1000.00));
        accountRepository.add(new Account("C456", 0.00));

        // check account balances before transfer
        assertThat(accountRepository.findById("A123").getBalance(), equalTo(1000.00));
        assertThat(accountRepository.findById("C456").getBalance(), equalTo(0.00));

        // perform transfer
        transferService.transfer(100.00, "A123", "C456");

        // check account balances after transfer
        assertThat(accountRepository.findById("A123").getBalance(), equalTo(900.00));
        assertThat(accountRepository.findById("C456").getBalance(), equalTo(100.00));
    }

}

바로 이거다! 간단하고, 순수한 자바이고(사실 저는 개인적으로 이부분에 대해서는 약간의 이견을 가지고 있지만;;), 타입-안정적인 설정이다. 이것이 스프링의 핵심 의존성 주입 기능의 편리하고 강력한 추가기능임을 알았으면 좋겠다.

물론, 오늘 여기서는 매우 간략하게 살펴봤다. @Configuration 클래스로 더 많은 것을 할 수 있으며 자세한 기능은 다음 글에서 설명하도록 하겠다. 하지만 날 기다리지말고 스스로 스프링 3 레퍼런스 문서의 @Configuration 부분을 읽어보길 바란다. 이 예제 프로젝트를 시작점으로 빠르게 새로운 기능의 나머지 부분을 확읺보기 바란다.

피드백을 기대한다. @Configuration과 모든 새로운 스프링 3 기능을 즐기기 바란다. 즐거운 연휴 되시길!

* Thanks to Erich Eichinger for (half-jokingly) coining the phrase 'Plain Old Configuration'. You can take a look at the work he and the Spring.NET team are doing with their similar 'CodeConfig' project here.

ps: 이번글에도 링크가 굉장히 많은데;; 귀찮아서 생략했습니다. 언젠가 안 귀찮을 때 수정하겠습니다.
top


[결혼] 반년차 부부의 걱정 - 적자

모하니?/Thinking : 2009.12.22 14:45


요즘 내가 버는 돈으로는 멀리보면 2세를 키우기가 힘들어 보이고, 당장은 적금을 줄여야 하는 상황이 닥쳐왔다. 당연히 대책 마련에 들어갔지만 별다른 대책은 없다. 내가 지금 버는 돈에서 갑자기 두 배 이상은 커녕 50만원도 더 벌어 올수가 없는 노릇이다. 현재도 그렇지만 장기적인 내 장래도 그리 밝지 않다. 물론 난 순수하게 노력을 하고 있지만 시간이 지날수록 이상하게 내 앞은 더 깜깜해지는 것만 같다. 대딩때도 이렇게 불안하진 않았는데 요새는 공부를 해도 불안하기만 하다.

아무튼, 이 상황에서 아내가 내 놓은 해결책은 아내가 더 빡쎄게 돈을 벌어오겠다는 것이었다. 아내는 요즘 내가 한달에 버는 돈의 세 배 이상을 벌어봤기 때문에 지금 내가 벌어다 주는 돈이 굉장히 답답하게 느낄 수도 있을 것 같다. 나야 그만큼 벌어본적이 없어서 잘 상상은 안되지만, 그 반대로 내가 지금 버는 돈의 절반도 못벌던 대딩때를 떠올려보니 가슴이 답답해진다.

처음 나는 아내의 이야기를 들었을 때 반대도 찬성도 하지 않았다. 잘 생각해보고 알아서 결정하라고 했다. 원래는 영어 학원 강사로 일을 하다가 지금 다니는 피아노 학원으로 옮긴 이유는 아내가 받는 스트레스를 줄이기 위해서였다. 버는 돈은 조금 차이가 나더라도 시험기간 마다 주말에 학원에 나가서 봐주는 것 때문에 우리 부부가 주말에 무언가를 할 수 있는 기회가 사라지고 요즘 애들과 부모들이 개념을 상실했기 때문에 받는 스트레스도 이만 저만이 아니었다. 그래서 좀 덜 부데끼는 피아노 학원 강사로 옮겼다. 월급은 20% 가량이 줄었지만 이전보다 직장 스트레스를 덜 받았으며 주말에는 즐겁게 보드도 타고 다니고 양가 부모님 댁도 간간히 왕래할 수 있게 되었다. 하지만 아내의 줄어든 월급과 나의 고정된 수입으로 인해 재정적인 적자가 발생하기 시작했다. 이런 상황에서 다시 직장 스트레스보다 재정적인 스트레스가 과중해졌다면 아내의 판단을 따르지 않을 이유가 없었다.

사실 나는 아내의 의견보다는 장모님의 의견을 더 신뢰한다. 아내는 약간 성급한 경향이 있다. 하지만 그만큼 타이밍을 빨리 잡아내기 떄문에 어떤 문제 상황을 조기 발견하고 그 해결책을 굉장히 빨리 찾을 수도 있다는 점에서는 정말 뛰어나다. 장모님은 다양한 경험을 가지고 있으며 아내를 잘 알고 있기 때문에 가장 현명한 해결책을 주신다.

(아내가 이 글을 보면 서운할 수도 있겠지만 원래가 그런거니까 서운해 할 필요가 없다고 말하고 싶다. 우린 절대로 장모님을 비롯한 어른들의 경험보다 더 많은 것을 경험해볼 수 없거니와 빠르게 문제 상황을 인지한다는 것 자체로도 나는 굉장히 고마워하고 있다는 걸 알아줬으면 한다. 나는 문제 상황을 인지도 못했을 뿐 아니라 어떠한 해결책도 못 내놨으니 정말 한심한 건 나뿐이다.)

어쨋거나 상의한 결과는 가장인 즉 내가 버는 수준에 맞춰 살라는 것이었다. 적금은 줄이면 되고 먹고 싶은 것과 하고 싶은 걸 안하면 된다. 애기는 나오면 그때에 가서 생각하면 되고 나오면 나오는대로 또 어떻게든 방법이 생긴다고 한다. 맞는 말씀이다.

내가 돈을 조금 밖에 못 벌어다 주는게 굉장히 미안하긴 하지만, 부끄럽지는 않다. 나는 돈이 목적인 인생을 살지 않으려고 노력한다. 되도록이면 재밌고 행복하게 살려고 노력한다. 돈을 벌려고 내가 하고 싶지도 않은 일을 하거나, 남을 속여서 돈을 벌거나, 남들의 앞길을 막거나, 집에서도 짜증을 낼 정도로 스트레스를 받아가며 일한다거나, 억지스러운 술자리에 참석하며, 가식적으로 누군가의 비위를 맞춘다던지, 주말도 없이 일을 한다던지, 실제 하는 일은 없으면서 그럴싸하게 포장된 명함으로 돈을 번다던가, 한마디로 드럽게 돈을 벌지 않으려고 노력한다는 것에 조금의 자부심 마저도 느끼고 있다.

원하는대로 못 살아서 조금 불편하겠지만 계속해서 이런 이런 신념을 지키며 살 수 있도록 노력할 것이고 언젠가는 이런 신념을 지키면서 살더라도 람보르기니를 선물할 수 있는 그날이 오면 난 정말 기쁠 것 같다.

당장은 많이 미안하지만 조금만 불편하게 살자꾸나.

ps: 이 얼마나 무능력하고 무대뽀인 가장이란 말인가.. 캬...
top


스프링 3.0의 MVC 간편화

Spring/3.0 : 2009.12.22 11:42


원문: http://blog.springsource.com/2009/12/21/mvc-simplifications-in-spring-3-0/
봄싹위키: http://springsprout.org/wiki/1556.do

유겐아렌이 언급했듯이, 모든 자바 개발자는 부담없이 스프링 3.0으로 버전을 올릴 수 있다. 이제 스프링 3이 나왔고 나는 여러분이 아직 모르고 있을 법한 MVC 기능을 소개하고자 한다. 이 기능들이 여러분에게 유용하고 웹 애플리케이션 개발을 빠르게 시작하는데 도움이 되기를 바란다.

또한 이 글은 "스프링 3 간편화" 시리즈의 시작으로 앞으로 몇일 또는 몇주 동안 이와 비슷한 글들이 올라올 것으로 예상한다.

설정 간편화

스프링 3은 mvc 네임스페이스를 도입하여 스프링 MVC 설정을 대폭 간편화했다. 지금까지 다른 개선사항들은 스프링 MVC 애플리케이션을 구성하고 실행하는것을 간편화 시켜주지는 않았다. mvc-basic 예제를 통해 살펴보도록 하자.

mvc-basic 예제는 스프링 MVC 기능의 기본 구성을 보여주도록 만들었다. 프로젝트는 spring-samples SVN 저장소에서 얻을 수 있으며 메이븐으로 빌드하고 이클립스로 import할 수 있다. web.xml 부터 시작하여 거기에 있는 설정부터 살펴보자. 주목할 것은 DispatcherServlet이 단일 마스터 스프링 설정 파일로 설정되어 있고 그 안에서 모든 애플리케이션 구성요소를 초기화한다. URL Rewrite를 설정하여 모든 요청을 깔끔하고 REST-스러운 URL로 DispatcherServlet에 보낸다.

마스터 설정 app-config.xml에서 일반적인 구성을 살펴볼 수 있다. 컴포넌트 스캔으로 클래스패스에서 애플리케이션 구성요소를 찾는다. MessageSource를 설정하여 지역화된 리소스 번들을 설정한다. 마지막으로 애플리케이션의 스프링 MVC 설정을 import한다.

mvc-config.xml 안에서 스프링 3의 첫번째 새 기능을 볼 수 있다.

<!-- Configures the @Controller programming model -->
<mvc:annotation-driven />

이 태그는 요청을 @Controller로 디스패치할 때 필요한 HadlerMapping과 HandlerAdapter를 등록한다. 또한 클래스패스에 무엇이 있는지에 따른 감각적인 기본값(sensible defaults)을 제공한다. 그러한 기본값은 다음과 같다.

- 자바빈 PropertyEditor 보다 더 견고한 대체제인 스프링 3 타입 ConversionService 사용하기
- @NumberFormat으로 숫자 필드 포매팅 지원
- @DateTimeFormat을 사용하여 Date, Calendar 포매팅 지원. Joda Time이 클래스패스에 있다면 Joda Time도 포매팅 지원.
- JSR-303 공급자가 클래스패스에 있다면 @Controller 입력값을 @Valid를 사용하여 검증
- JAXB가 클래스패스에 있다면 XML 읽기/쓰기 지원
- Jackson이 클래스패스에 있다면 JSON 읽기/쓰기 지원

꽤 멋지지 않은가? 훗

계속해서 mvc-config.xml 다음 줄에서 또다른 새로운 기능을 살펴보자.

<!-- Forwards requests to the "/" resource to the "welcome" view -->
<mvc:view-controller path="/" view-name="welcome" />

mvc:view-controller는 랜더링할 뷰를 선택하는 ParameterizableViewController를 등록한다. "/"를 요청하면 welcome 뷰를 랜더링하도록 설정했다. 실제 뷰 템플릿은 /WEB-INF/views 디렉토리의 .jsp가 랜더링 된다.

계속해서 mvc-config에서 살펴보지 않은 새로운 기능을 보도록 하자.

<!-- Configures Handler Interceptors -->
<mvc:interceptors>
    <!-- Changes the locale when a 'locale' request parameter is sent; e.g. /?locale=de -->
    <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" />
</mvc:interceptors>

mvc:interceptors 태그는 모든 컨트롤러에 적용할 HandlerInterceptor를 등록하게 해준다. 이전에는 반복적으로 모든 HandlerMapping 빈에 interceptor들을 등록했었다. 또한 이 태그를 사용하여 인터셉터를 적용할 URL 경로를 제한할 수 있다.

자 지금까지 살펴본 것을 종합하여, 예제를 배포하면 다음과 같은 welcome 뷰가 랜더링 될 것이다.


다른 지역 링크를 클릭하여 LocaleChangeInterceptor가 사용자 지역을 교체하도록 해보자.

데이터 바인딩 간편화

다음으로 설명할 새 기능은 @Controller 바인딩과 검증이다. 몇주 전에 글을 올렸다시피 이 부분에 새로운 기능이 많이 있다.

예제에서, @Controller Example 링크를 클릭하면 다음과 같은 폼이 랜더링 된다.


이 폼은 지역 정보 변경에 따라 국체화 필드 포매팅이 적용된다. 예를 들어, en에서 de로 바꾸면 날짜 형식을 12/21/10에서 12.12.10으로 바꾼다. 이런 동작과 폼 검증 규칙은 모델 애노테이션을 기반으로 한다.

public class Account {

    @NotNull
    @Size(min=1, max=25)
    private String name;

    @NotNull
    @NumberFormat(style=Style.CURRENCY)
    private BigDecimal balance = new BigDecimal("1000");

    @NotNull
    @NumberFormat(style=Style.PERCENT)
    private BigDecimal equityAllocation = new BigDecimal(".60");

    @DateTimeFormat(style="S-")
    @Future
    private Date renewalDate = new Date(new Date().getTime() + 31536000000L);

}

폼 서브밋은 다음의 AccountController 메서드가 처리한다.

@RequestMapping(method=RequestMethod.POST)
public String create(@Valid Account account, BindingResult result) {
    if (result.hasErrors()) {
        return "account/createForm";
    }
    this.accounts.put(account.assignId(), account);
    return "redirect:/account/" + account.getId();
}

이 메소드는 바인딩과 검증 작업 이후에 호출되며, Account 검증은 @Valid 애노테이션에 의해 수행된다. 만약 어떠한 검증 에러라도 존재한다면 createForm을 다시 랜더링 한다. 그렇지 않을 경우 Account는 저장되고 사용자는 http://localhost:8080/mvc-basic/account/1로 리다이렉트 된다.

또 다른 멋진 기능을 사용해보려면 /account/99 같이 존재하지 않는 계정을 요청해보기 바란다.

요약

스프링 3은 다양한 새 기능과 여러 기존 분야에 대한 간편화를 제공한다. 이 글을 통해 여러분에게 유용한 새로운 스프링 MVC 개선 사항들을 파악했기를 바란다. 맨 처음에 언급했듯이 더 많은 "스프링 3 간편화" 시리즈를 통해 계속해서 스프링 최신 버전의 새롭고 흥미로운 기능들을 소개할테니 기대하기 바란다.

즐거운 연휴되시길!

top


[웹 사이트 속도 향상 베스트 프랙티스 10] 자바스크립트 CSS 크기 줄이기

모하니?/Coding : 2009.12.19 12:53


http://developer.yahoo.com/performance/rules.html#minify

Minify JavaScript and CSS

tag: javascript, css

크기줄이기는 로드 타임을 향상시키기 위해 코드에서 불필요한 문자를 제거하여 그 파일 크기를 줄이는 방법이다. 코드를 최소화하면 모든 주석이 제거되고 불필요한 공백(스페이스, 새줄, 탭)등이 제거된다. 그렇게 하면 자바스크립트의 경우 다운로드 할 파일 크기가 줄어들어 응답 시간 퍼포먼스를 향상시켜준다. 자바스크립트 코드를 최소화하는데 사용하는 유명한 도구 두개로 JSMin과 YUI 압축기(Compressor)가 있다. YUI 압축기는 CSS도 최소화해준다.

Obfuscation은 소스 코드에 적용할 수 있는 최적화 대체기술이다. 이것은 최소화보다 복잡하고 obfuscation 과정 자체에서 버그를 만들어 낼 가능성이 더 많다. 미국 웹 사이트 탑 10을 조사한 결과 최소화는 21% 정도 크기를 줄인 반면 obfuscation은 25%를 줄였다. 비록 obfuscation이 더 많으 크기를 줄였지만 자바스크립트 최소화를 사용하는 것이 덜 위험하다.

외부의 스크립트와 스타일시트를 최소화하는 것과 더불어 인라인 <script>와 <style> 블럭도 최소화 해야 하며 그렇게 할 수 있다.  gzip으로 스크립트와 스타일 시트를 압축하더라도, 그것들을 최소화하는 것이 5% 이상 크기를 줄일 수 있다. 자바스크립트와 CSS 사용과 크기가 증가하면서 코드 최소화로 얻을 수 있는 것도 많아지고 있다.

ps: 구글에서도 뭔가 압축기 나왔담서요?? 
top


[스프링 테스트 확장] static member class를 빈으로 등록하는 테스트 로더 1

모하니?/Coding : 2009.12.18 13:19


테스트를 편하게 하기위한 기발한 아이디어를 읽는 도중 조금 맘에 안 드는 부분이 있었는데.. 그건 바로 ApplicationContext를 직접 생성하는 부분이었습니다. 게다가 원래는 @Configuration이 붙어있는 클래스를 넘겨받기로 되어있는 API에 static membe class를 넣어 빈으로 만들었습니다. 굉장히 기발하지만 약간 해킹스러운 방식이고 불편한 점들이 보였습니다. 빈으로 등록할 static member class가 많아지기라도 하다면.. 흠..

그래서 static member class를 빈으로 등록하겠다는 명시적인 이름을 가지고 있는 테스트 컨텍트스 로더를 만들기로 했습니다. 몇일전에 만들었던 애노테이션 설정 컨텍스트 로더를 조금만 고치면 가능할 것 같아서 시도했습니다.

그 결과 다음과 같이 테스트를 작성할 수 있게됐습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = StaticMemberClassLoader.class)
public class StaticInnerConfigTest {

    @Autowired ApplicationContext ac;

    static class Hello {
        @Inject Printer printer;
    }

    static class Printer {
    }

    class Foo {}

    @Test
    public void inject() {
        for(String beanName : ac.getBeanDefinitionNames()){
            System.out.println(beanName);
        }

        Hello hello = ac.getBean(Hello.class);
        assertThat(hello.printer, is(notNullValue()));
    }
}

사부님이 작성한것과 비교하자면, 우선 ApplicationContext를 직접 만들지 않아도 알아서 static member class를 빈으로 만들어주며, static member class를 ContextConfigruation으로 쓰겠다고 로더 설정이 되어있으니 좀 더 명시적이라고 할 수 있겠습니다. 음헤헷;


콘솔에는 위와 같이 찍힙니다. StaticMemberClassLoader를 확장하거나 수정하여 빈을 등록할 때 빈 이름이나, 등록할 빈을 골라내는 작업을 얼마든지 변경할 수도 있습니다.

사부님 덕분에 아침부터 잼난 코딩을 했더니 기분이좋습니다. 참고로 이 로더 구현체에는 문제가 좀 많습니다. 무슨 문제가 있는지 지적해주시는 분이 계실지 모르겠지만 퀴즈 삼아 문제가 많은 코드를 공유하고 싶어졌습니다. 발코딩을 까고 싶으신 분들에게는 절호의 기회입니다. 마음껏 까주세요!

public class StaticMemberClassLoader extends AnnotationContextLoader {

    @Override
    protected void createCustomBean(DefaultListableBeanFactory context, String[] locations) throws ClassNotFoundException {
        for(String string : locations){
            Class beanClass = ClassUtils.forName(string, getClass().getClassLoader());
            context.registerBeanDefinition(beanClass.getSimpleName(), BeanDefinitionBuilder.rootBeanDefinition(beanClass).getBeanDefinition());
        }
    }

    @Override
    protected String[] generateDefaultLocations(Class<?> clazz) {
        Class[] classes = clazz.getDeclaredClasses();
        List<String> modifiedLocations = new ArrayList<String>();
        for (int i = 0; i < classes.length; i++) {
            Class klass = classes[i];
            if(klass.getModifiers() == Modifier.STATIC)
                modifiedLocations.add(klass.getName());
        }
        return modifiedLocations.toArray(new String[modifiedLocations.size()]);
    }

}


top


뱀이다~ 뱀이다~ 몸에 좋고 맛도 좋은 뱀이다~




보아뱀을 두르고도 자연스러운 저 표정. 참 여유로와 보이지 않나요? 후훗..

뱀이 목을 졸라주면 (뱀 기분이) 좋은거고, 입을 벌리면,, 별로 안 좋은거임; 혀를 낼름 거리는 건 냄새를 맡기 위해서 라네요. 저 뱀은 저를 별로 좋아하지 않았어요.ㅋㅋ
top

TAG , 헤이리

봄싹 Career Path

모하니?/Planning : 2009.12.17 15:19


저는 봄싹 대표가 아니라 운영진 중 한명으로써 봄싹 스터디를 어떻게 키워나갈지 항상 고민하고 있습니다. 사실 대표가 왜 필요한지도 잘 모르겠어요. 규모가 작아서 그런가.. 아무튼 성윤군과 정우형 등 팀원들이 잘 도와주고 있어서 별다른 어려움은 못 느끼고 있습니다.

어쨋거나, 그건 그렇고 봄싹 커리어 패스를 생각해봤습니다.


현재 스터디 모임을 벗어나 개발자 모임으로 성장했다고 생각합니다. 이 단계에서 아직 못 이룬 것이 있다면 "세미나"입니다. 요즘 날이 추워서;; 내년 봄에 지금까지 봄싹에서 진행한 스터디와 프로젝트 내용을 종합하여 세미나를 가질 생각입니다. 아직 구체적으로는 계획하진 않았지만 꼭 할 겁니다.

2009년은 개발자 모임으로 진화했다면 2010년에는 좀 더 새로운 모습으로 진화하는 한 해가 되었으면 하는 바램입니다. 현재 스프링 3.0 레퍼런스 번역과 스프링소스 블로그 번역은 자체적으로 진행 중이며, 프로 스프링 2.5 베타리딩과 하이버네이트 베타리딩을 맡아 진행 중입니다. 그 외에도 저술과 원서 번역 계획을 가지고는 있지만 역시 이것도 아직 구체적으로 계획된 것은 없습니다. 하지만 아마도.. 할 것 같습니다.

물론, 진화를 한다고 해서 이전에 하던 것들을 모두 중단하지는 않습니다. 개발자 모임으로 바뀌었지만, 최근까지 오리지널 스프링 스터디를 진행했으며, 계속해서 새로운 스터디 주제거리(하이버네이트, 자바스크립트, JSF 등)가 생겨나고 있고, 새로운 스터디 방식을 모색 중입니다.

2010년에는 어떤 모습으로 변할지 기대 됩니다.
하지만 아직은 그 단계를 뭐라고 불러야 할지 떠오르질 않네요.

머라고 해야하지? @_@ "수퍼 울트라 캡숑 모임?"
top


스프링 프레임워크 3.0이 GA 됐다.

Spring/3.0 : 2009.12.17 11:12


원문: http://www.springsource.org/node/2266
봄싹 위키: http://springsprout.org/wiki/1495.do

긴 여정을 거쳐, 드디어 스프링 3.0 GA (배포)를 이용할 수 있게 됐다는 것을 발표하게 되어 기쁘다! 스프링소스는 지금 축제 중이다. 여러분도 같이 파티에 참여하기 바란다. :)

가장 최근 소식으로, 스프링 3.0 GA는 이제 (지난주에 배표된 GlassFish v3과 같은) 자바 EE 6 파이널 런타임 환경과 호환가능하며 이미 (EcliseLink 2.0 같은) JPA 2.0 파이널을 지원하고 있다. 또한 (JSR-250 v1.1에) 새로 추가된 @ManagedBean 컴포넌트 스캐닝을 지원하며 애노테이션-주도 의존성 주입에 활용할 (JSR-330) @Inject와도 잘 동작한다.

여러분의 편의를 위해 스프링 3.0 전체 주요 기능을 요약했다.

* 스프링 EL(SpEL): 빈 정의에서 사용할 코어 표현식 파서로, 내부 빈 구조와 환경적인 데이터 구조 속성 값에  #{...} 문법을 사용하여 접근할 수 있다.

* 애노테이션-기반 컴포넌트 지원 확장: (스프링 JavaConfig로 알려져 있는)설정 클래스 개념과 애노테이션 팩토리 메서드를 도입했다. 스프링은 또한 환경 값을 @Value 표현식으로도 주입할 수 있게 해주며, 동적인 #{...} 표현식과 정적인 ${...} 대체공간(placeholder)을 이용하여 설정 구성(configruation settings)을 참조할 수 있게 해준다.

* 강력한 스테레오타입 모델: 메타 애노테이션을 사용한 '숏컷' 애노테이션 생성을 지원한다. 예를 들어 기본 스코프와 기본 트랜잭션 특성을 가지고 있는 커스텀 스테레오타입을 만들 수 있다. @Service, @Scope("request"), @Transactional(readOnly=true)를 모두 가지고 있는 @MyService 애노테이션 한개를 만들어 사용할 수 있다.

* 표준화된 의존성 주입 애노테이션: 스프링 3.0은 자바 애노테이션-기반 의존성 주입에 관한 JSR-330 스펙을 완전히 지원한다. @Inject와 관련된 qualifier와 providier 모델을 스프링 내부의 @Autowired에 대한 대체제로 지원한다.

* 제약 애노테이션 기반의 선언적인 모델 검증: JSR-303 빈 검증기(validation provider)에 대한 스프링-스타일 구성을 했다. 애노테이션을 이용한 검증 옵션을 스프링 MVC에서 이용할 수 있다. 스프링의 바인딩 결과 기능을 통해 제약사항 위반에 대한 일관적인 뷰를 제공한다.

* 강화된 바인딩과 애노테이션-기반 포매팅: 표준 PropertyEditor에 대한 대체제로 Converter와 Formatter SPI를 제공한다. 포맷팅은 JSR-303 제약과 비스한 스타일로 애노테이션을 이용할 수 있다. 예를 들어 @DateTimeFormat을 사용할 수 있다. 또한 새로운 mvc 네임스페이스를 사용하여 스프링 MVC에 포맷팅과 검증기를 편하게 설정할 수 있으니 확인해 보기 바란다.

* 방대한 REST 지원: REST스타일 요청 매핑, @PathVariable 매개변수를 사용한 URI 변수 추출, 컨텐츠 교섭(content negotiation)을 통한 뷰 판단과 같은 고유의 REST를 스프링 MVC에서 사용할 수 있다. 클라이언트쪽 REST 지원은 RestTemplate 클래스를 이용할 수 있다.

* 풍부한 고유 포틀릿 2.0 지원: 스프링 MVC는 포틀릿 2.0 환경과 포틀릿 2.0의 새로운 이벤트와 리소스 요청 모델을 지원한다. 일반적인 포틀릿 요청 특징에 따른 구체화된 매핑 기능을 제공한다. @ActionMapping, @RenderMapping, @ResourceMapping, @EventMapping.

* 객체/XML 매핑(OXM): 스프링 웹 서비스에 있었지만, 지금은 스프링 프레임워크 코어로 이동했다. JAXB2, Castor 등을 지원하는 마샬링 언마샬링 추상화를 제공한다. 스프링 MVC와 스프링 JMS에 XML 페이로드(payload) 연동 옵션을 제공한다.

* 차세대 스케줄링 기능: cron을 지원하는 TaskScheduler와 Trigger 매터니즘을 새로 추가했다. 스프링 3.0은 편리한 task 네임스페이스와 @Async, @Scheduled 애노테이션을 제공한다. 고유의 쓰레드 풀 또는 서버가 관리하는 쓰레드 풀에서 실행될 수 있다.

이러한 큰 테마들 이외에, 스프링 2.5에 업그레이드 할 때 특히 좋아할 수백개의 구체적인 개선 사항사항이 있으니 변경사항과 자바독을 확인하기 바란다.

시스템 요구사항에 따르면, 스프링 3.0은 다양한 환경을 다룬다. 두 가지 주요 특징으로, 스프링 3.0은 자바 SE 5 이상서블릿 2.4 이상을 지원한다. 예를 들어 톰캣 5.x와 6.x를 지원한다. 또한 웹스피어 6.1과 웹로직 9.2와 같은 유명한 엔터프라이스 서버와도 호환가능하다. GlassFish v3은 이미 지원하고 있다.

마무리 하자면, 스프링 3는 여러분의 서버를 업그레이할 필요 없이 완전히 새로운 컴포넌트 모델 기능과 JSR-330 주입과 JSR-303 검증같은 표준을 제품 환경에 가져다 준다! 여러분이 해야 할 일은 스프링을 사용중인 애플리케이션의 라이브러리를 스프링 3.0으로 바꾸는 것 뿐이다.

줄겨라 그리고 다음에 이어질 구체적은 스프링 3 기능에 대한 글과 스프링 3.0에서 돌아가는 예제들을 기대하기 바란다!

ps: 반가운 마음에 급하게 번역하느라.. 좀 날림입니다. 이해해 주세요;
top


ProxyFactoryBean을 이용한 초간단 AOP 구현

모하니?/Coding : 2009.12.16 16:01


스프링 AOP가 어렵다고 생각하시는 분들은 저 클래스를 사용하는 방법부터 익히시면 도움이 될 것 같습니다.
아주 간략하게 ProxyFactoryBean을 사용해서 AOP를 적용해 보겠습니다.

AOP는 OOP 같은 프로그래밍 기법이지 무슨 기술이 아닙니다. 따라서 이 글에서 구현하는 내용은 AOP 적용 방법 중 하나라고 생각하시면 됩니다.

    interface Service {
        public void hi();
        public void hi2();
    }

이런 인터페이스가 있고

    class ServiceImpl implements Service {
        public void hi(){
            System.out.println("my business");
        }

        @Override
        public void hi2() {
            System.out.println("my2");
        }
    }

이런 구현체가 있을 때. hi() 호출 전 후에만 메시지를 출력하고 hi2()는 걍 저대로 출력하고 싶다면 ...  우선 해당 작업을 수행할 MethodIterceptor 인터페이스의 구현체를 만듭니다.

    class ServiceLoggingAdvice implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            System.out.println("before hi");
            invocation.proceed();
            System.out.println("after hi");
            return null; 
        }
    }

그리고 hi()와 hi2()를 선별해줄 NameMatchMethodPointcut을 만듭니다.

    class ServiceLoggingPointcur extends NameMatchMethodPointcut {

        ServiceLoggingPointcur() {
            setMappedName("hi");
        }
    }

이제 마지막으로 ProxyFactoryBean으로 위에서 만들었던 타겟(Service 구현체), 어드바이스(MethodInterceptor 구현체), 포인트컷(NameMatchMethodPointcut 구현체)을 ProxyFactoryBean에 설정 해주면 끝!

    Service service;
    ProxyFactoryBean proxyFactoryBean;

    @Before
    public void setUp(){
        proxyFactoryBean = new ProxyFactoryBean();
        proxyFactoryBean.setTarget(new ServiceImpl());
        proxyFactoryBean.addAdvisor(new DefaultPointcutAdvisor(
                new ServiceLoggingPointcur(),
                new ServiceLoggingAdvice()));
    }

    @Test
    public void logging(){
        Service service = (Service) proxyFactoryBean.getObject();
        service.hi();
        service.hi2();
    }

이렇게 하면 콘솔에


요런식으로 출력됩니다.

자세한 내용은 언젠간 출간될 Toby님의 스프링 책을 참조하세요.




top


[스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 4 - 일단 끝

모하니?/Coding : 2009.12.16 13:53


/**
 * @author Keesun Baik(Whiteship)
 * @author Seongyoon Kim(Is윤군)
 */
public class AnnotationContextLoader extends AbstractContextLoader {

    private static final String JAVA_FILE_SUFFIX = ".java";
    private static final String APP_CONFIG_FILE_PREFIX = "AppConfig";

    @Override
    public String getResourceSuffix() {
        return APP_CONFIG_FILE_PREFIX + JAVA_FILE_SUFFIX;
    }

    @Override
    protected String[] generateDefaultLocations(Class<?> clazz) {
        Assert.notNull(clazz, "Class must not be null");
        String suffix = getResourceSuffix();
        Assert.hasText(suffix, "Resource suffix must not be empty");
        return new String[] { clazz.getName() + suffix };
    }

    @Override
    protected String[] modifyLocations(Class<?> clazz, String... locations) {
        String[] modifiedLocations = new String[locations.length];
        for (int i = 0; i < locations.length; i++) {
            String path = locations[i];

            if(path.endsWith("/"))
                path = path.substring(0, path.length() - 1);

            if (path.startsWith("/")) {
                modifiedLocations[i] = ClassUtils.convertResourcePathToClassName(path.substring(1));
            }
            else if (!ResourcePatternUtils.isUrl(path)) {
                modifiedLocations[i] = getClassName(clazz, path);
            }
            else {
                throw new UnsupportedOperationException();
            }
        }
        return modifiedLocations;
    }

    private String getClassName(Class clazz, String path) {
        return ClassUtils.convertResourcePathToClassName(
            StringUtils.cleanPath(ClassUtils.classPackageAsResourcePath(clazz) + "/" + path));
    }

    public final ConfigurableApplicationContext loadContext(String... locations) throws Exception {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        prepareContext(context);
        customizeBeanFactory(context.getDefaultListableBeanFactory());
        context.register(getAppConfigClasses(context.getClassLoader(), locations));
        context.scan(getAppConfigPackages(context.getClassLoader(), locations));
        AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
        context.refresh();
        context.registerShutdownHook();
        return context;
    }

    private String[] getAppConfigPackages(ClassLoader classLoader, String[] locations) {
        List<String> packages = new ArrayList<String>();
        for(String location : locations){
            if(!location.contains(JAVA_FILE_SUFFIX))
                packages.add(location);
        }
        return packages.toArray(new String[packages.size()]);
    }

    private Class<?>[] getAppConfigClasses(ClassLoader classLoader, String[] locations) throws ClassNotFoundException {
        List<Class<?>> classes = new ArrayList<Class<?>>();
        for(String location : locations){
            if(location.contains(JAVA_FILE_SUFFIX))
                classes.add(ClassUtils.forName(location.replace(JAVA_FILE_SUFFIX, ""), classLoader));
        }
        return classes.toArray(new Class<?>[classes.size()]);
    }

    protected void prepareContext(GenericApplicationContext context) {
    }
    protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
    }
    protected void customizeContext(GenericApplicationContext context) {
    }

이걸 사용하시면 됩니다. 이제 스프링 이슈에 올려야지. 캬캬캬
스프링 레퍼런스 공부할 때 오타 찾아서 이슈 등록하던게 엊그제 같은데 이젠 코드도 기여할 수 있으려나..

코드를 보시면 아시겠지만, classpath:, file:, url: 등의 prefix 지원은 포기했습니다. 그래도..

1. 스프링 애노테이션 설정 파일만 가지고 쉽게 테스트 할 수 있으며
2. 스프링 애노테이션 설정 파일을 명시적으로 설정할 수 있으며
3. 임의의 패키지를 명시적으로 설정할 수 있습니다.

딱 제가 원하던 만큼이니 이정도면 저는 만족합니다. 이걸 스프링에서 고쳐서 넣어주던 말던~ 일단은 try.

ps: 같이 코딩해주고 상의해준 성윤군과, 스프링 러너와 로더 설정 방법 알려주신 사부님 썡큐!

top


[스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 3

모하니?/Coding : 2009.12.15 19:01


하악 하악.. 배가 너무 고파요. 언능 집에가서 집안일 도와줘야 하는데;; 오늘 너무 늦게 들어가서 와이프 고생할까봐 마음이 아프네요. ㅠ.ㅠ 미안해 내가 코딩을 못해서.. 너무 늦게 들어가게 생겼어;; 나머진 내일 해야지;; 후딱 갈께;; (이것만 쓰고;;)

public class AnnotationContextLoaderTest {

    AnnotationContextLoader acl = new AnnotationContextLoader();

    @Test
    public void generateDefaultLocations(){
        String[] result = acl.generateDefaultLocations(SpringAnnotationConfigTest.class);
        assertThat(result[0], is("sandbox.springtest.sample.SpringAnnotationConfigTestAppConfig.java"));

    }

    @Test
    public void modifyLocations(){
        // absolute location
        String[] result = acl.modifyLocations(SpringAnnotationConfigTest.class, "/sandbox/springtest/sample/SpringAnnotationConfigTestAppConfig.java");
        assertThat(result[0], is("sandbox.springtest.sample.SpringAnnotationConfigTestAppConfig.java"));
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "/sandbox/springtest/sample/");
        assertThat(result[0], is("sandbox.springtest.sample"));
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "/sandbox/springtest/sample");
        assertThat(result[0], is("sandbox.springtest.sample"));
        // relative location
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "./SpringAnnotationConfigTestAppConfig.java");
        assertThat(result[0], is("sandbox.springtest.sample.SpringAnnotationConfigTestAppConfig.java"));
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "./");
        assertThat(result[0], is("sandbox.springtest.sample"));
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, ".");
        assertThat(result[0], is("sandbox.springtest.sample"));
        // classpath: prefix
//        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "classpath:./SpringAnnotationConfigTestAppConfig.java");
//        assertThat(result[0], is("sandbox.springtest.sample.SpringAnnotationConfigTestAppConfig.java"));
    }

}


경로 가져오는 부분이 복잡하므로, TDD로 테스트! 이런건 테스트 먼저 만들기도 좀 쉬운편이죠.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"/sandbox/springtest/sample/SpringAnnotationConfigTestAppConfig.java"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"/sandbox/springtest/sample/"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"/sandbox/springtest/sample"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"./SpringAnnotationConfigTestAppConfig.java"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"./"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"."})
//@ContextConfiguration(loader = AnnotationContextLoader.class)
public class SpringAnnotationConfigTest {

    @Autowired ApplicationContext ac;

    @Test
    public void di(){
        assertNotNull(ac);
        String name = ac.getBean("name", String.class);
        assertNotNull(name);
    }

}

현재 구현한 것으로 사용할 수 있는 방법을 나열해 봤습니다. 주석을 바꿔가면서 테스트 해볼 수 있지요;; 물론 이미 테스트에서 경로 확인은 했지만, 그 뒤에 벌어지는 일에 대해서는 저걸로 통합 테스트;;

구현체는 비공개!!

prefix 붙이는 부분만 처리되면 공개할지도?? 말지도??

top


2010 매쉬업 경진대회 시작



http://mashupkorea.org/106

우왕 커피 마시자~

매쉬업도 나가볼까나~
top

TAG 매쉬업

[스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 2

모하니?/Coding : 2009.12.15 17:35


가장 먼저 떠올랐던 기본적인 시나리오는 다음과 같습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationContextLoader.class)
public class SpringAnnotationConfigTest {

    @Autowired ApplicationContext ac;

    @Test
    public void di(){
        assertNotNull(ac);
        String name = ac.getBean("name", String.class);
        assertNotNull(name);
    }

}

이런 테스트가 있고, @ContextConfigruation에 아무런 locations를 지정하지 않았을 때는 테스트 클래스 이름 + AppConfig.java 파일을 찾아서 해당 클래스를 애노테이션 설정 클래스로 인식하는 겁니다.

@Configuration
public class SpringAnnotationConfigTestAppConfig {

    @Bean
    public String name(){
        return "keesun";
    }

}

즉 위와같은 애노테이션 설정 클래스를 기본 설정으로 인식하는거죠. 물론 AppConfig라는 이름을 다른 이름으로 변경할 수 있도록 확장성을 고려해야겠습니다.

그 다음 시나리오는 조금 복잡한데, @ContextConfiguration에 AppConfig 같은 @Configuration을 사용한 자바 설정 파일과 springsprout.org.config 같은 패키지 명을 설정할 수 있게 하는 겁니다.

@ContextConfiguration(locations = {"AppConfig.java", "classpath:../"})

@ContextConfiguration(locations = {"classpath:./AppConfig.java", "../"})

.java로 끝나는 location 정보는 AnnotationConfigApplicationContext의 register를 이용하며, .java로 끝나지 않는 location 정보는 패키지로 인식하여 AnnotationConfigApplicationContext의 scan을 이용해주는 겁니다. 물론, 각각의 resource 정보는 스프링의 Resource prefix인 classpath:, file, url:을 이용할 수 있어야겠습니다.

이제 구현 ㄱㄱㅆ

top


[스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 1

모하니?/Coding : 2009.12.15 17:17


스프링 3.0 @Configuration에 대한 학습 테스트를 만들다 제일 먼저 찾게 된 것이 애노테이션 설정 기반 테스트 러너입니다. 스프링 XML 설정 파일 말고 애노테이션 설정 자바 파일만 주면 되는게.. 있을 줄 알았습니다.

하지만 없었습니다. 그래서 만들기로 마음 먹었습니다. 그리고 잘 만들어지면 스프링 이슈에 올려서 소스 코드를 기증할 생각도 가지고 있습니다.(받아 준다면 말이죠.ㅋ)

일단 처음 할 일은 적당한 확장 지점을 찾는 일인데, 이게 아주 재밌있습니다. 어제 사부님이 올려주신 주옥같은 글에서 스프링 내부에서의 DI 활용이 얼마나 멋진가를 보여주셨는데, 테스트 쪽에서도 그런 모습을 볼 수 있습니다.


스프링 최신 코드(3.0 RC3) 기준이기는 한데 2.5에 추가된 것들이고 그 후에는 변화가 없었습니다. 제가 만들려는 기능과 관련이 있는 클래스만 보고 있습니다. 소스 분석까지 쓰면 너무 길어지고 힘들기 때문에;; 그냥 바로 결론으로 넘어가겠습니다.

1. 처음에는 AGCL 화장을 시도했지만, AnnotationConfigApplicationContext가 내부에서 사용하고 있는 AnnotatedBeanDefinitionReader와 Scanner는 스프링의 BeanDefinitionReader 인터페이스를 따르고 있지 않아서 그럴 수가 없었습니다.

2. 한단계 올라가 AbstractContextLoader를 확장하기로 했습니다.

흠.. 다음은 시나리오를 좀 더 구체적으로 생각해봐야겠습니다.




top


[스프링 3.0] 스프링 bean과 일반 자바 객체가 호출하는 @Bean 메서드의 차이

Spring/3.0 : 2009.12.11 20:51


public class JavaConfigTest {

    AnnotationConfigApplicationContext ac;

    @Before
    public void setUp(){
        ac = new AnnotationConfigApplicationContext(AppConfig.class);
        assertThat(ac, is(notNullValue()));
    }

    @Test
    public void getBean(){
        SampleBean bean1 = ac.getBean("sampleBean", SampleBean.class);
        SampleBean bean2 = ac.getBean("sampleBean", SampleBean.class);
        assertThat(bean1, is(notNullValue()));
        assertThat(bean2, is(notNullValue()));
        assertThat(bean1, is(bean2));

        AppConfig config1 = new AppConfig();
        SampleBean bean3 = config1.sampleBean();
        assertThat(bean3, is(not(bean2)));

        AppConfig config2 = ac.getBean("appConfig", AppConfig.class);
        SampleBean bean4 = config2.sampleBean();
        assertThat(bean4, is(bean2));
    }

}

스프링 3.0 애노테이션 기반 설정을 익히기 위해서 처음 만들어본 테스트입니다. 이 테스트에서 알 수 있는 건 바로 이 글의 제목에서처럼 @Bean이 붙어있는 메서드를 어떤 객체를 이용해서 호출하느냐에 따라 그 결과가 다를 수 있다는 겁니다.

@Configuration
public class AppConfig {

    @Bean
    public SampleBean sampleBean(){
        return new SampleBean();
    }
}

이건 설정한 빈이고 SampleBean은 뭐 암거나;; @_@;

결론은 new 로 만든 AppConfig와 스프링에서 가져온 AppConfig의 @Bean이 붙은 메서드가 반환해주는 값이다르다는 겁니다. 전자는 일반적인 Java 문맥대로 sampleBean()에서 new SampleBean()으로 새로운 객체를 만들어서 받은 것이고, 후자는 sampleBean() 메서드 호출을 가로채서 기존의 bean을 반환해준 겁니다.

이 경우는 매우 간단한 경우에 속합니다. 하나는 스프링이 관리하는 빈이었고, 하나는 일반 자바 객체였으니까요. 그런데 만약에 둘 다 스프링의 빈이라면? 그 중에 하나는 @Configuration, 다른 하나는 @Component라면?

@Component로 설정한 빈 내부에 위와 똑같은 설정이 들어있다면? 반환하는 객체의 값이 조금 다르나면??

빈 스캔은 어찌하나?

정말 복잡하군요;; 하나씩 차근 차근 해보겠습니다.



top


[빈둥 빈둥] 닌텐도Wii 4.2k V2 홈브류 설치



감기 때문에 집에서 쉬는데 공부도 안 되고(코가 맹맹 거리면 머리가 웅웅 거리면서 무언가에 집중하기가 힘들어집니다.) 게임기나 만지작 거려야지 결심했습니다. (-- );;;

그런데 왜들 그렇게 정보를 꼭꼭 숨겨놓고 사시는지... 닌텐도 Wii 4.2k V2로 아무리 구글링을 해도 공개된 정보를 찾을 수가 없었습니다. 하지만 아주 없는 건 아니었습니다. 결국은 찾았죠. 영문자료 한 개와 한글 자료 두 개. 한글 자료 두 개는 그나마 이미 지워지거나 옮겨진 것이고 검색엔진의 '저장된 페이지 보기' 기능을 이용해서 볼 수 있었습니다.

기본라인: http://sites.google.com/site/completesg/hacking-guide/korea-4_2
파판: http://74.125.155.132/search?q=cache:kwRwCgmmwvoJ:www.finalfantasia.com/fantasybbs/board.php%3Fbo_table%3Dntdbbs%26wr_id%3D45713+Wii+4.2k+v2+%ED%99%88%EB%B8%8C%EB%A5%98+%EB%B0%8F+%ED%95%98%EB%93%9C%EB%A1%9C%EB%8D%94+%EC%84%A4%EC%B9%98&cd=1&hl=ko&ct=clnk&gl=kr&client=firefox-a

이 두 개를 섞어서 설치했더니 동작했습니다.

1. 파판 링크의 첫 번째 단계인 "홈 브류 설치"를 그대로 따라합니다. 별 문제 없이 잘 됩니다.

2. 파판 링크의 두 번째 단계 "기타 설치"는 그대로 따라하면 Trucha Bug Restorer에서 문제가 생깁니다.

3. 거기서 부터는 기본 라인과 파판을 짬뽕하여 제 맘대로 설치했습니다.
(맨 마지막 Wad Manager로 usb loader gx를 설치하기 전까지 모든 설치는 홈 브류 채널에 들어가서 합니다. 즉, SD 카드로 부팅하지 않았습니다.)

3-1. 기본라인 1~7 단계를 진행합니다.
3-2. 기본라인 8~9 단계를 진행합니다.
3-3. 파판링크의 '기타 설치' 11 단계를 진행합니다.
3-4. 기본라인의 11-12 단계를 진행합니다.
3-5. 파판링크의 '기타 설치' 14~16 단계를 진행합니다. (이 때만 SD 카드로 들어갔는데;; 이것 마저도 그냥 BHC에서 해도 될 것 같기도;;;)

끝난것 같지만 이제 시작입니다;;

Usb loader 설정해줘야 게임이 돌아가지 그냥 두면 게임이 안 돌아가고 검은 화면만 나오더군요. 마치 해킹한 PSP에서 커펌 버전이 낮다고 안 돌아가는 게임들처럼 말이죠; 게다가 게임 구해야지; 외장하드는 포맷해야지 덩덩덩;;

http://kws1070.tistory.com/46

위 링크를 기본으로 참조하되, 애플리케이션은 위 링크에 있는 걸 받지 마시고, 검색해서 최신 버전을 받으셔야 될 겁니다.

ps:  구글이 짱일세... 저장된 페이지 보기라... 캬..
top


[스프링 3.0] @Async 사용하기

Spring/3.0 : 2009.12.10 13:59


참조: http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/ch25s05.html#scheduling-annotation-support-async

봄싹 사이트에서 스터디 개설/변경, 모임 개설/변경 시에 알림 서비스로 이메일, 구글 메신저, 트위터로 알림 메시지를 전송합니다. 그런데, 사용자가 많아지다보니 해당 작업을 완료하는데 걸리는 시간이 너무 길어졌습니다. 불편해졌죠. 사실 관리자 권한이 있는 사람들만 만드는거라, 스터디 참여자 입장에서는 그런 불편을 알 수가 없을테지만, 저희는 내부적으로 좀 불편해 하고 있었습니다.

그러던 중 보게 된 것이 바로 @Async.. 딱 원하던 기능입니다.

필요한 빈 설정은

    <!-- ============================================================= -->
    <!--  Spring 3.0 @Task @Async                                      -->
    <!-- ============================================================= -->
    <task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>

    <task:executor id="myExecutor" />

    <task:scheduler id="myScheduler" />

적용하려면, 원래 작업을 별도의 클래스로 빼내고 그것을 참조하도록 수정하는 것이 좋겠습니다. 안그럼 동작하질 않더라구요,

@Service
public class BeanService {

    public void normal(){
        System.out.println("===========================");
        System.out.println(Thread.currentThread());
        System.out.println("===========================");
        System.out.println("do it");
        this.more();
    }

    @Async
    public void more() {
        System.out.println("===========================");
        System.out.println(Thread.currentThread());
        System.out.println("===========================");
        System.out.println("more");
    }

}

예를 들어, 위와 같은 경우 normal() 내부에서 more()를 호출할 경우에는 @Async가 적용되지 않습니다.
more()를 직접 호출할 떄는 적용됩니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class AsyncAnnoTest {

    @Autowired BeanService beanService;

    @Test
    public void async(){
        assertThat(beanService, is(notNullValue()));
        beanService.normal();
        beanService.more();
    }

}

대충 만든 테스트로 돌려보면 다음과 같은 결과를 볼 수 있습니다.





top

TAG @Async

스프링이 컴포넌트 스캔을 할 때 클래스로딩을 하지 않는다는 증거

모하니?/Coding : 2009.12.09 13:35


참조: InsideSpring (4) 빈 스캐너는 클래스를 로딩할까?

사부님의 주옥같은 글을 보다가.. 마지막 쯤에 클래스로딩이 되는지 안되는지 쉽게 확인한 방법이 안 떠오르신다고 해서;; 순간 생각난 원초적인 방법으로 실험해 봤습니다.

@Component
@Scope(value = "prototype")
public class Bean {

    static {
        System.out.println("I am loaded!");
    }
}

초간단 빈을 하나 만들고, 클래스가 로딩 될 때 실행되도록 static 블럭에 메시지를 출력합니다. 빈 스코프가 중요한데, 기본 스코프(singletone)이면 ApplicationContext를 만들 때 미리 인스턴스를 만들어 버리기 때문에, 만들다가 저 클래스를 로딩 하게되죠. 대충 로깅 메시지를 보고서 이미 컴포넌트 스캔이 끝난 뒤에 만들겠거니... 짐작할 수는 있지만 더 명시적으로 확인할 수 있게 애초에 prototype 스코프로 만듭니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class ClassLoadingTest {

    @Autowired ApplicationContext ac;
   
    @Test
    public void asm(){
        assertThat(ac, is(notNullValue()));
        for(String beanName : ac.getBeanDefinitionNames()){
            System.out.println(beanName);
        }
    }

}

그리고 초간단 테스트! 와 빈설정은.. 같은 패키지에 ClassLoadingTest-context.xml 이라는 이름으로

    <context:component-scan base-package="sandbox.asm" />

이렇게 달랑 한줄.

결과는..


반대로.. 리플렉션을 이용하면 어떻게 되는지 확인해봅니다.

역시;; 초간단 테스트 추가

    @Test
    public void reflection() throws ClassNotFoundException {
        Class bean = Class.forName("sandbox.asm.Bean");
        assertThat(bean, is(notNullValue()));
    }

메시지 확인


캬캬캬.. 스프링은 역시 좀 짱이구나;


top


스프링의 로깅 의존성

Spring/etc : 2009.12.08 17:56


원문: http://blog.springsource.com/2009/12/04/logging-dependencies-in-spring/

봄싹 위키: http://springsprout.org/wiki/1439.do

이 글은 스프링이 결정한 선택 사항들과 개발자들이 스프링을 사용하여 애플리케이션을 만들 때 로깅으로 이용할 수 있는 옵션을 다룬다. 무언가를 많이 변경했기 때문이 아니라, 여러분이 애플리케이션에서 로깅을 구현하고 설정할 때 정보에 근거할 수 있도록 이제는 출시가 임박한 스프링 3.0을 따라야 할 시점이다. 먼저 스프링에 어떤 필수 의존성이 있는지 살펴본 다음, common logging 라이브러리 예제를 사용하여 애플리케이션에 로깅을 설정하는 방법을 좀 더 자세히 살펴보겠다. 예를 들어 메이븐 중앙 스타일 구성물 작명 규약을 사용하는 의존성 설정을 보여주겟다.

스프링 의존성과 스프링에 의존하는 것들

스프링은 광범위한 엔터프라이즈 및 여러 외부 도구를에 대한 연동과 지원을 제공하지만, 내부적으로 필수 의존성을 최소한으로 유지한다. 간단한 경우에 스프링을 사용할 때는 많은 수의 라이브러리를 다운로드(혹은 자동으로) 받을 필요가 없다. 기본적인 의존성 주입은 오직 하나의 외부 의존성을 필요로 하며, 그건 바로 로깅이다.(로깅 옵션에 대해서는 아래의 보다 자세한 설명을 참조하자.) 만약 의존성 관리를 메이븐으로 하고 있다면 로깅 의존성을 명시적으로 설정할 필요도 없다. 예를 들어, 애플리케이션 컨텍트를 만들고 의존성 주입을 이용하고 있다면 애플리케이션에 다음과 같이 설정하기만 하면 된다. 메이븐 의존성은 다음과 같을 것이다.

<dependencies>
   <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.0.0.RELEASE</version>
      <scope>runtime</scope>
   </dependency>
</dependencies>

이게 전부다. 기본적인 의존성 주입을 사용하는 경우처럼, 일반저긍로 스프링 API를 컴파일 할 필요가 없다면 scope가 runtime으로 설정해도 된다는 것에 주목하자.

위 예제에서 메이븐 중앙 작명 규약을 사용했기 때문에, 메이븐 중앙 또는 스프링소스 S3 메이븐 저장소와 잘 동작한다. S3 메이븐 저장소를 이용하려면(마일스톤 또는 개발자 스냅샷 버전을 사용하기 위해), 저장소 위치를 메이븐 설정 파일에 명시해야 한다. 전체 배포는 저장소는 다음과 같다.

<repositories>
   <repository>
      <id>com.springsource.repository.maven.release</id>
      <url>http://s3.amazonaws.com/maven.springsource.com/release/</url>
      <snapshots><enabled>false</enabled></snapshots>
   </repository>
</repositories>

마일스톤 저장소는 다음과 같다.

<repositories>
   <repository>
      <id>com.springsource.repository.maven.milestone</id>
      <url>http://s3.amazonaws.com/maven.springsource.com/milestone/</url>
      <snapshots><enabled>false</enabled></snapshots>
   </repository>
</repositories>

스냅샷은 저장소는 다음과 같다.

<repositories>
   <repository>
      <id>com.springsource.repository.maven.snapshot</id>
      <url>http://s3.amazonaws.com/maven.springsource.com/snapshot/</url>
      <snapshots><enabled>true</enabled></snapshots>
   </repository>
</repositories>

스프링소스 EBR을 사요하려면 의존성을 나타낼 때 다른 이름 규약을 사용해야 한다. 이름은 보통 유추하기 쉽다. 예를 들어 이번 경우에는 다음과 같다.

<dependencies>
   <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>org.springframework.context</artifactId>
      <version>3.0.0.RELEASE</version>
      <scope>runtime</scope>
   </dependency>
</dependencies>

물론 저장소도 명시적으로 설정해 줘야 한다.

<repositories>
   <repository>
      <id>com.springsource.repository.bundles.release</id>
      <url>http://repository.springsource.com/maven/bundles/release/</url>
   </repository>
</repositories>

만약 의존성을 손으로 관리한다면, 위 저장소 설정에 보이는 URL은 브라우저로 볼수가 없고, http://www.springsource.com/repository에 사용자 인터페이스가 있으니 검색하여 의존성을 다운 받을 수 있다. 또한 이 사이트에는 간편한 메이븐과 Ivy 설정이 있으니 복사하여 붙여넣기로 쉽게 해당 도구에서 사용할 수 있다.

만약 Ivy를 사용하여 의존성 관리하는 것을 선호한다면 위와 비슷한 이름과 설정 옵션이 관리 시스템에 있거나 예제 코드를 살펴보기 바란다.

로깅

로깅은 매우 중요한 스프링 의존성이다. 왜냐하면 a) 유일한 외부 의존성이자, b) 모두가 자신이 사용하는 도구에서 무언가 출력되는 것을 보고 싶어하며, c) 스프링은 로깅에 의존성을 가지는 다양한 툴과 연동하기 때문이다. 애플리케이션 개발자의 목적 중 하나는 보통 외부 컴포넌트를 포함한 전체 애플리케이션에 대해 중앙의 통합된 로깅 설정을 가지는 것이다. 로깅 프레임워크가 다양해지면서 예전보다 선택하기가 더 어려워졌다.

스프링에 있는 필수 로깅 의존성은 Jakarta Commons Logging API(JCL)이다. JCL을 컴파일하고 JCL Log 객체를 스프링 프레임워크를 확장한 클래스에서 볼 수 있다. 스프링의 모든 버전에서 동일한 로깅 라이브러리를 사용하는 것이 사용자에게 중요하다. 스프링을 확장한 애플리케이션이더라도 뒷 단의 호환성이 보장되기 때문에 이전하기가 용이하다. 우리는 스프링의 모듈 중 하나를 명시적으로 commons-logging(JCL)에 의존하게 하고 다른 모듈들이 컴파일 시점에 해당 모듈에 의존하도록 하는 방법을 사용한다. 예를 들어, 메이븐을 사용하고 있으며, 어디서 commons-loggins에 대한 의존성을 가져와야 하는지 궁금하다면, 스프링에서 가져오도록 하고 명시적으로는 spring-core 모듈에서 가져온다.

commons-logging의 좋은 점은 애플리케이션이 동작하는데 필요한 것이 아무것도 없다는 것이다. 런타임 감지 알고리즘을 가지고 있어서 클래스패스에 잘 알려진 로깅 프레임워크가 있다면 적절한 것을 찾아 사용하도록 되어있다.(또는 여러분이 어떤것을 사용하고 싶은지 알려줄 수도 있다) 만약에 사용할 수 있는 것이 아무것도 없다면 JDK(java.util.logging 또는 줄여서 JUL)로부터 로그를 가져온다. 여러분의 스프링 애플리케이션이 대부분의 경우 잘 동작하며 잘 로깅 된다는 것을 알아야 하며, 그것은 중요하다.

불행히도, commons-logging의 안좋은 점은 역시 런타임 감지 알고리즘이다. 만약 시간을 되돌려 스프링을 새로운 프로젝트로 시작할 수 있다면 우리는 다른 로깅 의존성을 사용했을 것이다. 아마도 첫번째 선택은 많은 사람들이 애플리케이션에서 스프링과 함께 사용하고 있는 Simple Logging Facade for Java(SLF4J)일 것이다.

commons-loggins을 교체하는 것은 쉽다. 실행중에 클래스패스에 없도록 만들자. 메이븐에서 의존성을 제외할 수 있다. 스프링 의존성 선언 방법으로 인해 다음과 같이 한번만 선언하면 된다.

<dependencies>
   <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.0.0.RELEASE</version>
      <scope>runtime</scope>
      <exclusions>
         <exclusion>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
         </exclusion>
      </exclusions>
   </dependency>
</dependencies>

이제 이 애플리케이션은 JCL API가 클래스패스에 없으니 깨질 것이다. 새로운 것을 제공하여 그것을 고쳐보자. 다음 절에서 우리는 어떻게 JCL 구현체의 대안으로 SLF4J를 사용하는지 예제를 살펴보겠다.

SLF4J 사용하기

SLF4J는 연동할 다른 프레임워크를 찾는 작업을 실행중에 하지 않고 컴파일 시점에 바인딩 하기 때문에 commons-loggins 보다 보다 깔끔한 의존성과 실행시 보다 더 효율적이다. 이것은 즉 실행중에 여러분이 어떤 것을 사용하고 싶은지 보다 명시적으로 선언하고 그것을 적절하게 설정해야 한다는 뜻이다. SLF4J는 여러 로깅 프레임워크에 대한 바인딩을 지원하기 때문에 여러분들이 기존에 사용하고 있던 것을 선택하고 해당 설정과 관리로 바인딩할 수 있다.

SLF4J는 JCL을 포함한 여러 로깅 프레임워크에 대한 바인딩을 제공한다. 또한 그 반대로 마찬가지다. 다른 로깅 프레임워크와 자신을 연결해준다. 따라서 SLF4J를 스프링에서 사용하기 위해서는 commons-logging 의존성을 SLF4J-JCL 브릿지 의존성으로 교체해야 한다. 그렇게 한번 하면 스프링 내부의 로깅 호출이 SLF4J에 대한 로깅 호출로 바뀐다. 따라서 만약 애플리케이션에서 다른 라이브러리들도 해당 API를 사용하고 있다면, 로깅을 설정하고 관리할 곳은 한 부분이다.

일반적인 선택은 스프링에서 SLF4J로 연결하고, SLF4J에서 Log4J로 명시적인 바인딩을 하는 것이다. 4개의 의존성을  명시적으로 제공해야 한다. brigde, SLF4J API, binding to Log4J, Log4J 구현체. 메이븐에서 다음과 같이 설정할 수 있다.

<dependencies>
   <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.0.0.RELEASE</version>
      <scope>runtime</scope>
      <exclusions>
         <exclusion>
           <groupId>commons-logging</groupId>
           <artifactId>commons-logging</artifactId>
         </exclusion>
      </exclusions>
   </dependency>
   <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>1.5.8</version>
      <scope>runtime</scope>
   </dependency>
   <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.5.8</version>
      <scope>runtime</scope>
   </dependency>
   <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.5.8</version>
      <scope>runtime</scope>
   </dependency>
   <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.14</version>
      <scope>runtime</scope>
   </dependency>
</dependencies>

로깅을 하기위해서는 좀 많은 의존성처럼 보인다. 실제로 그렇긴 하지만 꼭 그래야 하는건 아니며 순수commons-logging 보다 더 잘 동작할 것이다. 특히 OSGi 플랫폼같이 제약적인 컨테이너를 사용한다면 말이다. 이른바 실행시점이 아니라 컴파일 시점에 바인딩을 하기 때문에 성능상 장점을 얻을 수 있다.

SLF4J 사용자들 사이에서 보다더 일반적인 선택은 Logback에 지접 바인딩하여 과정을 짧게 하고 의존성을 더 줄이는 것이다. Logback이 SLF4J를 지겁 구현하기 때문에 추가적인 바인딩 과정을 제거해주기 때문에, 4개가 아니라 오직 두 개 라이브러리만 필요하다. (jcl-over-slf4j와 logback). 만약 그렇게 하면 slf4j-api 의존성 자체로 명시적인 의존성에서 제거할 필요가 있을지 모르겠다. 오직 한 버전 API 만 클래스패스에서 사용하길 원하기 때문이다.

Log4J 사용하기

많은 개발자들은 Log4J를 로깅 프레임워크로 사용하고 설정하고 관리한다. 효율적이며 잘 만들어졌다. 실제로 스프링을 만들고 테스트할 때 실행시에 사용하는 것이기도 하다. 스프링 또한 Log4j 설정과 초기화를 간편히 할 수 있는 것들을 제공한다. 따라서 몇몇 모듈은 Log4J에 대한 부가적인 컴파일 시점 의존성을 가지고 있다.

Log4j를 기본 JCL 의존성으로 사용할 때 필요한 것은 Log4J를 클래스패스에 두는것이 전부이다. 그리고 (log4j.properties 또는 log4j.xml을 클래스패스 루트에) 설정 파일을 제공하면 된다. 따라서 메이븐 사용자는 다음과 같이 의존성 선언을 하면된다.

<dependencies>
   <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.0.0.RELEASE</version>
      <scope>runtime</scope>
   </dependency>
   <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.14</version>
      <scope>runtime</scope>
   </dependency>
</dependencies>

그리고 다음은 콘솔에 로깅하는 log4j.properties다.

og4j.rootCategory=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2}:%L – %m%n

log4j.category.org.springframework.beans.factory=DEBUG

네이티브 JCL과 런타임 컨테이너

많은 사람들이 스프링 애플리케이션을 JCL 구현체 자체를 제공하는 컨테이너에서 구동한다. IBM 웹스피어 애플리케이션 서버(WAS)가 그중 하나이다. 이것은 보통 문제를 야기하고, 불행히도 정답은 없다. commons-logging을 애플리케이션에서 간단하게 제외하는 것이 대부분 상황에서 충분한건 아니다.

정리하자면: 문제는 JCL 또는 common-logging 자체가 아니라 commons-logging을 다른 프레임워크(보통 Log4J)에 바인딩하는 것과 관련이 있다. commons-logging이 런타임 탐색하는 방법이 몇몇 컨테이너가 사용하고 있는 예전 버전(1.0)과 현재 대부분의 사람들이 사용하고 있는 버전(1.1)과 다르기 때문이다. 스프링은 JCL API에서 별다른 것을 사용하지 않기 때문에 깨질것은 없지만, 조만간 로깅을 하려는 스프링 또는 여러분의 애플리케이션이 Log4J로 바인할 때 동작하지 않을 수 있다.

그런 경우 WAS를 사용할 때는 클래스로더 계층 구조(IBM은 이것을 "parent last"라 부른다.)를 변경하여 컨테이너가 아니라 애플리케이션이 JCL 읜존성을 제어하게 할 수 있다. 하지만 이 방법이 항상 가능한 건 아니지만,  여러분이 사용하는 컨테이너틔 버전과 기능에 따라 다양한 대안들이 공공연히 제시되고 있다.
top


[봄싹] 하이버네이트와 자바 퍼시스턴스 베타리딩 합니다.



Java Persistence With Hibernate 번역서 입니다. 이 책이 드디어 한국어로 나올 날이 가까워 오는군요. 물개선생님한테서 이 작업을 인수 받고 상당히 버거웠었는데, 다행히도 번역계의 마이다스이신 대엽님께서 번역 팀장을 맡고 계십니다.

베타리딩 방법은 이전의 프로 스프링 2.5 베타리딩 방식과 동일합니다. 프리스타일 이지만 어느 정도 중심적인 관점(Aspect or Concern)을 가지고 봐주셨으면 합니다.

"문법 위주로 보고 싶으신 분은 문법 위주로.." 예) 맞춤법, 올바른 한글 표현인지 아닌지..
"코드 위주로 보고 싶으신 분들은 코드 위주로.." 예) 코드를 다운 받을 수 있는지, 실행하는 방법은 어떤지, 코드가 틀리진 않았는지..
"내용 위주로 보고 싶으신 분들은 내용 위주를.." 예) n+1 select 문제에 대한 설명이 올바른지..

1. 때가 되면 제가 워드 문서를 그룹스로 보내드릴겁니다. 예) [하이버네이트 베타리딩] n장
2. 그럼 워드의 메모 기능을 이용하여 검토해주세요.
3. 파일 이름은 "1장_베타리딩_누가_언제.doc" 규약을 따라주세요.
4. 해당 파일을 gmail에서 그룹스에 답멜로 첨부해 주시거나, 저에게(이 블로그 맨 밑에 주소가 있어요) 이메일로 보내주세요.

참 쉽죠?

아참. 베타리딩은 봄싹 스터디가 전담하기 때문에, 봄싹 그룹스에 가입하셔야 합니다. 설마... 베타리딩을 하고 싶으셔서 가입하시는 분들은...;; 있으시려나;; 환영합니다. ;-)
top


조금 친절한 코드 생성기 7(일단 완성) - DAO 생성기 코딩

모하니?/Coding : 2009.12.04 16:15


이전 글에서 예고한데로, 테스트 코드 부터 작성했습니다.

    @Test
    public void generateDao(){
        RepositorySettings settings = new FreemarkerRepositorySettings("test", "repository.ftl", "repository_impl.ftl", "test/springsprout/modules", Study.class);
        service.generateRepository(settings);

        assertTrue(new File("test/springsprout/modules/test/StudyRepository.java").exists());
        assertTrue(new File("test/springsprout/modules/test/StudyRepositoryImpl.java").exists());
        service.deleteRepository();
        assertFalse(new File("test/springsprout/modules/test/StudyRepository.java").exists());
        assertFalse(new File("test/springsprout/modules/test/StudyRepositoryImpl.java").exists());
    }


그리고 컴파일 에러를 없애기 위해서 필요한 인터페이스, 클래스, 메서드들을 추가했습니다. 먼저, RepositorySettings를 만들었습니다.

public interface RepositorySettings {
   
}

이 구현체인 FreemarkerRepositorySettings는 다음과 같습니다.

public class FreemarkerRepositorySettings implements RepositorySettings {

    private String module;
    private String interfaceTemplateName;
    private String implTemplateName;
    private String destinationDirPath;
    private Class domainClass;

    private Map<String, String> modelMap;
    private List<File> destinationDirs;
    private File interfaceFile;
    private File implFile;

    public FreemarkerRepositorySettings(String module, String interfaceTemplateName, String implTemplateName, String destinationDirPath, Class domainClass) {
        this.module = module;
        this.interfaceTemplateName = interfaceTemplateName;
        this.implTemplateName = implTemplateName;
        this.destinationDirPath = destinationDirPath;
        this.domainClass = domainClass;

        String domainClassName = domainClass.getSimpleName();
        modelMap = new HashMap<String, String>();
        modelMap.put("module", module);
        modelMap.put("domainClass", domainClassName);

        interfaceFile = new File(destinationDirPath + "/" + module + "/" + domainClassName + "Repository.java");
        implFile = new File(destinationDirPath + "/" + module + "/" + domainClassName + "RepositoryImpl.java");

        destinationDirs = new ArrayList<File>();
        destinationDirs.add(new File(destinationDirPath));
        destinationDirs.add(new File(destinationDirPath + "/" + module));
    }

...

}

이전에 만들었던 FreemarkerControllerSettings와 비슷하지만, 살짝 다릅니다. 그래도 비슷한 코드가 많으니.. 어떻게 중복을 좀 제거할 방법을 생각해 봐야겠습니다. 일단은 계속 ㄱㄱ

이제는 CodeGenerationService 인터페이스에 메서드를 추가할 차례로군요.

public interface CodeGenerationService {

    void generateController(ControllerSettings settings) throws CodeGenerationException;

    void generateRepository(RepositorySettings settings) throws CodeGenerationException;

}

Settins를 만들 때 빼고는 여태까진 편했습니다. 아직까지는 테스트를 돌려도 분명히 에러가 날 것이기 때문에 별로 돌려보고 싶지도 않습니다.

이제 본격적으로 프리마커 코드 생성기에 위 인터페이스 구현체를 만들겠습니다.

    public void generateRepository(RepositorySettings settings) throws CodeGenerationException {
        generatedFiles = new Stack<File>();
        FreemarkerRepositorySettings frSettings = (FreemarkerRepositorySettings)settings;
        generateDirs(frSettings.getDestinationDirs());
        generateCode(frSettings.getInterfaceTemplateName(), frSettings.getModelMap(), frSettings.getInterfaceFile());
        generateCode(frSettings.getImplTemplateName(), frSettings.getModelMap(), frSettings.getImplFile());
    }

어머나.. 끝이로군요!! 이전 코드를 리팩토링한 효과가 있었습니다. 원래는 이 부분도 굉장히 장황해질뻔 했는데, 다행입니다. 이제 테스트를 돌려볼까요.


로깅은 귀찮아서 Sout으로 해결했습니다.

만들때
삭제 대상
삭제된 후

조금 친절한 코드 생성기는 그럼 여기서 끝!!








top


조금 친절한 코드 생성기 6 - 테스트 코드 리팩토링

모하니?/Coding : 2009.12.04 16:06


이번에는 테스트 코드부터 만들기로 마음 먹었습니다. 그랬더니, 테스트 코드에서도 리팩토링 할 부분이 있어서 수정했습니다.

일단 프리마커 Configuration을 여러 테스트에서 공유해야 하기 때문에, 인스턴스 변수로 선언했고, @Before를 이용해서 모든 테스트 마다 새로 만든 Configuration 객체를 사용하게 했습니다. 사실 그럴 필요까진 없는데 말이죠;

public class FreemarkerCodeGenerationServiceTest {

    FreemarkerCodeGenerationService service;

    @Before
    public void setUp() throws IOException {
        Configuration configuration = new Configuration();
        configuration.setObjectWrapper(new DefaultObjectWrapper());
        configuration.setDirectoryForTemplateLoading(new FileSystemResource("doc/template").getFile());

        assertNotNull(configuration);

        service = new FreemarkerCodeGenerationService(configuration);
    }

    @Test
    public void generateController() throws IOException {
        service.generateController(new FreemarkerControllerSetting("test", "controller.ftl", "test/springsprout/modules", Study.class));

        assertTrue(new File("test/springsprout/modules/test/StudyController.java").exists());
        service.deleteController();
        assertFalse(new File("test/springsprout/modules/test/StudyController.java").exists());
    }

    //TODO template file loading fail test

    //TODO destination file make fail test

    //TODO template processing fail test

    //TODO destination folder mkdir test
}

자.. 이제는 정말로;;
top


조금 친절한 코드 생성기 5 - 리팩토링

모하니?/Coding : 2009.12.04 15:40


이번에도;; 새 기능을 추가하려다보니까 기존의 코드에서 분리 시켜야 새 메서드에서 재사용 할 수 있는 부분들이 보이길래, 기존의 코드를 쪼개서 리팩토링 했습니다. 그러면서 일부 기능은 프리마커 코드 생성기에서 컨트롤러 설정 부분으로 이동시켰습니다. 아무래도, 최종적으로 생성해야 할 장소와, 파일에 대한 정보는 설정 객체가 받은 기본 정보를 토대로 가공해서 코드 생성기 쪽으로 전달해주는게 나을 것 같아서요.

    public void generateController(ControllerSettings settings) throws CodeGenerationException {
        generatedFiles = new Stack<File>();
        FreemarkerControllerSetting fcSettings = (FreemarkerControllerSetting)settings;
        generateDirs(fcSettings.getDestinationDirs());
        generateCode(fcSettings.getTemplateFileName(), fcSettings.getModelMap(), fcSettings.getDestinationFile());
    }

이 부분이 이전 글에서 엄청나게 길었던, 코드 생성코드입니다.
1. 먼저 생성된 파일 정보를 저장해둘 컬렉션을 초기화 합니다.
2. 프리마커 컨트롤러 설정으로 타입을 변환하고
3. 최종 코드가 생성될 디렉토리를 만듭니다.
4. 최종 코드를 만듭니다.

3, 4 번이 핵심 기능인데, 이 기능들은 다른 코드 생성 메서드에서도 사용할 수 있어야 하기 때문에 기존 코드에서 분리했습니다.

그리고 기존 코드 생성기에 있던 코드 중에 일부를 컨트롤러 설정 쪽으로 옮겼습니다.

public class FreemarkerControllerSetting implements ControllerSettings {

    private String module;
    private String templateFileName;
    private String destinationDirPath;
    private Class domainClass;

    private Map<String, String> modelMap;
    private File destinationFile;
    private List<File> destinationDirs;

    public FreemarkerControllerSetting(String module, String templateFileName, String destinationDirPath, Class domainClass) {
        this.module = module;
        this.templateFileName = templateFileName;
        this.destinationDirPath = destinationDirPath;
        this.domainClass = domainClass;

        modelMap = new HashMap<String,  String>();
        String className = domainClass.getSimpleName();
        modelMap.put("module", module);
        modelMap.put("domainClass", className);
        modelMap.put("domainName", ClassUtils.getShortNameAsProperty(domainClass));

        destinationFile = new File(destinationDirPath + "/" + module + "/" + domainClass.getSimpleName() + "Controller.java");

        destinationDirs = new ArrayList<File>();
        destinationDirs.add(new File(destinationDirPath));
        destinationDirs.add(new File(destinationDirPath + "/" + module));
    }

    public String getModule() {
        return module;
    }
    public String getTemplateFileName() {
        return templateFileName;
    }
    public String getDestinationDirPath() {
        return destinationDirPath;
    }
    public Class getDomainClass() {
        return domainClass;
    }

    public Map<String, String> getModelMap() {
        return modelMap;
    }
    public File getDestinationFile() {
        return destinationFile;
    }
    public List<File> getDestinationDirs() {
        return destinationDirs;
    }
}

1. 프리마커 Template을 가공할 때 사용할 modelMap.
2. 코드가 될 파일.
3. 코드가 담길 폴더들(기본 목적 폴더 + 모듈 폴더)

아 그리고 은근슬쩍 수정한 것이 있는데, domainClass를 generateContoller() 호출시에 넘겨주지 않고, ControllerSettings에 포함시켰습니다. 이녀석도 생성시에 필요한 설정 정보에 해당하는 건데 왜 이전 설계 변경할 때 빼먹었는지 모르겠네요. @_@;;

자 이번엔 진짜로 새 기능을 만들어 볼까요;;

아차차.. 테스트는 잘 돌아갑니다. 리팩토링 중에 중간 중간 돌려서 제대로 돌아가는지 확인할 수 있어서 좋았습니다. 코드를 엄청 많이 바꿨는데도 맘이 놓이네요. 안심이에요.
top


조금 친절한 코드 생성기 4 - 설계 변경 적용(인터페이스)

모하니?/Coding : 2009.12.04 14:02


이번에는 ControllerSettings라는 마커 인터페이스를 도입하여 설계를 변경해보겠습니다. 이번에도 저는 TDD 프로가 아니라서;; 테스트 코드 부터 수정하진 못했습니다.. (흑흑.. 다음 부턴;; 테스트부터;???)

public interface CodeGenerationService {

    void generateController(ControllerSettings settings, Class domainClass) throws CodeGenerationException;

}

ControllerSettings 라는 인터페이스에는 아무것도 없습니다. 단순히 타입만 맞추가 위한 거죠.

그리고 FreemarkerControllerSettings 구현체를 만듭니다. 여기에 실제 프리마커 코드 생성기가 필요로 하는 인자값들이 들어갑니다. 이전 글에서 Map이 하던 역할을 이녀석이 하는거죠.

public class FreemarkerControllerSetting implements ControllerSettings {

    private String module;

    private String templateFileName;

    private String destinationDirPath;

    public FreemarkerControllerSetting(String module, String templateFileName, String destinationDirPath) {
        this.module = module;
        this.templateFileName = templateFileName;
        this.destinationDirPath = destinationDirPath;
    }

    public String getModule() {
        return module;
    }

    public String getTemplateFileName() {
        return templateFileName;
    }

    public String getDestinationDirPath() {
        return destinationDirPath;
    }
}

별도의 세터는 없어서 변경을 막고, 생성자를 이용해서 강제적으로 세 값 모두 받도록 했습니다. (이렇게 해도 널 체크는 하긴 해야겠지만... 아 이 귀찮은 널 체크... 하긴.. 어차피 뭔가가 null이면 어디선가 에러가 날테니 굳이 안해도 되겠네요. 캬캬캬)

그리고 이제 프리마커 코드 생성기 코드를 수정합니다.

public class FreemarkerCodeGenerationService implements CodeGenerationService {

    private Configuration configuration;
    private Stack<File> createdFilesWhileGenerateController;
    private Stack<File> createdFilesWhileGenerateDao;

    public FreemarkerCodeGenerationService(Configuration configuration){
        this.configuration = configuration;
    }

    public void generateController(ControllerSettings settings, Class domainClass) throws CodeGenerationException {
        FreemarkerControllerSetting fcSettings = (FreemarkerControllerSetting)settings;
        String module = fcSettings.getModule();
        String templateFileName = fcSettings.getTemplateFileName();
        String destinationDirName = fcSettings.getDestinationDirPath();

        Map<String, String> map = new HashMap<String,  String>();
        String className = domainClass.getSimpleName();
        map.put("module", module);
        map.put("domainClass", className);
        map.put("domainName", ClassUtils.getShortNameAsProperty(domainClass));

        createdFilesWhileGenerateController = new Stack<File>();

        Template controllerTemplate = null;
        try {
            controllerTemplate = configuration.getTemplate(templateFileName);
        } catch (IOException e) {
            throw new CodeGenerationException("template file loading fail with [" + templateFileName + "]", e);
        }

        File desticationFolder = new File(destinationDirName);
        boolean created = desticationFolder.mkdir();
        if(created)
            createdFilesWhileGenerateController.push(desticationFolder);
       
        desticationFolder = new File(destinationDirName + "/" + module);
        created = desticationFolder.mkdir();
        if(created)
            createdFilesWhileGenerateController.push(desticationFolder);

        File destinationFile = new File(destinationDirName + "/" + module + "/" + className + "Controller.java");
        FileWriter writer = null;

        try {
            writer = new FileWriter(destinationFile);
            controllerTemplate.process(map, writer);
            writer.flush();
            writer.close();
            System.out.println(destinationFile.getAbsolutePath()  + " created");
             createdFilesWhileGenerateController.push(destinationFile);
        } catch (IOException e) {
            throw new CodeGenerationException("destincation file creation fail", e);
        } catch (TemplateException e) {
            throw new CodeGenerationException("template processing fail", e);
        } finally {
            try {
                writer.close();
            } catch (IOException e) {
            }
        }
    }

...

}

음.. 확실히 Map에서 꺼내올 때에 비해서 뭔가 편합니다. 대체 Map에 어떤 키 값으로 데이터들이 들어있는지 애매한데다, 자쳇해서 스펠링이라도 틀리면;; 아무래도 그냥 이 방법으로 가야겠습니다.

마지막으로 테스트를 해봅니다. 물론 약간 고쳐야 하죠.

public class FreemarkerCodeGenerationServiceTest {

    @Test
    public void generationTst() throws IOException {
        Configuration configuration = new Configuration();
        configuration.setObjectWrapper(new DefaultObjectWrapper());
        configuration.setDirectoryForTemplateLoading(new FileSystemResource("doc/template").getFile());

        assertNotNull(configuration);

        FreemarkerCodeGenerationService service = new FreemarkerCodeGenerationService(configuration);

        service.generateController(new FreemarkerControllerSetting("test", "controller.ftl", "test/springsprout/modules"), Study.class);

        assertTrue(new File("test/springsprout/modules/test/StudyController.java").exists());
        service.deleteController();
        assertFalse(new File("test/springsprout/modules/test/StudyController.java").exists());
    }
}

잘 돌아갑니다. 그다지 여러 경우를 테스트하고 있지는 않지만 나름대로 최소한의 기능 보장은 해주기 때문에 안심하고 코딩을 계속할 수 있겠습니다.



현재 상태에서 커버리지를 색으로 표시해봤습니다. 몇가지 특수한 상황에 대한 테스트가 안 된 부분이 있는데 저 부분에 대한 테스트는 나중에 만들기로 하고 테스트 코드에 요약해 둡니다.

public class FreemarkerCodeGenerationServiceTest {

    @Test
    public void generationTst() throws IOException {
        Configuration configuration = new Configuration();
        configuration.setObjectWrapper(new DefaultObjectWrapper());
        configuration.setDirectoryForTemplateLoading(new FileSystemResource("doc/template").getFile());

        assertNotNull(configuration);

        FreemarkerCodeGenerationService service = new FreemarkerCodeGenerationService(configuration);

        service.generateController(new FreemarkerControllerSetting("test", "controller.ftl", "test/springsprout/modules"), Study.class);

        assertTrue(new File("test/springsprout/modules/test/StudyController.java").exists());
        service.deleteController();
        assertFalse(new File("test/springsprout/modules/test/StudyController.java").exists());
    }
   
    //TODO template file loading fail test
   
    //TODO destination file make fail test
   
    //TODO template processing fail test
   
    //TODO destination folder mkdir test
}

자 이제서야;; 본격적으로 Dao 코드 생성 작업에 들어갈 수 있겠군요.

top


조금 친절한 코드 생성기 3 - 설계 변경 적용(Map)

모하니?/Coding : 2009.12.04 13:40


먼저 인터페이스를 변경합니다. (TDD 프로라면 테스트 코드부터 바꾸셨겠지만, 저는 TDD 아마추어라;;)

public interface CodeGenerationService {

    void generateController(Map<String, String> settings, Class domainClass) throws CodeGenerationException;

}


Map을 사용하도록 바꿨습니다. 그리고 구현체에서 에러가 날테니 에러를 따라가서(인텔리J에서는 에러나는 코드로 알아서 자동으로 이동해 줍니다. 저는 걍 눈뜨고 손들고 있으면 알아서 코딩할 곳으로 데려다주죠.ㅋ)

public class FreemarkerCodeGenerationService implements CodeGenerationService {

    private Configuration configuration;
    private Stack<File> createdFilesWhileGenerateController;
    private Stack<File> createdFilesWhileGenerateDao;

    public FreemarkerCodeGenerationService(Configuration configuration){
        this.configuration = configuration;
    }

    public void generateController(Map<String, String> settings, Class domainClass) throws CodeGenerationException {
        createdFilesWhileGenerateController = new Stack<File>();

        String module = settings.get("module");
        String templateFileName = settings.get("templateFileName");
        String destinationDirName = settings.get("destinationDirName");
       
        //TODO check not null

        Map<String, String> map = new HashMap<String,  String>();
        String className = domainClass.getSimpleName();
        map.put("module", module);
        map.put("domainClass", className);
        map.put("domainName", ClassUtils.getShortNameAsProperty(domainClass));

        Template controllerTemplate = null;
        try {
            controllerTemplate = configuration.getTemplate(templateFileName);
        } catch (IOException e) {
            throw new CodeGenerationException("template file loading fail with [" + templateFileName + "]", e);
        }

        File desticationFolder = new File(destinationDirName);
        boolean created = desticationFolder.mkdir();
        if(created)
            createdFilesWhileGenerateController.push(desticationFolder);
       
        desticationFolder = new File(destinationDirName + "/" + module);
        created = desticationFolder.mkdir();
        if(created)
            createdFilesWhileGenerateController.push(desticationFolder);

        File destinationFile = new File(destinationDirName + "/" + module + "/" + className + "Controller.java");
        FileWriter writer = null;

        try {
            writer = new FileWriter(destinationFile);
            controllerTemplate.process(map, writer);
            writer.flush();
            writer.close();
            System.out.println(destinationFile.getAbsolutePath()  + " created");
             createdFilesWhileGenerateController.push(destinationFile);
        } catch (IOException e) {
            throw new CodeGenerationException("destincation file creation fail", e);
        } catch (TemplateException e) {
            throw new CodeGenerationException("template processing fail", e);
        } finally {
            try {
                writer.close();
            } catch (IOException e) {
            }
        }
    }

...

}

코드는 이런식으로 바꼈습니다. 생성자가 한결 깔끔해 졌으며 generateCotroller에서는 settings 맵에서 필요한 값들을 꺼내서 작업하고 있습니다.

문제는
1. Map에 필요한 데이터가 없으면 어쩐댜?
2. 클라이언트 입장에서 필요한 속성이 뭔지 모르겠다.. @_@

일단 계속해서 테스트 코드를 수정합니다.

public class FreemarkerCodeGenerationServiceTest {

    @Test
    public void generationTst() throws IOException {
        Configuration configuration = new Configuration();
        configuration.setObjectWrapper(new DefaultObjectWrapper());
        configuration.setDirectoryForTemplateLoading(new FileSystemResource("doc/template").getFile());

        assertNotNull(configuration);

        FreemarkerCodeGenerationService service = new FreemarkerCodeGenerationService(configuration);

        Map<String, String> settings = new HashMap<String, String>();
        settings.put("module", "test");
        settings.put("templateFileName", "controller.ftl");
        settings.put("destinationDirName", "test/springsprout/modules");

        service.generateController(settings, Study.class);

        assertTrue(new File("test/springsprout/modules/test/StudyController.java").exists());
        service.deleteController();
        assertFalse(new File("test/springsprout/modules/test/StudyController.java").exists());
    }
}

맵을 만들어서 넘겨줘야 했기 때문에 약간 길어졌지만, 테스트는 무사히 통과했습니다. 그리 큰 설계 변경은 아니지만 이 테스트 덕분에 맘이 편히 할 수 있었네요. 이제는 3번째 방안으로 구현해 보겠습니다.
top


조금 친절한 코드 생성기 2 - 설계 변경

모하니?/Coding : 2009.12.04 13:30


흠.. 그렇다. 어제 코딩하고 나서부터 계속 뭔가가 찜찜했는데, 그 원인을 (다른 기능을 추가하려다 보니까) 이제서야 알 것 같다. 컨트롤러만 생성할 것이었다면, 이런 발견은 못하고 찜찜한채로 넘어갔곘지만, DAO를 생성하려다 보니 발견한 것 같다.

문제의 원인은 컨트롤러 코드 생성에 필요한 정보를 생성자에서 받았는다는 것이었다.

    public FreemarkerCodeGenerationService(Configuration configuration, String controllerTemplateName, String destinationDir){
        this.destinationDir = destinationDir;
        this.configuration = configuration;
        try {
            this.controllerTemplate = configuration.getTemplate(controllerTemplateName);
        } catch (IOException e) {
            throw new CodeGenerationException("template file loading fail with [" + controllerTemplateName + "]", e);
        }
    }

이것은 CodeGenerationService 인터페이스 구현체 중에 하나인 프리마커 구현체로, 그 생성자에서 컨트롤러 코드 생성에 필요한 정보(코드를 생성할 위치와, 템플릿 파일이름)를 받고 있었다. 이런식이라면 DAO 코드 생성에 필요한 정보도 이 생성자에서 받아와야 할텐데;;; 그건 좀 아닌 것 같다. 그리고 저 정보를 굳이 인스턴스 레벨로 들고 있을 이유가 없지 않은가;

또한 컨트롤러를 어떤 모듈 밑에 생성할지에 대한 정보는 컨트롤러를 생성하는 메서드 안에 들어있다. 이건 대체 무슨 일인가. 왜 컨트롤러 생성에 필요한 정보가 분산 됐을까? 왜 이렇게 코딩했는지 나도 모르겠다. 이런걸 보고 응집도가 떨어졌다고 하는 것은 아닐까? 아님 말고.

"컨트롤러 생성에 필요한 정보는 컨트롤러를 생성할 때 주자."

설계가 바뀌었다 하지만 걱정은 없다. 나에겐 테스트가 있으니까. 변경해보자. 그런데, 변경 하자고 마음 먹은 순간부터 고민 거리가 떠올랐다. 어떤 타입으로 정보를 전달 할까.

1. 전부 나열

generateController(String module, String destinationDirPath, String templateFileName, Class domainClass)

2. 맵으로 압축

generateController(Map settings, Class domainClass)

3. 여러 매개변수를 하나의 타입으로 압축

generateController(ControllerSettings settings, Class domainClass)

어떤 방법이 더 개발하기 편하고 확장에 용이하며 직관적일까? 쉽지 않은 고민이다. 직관적인걸로 따지자면 1번과 3번이다. 2번은 대체 무슨 설정 값들을 맵에 넣어줘야 하는지;; 나만 알고 있다.

그러나 확장하자니 일단 1번은 아니다. 프리마커 코드 생성이 아니라 다른 방법으로 코드 생성을 할 떄는 위의 설정 내용이 필요없거나 다른 설정 값이 필요할 수도 있겠다. 그런 경우에는 2번이 제일 유리하고, 3번의 ControllerSettings를 단순 마커 인터페이스로 사용한다면 역시 유연하다고 볼 수 있겠다.

그럼 3번이 일단은 가장 유력한 후보로 보인다. 그러나 매번 이 마커 인터페이스의 구현체를 만들어 CodeGeneration 구현체 마다 하나씩은 만들어 줘야 할 것이며, DaoSettings, ServiceSettings 처럼 인터페이스도 늘어날 것이다. 클래스와 인터페이스 파일을 만들기가 귀찮을 수도 있겠다.

2번과 3번 사이에서 많이 고민이 된다. 둘 다 해볼까?
top







티스토리 툴바