Whiteship's Note

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

  1. 2009.07.14 스프링 JSON view와 jQuery 이용하여 자동완성 기능 만들기 3
  2. 2009.07.14 스프링 JSON view와 jQuery 이용하여 자동완성 기능 만들기 2 (2)
  3. 2009.07.13 스프링 JSON view와 jQuery 이용하여 자동완성 기능 만들기 1 (6)
  4. 2009.07.08 스프링 WebContentGenerator로 자바스크립트 캐싱하기4 (4)
  5. 2009.07.08 스프링 WebContentGenerator로 자바스크립트 캐싱하기3
  6. 2009.07.08 스프링 WebContentGenerator로 자바스크립트 캐싱하기2 (2)
  7. 2009.07.07 스프링 WebContentGenerator로 자바스크립트 캐싱하기 (2)
  8. 2009.07.06 하이버네이트, 스프링 MVC에서 enum 사용하기 4
  9. 2009.07.03 database.properties 모음 (2)
  10. 2009.07.03 하이버네이트, 스프링 MVC에서 enum 사용하기 2
  11. 2009.07.02 하이버네이트, 스프링 MVC에서 enum 사용하기 (4)
  12. 2009.07.02 결국 그냥 만들어버린 JPA 문서 자동화 툴 (4)
  13. 2009.06.10 AspectJ를 이용한 코드젠과 프레임워크 2
  14. 2009.06.09 스프링 ROO에서 RESTful 코드 보기 (2)
  15. 2009.06.08 AspectJ를 이용한 코드젠과 프레임워크
  16. 2009.05.19 WebTUnit 1.0 M1 배포합니다. (6)
  17. 2009.05.19 WebTUnit에 CoC 적용 결과
  18. 2009.05.18 Page Object 패턴을 이용한 웹 CRUD 테스트 with WebTUint
  19. 2009.05.15 WebTUnit 사용 시나리오 3. 테스트 데이터가 필요 없는 경우
  20. 2009.05.15 WebTUnit 사용 시나리오 2. 웹 테스트 + 테스트 메서드 단위 데이터 관리
  21. 2009.05.15 웹 테스트 프레임워크(WebTUnit) 사용 시나리오 1. 웹 테스트 + 테스트 클래스 단위 데이터 관리 (2)
  22. 2009.05.14 웹 통합 테스트 프레임워크 개발 중 (4)
  23. 2009.05.13 내부에서 new 로 객체를 생성하는 코드 테스트 하기 (2)
  24. 2009.05.08 DBUnit + Cargo + Webdriver를 이용한 웹 테스트 삽질 중
  25. 2009.04.29 이클립스 WTP server.xml 설정이 적용되지 않을 때 (2)
  26. 2009.04.27 문자열 연결 성능 비교 (4)
  27. 2009.04.24 토비님의 S3Matrix 소스 보기 1. pom.xml (2)
  28. 2009.04.17 초간단 CURD, 검색, 페이징, 정렬 구현 완료 (2)
  29. 2009.03.27 JUnit에서 setUp() 또는 @Before를 사용하는 이유? (2)
  30. 2008.12.28 스프링, 하이버 게시판 Whiteboard 1차 완성

스프링 JSON view와 jQuery 이용하여 자동완성 기능 만들기 3

모하니?/Coding : 2009.07.14 16:25


조금 더 개선해서, 화면에서 검색 조건을 한 글자 한 글자 입력하면, 해당 글자를 가지고 있는 모든 것들을 검색해 올지.. 아니면, 해당 글자로 시작하는 것들을 가져올지 설정할 수 있는 기능을 서버쪽과 연결 했습니다.

    $("#name").autocomplete("/ajax/user/names.do", {
        minChars: 1,
        width: 200,
        max: 100,
        model: "users",
        delay:10,
        selectFirst: true,
        formatItem: function(row, i, total) {
            return row[1] + " " + row[2];
        },
        formatResult: function(row){
            return row[1];
        }
    });

화면에서는 selectFirst의 값만 바꾸면 되지만, 저 값이 서버에서 검색을 할 때도 영향을 줘야 하기 때문에, 저 값을 서버로 보내도록 jquery autocomplete js를 수정했습니다. jqeury ajax의 getJSON 함수에 서버로 보낼 parameter 목록을 보낼 수 있기 때문에 이건 뭐 아주 간단하게 수정할 수 있었습니다.

그런 다음 컨트롤러에서 이 값을 받아서 Serivce -> Dao 로 쭉쭉쭉 보내면 되는데.. 

public ModelAndView userNames(ModelMap map, String keyword, Boolean selectFirst)

이런 시그너처 만으로도 바인딩이 된다는 건 스프링 MVC 유저라면 당연히 아시리라 생각합니다. 문제는 저게 계속 길어질 우려가 있다는 거죠. 그래서 AjaxParams라는 클래스를 만들고 자바빈 스펙에 맞게 게터, 세터를 추가해준 다음 컨트롤러의 메서드 시그너처를 다음과 같이 바꿨습니다.

public ModelAndView userNames(ModelMap map, AjaxParams ajaxParams)

짜잔... 이렇게 해도 스프링 MVC는 똑똑해서 자동으로 바인딩 해줍니다. 그럼 이제 service랑 dao에는 ajaxParam을 넘겨주고 저기서 값을 꺼내서 검색해오면 되겠습니다.

이정도면.. 이제 대충 쓸만하게는 만든 것 같습니다.
혹시 모르죠. 뭔가 또 빠져서 다시 손을 대야할지도..


top


스프링 JSON view와 jQuery 이용하여 자동완성 기능 만들기 2

모하니?/Coding : 2009.07.14 15:32


jQuery 플러그인 중에 autocomplete가 있는데, 이 녀석은 배열을 사용하기 때문에 URL에서 넘어온 JSON 데이터를 제대로 인식하지 못할 뿐더러, JSON을 인식한다쳐도 스프링 JSONView가 만들어주는 JSON에서 모델이름으로 값을 꺼내와야 하는데, 그런 장치가 전혀 없기 때문에... 뜯어 고쳤습니다.

새로운 option을 추가하고, Ajax 요청을 보내는 부분을 고치고, Ajax로 받아온 데이터를 파싱하는 부분을 수정했더니 잘 동작했습니다. 스프링 JsonView에 특화된 jQuery 자동완성 플러긴으로 보면 되겠군요.


목록에 보여줄 형태를 옵션으로 지정해 줄 수 있어서 이름 + " " + 이메일 형태를 보여주도록 설정했습니다. 넓이도 조정할 수 있으니.. 글자가 잘리지 않게 너비를 조정할 수도 있겠네요.

자세한 옵션은 http://docs.jquery.com/Plugins/Autocomplete/autocomplete#url_or_dataoptions

여러가지 사용 예제는 http://jquery.bassistance.de/autocomplete/demo/

모든 옵션을 확인하진 않았지만, 대부분의 옵션(목록 갯수 설정, 검색할 때 필요한 최소 글자 수, 등등등)이 제대로 동작했습니다.

기본으로 화살표 이동도 되고, 캐싱도 지원해주니.. 이 정도면 썩 괜찮으 듯 합니다. 선택이 했을 때는 선택한 데이터 row를 가지고 function을 실행할 수 있게 해주는데, 그걸 이용해서 입력한 값 주변에 기타 정보를 출력하도록 만들 수 있었습니다.

그러나 한글값을 넘겨봤더니 깨지더군요. 그래서 encodeURIComponent()로 값을 감싸서 UTF-8로 보냈습니다. 그런 다음 컨트롤러에서 받아서 디코딩을 했죠.

keyword = URLDecoder.decode(keyword,"UTF-8");

디코딩 하는 방법은 성윤이가 도와줬습니다. 흠.. 사부님 말씀으로는 이렇게 UTF-8로 인코딩/디코딩 하지 않고도 처리할 수 있는 방법이 있는 것 같은데.. 일단 거기까진 신경쓰지 않기로 했습니다. 지금도 충분히 어지러우니까요. @_@

라이선스가 GPL하고 mit 라이선스던데 소스를 공개해야 라이선스를 위반하지 않는거 아닌지 모르겠네요. 대충 고친건데.. 흠..

이젠 테스트 데이터를 왕창 넣고 확인해 봐야겠습니다.
top


스프링 JSON view와 jQuery 이용하여 자동완성 기능 만들기 1

모하니?/Coding : 2009.07.13 23:29


간만에 11시에 집에 왔습니다. 음... 오랜만이네요.

오늘은 jQuery Ajax로 자동완성 기능을 만들다가 왔습니다. 스프링 JSONView를 이용해서 스프링이 보내준 JSON을 가지고 자동완성을 만드는 겁니다.

1. MappingJacksonJsonView 라는 클래스가 있는데, 좀 전까지만 해도 BindingJacksonJsonVIew였습니다. 아직은 배포된 버전에 이 클래스가 없고, 개발중인 프로젝트에 fisheye에 접속해서 보면 소스를 볼 수 있습니다. 또는 톱님이 배포하시는 최신 스프링을 사용하시면 이용할 수 있을 겁니다.

2. 일단 필요한 라이브러리를 구합니다. 최신 스프링 or 저 파일만 구하면 됩니다. 또 Jackson 관련 라이브러리를 추가합니다.

3. 컨트롤러를 만들고, ModelAndView를 반환합니다. ModelMap에는 컬렉션을 주던, 필요한 모델 객체를 주던 주면 되고, viewname에는 MappingJacksonJsonView의 빈 이름을 설정해 줍니다.

4. xxx-servlet.xml에서 MJJV 빈을 등록하고, 빈 이름은 jsonView라고 했습니다. 3번 컨트롤러에서도 jsonView란 이름을 사용했죠. 그리고 뷰리졸버를 하나 추가했습니다. BeanName머시기 뷰리졸버가 있죠. 그걸 추가했습니다.

5. jQuery.getJson(url, param, function)를 이용해서 저 컨트롤러에 요청을 보내고, 컨트롤러가 돌려주는 값을 확인해 봤습니다.

usernames:List<String> => {"userNames":["aaa", "vvv", "dddd"]}
users:List<User> => {"users":[["1", "기선", "whiteship@email.com"],["2", "toby", "toby@email.com"]]}
user:User => {"user":[name:"기선", "id":1, "email":email]}
user1:User, user2:User=> {"user1":[name:"기선", "id":1, "email":email],"user2":[name:"기선", "id":1, "email":email]}

이런 형태의 결과가 나왔습니다.(기억으로 적은거라 틀릴 수도 있습니다.) 주로 사용하게 될 첫 번째 형태를 보니, 맵(문자열-배열) 형태의 JSON으로 보입니다. 필드명이 없어서 인덱스로 접근해야 한다는게 불편해 보입니다. 세번째 결과를 보면 객체 하나를 줬을 때는 필드명까지 읽어서 넣어주는데 말이죠. 컬렉션도 그렇게 해주면 좋을텐데.. 좀 아쉽습니다.

6. 다음은 jQuery Ajax로 JSON 데이터를 받아왔으니, 이제 자동완성 기능만 구현하면 됩니다.

6-1. 텍스트 박스에 어떤 값을 입력하면, 서버로 요청을 보내고 그 결과를 리스트로 뿌리고, 그 중에 하나를 선택하면 텍스트 박스에 채워주고, 텍스트 박스 옆에 기타 자세한 정보를 뿌려줍니다.

6-2. 텍스트 박스에 값을 입력해서 검색 리스트가 나왔을 때, 키보드의 화살표로 상하 이동이 가능해야 합니다.

6-3. 캐시 설정을 통해 같은 검색어의 경우 매번 요청을 보내는 것이 아니라, 특정 시간동안은 캐시에서 데이터를 읽어서 보여줍니다. 캐시 유효 시간을 설정할 수 있어야겠네요.

대략 이정도 기능인데, 여기서 좀 시간이 많이 걸리고 있네요. 내일은 꼭 마무리 해야겠습니다.
비도 오는데 허리가 쑤실만큼 앉아있었지만 결과는 좀 허무하네요.
에잇... 내일은 파이팅.
top


스프링 WebContentGenerator로 자바스크립트 캐싱하기4

모하니?/Coding : 2009.07.08 14:42


이번엔 정말 마지막이라는 느낌입니다.

스프링 ResourceEditor라는 PropertyEditor가 있었군요. ResourceEditor는 ResoruceLoader를 사용해서 Resource 인스턴스를 만드는데, 이 때 resourcePath의 prefix에 맞게 적절한 Resource 인스턴스를 만들어 줍니다. ResourceLoader가 읽어들이는 기본 prefix는 레퍼런스에 잘 설명이 되어 있으니 참고해 보시기 바랍니다. file:, classpath:, http: 입니다.

재미있는건, prefix가 붙어있지 않을 때 인데, 이 때는 applicationContext 객체의 타입을 보고서 기본 Resource 타입을 결정짓습니다. ClassPathApplicationContext인 경우에는 ClassPathResource로 보는거죠. WebApplicationContext인 경우에는 ServletContextResource로 봅니다. 따라서...

이전에 만든 컨트롤러에 전부 Stirng 타입 목록을 받고 type 또한 String으로 받아서 Resource를 판단하는 작업까지 했었다면, 이제 그 작업들은 전부 스프링이 기본으로 제공하는 ResourceEditor로 넘기고 List<Resource>를 받으면 끝납니다. 그렇다고 해서 xml에 별도로 등록해줘야 할 빈이 있는 것도 아니고, 이로인해 좀 더 유연한 설정이 가능해졌고, 설정이 짧아졌으며, 컨트롤러 코드도 줄어들었습니다.

    <bean name="/js.do" class="koma.base.main.JavaScriptController" p:cacheSeconds="300">
        <property name="jsResources">
            <list>
                <value>js/koma.js</value>
                <value>file:c:/springsource/workspace/koma/webapp/js/jquery-1.3.2.min.js</value>
                <value>js/jquery-ui-1.7.2.min.js</value>
            </list>
        </property>
    </bean>

테스트 삼아 이렇게 등록해 보았는데 잘 동작합니다. 이젠 정말 끝인 것 같다는 느낌이네요. 후훗

top


스프링 WebContentGenerator로 자바스크립트 캐싱하기3

모하니?/Coding : 2009.07.08 13:04


2차 작업이 끝인 줄 알았으나... 컨트롤러에 직접 속성으로 js 리소스 위치 목록과 리소스 타입을 알려도록 설정할 수 있는 기능을 추가했습니다.

    <bean name="/js.do" class="koma.base.main.JavaScriptController" p:cacheSeconds="300">
        <property name="resourceType" value="servlet"/>
        <property name="jsResourceNames">
            <list>
                <value>js/koma.js</value>
                <value>js/jquery-1.3.2.min.js</value>
                <value>js/jquery-ui-1.7.2.min.js</value>
            </list>
        </property>
    </bean>

2차까지의 기능도 유효하고 위와 같이 컨트롤러에 리소스 타입과 리소스 path 목록을 넘겨주면 리소스 타입에 따라 적절한 스프링 Resource를 사용해서 파일들을 읽어줍니다.

이렇게 하면 성윤이가 2차 작업에서 말한대로 js가 여러 곳에 분산되어 있을 경우에 resourceType을 url로 설정하고, value에 js 리소스를 나타내는 url 목록을 주면 되겠습니다.

(흠... map 형태로 가야 하는 건가... 이 리소스는 이 타입 저 리소스는 저 타입... 설정하기도 귀찮을 테니 그냥 한 타입으로 가도록 하죠. 일단은.)

url 뒤에 붙이던 것 중에 고정적으로 사용하는 js의 경우 위와같이 컨트롤러에 설정해주고, 페이지에 따라 사용하고 안 하고 하는 것은 url 뒤에 붙여주면 되겠습니다.

기본 js만 사용하는 페이지에서는 이렇게..

<script language="JavaScript" src="/js.do"></script>

부가적인 js까지 사용하는 페이지에서는 이렇게..

<script language="JavaScript" src="/js.do?name=springsprout.js"></script>


top


스프링 WebContentGenerator로 자바스크립트 캐싱하기2

모하니?/Coding : 2009.07.08 11:12


어제의 문제들을 해결했습니다.

문제1 해결: 파일 경로 문제는 스프링 Resource를 이용해서 해결했습니다. 스프링이 제공하는 Resource 중에 ServletContextResource가 있는데, 웹 루트를 기준으로 리소스를 가져올 수 있도록 해주는 편리한 클래스입니다.

Resource jsFolderResource = new ServletContextResource(getServletContext(), getJsFolder());

이런식으로 사용하면 되는데 여기서 ServletContext 객체는 reqeust -> session -> servletRequest로 쭉쭉 타고가서 가져와도 되지만, AbstractController가 상속받은 WebContentGenerator가 상속받은 WebApplicationObjectSupport가 스프링의 ServletContextAware 인터페이스를 구현했기 때문에, 위와같이 getServletContext()만 호출하면 가져다 쓸 수 있습니다.

문제2 해결: 뷰에서 js를 요청할 때 사용하는 URL 중에서 프로토콜과 서버명을 빼고 리소스 부분만 보냈습니다.

        <script language="JavaScript" src="http://localhost:8080/js.do?name=koma.js&name=jquery-1.3.2.js">
        </script>

이상태에서

        <script language="JavaScript" src="js.do?name=koma.js&name=jquery-1.3.2.js">
        </script>

이렇게 말이죠. 잘 동작합니다~ 캬캬캬

아.. cacheSeconds 속성에 입력한 값은 밀리초가 아니라 정말 '초' 단위더군요. 자칫하면 예상보다 너무 오래 캐시를 유지하는 일이 발생할 수도 있습니다. 주의하세요~

자바스크립트 캐싱하기 끝!
top


스프링 WebContentGenerator로 자바스크립트 캐싱하기

모하니?/Coding : 2009.07.07 18:41


http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/web/servlet/support/WebContentGenerator.html

링크를 보면, WebContentGenerator가 해주는 일을 대략 알 수 있습니다. HTTP cache 헤더를 제어할 수 있는 옵션들을 제공해 줍니다.

여러가지 속성들에 따라 HTTP cache-control 헤더 내용이 달라지는데, 그 중 중요한 녀석으로 cacheSeconds가 있습니다. 이 녀석은 기본 값이 -1로 되어 있고, 그 내용인 즉슨 cache 관련 헤더를 전혀 사용하지 않겠다는 것입니다.

캐시를 유지할 기간만 설정해주면 캐시 관련 헤더들이 다시 설정 되는데, 이 떄 설정되는 녀석들 중 하나가 HTTP 1.1에 추가된 cache-control이라는 헤더입니다.

스펙에서는 다음과 같이 정의하고 있습니다.
The Cache-Control general-header field is used to specify directives that MUST be obeyed by all caching mechanisms along the request/response chain.
요청/응답 체인과 관련된 모든 캐싱 매커니즘이 반드시 따라야 하는 지시를 설정한다고 요약해도 될 듯 합니다.

자바스크립트가 들어있는 어떤 페이지를 요청하면, 해당 자바스크립트 파일을 브라우저가 매번 읽어오는데, 그걸 좀 더 효율적으로 개선하기 위해, 자바스크립트를 어떤 url을 통해 가져오도록 src="js.do?name=base.js" 이런식으로 설정하고, 컨트롤러에서는 요청한 자바스크립트를 찾아서 response에 write해 줍니다. 이 때 캐시를 사용하도록 하는거죠.

컨트롤러는 WebContentGenerator를 상속 받은 AbstractController를 상속받는 클래스를 하나 만들고, 내용은 name이라는 매개변수의 값에 해당하는 File을 읽어와서, HttpServletResponse에 쭉~ write() 해주고, 컨텐츠 타입을 자바스크립트로 response.setContentType("application/javascript"); 이렇게 설정해줍니다. 그리고 WebContentGenerator를 가 제공해주는 setCacheSeconds로 캐시를 유지할 시간 설정을 해 줍니다.

파폭과 live HTTP 헤더 플러긴으로 확인해본 결과, 초기 한 번 만 js를 읽어오고 그 뒤 요청 부터는 js를 읽어오지 않았습니다.

문제 1. 컨트럴러에서 자바스크립트 파일을 읽어오는 부분이 녹녹치 않더군요. @_@.. 그래서 자바스크립트가 담겨있는 폴더를 컨트롤러에 설정해줘야 합니다. 개발 환경은 사람 마다 다를테고 배포 환경도 다를텐데.. 이 값을 안 쓰는 방법(상대 경로로 웹 루트에서 부터 js 파일을 찾아가면 좋을 텐데, 제 이클립스에서 돌려보니까 이클립스 루트 폴더부터 경로를 탐색하더군요. @_@)

문제 2. 자바스크립트를 요청하는 URL이 역시 개발 환경과 배포 환경 마다 다를텐데 이건 또 어떻게 해결해야 할지.. 고민입니다.

        <script language="JavaScript" src="http://localhost:8080/js.do?name=koma.js">
        </script>

이런 URL이 되버리는데.. 개발할 때는 이렇게 써도 돌아갈테지만, 배포하면?... @_@

일단 이 두 문제가 맘에 많이 걸리네요.


top


하이버네이트, 스프링 MVC에서 enum 사용하기 4

모하니?/Coding : 2009.07.06 15:00


아마 이번이 시리즈의 마지막이 아닐까 싶습니다. 지난번까지 DB에 저장할 값으로, Integer, String, Character를 지원하는 enum을 만들었고, 이번에는 enum의 목록을 특정 기준으로 정렬할 수 있는 기능을 추가했습니다.

enum 목록을 가져올 때, enum의 valus() 메서드를 사용하면, enum에 등록되어 있는 순서대로 상수들을 가져옵니다. 그러나 새로운 enum을 추가하고 변경하다보면 그 목록 순서도 변경되겠죠. 사용자가 특정 순서대로 목록이 보여지길 원한다면, 상수를 등록한 순서를 조정하는것이 아니라, 특정한 필드 기준으로 정렬을 해야겠다는 판단을 내렸습니다. 결국 새로운 필드를 하나 추가했습니다.

public enum UserCate implements PersistentEnum {
   
    ADMIN("admin", "관리자", 3), STAFF("staff", "직원", 1), SUPP("supp", "협력업체", 2);

...

    private UserCate(String value, String descr, int order) {
        this.value = value;
        this.descr = descr;
        this.order = order;
    }

...

    public int getOrder(){
        return order;
    }

}

이렇게 order라는 int 타입 변수를 하나 추가하고, 이 값으로 순서를 비교하는 Comparator를 만들었습니다. PersistentEnumUtils라는 클래스에 sortedListOf(Class<E> enumClass)를 추가로 만들어 줬습니다.

이제 정렬된 enum 목록이 필요할 떄는 다음과 같이 한 줄이면 됩니다.

    public List<CodeCate> getCodeCate(){
        return PersistentEnumUtil.sortedListOf(CodeCate.class);
    }

    public List<UserCate> getUserCate() {
        return PersistentEnumUtil.sortedListOf(UserCate.class);
    }

    public List<FamillyCate> getFamillyCate(){
        return PersistentEnumUtil.sortedListOf(FamillyCate.class);
    }

이런식으로 말이죠.

스프링, 하이버에서 enum 쓰기... 편하게 하기 위해 만들어 낸 결과물 구조는 다음과 같습니다.

- PersistentEnum 인터페이스
- PersistentEnumUtils 클래스
- PersistentEnumComparator 클래스
- GenericEnumPropertyEditor 클래스

PersistentEnum 인터페이스

    String getDescr();
   
    Object getValue();
   
    int getOrder();

이 셋은 스프링, 하이버네이트에서 enum을 사용할 때 거의 대부분 필요로 하게 될 것이며, GenericEnumPropertyEditor와 PersistentEnumUtils에서 사용할 인터페이스가 필요하기 때문에 만들었습니다.

PersistentEnumUtils 클래스

public static <E extends PersistentEnum> E valueOf(Class<E> enumType, Object value)

public static <E extends PersistentEnum> List sortedListOf(Class<E> enumType)

enum 클래스와 value로 enum을 찾아주는 메서드와 PersistentEnumComparator로 정렬한 list를 넘겨주는 메서드가 있습니다.

PersistentEnumComparator 클래스

    @Override
    public int compare(E e1, E e2) {
        return e1.getOrder() - e2.getOrder();
    }

이게 전부입니다.

GenericEnumPropertyEditor 클래스

PersistentEnum 인터페이스와 enum 클래스를 받아서 생성자, getAsText, setAsText를 재정의했습니다.

이렇게 네 개의 파일만 추가하면... enum을 다음과 같이 구현할 수 있습니다.

public enum UserCate implements PersistentEnum {
   
    ADMIN("admin", "관리자", 3), STAFF("staff", "직원", 1), SUPP("supp", "협력업체", 2);
   
    private final String value;
    private final String descr;
    private final int order;
   
    private UserCate(String value, String descr, int order) {
        this.value = value;
        this.descr = descr;
        this.order = order;
    }

    public String getValue() {
        return value;
    }
    public String getDescr() {
        return descr;
    }
    public int getOrder(){
        return order;
    }

}

뭐 특별한 건 없습니다. 그냥 enum에 인터페이스 하나 추가했을 뿐입니다.

하이버네이트를 사용할 땐, UserType 클래스가 필요하니까 UserType을 다음과 같이 만들어 줍니다.

public class UserCateType extends GenericEnumUserType<UserCate>{
   
}

이게 전부입니다. 클래스도 만들고 싶지 않지만, 하이버네이트 설정에 클래스 풀 패키지 경로를 줘야 하기 때문에 클래스를 안 만들 수가 없습니다.

    @Type(type="koma.domain.usertype.UserCateType")
    UserCate userCate;

자 이렇게 하면 하이버네이트 UserType 설정이 끝났습니다. 아무런 추가 구현도 없이 클래스 하나 만들었을 뿐인데 말이죠. DB 컬럼 타입과 그 안에 들어가는 값은 모두 getValue() 메서드의 리턴타입과 값에 맞게 정해집니다.

스프링 MVC에서 사용할 땐 PropertyEditor가 필요할 텐데.. 이건 전에도 말했듯이 클래스도 만들 필요없이 binder 설정하는 부분에 한 줄만 추가하면 됩니다.

binder.registerCustomEditor(UserCate.class, new GenericEnumPropertyEditor<UserCate>(UserCate.class));

끝~

top


database.properties 모음

모하니?/Coding : 2009.07.03 19:34


외울 수가 없어서 항상 이전에 만들었던 프로젝트들을 뒤적거리는데.... 그게 귀찮아서 한 곳에 모아둡니다.

HSQL

db.driver=org.hsqldb.jdbcDriver
db.url=jdbc:hsqldb:mem:springsprout
db.username=sa
db.password=
hibernate.dialect=org.hibernate.dialect.HSQLDialect

PostgreSQL

db.driver=org.postgresql.Driver
db.url=jdbc:postgresql://localhost/springsprout
db.username=springsprout
db.password=springsprout
hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

MySQL

db.driver=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost/springsprout?autoReconnect=true&characterEncoding=euckr
db.username=springsprout
db.password=springsprout
hibernate.dialect=org.hibernate.dialect.MySQL5Dialect

오라클이 없군요. Derby는 패스 합니다. 비추에요. 대충 이 셋 중에서 골라 쓸 듯. HSQL은 테스트 용이고, 실제 DB 용으로 둘 중 하나.. 혹은 오라클 쓰시면 될 듯.




top


하이버네이트, 스프링 MVC에서 enum 사용하기 2

모하니?/Coding : 2009.07.03 16:37


어제에 이어 오늘도 물고 늘어진다. 이번에 해결한 문제는, DB에 저장할 값을 int 타입 뿐만이 아니라 String 타입을 사용해도 무방하도록 코드를 수정했다.

public enum UserCate implements PersistentEnum {
   
    ADMIN("admina", "관리자"), STAFF("staff", "직원"), SUPP("supp", "협력업체");
   
    private final String value;
    private final String descr;
   
    private UserCate(String value, String descr) {
        this.value = value;
        this.descr = descr;
    }

    public String getValue() {
        return value;
    }
    public String getDescr() {
        return descr;
    }
   
...

}

이건 DB에 저장할 값으로 String 값을 사용할 enum 이고..

public enum CodeCate implements PersistentEnum {

    COLOR(10, "색상"), SIZE(20, "사이즈"), PAYTERM(30, "지불조건"), SHIPVIA(40, "운송방식");
   
    private final Integer value;
    private final String descr;
   
    private CodeCate(Integer value, String descr) {
        this.value = value;
        this.descr = descr;
    }
   
    public int getValue() {
        return value;
    }
    public String getDescr() {
        return descr;
    }

...

}

이건 DB에 저장할 값으로 int 값을 사용할 enum이다.

하이버네이트가 필요로 하는 UserType을 만들어 보자.

public class CodeCateType extends GenericEnumUserType<CodeCate>{

    public CodeCateType() {
        super(CodeCate.class);
    }
   
}

public class UserCateType extends GenericEnumUserType<UserCate>{

    public UserCateType() {
        super(UserCate.class);
    }
   
}

끝이다. 도메인 객체 타입에 설정해보자. 구현에 필요한 코드만 파란색으로 강조를 했다. 생성자 만드는 부분까지 없앨 수 있을 것 같다. 3차 구현에서 해보자.

Code.java

    @Column
    @Type(type="koma.domain.usertype.CodeCateType")
    CodeCate codeCate;

User.java

    @Column
    @Type(type="koma.domain.usertype.UserCateType")
    UserCate userCate;

자.. 이러면 하이버네이트가 CodeCate는 Integet SQL 타입 컬럼을 만들어 주고 UserCate는 String SQL 타입(varchar)를 만들어 줄 것이다.

스프링 MVC에서 바인딩 할 때 사용할 PropertyEditor는 어떨까? 한 줄 씩이다.

        binder.registerCustomEditor(CodeCate.class, new GenericEnumPropertyEditor<CodeCate>(CodeCate.class));
        binder.registerCustomEditor(UserCate.class, new GenericEnumPropertyEditor<UserCate>(UserCate.class));

끝인가?? 글쎄.. 모르곘다. Charater를 추가하는 것도 별 문제 없을 듯 하다. 해보자. 이러다가 모든 타입을 지원해야 하는건 아니겠지?? Char까지만 해보자. 여기서도 생성자에 클래스를 주고 있는데 3차에서는 저 코드를 없앨 수 있게 구현해보자.

자.. 하이버, 스프링에서 자바 enum 사용 간편화 3차 구현 ㄱㄱㅆ
top


하이버네이트, 스프링 MVC에서 enum 사용하기

모하니?/Coding : 2009.07.02 18:19


하이버네이트와 스프링 풀셋으로 구성되어 있는 웹 애플리케이션에서 자바 enum을 사용할 때 생기는 이슈가 뭘까?

1. DB에 어떤 값을 넣을 것이고,
2. 화면에는 어떤 값을 보여주고 어떻게 바인딩 할 것인가?

이 두 가지라고 한다. 그 밖에 이슈 될만한 것은.. 흠.. 뭐.. 없지 않을까 싶다. 왜 이슈일까?

1번 문제를 보자. DB에 잘 안 들어갈까? 하이버네이트로 맵핑을 해보자.

enum UserType {
 MEMBER, MANAGER
}

@Entity
class Member {
...
  @Column
  UserType userType;
}

이 상태로도 SessionFactory를 생성하는데 별 문제도 없을 뿐더러, 읽고 저장하기가 잘 된다. 문제는 DB에 들어가는 값이다. DB에 들어가는 값을 보면, UserType.MEMBER는 0, UserType.MANAGER는 1이 integer 컬럼에 저장된다. enum의 ordinal() 메서드가 반환해주는 값을 그대로 저장한 것이다. 문제는 ordinal() 값이 고정이 아니라, enum 순서에 따라 바뀐다는 것이다. 이런... 그럼 안 되겠다. ordinal 말고, String을 저장하고 싶다면, JPA의 @Enumerated 애노테이션을 추가해주면 된다. @Enumerated(EnumType.STRING) 이렇게 말이다. 이 것을 사용하면 방금 말한 ordinal 문제는 사라질 것이다. DB에는 ordinal이 반환하는 Integer대신 enum의 name이 저장될 것이다.

그런데.. 어떤 이유에선가 굳이 integer 값을 DB에 저장하고 싶다면 어찌해야 될까? 이제부터 복잡해진다. 일단 enum에 필드를 하나 추가하고, EJ2가 추천하는 방법으로 enum을 구현한다. 다음에는 하이버네이트의 UserType 인터페이스를 구현한 클래스를 하나 만들고,

    @Type(type="koma.domain.usertype.CodeCateUserType")
    @Column
    CodeCate codeCate;

이런식으로 하이버네이트의 @Type 애노테이션을 이용하여 맵핑 방법(db에 어떻게 저장하고, db에서 어떻게 꺼내 올 것인가)을 담고 있는 UserType 구현체를 지정해주어야 한다. 이 구현체를 만들 때는 UserType 인터페이스가 제공하는 메서드 10개 정도를 구현해야 된다. 귀찮은 일이다. 그래서 GenericEnumUserType 이라는 클래스를 만들었다. 간단하게 상속만 하고, 생성자만 만들면 되도록 귀찮을 일을 줄여놨다. 자 그럼 일단 첫 번째 문제는 해결이다.

두 번째 문제는 첫 번째에 비하면 비교적 쉽다. 지난 프로젝트에서 PropertyEditor와 씨름을 했던탓에 면역이 생긴 것 같다. 화면에 Enum을 보여줄 떄 enum의 name을 보여주고 싶진 않을것이다. 역시 새로운 필드를 추가해야겠다. 그리고 화면에 보여줄 때는 그 값을 출력하고, 화면에서 어떤 것을 선택했을 때에는 아까 DB에 입력한 값을 선택해서 가져오도록 화면 코드를 작성했다.

다음은 그렇게 해서 가져온 integer 값을 Enum 객체로 샥 바꿔주는 일을 할 PropertyEditor를 만드는 것이다. 간단하다. getAsText()에서는 getValue()로 가져온 객체를 내가 사용하는 enum으로 타입을 변환 한 다음 아까 추가한 필드의 getter를 사용하여 String 값을 넘겨주었다. 이제 화면에서 사용자 친화적인 문구를 볼 수 있을 것이다. 다음은 setAsTest(String text)를 재정의 하여 text는 화면에서 선택한 enum이 DB에 입력하는 값인 integer 값일 것이다, 일단 Integer.parseInt()를 해야 겠다. 아.. 이런.. Enum 클래스에서 valueOf(Class, String) 메서드를 제공해준다. 하지만 난 int 값을 사용하기로 마음 먹었으니 저 클래스는 사용하지 못하겠다. 유틸을 하나 만들었다. Enum 클래스와 int 값을 받아서 해당 int 값을 가지고 있는 Enum을 돌려받는.. 그런 클래스다. 자 그럼 이제 이 유틸을 이용해서 setAsText(String text) 구현도 마칠 수 있다. 이러한 PropertyEditor 역시 매번 만들어 쓰기 귀찮으니깐, 아예 클래스를 만들지 않고 객체만 만들어 사용할 수 있는 GenericEnumPropertyEditor를 만들었다. 두 번째 문제도 해결됐다.

오늘 내가 할 일은 이게 끝인 듯 하다. 자 그럼 잠깐 회고를 해보자.

DB에 int 값이 아닌 enum의 name 문자열을 저장한다면 어떻게 될까?

일단, UserType을 만들 필요가 없어진다. 아까도 이야기 했듯이 @Column과 @Enumerated(EnumType.STRING)를 사용하면 UserType 없이고, 문자열로 enum을 DB에 저장할 수 있다. GenericEnumUserType도 필요가 없고, 매번 UserType 클래스를 만들어야 하는 수고도 줄어든다.

다음, 화면에서 enum 목록(Arrays.asList(enum.values());를 사용하면 간단)을 보여줄 때, enum에 추가한 사용자 친화적인 설명을 담고 있는 descr 속성에 담겨있는 값을 보여주고, 실제로 선택하는 값이 DB에 저장하는 int값이 아닌 enum의 name이라면 어떻게 바뀔까? getAsText() 구현은 동일하고, setAsText()에서 받아오는 값이 Enum의 name이니깐, Enum.valueOf(Class, String)을 사용할 수 있다. 굳이 Util 클래스를 만들 필요도 없고, setAsText() 구현도 간단해진다. 다만, Enum 마다 PropertyEditor 객체를 지정해 줘야 하는 건 어쩔 수 없다. 하지만 이건 정말 일도 아니다. 새로운 클래스를 추가하는 것도 아닌데 이 일이 뭐 크게 대수겠는가.

결국.. DB에 어떤 이유로 인해 enum의 interger 값을 저장하는 것이 enum의 name 문자열을 저장하는 것보다 훨씬 복잡하고, 귀찮은 것 같다.

DB에 int를 저장하는게 좋을까 string을 저장하는게 좋을까? integer 값을 저장해야 하는 별다른 이유가 없다면 나는 enum의 name을 저장하고 싶다.

수정은 내일.. 오늘은 이만 퇴근..

===========================

할려고 했으나.. 이게 끝이 아니란다. DB에 저장할 enum 필드를 선택할 수 있게 해야 되고(결국 위에서 실컷 고민한게.. 물거품처럼 하얘지는 느낌이다.),

enum 목록을 가져올 때 정렬을 할 수 있어야 한단다.(그럼 이것도 Arrays.asList(enum.values()); 만으로는 어림 없을 듯 하다.)

또한 i18n까지도..

@_@
top


결국 그냥 만들어버린 JPA 문서 자동화 툴

모하니?/Coding : 2009.07.02 11:42


지난 번에 살펴본 hbm2doc로는 사부님이 원하는 문서를 만들기가 버거워서, 예전에 물개선생님이 만드셨다는 코드를 참조해서 비슷하게 만들었습니다.

@Entity
@Table(name = "users", uniqueConstraints = @UniqueConstraint(columnNames = { "loginid" }))
@SequenceGenerator(name="user_sq", sequenceName="user_sq")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Integer id;
    @Version
    Integer version;
    @ColumnInfo(value = "아이디", description="중복 되는 값은 사용할 수 없음")
    @Column(length = 50, unique = true)
    String loginid; // identity

    @ColumnInfo("이름")
    @Column(length = 50)
    String name;
    @Column(length = 50)
    String pwd;
    @Column(length = 50)
    @Index(name="email_idx")
    String email;
    @Column
    int usertype;
    @Column(length = 50)
    String title;
    @Column(length = 50)
    String dept;
    @Column(length = 50)
    String tel;
    @Column(length = 50)
    String mobile;
    @Column(length = 50)
    String addr;
    @Column
    @Temporal(TemporalType.DATE)
    Date birthday;

    @ManyToMany
    Set<Role> roles;

    @OneToMany(cascade = { CascadeType.ALL})
    Set<Familly> famillies;

이렇게 애노테이션이 난무 하는 도메인 클래스에 대한 정보 + 새로 추가한 애노테이션 @ColumnInfo를 사용하여 화면에서 해당 필드를 보여주는 이름(또는 도메인 전문가가 사용하는 용어)과 설명을 추가할 수 있습니다.

어제 하루 종일 만들고 오늘 아침에 조금 다듬은 결과물은 다음과 같습니다.


코딩은 TDD로 시작했는데, 저 위에 보이는 HTML 만드는 코드는 참조하던 코드를 짜집기해서 만들었습니다.

클래스 구조는 대충.

DocGenerator ---> DocWriter ---> EntityInfo

이렇습니다. DocGenerator에 엔티티 클래스 목록을 주면, DocWriter로 문서를 생성해 내는데, 이 때 new EntityInfo(엔티티 클래스) 생성자를 사용하여 애노테이션에서 정보를 축출하여 담고 있는 Info 클래스를 만들어 사용합니다.

DocGenerator는 기초 정보(Dialect, 엔티티 클래스, 타겟 폴더, ...) 수집 및 퍼사드 역할을 하고, DocWriter는 실제로 문서 생성을 책임지는데, 인터페이스를 둬서 여러 형태의 문서 작성기를 사용할 수 있도록 했습니다. 현재는 HTMLDocWriter만 구현되어 있습니다. EntityInfo는 리플렉션을 사용하여 주어진 클래스와 그 클래스에 붙어있는 애노테이션을 활용하여 화면 출력에 필요한 정보들을 수집해 두는 역할을 합니다. 일종의 DTO라고 봐야하나..

실행은 DocGeneratorRunner의 main 메서드를 약간 수정해서 실행하면 되겠습니다.


의존성, 프로젝트 이런거 없이 그냥 소스 코드만 묶었습니다. 아마.. 하이버네이트 애노테이션.jar, persistence.jar, 그리고 HTML을 처리할 때 사용한 hq-1.jar라는 라이브러리가 필요할 겁니다.

코드에 보시면 Writer 쪽에 심각한 중복 코드가 있는데.. 아직 해결하진 않았습니다. 캬캬캬
Info 쪽 코드도 전혀 깔끔하지 않습니다. 애노테이션에서 정보를 축출하여 초기화 하는 부분(생성자)을 일관성 있게 수정하고 싶은데.. 하진 않았습니다.
HTML에 어떤 DB 스키마에 해당하는 컬럼 타입인지 알려주기 위해 Dialect도 출력할까 했지만.. 역시 아직 하지 않았습니다. 이건 뭐 간단할 것 같으니 금새 추가할 수 있을지도 모르겠네요.
top


AspectJ를 이용한 코드젠과 프레임워크 2

모하니?/Coding : 2009.06.10 19:57


이전 글 AspectJ를 이용한 코드젠과 프레임워크에 이어지는 글로 현재는 Service과 Controller까지도 적용을 마치고 실행 후 잘 돌아가는 모습을 살펴보습니다. 지금은 웹 쪽을 REST 스타일로 고칠겸, 새 프로젝트로 깔끔하게 정리하고, 새로 디자인한 것까지 모두 합치고 있습니다. (역시 적당한 단계별로 해야지 한꺼번에 하려니깐 정신없고 오래 걸리는 듯합니다.)

1. AJ 내에서 코드 젠을 이용하여 상속을 없앨 수 있다.

AJ 코드를 어떻게 작성하느냐에 따라 다르겠지만, ROO가 제공하는 AJ를 스타일대로 작성하면, 상속을 이용하지 않고도 추가 기능을 넣을 수 있다. 장담점이 있다고 생각된다. 상속을 없앤건 좋은 일이라고 생각된다. 프레임워크 코드가 아닌, 비즈니스 도메인에 의한 상속을 사용할 수 있을테니말이다. 하지만, 상속을 포기하고 일일히 다 주입하는 방법을 택하다보니, 비슷한 코드가 여러 AJ에 널려있게 된다. 이 때는 또 한 가지 변수 AJ 파일의 길이를 고려해야 할 것 같다. AJ가 일일히 작성하기에 너무 길다고 생각 된다면, AJ 상속 .. 보다는 AJ에도 Generic을 도입할 수 있다면 편할지 모르겠다. 또한 AJ 작성에 걸리는 시간을 감안할 때 AJ를 코드젠하는 기술은 거의 필수라고 생각된다. 하지만 AJ가 코드젠 없이도 만들만큼 분량이 적다면? 그렇다면 굳이 코드젠 없이 그냥 손수 AJ 파일을 만들어 쓰면 될 것 같다. 하지만 내가 오늘 작업해본 AJ 코드는 일일히 작성하기에는 너무 길었다. 코드젠이 필요하다고 느껴졌다. 

2. 테스트가 필요하다.

코드가 눈에 보이면 고치기도 간편하다. 그래서인지, 왠만큼 돌아갈 것으로 만단되는 코드는 테스트를 잘 하지 않는다. 복잡한 로직이나 확신이 없는 코드에 대한 테스트만 작성하는 편이다. 자신감이라기 보다는 게으름이 맞을 것이다. 아무튼.. 그런데 이 게으름을 부리지 못할 정도로 날 불안하게 만드는 것이 바로 AJ다. 내가 아직 AJ에 익숙하지 않아서 일지도 모른다. 그래서 인지 불안하다. 그래서 인지 테스트가 절실하다.


컨틀로러 코드는 비어있지만, 아래 보시다시피 엄청나게 많은 것들이 추가됐다. 잘 동작하는지 어떻게 확신할 수 있겠는가? 글쎄.. 확신하지 못하겠다. 오랜만에 컨트롤러 테스트를 해보게 생겼다.

가짜 데이터 -> 컨트롤러 메서드 -> 결과 확인

스타일로 해볼 생각이다. 예를 들어, 위에 보이는 것중에 delete(int)가 있다. 가짜 데이터는 int 타입 변수 하나가 될 것이고, 메서드에 넘겨주고, service.delete()가 호출되는지 확인할 것이다.

3. 자동완성과 코드 네비게이션에 대한 대안을 찾았다.

위에 보이는 Cross References 뷰를 이용하는 것이다. Ctrl + 스페이스와 Ctrl + 클릭에 비할바는 못 되지만, 어떤 것들이 주입되었는지 한눈에 볼 수 있고 클릭을 하면 바로 AJ 파일로 이동해 주기 때문에 어느 정도 B급 대안이 될 수 있지 않을까 생각된다.

4. 배포는 어떨까?

어제도 포스팅 했지만, 메이븐에서 AJ를 컴파일 하도록 설정해주기만 하면 배포에도 전혀 문제가 없다. AJ는 스프링 AOP와 달리 컴파일 시점 위빙을 사용할 수 있기 때문에 더 간편한 것 같다. 스프링 AOP처럼 agent 설정을 하지 않아도 되니 얼마나 좋은가. 심지어 메이븐 설정을 스프링 ROO예제에서 그대로 배낄 수도 있다.ㅋㅋ

5. 활용 방법이 여러 가지다.
 
잘 모르겠지만, 기존의 자바 프레임워크는 (이클립스 플러긴 같은 툴은 빼고) 아마도 Generic을 활용한 DAO, Service, Controller 추상 계층과, View 자동 생성 그리고 유틸 라이브러리를 제공해주는 것일 것이다. 여기에 AspectJ 활용을 극대화 하여 새로운 프레임워크를 만드는 방법이 몇 가지 있겠다.

a. 기존 프레임워크에 AJ 파일 소수(하나 내지 두개)를 추가하여 자동 (또는 수동으로) 생성되는 코드를 깔끔하게 만드는 방법이 있다.
b. 스프링 ROO처럼 기존에 Generic 클래스 상속으로 제공하던 기능들을 일일히 AJ로 주입해주는 것이다.
c. 위 두 가지 방법을 혼용하는 것이다.

이 셋의 차이점은 좀 더 코딩하면서 느껴봐야 확실해 지겠지만, 지금까지 느낀점을 간략하게 정리하자면 이렇다.

a: AJ 코드량과 생성된 코드량이 모두 적다. 생성된 코드가 POJO 처럼 보이지만 사실은 이전과 동일하다.(extends Generic머시기..) 코드젠이 필요없거나, 간단한 코드젠이 필요하다.

b: AJ 코드량이 엄청 많지만, 생성된 코드량은 적다. 생성된 코드는 POJO에 가깝다. 다소 복잡한 코드젠이 필요할 수 있다. AJ가 커서 여러 AJ로 나누는 것이 깔끔해 보일 수 있다.(스프링 ROO) 중요 로직이 눈에 확 띈다.

c: a방법을 사용하는 AJ와 b방법을 사용하는 AJ를 분리하는게 좋다. 결국엔 코드젠이 필요하다.

오늘의 결론

어떻게 하나 결국에 생성되는 코드는 (실제 그런지 어떤지를 떠나서) 깔끔해 보이기 때문에 핵심 로직은 눈에 확 들어 올 듯 하다. 하지만 그러기 위해서 드는 노고를 생각해보면... 이걸 내가 꼭... 해야되는건가.. 싶기도 하다. 특히 a 방법을 쓸꺼라면.. 그냥 겉치례에 불과하다고 느껴진다. 그럴바엔 그냥 기존 방식을 쓰는게 나아보인다. 많은 노고를 들여서 b 방법으로 개발해 낸다면.. 멋질 것이다. 스프링 ROO 처럼 말이다. 단, 스프링 ROO는 DDD 다른 또 다른 학습 부담이 있어서 망설여지지, 그렇지 않고 기존의 계층형 아키텍처 스타일로 b 형태의 프레임워크를 제공한다면, 어떨까?? OSAF-ROO가 되는건가..

top


스프링 ROO에서 RESTful 코드 보기

모하니?/Coding : 2009.06.09 22:58


vote.roo 예제에서 ChoiceController_Roo_Controller 애스팩트에서 URL 맵핑과 뷰 맵핑 정보를 위주로 살펴보겠습니다.

/choice, POST => create => "choice/create" or "redirect:/choice/" + choice.getId();
/choice/form, GET => createForm => "choice/create"
/choice/{id}, GET => show => "choice/show"
/choice, GET => list => "choice/list"
PUT => update => "choice/update" or "redirect:/choice/" + choice.getId();
/choice/{id}/form, GET => updateForm => "choice/update"
/choice/{id}, DELETE => delete => "redirect:/choice"

이 정보들이 개인적으로 제일 궁금했었기 때문에, 이렇게 간추려서 정리해봤습니다. 이 정보들을 보다가 뷰에서 요청을 보내는 방법이 궁금했습니다. 흠.. GET은 알겠고, POST는 폼에서 보냈을 테고 DELETE는 어케 보냈을까나... 하면서 /choice/list.jsp 파일을 열어 보았습니다.

                    <td>
                        <form:form action="/vote/choice/${choice.id}" method="GET">
                            <input alt="Show choice" src="/vote/static/images/show.png" title="Show choice" type="image" value="Show choice"/>
                        </form:form>
                    </td>
                    <td>
                        <form:form action="/vote/choice/${choice.id}/form" method="GET">
                            <input alt="Update choice" src="/vote/static/images/update.png" title="Update choice" type="image" value="Update choice"/>
                        </form:form>
                    </td>
                    <td>
                        <form:form action="/vote/choice/${choice.id}" method="DELETE">
                            <input alt="Delete choice" src="/vote/static/images/delete.png" title="Delete choice" type="image" value="Delete choice"/>
                        </form:form>
                    </td>

헛 글쿠나.. 걍 폼에서 method를 명시해 줬구나.. @_@ 설정은 별로도 해줘야 할 거 없으려나..하고 web.xml과 vote-servlet.xml을 살펴봤지만 별다른 건 없더군요 아.. web.xml에 urlrewriter 필터를 적용했던데 스프링에서 사용하는 녀석이니 한 번 봐봐야겠습니다.

org.tuckey.web.filters.urlrewrite.UrlRewriteFilter

vote-servlet.xml에 있던 설정은 간단했습니다. internalResourceViewResolver에 prefix suffix 줘서 뷰이름으로 jsp 뷰 찾아가도록 빈을 등록했고, 파일 업로드를 하는지 머티파트 머시기 빈을 하나 등록했고, 발생한 예외에 따라 보여줄 뷰 이름을 등록한 예외처리뷰도 보이고 컴포넌트 스캔까지 네 개가 명시적으로 등록되어 있습니다.

REST 적용 간단해 보이네요~

참고자료: RESTful한 웹 서비스 만들기
top


AspectJ를 이용한 코드젠과 프레임워크

모하니?/Coding : 2009.06.08 20:54


오늘은 도메인과 DAO쪽에만 AOP를 적용하는 AspectJ 파일을 만들어보았습니다. ROO를 참고하면서 말이죠. ROO와 다른 점은 프레임워크 코드를 이용한다는 거죠. (ROO는 제품 코드에서 ROO 코드는 하나도 이용하지 않는 완전한 non-intrusive 내지 transparent 코드젠 기술을 제공합니다.) 이런 식으로 새로운 형태의 OSAF도 만들어 낼 수도 있겠습니다. 하지만.. 할지 말지는 고민을 해봐야겠네요.

그 고민에 대한 시작으로, 아직은 충분한 예제를 못 만들었지만, 일단 여기까지 AspectJ를 이용한 프레임워크를 만들면서 느낌점을 정리해봐야겠습니다.

1. 자동완성 기능 사용 못 합.

이전 글처럼, AspectJ로 (메서드를 추가하거나 클래스 또는 인터페이스 상속을 추가하여) 어떤 클래스에 추가적인 기능들을 줬지만, 막상 이클립스에서 해당 클래스를 써먹을 때 코드 자동 완성을 사용할 수 없다는 점입니다. 원래 해당 클래스가 가지고 있던 멤버는 당연히 자동 완성이 되지만, AspectJ로 주입한 기능들은 참조가 되지 않습니다. 이 점은 AJDT에서 개선해주면 가능할지 싶은데... STS 최신 버전에선 어떨지 모르겠네요. 아무튼.. 이게 안 된다면.. 아.. 불편해..

2. 대체 뭐하는 녀석이람?

위 얘기랑 이어지는 이야기일 수도 있는데 해당 클래스가 하는 일이 숨겨져(?) 있다보니, 대체 어떤 일을 하는 지 눈치 채기가 쉽지 않습니다. 작명을 잘 해줘야겠죠.

3. 핵심 로직은 눈에 확 들어올 듯.

AJ 파일로 빼내는 로직들은 대부분 공통적인 내용일 겁니다. CRUD가 대부분이고 ROO의 경우에는 finder도 제공해주겠죠. 즉 감춰져 있는 부분이 무엇인가를 명확히 인지하고 있다면, 그 뒤에는 핵심 로직만 작성하면 될테고 코드에서 확 들어나게 되어 있겠죠. 이렇게 되면 2번에서 대체 뭐하는 녀석인가?라고 고민하는 시간도 줄어들테지요.

4. 코드 네비게이션 불편.

역시나 AJDT가 개선해 주길 바라지만, 현재로서는 AspectJ로 추가한 메서드나 필드로 Ctrl + 클릭으로 이동하는 것이 안 됩니다. 툴 측면에서 보면 1번과 비슷한 불편사항으로 볼 수 있겠습니다.

5. 성능?

AspectJ를 사용해서 컴파일 시점에 위빙을 하면 런타임 시에 성능 문제는 거의 없겠지만, 이 컴파일 작업이 매번 테스트를 돌릴 때 마다 일어나기 때문에 일반적인 테스트를 돌리는 것 보다는 조금 오래 걸리는 것이 사실입니다. 하지만 뭐 그정도 차이는 무시할만 하더군요.

오늘의 결론..

툴 지원이 조금만 더 보완된다면, AspectJ를 활용한 코드젠과 프레임워크를 활용하여 좀 더 깔끔하고 유연한 코딩을 즐길 수 있을 것으로 예상 됩니다. 코드 자동 완성과 네비게이션이 불편한 지금도 만약 AspectJ로 추가한 코드에 접근 할 필요가 없다는 가정을 한다면, 해볼 만 하다고 생각합니다.

Roo처럼 콘솔까지 제공하고, 변경 사항을 트래킹하여 롤백한다거나 코드젠 이후에 직접 코드를 수정해도 유기적으로 반영해주는 기능을 구현하긴 힘들겠지만, 단순한 코드젠으로 AspectJ를 생성하고 이 AspectJ가 (OSAF 같은) 프레임워크 코드를 이용하도록 한다면, 기존의 프레임워크를 한 단계 업그레이드 할 수 있는 방안이 되지 않을까 생각합니다.

ps: 흠.. 일주일을 쉬고 왔더니 머리가 빙빙 도네요. 색시한테 9까지 간다고 했는데, 9시에 출발하겠네.. 쏘리 쏘리 쏘리 쏘리~
top


WebTUnit 1.0 M1 배포합니다.

모하니?/Coding : 2009.05.19 21:43


아래 매뉴얼을 참조하세요.
영어로 작성했는데, 너그러이 봐주세요.

이번 주 내로 배포하고 싶은.. M2 버전에서는 다음 두 가지를 중점접으로 다루겠습니다.
- 샘플 애플리케이션을 추가
- Jetty 추가

저저번주 금요일부터 오늘까지 달렸으니까..
금,  월~금, 월, 화. 8일이네요.
이제 쬐끔 쓸만해져서 공개했습니다.

웹 테스트 하실 때 조금이나마 보탬이 되었음 합니다.
막심 므라비차의 축하공연을 들으며 찬찬히 읽어 보세요. 간단합니다.



0. add repository setting to pom.xml or settings.xml

        <repository>
            <id>opensprout nexus</id>
            <name>OpenSprout Nexus public</name>
            <url>http://www.opensprout.org/nexus/content/groups/public</url>
        </repository>

        WebTUnit is provided by OpenSprout Nexus.

1. add dependency to pom.xml

        <!-- WebTUnit -->
        <dependency>
            <groupId>org.opensprout</groupId>
            <artifactId>webtunit</artifactId>
            <version>1.0-M1</version>
        </dependency>

2. add cargo-maven-plugin and maven-surefire-plugin to pom.xml

    2-1. cargo-maven-plugin configuraion

        <build>
        ...
            <plugin>
                <groupId>org.codehaus.cargo</groupId>
                <artifactId>cargo-maven2-plugin</artifactId>
                <version>1.0.1-SNAPSHOT</version>
                <configuration>
                    <wait>false</wait>
                    <container>
                        <containerId>tomcat6x</containerId>
                        <type>installed</type>
                        <home>tomcat/apache-tomcat-6.0.18</home>
                    </container>
                    <deployer>
                        <type>installed</type>
                    </deployer>
                    <configuration>
                        <properties>
                            <cargo.servlet.port>8080</cargo.servlet.port>
                        </properties>
                    </configuration>
                </configuration>
                <executions>
                    <execution>
                        <id>start-container</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>start</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>stop-container</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>stop</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

        First, you must set up your tomcat home to <home> element above or,
        just download and unzip tomcat archive to project/tomcat folder.

        If you set <wait> to true, then when you run a test server by 'mvn cargo:start',
        the console will be wait untile you stop the process by CTRL + C.
        This is useful when you want to test on Eclipse by CTRL + ALT + X -> T or CTRL + F11.
        But, you shold be careful when you use phase that through pre-integration-test phase.
        That process will stop and wait in pre-integration-test until you stop the test server.

        By default setting, you can use cargo with phases thar through pre-integration-test phase.
        For example, when you run 'mvn verify', you will pass pre-integration-test phase,
        but you will not be stopped by process, because it dooesn't wait.
        This will help you when you set up this project in continuous integration environment.

    2-2. maven-surefire-plugin configuration

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>**/*WebTest.java</exclude>
                    </excludes>
                </configuration>
                <executions>
                    <execution>
                        <id>integration-test</id>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <phase>integration-test</phase>
                        <configuration>
                            <excludes>
                                <exclude>none</exclude>
                            </excludes>
                            <includes>
                                <include>**/*WebTest.java</include>
                            </includes>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        ...
        </build>

        If you want to use another naming pattern, or package pattern,
        change '**/*WebTest.java' value to what you want.

3. make some web test with webdriver.

    @RunWith(WebTestRunner.class)
    @WarConfiguration("opensprout")
    public class MemberWebTest {

        @WebTest
        public void memberPages(){
            WebDriver driver = new HtmlUnitDriver();
            driver.get("http://localhost:8090/springsprout/member/list.do");
            MemberListPage listPage = PageFactory.initElements(driver, MemberListPage.class);
            assertEquals(2, listPage.getTableRows());
        }
    }

    If you want to feel comfortable when you write some web test codes,
    I recommend Page Object Pattern(http://code.google.com/p/webdriver/wiki/PageObjects).
   
    You can also use @DataConfiguration when you want to input and delete some test data.
    Check some usecases.
    - http://whiteship.me/2237
    - http://whiteship.me/2238
    - http://whiteship.me/2239

4. run test.

    Open a console, type 'mvn verify'.
    Or, modify, cargo-maven-plugin's <wait> to true, and run it with JUnit in Eclipse.
   
5. give me a feedback

    http://github.com/whiteship/WebTUnit/issues
   

top


WebTUnit에 CoC 적용 결과

모하니?/Coding : 2009.05.19 15:39


한 가지를 더 수정해서 이제는 WarConfiguration 설정이 더 간편해졌다. 8080 포트를 사용하여 웹 테스트를 할 때는 appName이라는 속성을 명시하지 않고 그냥 애플리케이션 이름만 명시하면 된다.

@RunWith(WebTestRunner.class)
@WarConfiguration("springsprout")
@DataConfiguration
public class MemberWebTest {

    @WebTest
    public void memberPages(){
       ...
    }
}

이제 한 가지만 더 정리하면 1.0-M1 버전을 공개할 수 있을 듯 하다. 캬캬캬

--------------------------------
@RunWith(WebTestRunner.class)
@WarConfiguration(appName="springsprout")
@DataConfiguration(dataType=DataType.XML, location="org/springsprout/testData.xml")
public class MemberWebTest {

    @WebTest
    public void memberPages(){
       ...
    }

설정하는 방법을 위와 같은 형태에서 아래와 같이 변경됐습니다.

@RunWith(WebTestRunner.class)
@WarConfiguration(appName="springsprout")
@DataConfiguration
public class MemberWebTest {

    @WebTest
    public void memberPages(){
          ...
    }
}

이 것이 전부입니다.

먼저, 바뀐 내용 중 하나는 파일 확장자를 통해 데이터 타입을 짐작한다는 것입니다. CoC라기 보다는 아예 바뀐 내용입니다.

1. 적용한 CoC는 우선, 기본 테스트 데이터 파일 이름을 testData.xml로 생각하며, 테스트하려는 클래스와 동일한 패키지에 들어있다고 가정합니다.

2. 하지만, /를 사용하여 파일 이름을 명시 했을 때에는 해당 패키지 경로에서 테스트 파일을 찾게 합니다.

@RunWith(WebTestRunner.class)
@WarConfiguration(appName="springsprout")
@DataConfiguration(fileName="integration/member/testData.xml")
public class MemberWebTest {

    @WebTest
    public void memberPages(){
          ...
    }
}

이런식으로 말이죠.
top


Page Object 패턴을 이용한 웹 CRUD 테스트 with WebTUint

모하니?/Coding : 2009.05.18 20:32


참조
http://code.google.com/p/webdriver/wiki/PageObjects
http://code.google.com/p/webdriver/wiki/PageFactory

화면의 페이지를 나타내는 Page 객체들을 작성합니다. 화면에서 테스트할 주요 엘리먼트들은 WebElement로 표현하고, 각각의 엘리먼트를 애노테이션으로 찾을 수 있게 id나 name 또는 xpath를 사용하여 지정해 줍니다.
public class MemberForm {

    protected WebElement name;

    protected WebElement email;

    protected WebElement password;

    @FindBy(how = How.ID, using = "name.errors")
    protected WebElement nameError;

    @FindBy(how = How.ID, using = "email.errors")
    protected WebElement emailError;

    @FindBy(how = How.ID, using = "password.errors")
    protected WebElement passwordError;

    @FindBy(how = How.XPATH, using = "//input[@type='submit']")
    protected WebElement submit;

    public WebElement getName() {
        return name;
    }

...

}

원랜 Page 클래스에 있어야 할 속성들인데, 다른 Page에서도 공통으로 사용하는 속성들이라 상위 클래스로 빼냈습니다. 애노테이션을 붙이지 않으면, 자동은 화면에서 클래스의 변수명에 해당하는 id를 가진 엘리먼트를 찾아줍니다.
 
그 다음으로, 페이지에서 일어나는 사용자 액션을 메서드로 표현합니다.

public class MemberAddPage extends MemberForm {

    private WebDriver driver;

    public MemberAddPage(WebDriver driver) {
        this.driver = driver;
    }

    public MemberAddPage addFail() {
        name.sendKeys("keesun");
        submit.submit();
        return PageFactory.initElements(driver, MemberAddPage.class);
    }

    public MemberListPage addSuccess() {
        password.sendKeys("123");
        email.sendKeys("keesun@whiteship.me");
        submit.submit();
        return PageFactory.initElements(driver, MemberListPage.class);
    }

}

이것을 이용하여 다음과 같은 테스트를 작성할 수 있습니다.

@RunWith(WebTestRunner.class)
@WarConfiguration(appName="springsprout")
@DataConfiguration(dataType=DataType.XML, location="integration/member/testData.xml")
public class MemberWebTest {

    @WebTest
    public void memberPages(){
        WebDriver driver = new HtmlUnitDriver();
        driver.get("http://localhost:8080/springsprout/member/list.do");
        MemberListPage listPage = PageFactory.initElements(driver, MemberListPage.class);
        assertEquals(2, listPage.getTableRows());

        MemberAddPage addPage = listPage.toAddForm();
        assertNotNull(addPage);

        MemberAddPage failedAddPage = addPage.addFail();
        assertNotNull(failedAddPage);
        assertEquals("required", failedAddPage.getEmailError().getText());
        assertEquals("required", failedAddPage.getPasswordError().getText());

        listPage = failedAddPage.addSuccess();
        assertNotNull(listPage);
        assertEquals(3, listPage.getTableRows());

...

}

물론, WebTUnit이 없다면, 수동으로 서버를 켜고, 데이터 넣고, 저 테스트를 실행하고, 다시 테스트 데이터를 정리하고, 서버를 내리는 작업을 반복해야겠지만.. WebTUnit이 있기 때문에 메이븐 콘솔에서 mvn verify를 실행하기만 하면 위의 작업들을 자동화 할 수 있습니다. 조금더 빠르고 이클립스에서 실행해 보고 싶다면, pom.xml을 약간 수정하여 cargo.wait 속성을 true로 바꾸고 mvn cargo:start로 서버를 실행한 다음, 이클립스에서 JUnit 테스트로 실행하면 됩니다.
top


WebTUnit 사용 시나리오 3. 테스트 데이터가 필요 없는 경우

모하니?/Coding : 2009.05.15 14:05


제일 간단한 경우 입니다. 테스트 데이터가 필요 없기 때문에 @WarConfiguration만 사용하면 됩니다.

@RunWith(WebTestRunner.class)
@WarConfiguration(appName="springsprout")
public class SampleWebTest {

    @WebTest
    public void sinario1(){
        System.out.println("test1");
    }

    @WebTest
    public void sinario2(){
        System.out.println("test2");
    }

}

이렇게만 하면,

1. WAR 패키징
2. WAR 배포
3. 테스트~
4. WAR un배포

과정을 거치게 됩니다~ 이건 뭐 너무 간단해서 더 설명할 것도 없네요~


이제부터 본격적인 테스트는 WebDriver나 Selenium을 이용해서 작성하면 되겠죠. ㄱㄱㅆ~
top


WebTUnit 사용 시나리오 2. 웹 테스트 + 테스트 메서드 단위 데이터 관리

모하니?/Coding : 2009.05.15 12:50


이번에는 이전 시나리오와 비슷하지만, 테스트 데이터를 클래스 단위가 아니라 메서드 단위 즉, JUnit에서 테스트 단위로 관리하는 방법입니다. 아직은 미완성이라고 볼 수 있는데, 지금 상태에서도 아래와 같이 코딩을 한다면, 이 시나리오가 가능합니다.

@RunWith(WebTestRunner.class)
@WarConfiguration("springsprout")
//@DataConfiguration(fileName="testData.xml")
public class SampleWebTest {

    @WebTest
    public void sinario1(){
        DataManager dm = new DefaultDataManager("integration/sample/testData1.xml", DataType.XML);
        dm.insertTestData();
        System.out.println("test1");
    }

    @WebTest
    public void sinario2(){
        DataManager dm = new DefaultDataManager("integration/sample/testData2.xml", DataType.XML);
        dm.insertTestData();
        System.out.println("test2");
    }

}

@DataConfiguration을 클래스에서 제거하면 되고, 소스코드에서 직접 DataManager API를 사용하여, 테스트 데이터를 넣을 수 있습니다. 위 예제에서는 두 개의 테스트에서 각각 다른 테스트 데이터를 넣고 있습니다.


테스트 데이터를 매번 지우고 싶다면, deleteTestData() 메서드를 이용해도 되지만, insertTestData가 내부적으로 DBUnit의 CLEAN_INSERT를 이용하기 때문에 그럴 필요는 없습니다.

이 코드는 차후에 JUnit 4.7의 인터셉터 기능을 이용해서 개선할 예정입니다. 지금은 스냅샷 정도록 생각해주세요
top


웹 테스트 프레임워크(WebTUnit) 사용 시나리오 1. 웹 테스트 + 테스트 클래스 단위 데이터 관리

모하니?/Coding : 2009.05.15 11:36


해당 테스트 클래스를 웹 테스트 하고, 그 안에 있는 모든 테스트들이 공통의 데이터를 이용할 때 사용할 수 있습니다. 단, 주의할 것은 테스트들 사이에 순서가 없기 때문에(JUnit과 동일) 테스트 데이터를 잘못 조작하면 테스트끼리 의존성 생겨 실패할 수 있습니다. 테스트 마다 각자의 테스트 데이터를 사용하는 시나리오는 다음에 살펴보겠습니다. 이 시나리오는 테스트 데이터를 조작하지는 않고 주로 참조 용으로 테스트 하는 경우에 적당합니다.

@RunWith(WebTestRunner.class)
@WarConfiguration("springsprout")
@DataConfiguration(fileName="integration/sample/testData.xml")
public class SampleWebTest {

    @Before
    public void setUp(){
        System.out.println("===================================");
        System.out.println("===================================");
    }

    @WebTest
    public void test1(){
        System.out.println("test1");
    }

    @WebTest
    public void test2(){
        System.out.println("test2");
        fail("for test");
    }

    @After
    public void tearDown(){
        System.out.println("***********************************");
        System.out.println("***********************************");
    }

    @Test
    public void noTest(){
        System.out.println("this will not be print");
    }

}

이렇게 했을 경우
1. sprignsprout라는 이름의 WAR 파일을 생성하고,
2. 배포하고,
3. integration/sample/testData.xml에 있는 데이터를 DB에 넣고
4. 테스트를 쫙 실행(순서 무작위, JUnit 동작 방식을 따름), 각각의 테스트는 @WebTest를 붙여줌. @Test는 동작하지 않습니다. @Before, @After, @BeforeClass, @AfterClass, @Ignore 모두 적용 됨. 딱 하나. @Test 대신 @WebTest를 사용하면 됨. @Test를 사용해도 되지만, 테스트 구분을 위해.. 차후에 WebTest관련 기능을 추가할 때 유리할 듯..
5. 테스트가 끝나면(중간에 몇 개가 실패하더라도), 테스트 데이터를 삭제하고,
6. WAR를 unploy합니다.

물론 세부적 예외가 언제 발생하느냐에 따라 그 처리가 조금씩 달라집니다. 지금도 계속해서 이부분을 작업하고 있으니 자세한 설명은 나중에~


top


웹 통합 테스트 프레임워크 개발 중

모하니?/Coding : 2009.05.14 23:10


웹 통합 테스트를 지시 받고, 몇일 전 삽질부터 시작해서 오늘까지 조금 열심히 달렸습니다. 오늘은 오후 6시쯤 되니까 머리가 아파서 더 못 앉아 있겠더군요. 아침에 오자마자 만들기 시작해서 조금 하다 보면 오후 1, 2시가 금방 되고, 그러면 밥 먹고 나서 또 조금 하다 보면 4, 5시가 되고, 또 조금 하다 보면 7, 8시가 되니까 하루가 정말 짧더군요. @_@

아직도 많이 손봐야 하지만, 이제 조금 윤곽이 잡혔습니다.
@RunWith(WebTestRunner.class)
@WarConfiguration(appName="springsprout")
@DataConfiguration(dataType=DataType.XML, location="integration/sample/testData.xml")
public class IndexPageWebTest {

    @WebTest
    public void sample(){
        WebDriver driver = new HtmlUnitDriver();
        driver.navigate().to("http://localhost:8080/springsprout/index.do");
        assertTrue(driver.getTitle().equals("SpringSprout"));
    }

}

WebDriver를 이용한 초간단 웹 테스트 코드입니다. 이렇게 설정한 다음 테스트를 실행하면..
1. 현재 프로젝트를 WAR 패키징을 합니다.
2. 생성한 WAR를 테스트용 톰캣 서버에 springsprout 컨텍스트 패스로 배포합니다.(maven/tomcat6x/webapp/springsprout)
3. 배포가 잘 됐으면, 테스트 데이터를 넣어줍니다.
4. 이제 테스트를 실행합니다.
5. 테스트 데이터를 삭제합니다.
6. 앞서 배포한 WAR 파일을 undeploy 합니다.

여기서 4번에 해당하는 코드만 작성하면 됩니다. 나머지는 애노테이션만 붙여주면 되죠. 테스트 케이스 마다 서버를 켰다 껐다 하는 건 좀 무리고, WAR만 그때 그때 배포하도록 했습니다.

@RunWith(WebTestRunner.class)
@WarConfiguration(appName="springsprout")
@DataConfiguration(dataType=DataType.XML, location="integration/sample/testData.xml")
public class IndexPageWebTest {

    @WebTest
    public void test1(){
   
    }
   
    @WebTest
    public void test2(){
       
    }

}

이렇게 테스트를 하나 더 추가하면? WAR 배포 이후에 두 개의 테스트를 모두 실행한 다음에 WAR를 unploy합니다.

약간의 변화를 줄 수 있습니다. 테스트 데이터 입력이 필요 없다면, @DataConfiguration을 생략해도 됩니다. 그리고 테스트 서버 포트를 기본으로 8080을 사용하는데, 그 값을 @WarConfiguration의 port 속성에 줄 수 있습니다. 그럼 해당 포트에 배포를 시도하죠. 물론 그럴 때 해당 포트에 서버가 실행되고 있지 않다면, debug 모드의 로깅과 적절한 이름의 RuntimeException이 발생합니다.

꾸준히 가꿔야겠지만, 이제 내일 부터는 이녀석을 이용해서 본격적으로 웹 통합 CRUD 테스트를 만들고, CI를 해보려고 합니다.

얘 때문에 조금이라도 뒤적여 본 것들은 다음과 같습니다.
- JUnit 4.6
- Spring Test
- Cargo
- DBUnit
- WebDriver
- Maven Surefire Plugin
- Appfuse
- Maven Embedder

지난주 금, 월, 화, 수, 목.. 거의 일주일이네요.
개발이 참 더디고 어설프네요. ㅎㅎ 갈 길이 멀었습니다. @_@
top


내부에서 new 로 객체를 생성하는 코드 테스트 하기

모하니?/Coding : 2009.05.13 23:53


어렵죠? 어떻게 해야될까요? 제가 오늘 맞딱드린 상황은 생성자 내부에서 필요한 필드를 세팅하는데 new 를 사용해서 객체를 생성하더군요. 그런데 그 필드들이 굉장히 중요한 역할을 해주고 있었습니다. 하지만 너무 복잡해서 그 녀석들 그대로 테스트를 하면 테스트 하는 범위도 너무 넓고 이미 다른 곳에서 여러번 테스트 해 본 것들이라 테스트에 오히려 방해가 되는 수준까지 이르렀습니다.

    public WebTestRunner(Class<?> klass) throws InitializationError {
        super(klass);
        WebTestConfiguration wtc = klass.getAnnotation(WebTestConfiguration.class);
        if(wtc == null)
            throw new IllegalStateException();

        dataManager = createDataManager((String)getValue(wtc, "testDataLocation"));
        warManager = createWarManager((String)getValue(wtc, "appName"));
    }

대충 이렀습니다. 저 두 녀석을 mockup 하고 싶은데, 저 녀석들을 생성하는데 필요한 또 다른 설정까지 있습니다. 어떻게 해야 될까요?

결과물은 자꾸 늦게 나오는 것 같고, 시간은 마구 달려가고, 정신은 점점 혼미해지고, 어깨는 누가 짓누르는 것 같은 고통 속에서 생각난건, 테스트 하는 대상의 스텁을 만들고 스텁으로 테스트하고 싶은 부분을 테스트하면서 원본에 테스트 결과를 반영하는 것이었습니다. 그리 아름다운 방법은 아닌 것 같지만, 유용하게 써먹었습니다.

이런 스텁 구현체를 만들었죠.

    public WebTestRunnerStub(Class<?> klass) throws InitializationError {
        super(klass);
        // WebTestConfiguration wtc =
        // klass.getAnnotation(WebTestConfiguration.class);
        // if(wtc == null)
        // throw new IllegalStateException();
        //
        // dataManager = createDataManager((String)getValue(wtc,
        // "testDataLocation"));
        // warManager = createWarManager((String)getValue(wtc, "appName"));
    }

src-springsprout-WebTestRunner (원본 테스트 대상)
test-springsprout-WebTestRunnerStub (원본 테스트 대상의 스텁 구현체)
                         -WebTestRunnerTest (WebTestRunnerStub에 대한 테스트)

이 상태에서 테스트 코드를 작성합니다,

    WebTestRunnerStub runner;
    WarManager mockWarManager;
    DataManager mockDataManager;

    @Before
    public void setUp() throws InitializationError{
        runner = new WebTestRunnerStub(this.getClass());
        mockWarManager = mock(WarManager.class);
        mockDataManager = mock(DataManager.class);
        runner.setWarManager(mockWarManager);
        runner.setDataManager(mockDataManager);
    }

이런식으로 mocking을 할 수 있습니다. 이 뒤에는 뭐~ 걍 하고 싶을대로 하면 되는거죠.

이렇게 해두면, 일단 네비게이션은 그다지 불편하지 않습니다.
- 테스트 코드에서 스텁으로의 이동은 Ctrl+클릭을 이용하면 되고,
- 테스트 코드에서 원본 대상으로의 이동은 Ctrl+J(MoreUnit 플러그인 설치 해야함)를 사용하면 됩니다.

테스트용 코드를 마구 심어놓을 수 있습니다.
- 스텁을 어떻게 구현해서 지지고 볶던 제 맘이고, 실제 코드에 아무런 영향도 주지 않습니다.
- 따라서 저처럼 Sysout을 좋아하시는 분들은 맘대로 원한는 곳에 Sysout을 심어 두어도 관계 없습니다.

하지만 다소 큰 단점이 있습니다. 눈으로 동기화를 해줘야 합니다.
- 원본과 스텁이 똑같을 필요는 없지만, 테스트 하는 부분에 있어서 중요 로직은 동일해야겠죠.

젤 좋은 방법은 IoC가 가능한 구조로 바꾸는 건데 JUnit의 BlockJUnit4ClassRunner 클래스를 상속하여 만든 클래스라서...  IoC 구조로 만들어 사용하기가 좀 애매하네요..@_@

좋은 방법은 아닌거 같지만 나름 편리한 방법이오니, 테스트하기 불편한 클래스의 테스트가 급하신 분들은 이용해 보세요. ㅋㅋ
top


DBUnit + Cargo + Webdriver를 이용한 웹 테스트 삽질 중

모하니?/Coding : 2009.05.08 19:15


하려던 것은 간단합니다.

1. DBUnit으로 테스트 데이터를 넣고,
2. Cargo로 톰캣6를 돌리고
3. Webdriver로 HTML, IE, Firefox에서 CRUD+S(검색) 테스트를 하는 겁니다.

이게 되면 PageObject 패턴을 도입해서 테스트를 만들어 볼까 했는데, 아직 이 늪을 못 벗어나고 있습니다.

일단, Webdriver를 이용한 단독 테스트는 성공적이었습니다. 물론 아직 PageObject 패턴을 도입하진 않았었죠. 이 녀석이 해주는 일은 화면에 있는 정보를 쉽게 참조할 수 있게 도와주는 API를 제공해 주는 것입니다. 따라서 화면 테스트를 보다 쉽게 작성할 수 있곘죠. 그밖에 동일한 URI를 파이어폭스, 인터넷익스플로러, HTML, 사파리 드라이버를 이용하여 참조할 수 있어서 다기종 브라우저를 지원하는 자동화 테스트를 작성할 때 매우 유용할 것으로 보입니다.

Cargo는 서버를 조작할 수 있는 API를 제공하며, 여러 종류의 서버를 설정하고, WAR 파일을 배포하는 작업등을 할 수 있습니다. 자세히는 모릅니다. 매우 다양한 서버를 지원하며, 웹 테스트를 자동화 할 때 필수 도구로 보입니다. 또한 다양한 모드로 서버를 실행할 수가 있습니다.

DBUnit은 많이들 아실 것으로 생각하고 생략하겠습니다.

저는 먼저 Webdriver부터 실습해봤습니다. 홈페이지로 시작하기 같은 문서를 찾아서 살펴봤습니다. 그리고 바로 테스트를 작성해 봤습니다.

        WebDriver driver = new HtmlUnitDriver();
        driver.get("http://localhost:8080/springsprout/index.do");
        WebElement element = driver.findElement(By.linkText("Login"));
        assertNotNull(element);
        assertEquals("/login.do", element.getAttribute("href"));

음.. 잘 동작하네! API를 익혀야겠군...@_@ 이제 PageObject 패턴을 어떻게 도입해야 하나~

고민할 새도 없이 다음은 Cargo를 시작했습니다.

        Deployable war = new WAR("target/springsprout.war");
        LocalConfiguration configuration = new Tomcat6xStandaloneLocalConfiguration("target/springsprout");
        configuration.addDeployable(war);
        container = new Tomcat6xInstalledLocalContainer(configuration);
        container.setHome("c:/apps/apache-tomcat-6.0.18");
        container.start();
...
        container.stop();

이런식으로 서버를 동작 시킬 수 있었습니다. 저 ... 안에 위에 작성한 웹 드라이버를 넣어보니 잘 동작했습니다. 문제는 이렇게 서버를 매번 올리고 내리는 작업을 테스트케이스마다 하면 굉장히 테스트가 오래 걸리고 비효율적이라는 겁니다. 따라서 이 작업은 반드시 Cargo 메이븐 플러긴을 이용해서 모든 테스트를 실행하기 전에 테스트 서버를 올렸다가 내리도록 해야겠습니다. 아직 해보진 않았습니다 @_@

일단 또 다시 고민할 새 없이 바로 DBUnit 까지 적용해 봤습니다.

    private void insertXMLData() throws IOException, DataSetException, DatabaseUnitException, SQLException {
        InputStream sourceStream = new ClassPathResource("testData.xml", getClass()).getInputStream();
        IDataSet dataset = new FlatXmlDataSet(sourceStream);
        DatabaseOperation operation = DatabaseOperation.CLEAN_INSERT;
        operation.execute(new DatabaseConnection(DataSourceUtils.getConnection(dataSource)), dataset);
    }

이런 메서드를 이용해서 testData.xml에 만들어 둔 테스트 데이터를 DB에 넣고 Cargo로 서버를 돌리고 Webdriver로 화면에 출력된 데이터를 검증하면 되리라 생각했습니다.

    @Test
    public void listMember() throws Exception {
        insertXMLData();
        assertEquals(2, memberRepository.getMemberList().size());

        Deployable war = new WAR("target/springsprout.war");
        LocalConfiguration configuration = new Tomcat6xStandaloneLocalConfiguration("target/springsprout");
        configuration.addDeployable(war);
        container = new Tomcat6xInstalledLocalContainer(configuration);
        container.setHome("c:/apps/apache-tomcat-6.0.18");
        container.start();

        WebDriver driver = new HtmlUnitDriver();
        driver.get("http://localhost:8080/springsprout/member/list.do");

        // logging
        System.out.println(driver.getTitle());
       assertEquals(2, memberRepository.getMemberList().size());
        System.out.println(driver.getPageSource());

        WebElement element = driver.findElement(By.linkText("keesun@whiteship.me"));
        assertNotNull(element);
        element.getAttribute("href");
        container.stop();
    }

이런 식으로 말이죠. 하지만 결과는 참담했습니다. 분명히 빨간색 줄은 테스트가 assertion이 됩니다. Cargo로 서버도 잘 동작합니다. 하지만 WebDriver로 접근해 봤을 때 DB에 넣었던 데이터가 화면에 나와야 하는데 나오지 않습니다. sysout으로 HTML을 찍어 봤지만, 정말로 데이터가 없었습니다.

1. WAR로 배포한 애플리케이션과 테스트 코드가 다른 DB를 사용한다.

이런... 그렇치..mvn package로 묶었을 때 그 안에 들어가는 건 src에 있는 설정 파일이지 test가 아니니깐, 지금 test할 때 사용하는 DB랑은 다른 걸 쓸꺼 아냐 ㅠ.ㅠ 이런 바로.. 그럼 일단 src랑 test랑 같은 DB를 사용하게 설정해보자.(원래 이럼 안되는 건데..)

2. 같은 DB를 사용하지만 여전히 동일한 상황.

@_@ 뭐지.. 왜 이럴까? DBUnit이 데이터를 넣고 확인하는 DB와 서버가 참조하는 DB가 완전히 별개 인 것처럼 보이는데, 트랜잭션이 아예 달라서 그런건가. @_@ 어찌해야 되나.. 아 괴룹네요. 괴로워..
top


이클립스 WTP server.xml 설정이 적용되지 않을 때

모하니?/Coding : 2009.04.29 13:22


<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443" />

server.xml에 이런 설정이 들어있는데 이것을 인코딩 문제로

<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443" useBodyEncodingForURI="true"/>

이렇게 변경했습니다. 그런데 변경한 설정이 먹질 않더군요.

server를 우클릭하고 cleanup도 해봤습니다. 적용되지 않았습니다.

그래서 server를 더블클릭하고 HTTP/1,1 포트를 변경한 다음에 서버를 재가동 해봤더니 설정이 적용되서 원하는 결과를 얻을 수 있었습니다.

다시 포트를 원래대로 돌린다음에 실행해 봤더니 이번에도 적용이 됐습니다.

server.xml을 직접 편집할 경우 설정이 제대로 안 먹을 수 있으니, server를 더블클릭해서 어떤 설정을 변경한 다음 저장하고 다시 시도해보시기 바랍니다. 저처럼 될지도 몰라요~


top


문자열 연결 성능 비교

모하니?/Coding : 2009.04.27 18:06


예전에도 한 번 해본 것 같은데 잘 기억이 안나서 다시 해봤습니다.

package perfomance;

import org.junit.Before;
import org.junit.Test;
import org.springframework.util.StopWatch;

public class StringAppendTest {

    StopWatch stopWatch;

    @Before
    public void setUp(){
        stopWatch = new StopWatch();
        stopWatch.start();
    }

    @Test
    public void appendByPlus() throws Exception {
        int i = 0;
        while(i < 100000){
            runAppendByPlusExample();
            i++;
        }
        stopWatch.stop();
        System.out.println("by Plus: " + stopWatch.getLastTaskTimeMillis());
    }

    @Test
    public void appendByContinuousPlus() throws Exception {
        int i = 0;
        while(i < 100000){
            runAppendByContinuousPlusExample();
            i++;
        }
        stopWatch.stop();
        System.out.println("by Continuous Plus: " + stopWatch.getLastTaskTimeMillis());
    }

    @Test
    public void appendByBuilder() throws Exception {
        int i = 0;
        while(i < 100000){
            runAppendByBuilderExample();
            i++;
        }
        stopWatch.stop();
        System.out.println("by Builder: " + stopWatch.getLastTaskTimeMillis());
    }

    @Test
    public void appendByBuffer() throws Exception {
        int i = 0;
        while(i < 100000){
            runAppendByBufferExample();
            i++;
        }
        stopWatch.stop();
        System.out.println("by Buffer: " + stopWatch.getLastTaskTimeMillis());
    }

    private void runAppendByContinuousPlusExample() {
        String result = "a";
        result += "bc" + "de";
    }

    private void runAppendByBufferExample() {
        StringBuffer buffer = new StringBuffer("a");
        buffer.append("bc");
        buffer.append("de");

    }

    private void runAppendByBuilderExample() {
        StringBuilder builder = new StringBuilder("a");
        builder.append("bc");
        builder.append("de");
    }

    private void runAppendByPlusExample() {
        String result = "a";
        result += "bc";
        result += "de";
    }


}

결과는 연속 + 연산과 Builder를 사용한 것이 제일 빠릅니다. 연속해서 +를 사용하면 JVM이 내부적으로 StringBuilder를 사용해서 연결해준다고 합니다. 이 테스트 결과를 확인해도 비슷하다는 것을 알 수 있습니다. 결과는 매번 다르지만, 평균적으로 다음과 같이 나옵니다.

위에서 사용한 StopWathch는 스프링 라이브러리에 들어있습니다.
3.0 기준으로 core 번들에 들어있습니다.

by Plus: 46
by Continuous Plus: 16
by Builder: 16
by Buffer: 31

top


토비님의 S3Matrix 소스 보기 1. pom.xml

모하니?/Coding : 2009.04.24 10:55


하루(실제로는 몇 시간 만에 작성했으로 예상되지만)만에 TDD로 개발한 프로젝트 소스를 볼 수 있다니 정말 즐겁습니다.

이 프로젝트는 취지부터가 멋집니다. 사부님이 자신이 하던 일이 반복적이여서 그걸 프로그래밍으로 간편화한 것이 바로 이 프로젝트 입니다. pom.xml 파일을 뒤져가며 엑셀로 그 의존성들을 정리하던 것을 자동화 한 거죠. 멋집니다. +_+ 그러면서도 다른 사람들에게 유용한 정보를 줄 수 있으니... 그저 멋지다는 표현밖엔 안 떠오르네요. 역시 사부님이라고 부를만 합니다.

생각난김에 이쯤에서 왜 자꾸 토비님을 사부님이라고 하는지 언급해야겠네요. 토비님은 제가 '토비님'이라고 부르는걸 달갑지 않게 생각하십니다. 채팅에서도 몇 번이나 지적을 받았고, 실제 업무 중에도 오프라인에서 '토비님'이라고 불렀다가 '이건 아니다.' 싶었습니다. 그래서 회사에서 직책 명칭인 '이사님'이라고 불렀지만 업무 중이 아닐 때에도 자꾸 '이사님'이라고 부르기는 뭐해서 마땅한 표현이 없을까 고민하던 중 떠오른 단어가 '사부님'입니다. 원래는 형이라고 부르라고 했었는데, '형'이라고 하기엔 나이차도 좀 있으시고 '형'같지 않은 포스가 느껴져서 '형님'이라고 하니까 무슨 안 그래도 덩치도 좀 있으신데 '형님' '형님'하면 안 좋은 이미지가 떠오르자나요. 그래서 이것 저것 많이 알려주시기도 하고, 업무 할 때 사수 역할도 해주시니까 '사부님'이라고 부르기로 했습니다.

사설이 엄청 길었는데요. 각설하고, 본론으로 들어가겠습니다. 일단 사부님 코드를 받으시면 에러가 납니다. 의존성 에러죠. 해결하기가 힘들겁니다. 왜냐면, 상위 pom을 사용했는데 그 상위 pom이 여러분 로컬에 있을리 만무하자나요.ㅋㅋ 하지만 그렇다고 해서 pom.xml에 상위 pom이 배포되어 있는 저장소 정보가 있는 것도 아닙니다. 결국 빨간 불이 들어온 소스 코드를 보시면서 테스트도 돌려보지 못하고 눈팅만 해야 할지도 모릅니다.

캬캬캬. 그래서 제가 pom.xml을 조금 수정했습니다. 이걸 쓰시면 테스트를 돌려보실 수 있을 겁니다.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.opensprout</groupId>
    <artifactId>s3matrix</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <dependencies>
        <!-- opensprout's spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.web.servlet</artifactId>
            <version>[3.0.0.BUILD-00000000000000,9.9.9.BUILD]</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.aspects</artifactId>
            <version>[3.0.0.BUILD-00000000000000,9.9.9.BUILD]</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.orm</artifactId>
            <version>[3.0.0.BUILD-00000000000000,9.9.9.BUILD]</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.test</artifactId>
            <version>[3.0.0.BUILD-00000000000000,9.9.9.BUILD]</version>
        </dependency>
        <!-- excel -->
        <dependency>
            <groupId>net.sourceforge.jexcelapi</groupId>
            <artifactId>com.springsource.jxl</artifactId>
            <version>2.6.6</version>
        </dependency>
        <!-- test -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.6</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-library</artifactId>
            <version>1.1</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>spring-latest</id>
            <name>Spring Latest by OpenSprout</name>
            <url>http://www.opensprout.org/nexus/content/repositories/spring-latest</url>
        </repository>
        <repository>
            <id>com.springsource.repository.bundles.external</id>
            <name>SpringSource Enterprise Bundle Repository - External Bundle Releases</name>
            <url>http://repository.springsource.com/maven/bundles/external</url>
        </repository>
        <repository>
            <id>opensprout nexus</id>
            <name>OpenSprout Nexus public</name>
            <url>http://www.opensprout.org/nexus/content/groups/public</url>
        </repository>
    </repositories>
</project>


top


초간단 CURD, 검색, 페이징, 정렬 구현 완료

모하니?/Coding : 2009.04.17 11:58


Opensprout Nexus에서 제공하는 스프링 최신 버전(3.0 기반)을 사용했고 스프링 번들 저장소에 있는 하이버네이트 라이브러리를 사용했습니다. 그밖에는 뭐 서블릿 API, JSP, JSTL, taglib 등을 사용했고 로깅은 slf4j-log4j를 사용했습니다. DB는 별도의 설치가 필요없게 derby를 사용했고 c3po 풀링 라이브러리를 사용했습니다.

빌드는 메이븐으로 했고 War 배포 방식은 inplace로 했으며 라이브러리를 가져올 저장소는 모두 pom.xml에 등록해 두었습니다.(war 플러긴을 못 가져오는 버그가 발생하고 있다는데 조만간 해결되리라 생각합니다.)

도메인은 Member 하나이고, 하이버네이트 애노테이션이 추가되서 POJO라고 보기는 좀 힘들겠습니다. 하이버네이트 설정을 XML로 바꿀까 생각중입니다. 아니면 스프링처럼 JavaConfig 같은 스타일을 사용할 수는 없을지 궁금하네요. 지원해준다면 기꺼이 그렇게 바꾸고 싶습니다. 도메인 클래스는 POJO로 유지하고 하이버네이트 설정은 밖으로 분리할 수 있으니까요. 대신 그 설정 파일이 자바 파일이라면 이름 변경과 같은 리팩터링에도 유기적으로 반응할테니 JavaConfig와 유사한 장점을 지닐 수 있지 않을까 싶습니다.

Controller는 스프링 3.0의 URI 템플릿 기능을 사용해봤습니다. 재밌더군요. list와 add를 제외한 view, updat, delete에 적용해봤습니다. 기본 CRUD만 구현했을 때에는 컨트롤러가 썰렁했는데, 검색, 페이징, 정렬 기능이 추가될 때 마다 코드가 조금씩 늘어나기 시작해서 지금은 이런 모습이 되었습니다.

package whiteship.member;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

import whiteship.domain.Member;
import whiteship.member.support.OrderParam;
import whiteship.member.support.SearchParam;
import whiteship.paging.PageParam;

@Controller
@SessionAttributes("member")
public class MemberController {

    @Autowired
    MemberService service;

    @Autowired
    MemberValidator validator;

    // Create
    @RequestMapping(value = "/member/add", method = RequestMethod.GET)
    public String addForm(Model model) {
        Member member = new Member();
        model.addAttribute(member);
        return "member/add";
    }

    @RequestMapping(value = "/member/add", method = RequestMethod.POST)
    public String addForm(Member member, BindingResult result,
            SessionStatus status) {
        validator.validate(member, result);
        if (result.hasErrors()) {
            return "member/add";
        } else {
            service.add(member);
            status.isComplete();
            return "redirect:/member/list.do";
        }
    }

    // Read
    @RequestMapping("/member/list")
    public ModelMap list(PageParam pageParam, SearchParam searchParam,
            OrderParam orderParam) {
        ModelMap modelMap = new ModelMap();
        modelMap.addAttribute(service
                .getMemberListByPageAndSearchAndOrderParam(pageParam,
                        searchParam, orderParam));
        modelMap.addAttribute(pageParam);
        modelMap.addAttribute(searchParam);
        modelMap.addAttribute(orderParam);
        return modelMap;
    }

    @RequestMapping("/member/{id}")
    public String view(@PathVariable int id, Model model, PageParam pageParam,
            SearchParam searchParam, OrderParam orderParam) {
        model.addAttribute(service.getMemberById(id));
        model.addAttribute(pageParam);
        model.addAttribute(searchParam);
        model.addAttribute(orderParam);
        return "member/view";
    }

    // Update
    @RequestMapping(value = "/member/update/{id}", method = RequestMethod.GET)
    public String updateForm(@PathVariable int id, Model model,
            PageParam pageParam, SearchParam searchParam, OrderParam orderParam) {
        model.addAttribute(service.getMemberById(id));
        model.addAttribute(pageParam);
        model.addAttribute(searchParam);
        model.addAttribute(orderParam);
        return "member/update";
    }

    @RequestMapping(value = "/member/update/{id}", method = RequestMethod.POST)
    public String updateForm(PageParam pageParam, SearchParam searchParam,
            OrderParam orderParam, Member member, BindingResult result,
            SessionStatus status) {
        validator.validate(member, result);
        if (result.hasErrors()) {
            return "member/update";
        } else {
            service.update(member);
            status.isComplete();
            return redirectURLWithPageAndSearchAndOrderParam(pageParam,
                    searchParam, orderParam);
        }
    }

    // Delete
    @RequestMapping("/member/delete/{id}")
    public String delete(@PathVariable int id, PageParam pageParam,
            SearchParam searchParam, OrderParam orderParam) {
        service.deleteById(id);
        return redirectURLWithPageAndSearchAndOrderParam(pageParam, searchParam, orderParam);
    }

    private String redirectURLWithPageAndSearchAndOrderParam(
            PageParam pageParam, SearchParam searchParam, OrderParam orderParam) {
        return redirectURLWithPageAndSearchParam(pageParam, searchParam)
                + "&field=" + orderParam.getField() + "&direction="
                + orderParam.getDirection();
    }

    private String redirectURLWithPageAndSearchParam(PageParam pageParam,
            SearchParam searchParam) {
        return pagedListURL(pageParam) + "&name=" + searchParam.getName()
                + "&email=" + searchParam.getEmail();
    }

    private String pagedListURL(PageParam pageParam) {
        return "redirect:/member/list.do?size=" + pageParam.getSize()
        + "&page=" + pageParam.getPage();
    }

}

List에서 페이징, 검색, 정렬 정보를 가지고 View로 간 다음 Update와 Delete 또는 뒤로가기(List) 기능에 그 정보들을 사용하거나 넘겨 줍니다. Update와 Delete에서는 해당 정보들을 이요하고, 처리를 끝 낸다음 페이징, 검색, 정렬 정보를 유지한채 List로 다시 넘어갑니다. 따라서 삭제 하기 전에 했던 페이징, 검색, 정렬 정보를 그대로 유지한 화면으로 돌아가게 됩니다.

이게 핵심이었습니다. 이 것 때문에 복잡해진 겁니다. 매개변수를 계속 물고 다녀야 하고, URL은 계속 복잡해지고, 페이징, 검색, 정렬 기능이 추가될 때마다 혹은 검색 변수가 추가될 때 마다 URL이 바뀌게 되는데 URL이 사방에 있기 때문에 수정 작업을 할 곳이 한 두곳이 아닙니다.

1. 일단 컨트롤러에서 URL 만드는 부분을 수정해야 합니다.
2. list.jsp
3. view.jsp
4. update.jsp

총 네 개의 파일을 수정해야 하고, 특히 페이징이 들어있는 list.jsp 파일의 경우 10~12줄 가량의 코드를 손봐야 합니다.


<%@ page language="java" contentType="text/html; charset=EUC-KR" pageEncoding="EUC-KR"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>

<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
<title>Whiteboard2</title>
</head>

<body>
<div>
<a href="/member/add.do">회원 추가</a>
</div>

<div>
<c:if test="${empty memberList}">
회원 목록이 없습니다.
</c:if>

<c:if test="${! empty memberList}">
<form:form method="GET" commandName="searchParam">
    이름: <form:input path="name" />
    이메일: <form:input path="email" />
    <input type="submit" value="검색" />
</form:form>


페이지 사이즈: ${pageParam.size}<br/>
현재 페이지: ${pageParam.page}<br/>
총 갯수: ${pageParam.totalRowsCount}<br/>
현재 페이지 첫 번째 목록 인덱스: ${pageParam.firstRowNumber}<br/>

<c:if test="${pageParam.totalRowsCount > pageParam.size}">

    <a href="/member/list.do?page=1&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}">처음</a> |

    <c:if test="${pageParam.beginPage - 10 > 0}">
        <a href="/member/list.do?page=1&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}">이전</a> |
    </c:if>

    <c:forEach begin="${pageParam.beginPage}" end="${pageParam.endPage}" varStatus="current">
        <c:choose>
            <c:when test="${current.count == pageParam.page}">
                <a href="/member/list.do?page=${current.count}&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}"><strong>${current.count}</strong></a> |
            </c:when>
            <c:otherwise>
                <a href="/member/list.do?page=${current.count}&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}">${current.count}</a> |
            </c:otherwise>
        </c:choose>
    </c:forEach>

    <c:if test="${pageParam.beginPage + 10 < pageParam.totalPage}">
        <a href="/member/list.do?page=${current.count + 10}&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}">다음</a> |
    </c:if>

    <a href="/member/list.do?page=${pageParam.totalPage}&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}">마지막</a>

</c:if>

<table>
    <tr>
        <th>
            <c:choose>
                <c:when test="${orderParam.field == 'email' && orderParam.direction == 'asc'}">
                    <a href="/member/list.do?page=1&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=email&direction=desc">이메일V</a>
                </c:when>
                <c:otherwise>
                    <a href="/member/list.do?page=1&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=email&direction=asc">이메일^</a>
                </c:otherwise>
            </c:choose>
        </th>
        <th>
            <c:choose>
                <c:when test="${orderParam.field == 'name' && orderParam.direction == 'asc'}">
                    <a href="/member/list.do?page=1&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=name&direction=desc">이름V</a>
                </c:when>
                <c:otherwise>
                    <a href="/member/list.do?page=1&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=name&direction=asc">이름^</a>
                </c:otherwise>
            </c:choose>
        </th>
    </tr>
<c:forEach var="member" items="${memberList}">
    <tr>
        <td><a href="/member/${member.id}.do?size=${pageParam.size}&page=${pageParam.page}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}">${member.email}</a></td>
        <td>${member.name}</td>
    </tr>
</c:forEach>
</table>
</c:if>
</div>
</body>

</html>

이게 현재 list.jsp 파일입니다. 이클립스에서 보면 96줄에 해당합니다.

자 이제는 이것을 어떻게 하면 깔끔하고 편리하게 다듬을지 고민할 시간입니다. 좋은 의견 있으신 분들은 댓글 주시면 감사하겠습니다~ ^^

ps: 티스토리에서 아직도 코드 하이라이팅 플러긴 같은거 안 나왔죠?? 티스토리 서비스 한지가 몇 년 짼데 아직도 안 나와~~~ ㅠ.ㅠ


top


JUnit에서 setUp() 또는 @Before를 사용하는 이유?

모하니?/Coding : 2009.03.27 11:50


참조: http://stackoverflow.com/questions/512184/best-practice-initialize-junit-class-fields-in-setup-or-at-declaration

JUnit은 각각의 테스트를 독립적으로 테스트하기 위해 테스트 마다 개별적인 객체를 생성합니다. 따라서..

public class SomeTest extends TestCase
{
   private final List list = new ArrayList();

    public void testPopulateList()
   {
       // Add stuff to the list
       // Assert the list contains what I expect
   }
}

이런식으로 테스트를 작성해도

public class SomeTest extends TestCase
{
   private List list;

    @Override
   protected void setUp() throws Exception
   {
       super.setUp();
       this.list = new ArrayList();
   }

    public void testPopulateList()
   {
       // Add stuff to the list
       // Assert the list contains what I expect
   }
}

이렇게 작성한 것과 동일하게 각각의 테스트를 실행하기 전에 초기화 하는 코드를 실행합니다.

그럼 대체 setUp이나 @Before는 왜 사용할까요? 그냥 생성자를 사용하거나 저렇게 필드에 직접 객체를 생성하게 해도 비슷한데 말이죠.

윗 글을 찾아보기 전에는 '그냥 tearDown이랑 균형을 맞출려고 만들었나?', '생성자를 쓰면 안 좋은 뭔가가 있나?' 등등 막연하게 생각하고 있었습니다. 그러다가 도무지 궁금해서 구글신에게 문의했더니 윗 글을 찾을 수 있었습니다.

댓글들을 조사해 본 결과.

1. Exception 발생: setUp()에서 예외를 던지면 JUnit이 유용한 스택 트레이스 정보를 돌려주지만, 생성자에서 예외가 발생하면 그냥 테스트 객체를 못 만드는 것이기 때문에 유용한 정보를 얻을 수 없음.

2. 베스트 프랙티스: 테스트 하려는 클래스의 인스턴스는 테스트 또는 setUp()에서 생성하고, 테스트 대상이 아닌 속성들은 필드에서 직접 new로 생성해도 상관없다.

이 두 가지가 유력해 보이네요.
top


스프링, 하이버 게시판 Whiteboard 1차 완성

모하니?/Coding : 2008.12.28 13:42


사용자 삽입 이미지
검색하고 페이징까지 구현 완료 했습니다. 이번 봄싹 스터디의 목표가 딱 여기까지 였죠.

게시판 구현 2차에서 할 일
- 프레임 없애고 한 프레임으로..
- 화면 UI 개선
- 댓글
- 정렬
- ...
top




: 1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : ··· : 10 :