Whiteship's Note

'전체'에 해당되는 글 2638건

  1. 2010.07.26 [Spring BlazaDS Integration 레퍼런스] 2장 스프링에서 BlazeDS MessageBroker 설정 및 사용 (2)
  2. 2010.07.23 리눅스 시간 맞추기
  3. 2010.07.23 [하이버네이트 배치 추가] flush와 clear (2)
  4. 2010.07.20 법치와 민주주의... (2)
  5. 2010.07.20 주말 강의는 2주 연기
  6. 2010.07.20 [Adobe] BlazeDS는 뭔가? (2)
  7. 2010.07.19 [스프링 3 이해와 선택] 2차 강의 진행합니다. (4)
  8. 2010.07.16 [토비의 스프링 3] 2차 완독 후기 (8)
  9. 2010.07.13 [스프링 3.0] 상속구조에서 @RequestMapping 퀴즈
  10. 2010.07.13 [스프링 3.0] 클래스-메서드 레벨 @RequestMapping 퀴즈
  11. 2010.07.12 네가 있어서 정말 감사하다. (4)
  12. 2010.07.07 버즈 버그 미투 장애
  13. 2010.07.07 [이전글에 이어지는 이야기] 숫자에서 객체로...
  14. 2010.07.07 복잡한 로직은 복잡한 DAO로 직결되는가? (6)
  15. 2010.07.06 [IBM DW] HTML5 navigator
  16. 2010.07.02 스프링 DAO 3파전 (6)
  17. 2010.06.29 [Spring Security] http 네임스페이스 쓰려면 필터 이름은 항상 고정(?)
  18. 2010.06.25 전자정부 프레임워크 공통 컴포넌트 실행 성공(?) (2)
  19. 2010.06.23 OSAF 2.0 준비중 (4)
  20. 2010.06.23 [스프링 3.0 이해와 선택] 2차 교육 공지 (2)
  21. 2010.06.23 [회사일] GenericExcelView 만들기
  22. 2010.06.18 [회사일] JPA로 계층구조 매핑하기
  23. 2010.06.17 [하이버네이트 완벽 가이드] 드뎌.. 하이버 번역서가 나왔습니다. (16)
  24. 2010.06.15 테스트 주도 개발 : 고품질 쾌속개발을 위한 TDD 실천법과 도구 (2)
  25. 2010.06.15 [회사일] 다대다 관계 서브 그리드 CRUD 완성
  26. 2010.06.14 [회사일] 서브 그리드 뿌리기
  27. 2010.06.14 [회사일] Right 도메인 CRUD 구현 도메인 클래스부터 화면까지
  28. 2010.06.14 [회사일] DateRange 추가
  29. 2010.06.14 [회사일] view.jsp 태그파일 적용
  30. 2010.06.14 [회사일] new.jsp 태그파일 적용

[Spring BlazaDS Integration 레퍼런스] 2장 스프링에서 BlazeDS MessageBroker 설정 및 사용

Adobe : 2010.07.26 22:06


참조: http://static.springsource.org/spring-flex/docs/1.5.x/reference/html/index.html#introduction


2.1. 도입

스프링 BlazeDS Integration을 사용할 때 반드시 설정해야 하는 핵심 요소는 MessageBroker다. 플렉스 클라이언트에서 발생한 HTTP 메세지는 스프링 DispatcherServlet 을 통해 스프링이 관리하는 MessageBroker로 전달된다. 스프링이 관리하는  MessageBroker를 사용할 때는 BlazeDS MessageBrokerServlet 을 설정할 필요 없다.


2.2. 스프링 DispatcherServlet 설정

스프링 WebApplicationContext의 시작점(bootstrap)으로 보통 web.xml에 DispatcherServlet 을 다음과 같이 설정한다.

<!-- The front controller of this Spring Web application, responsible for handling all application requests -->

<servlet>

    <servlet-name>Spring MVC Dispatcher Servlet</servlet-name>

    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <init-param>

        <param-name>contextConfigLocation</param-name>

        <param-value>/WEB-INF/config/web-application-config.xml</param-value>

    </init-param>

    <load-on-startup>1</load-on-startup>

</servlet>


2.3. 스프링에 MessageBroker 설정하기

WebApplicationContext에 MessageBroker를 설정할 때 편리한 스프링 XML 설정 네임스페이스가 제공된다. 그 네임스페이스를 사용하려면 스프링 XML 설정 파일에 스키마 위치를 추가해야 한다. 보통 다음 설정과 같을 것이다.


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

  xmlns:flex="http://www.springframework.org/schema/flex"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="

           http://www.springframework.org/schema/beans

           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd

           http://www.springframework.org/schema/flex 

           http://www.springframework.org/schema/flex/spring-flex-1.0.xsd">

...

</beans>

    

    


이렇게 하면 설정 파일에서 flex 네임스페이스로 스프링 BlazeDS Integration 설정 태그를 사용할 수 있다. 다음 예제부터는 위와같이 설정했다고 가정을 하겠다. 이 네임스페이스에서 사용할 수 있는 모든 태그과 애트리뷰트는 spring-flex-1.0.xsd를 참조하기 바란다. 이클립스 같은 XSD를 인식하는 XML 편집기에서는 우리가 타이핑하는 것에 따라 문서를 자동으로 읽어줄 것이다.


스프링 WebApplicationContext에 최소한 MessageBrokerFactoryBean 을 설정하여 MessageBroker를 가동시켜야 하며 MessageBrokerHandlerAdapter 와 적절한  HandlerMapping (보통 SimpleUrlHandlerMapping) 을 사용하여 요청을 스프링이 관리하는  MessageBroker에 전달되게 해야한다. 


빈 설정 파일에 message-broker 태그를 등록하면 그러한 빈들을 자동으로 등록해 준다. 다음 예제가 가장 간단한 형태다.


<flex:message-broker/>    

    


이렇게 하면 MessageBroker 를 설정하고 필요한 기반 요소를 감각적인 기본값(sensible defaults)으로 설정해 준다. 그렇게 사용하는 기본값들은 message-broker 태그의 애트리뷰트 또는 하위 엘리먼트를 사용해서 재정의 할 수 있다. 예를 들어 BlazeDS XML 설정 파일의 기본 위치(/WEB-INF/flex/services-config.xml)를 services-config-path 애트리뷰트로 재정의할 수 있다. MessageBrokerFactoryBean 은 스프링의 ResourceLoader 추상화를 사용하기 때문에 스프링 리소스 패스를 사용할 수 있다. 그 예로, 애플리케이션 클래스패스에서 설정을 읽어오도록 다음과 같이 설정할 수 있다.


<flex:message-broker services-config-path="classpath*:services-config.xml"    

    


이와 동일한 순수 스프링 설정을 사용한 MessageBrokerFactoryBean 정의는 다음과 같다. 



<!-- Bootstraps and exposes the BlazeDS MessageBroker -->

<bean id="_messageBroker" class="org.springframework.flex.core.MessageBrokerFactoryBean" >

<property name="servicesConfigPath" value="classpath*:services-config.xml" />

</bean>    

    


message-broker 태그에서 특히 주목할 점은 MessageBroker에 커스텀 id를 설정할 필요가 없다는 것이다. 나중에 참조할 일도 없기 때문에 사실 그렇게 하는 것을 권장하지 않는다. 커스텀 id를 설정해야 하는 상황은 오직 WebApplicationContext에 MessageBroker를 두개 이상 가동할 경우이다.


2.4. 요청을 MessageBroker쪽으로 매핑하기


들어오는 요청을 스프링이 관리하는 MessageBroker로 전달하려면 세곳에 요청 매핑을 설정해야한다. 

  • web.xml에 DispatcherServlet 매핑
  • 스프링 WebApplicationContext에 HandlerMapping
  • BlazeDS services-config.xml에 채널 정의

가장 간단한 요청 매핑 시나리오는 앞단에 위치한 플렉스가 애플리케이션의 유일한 클라이언트인 경우이다. 이런 경우 /messagebroker를 요청 최상위 패스로 매핑할 수 있다. 그런 경우 web.xml에 다음과 매핑할 것이다.


<!-- Map all /messagbroker requests to the DispatcherServlet for handling -->

<servlet-mapping>

    <servlet-name>Spring MVC Dispatcher Servlet</servlet-name>

    <url-pattern>/messagebroker/*</url-pattern>

</servlet-mapping>    


message-broker 설정 태그를 사용하면  SimpleUrlHandlerMapping 이 설정되어 DispatcherServlet 으로 전달되는 모든 요청을 /* 경로 패턴을 따라 스프링이 관리하는 MessageBroker 에 매핑한다. 자신이 직접 작성한 HandlerMapping 빈을 설정할 때는   message-broker 태그의 disable-default-mapping 애트리뷰트를 사용해서 기본 매핑 사용을 제어할 수 있다. 기본으로 설정되는 SimpleUrlHandlerMapping 의 순서는 mapping-order 애트리뷰트로 설정할 수 있다. (동일한 컨텍스트에 여러 핸들러 매핑 타입이 존재하는 복잡한 경우에 사용할 수 있겠다.)


스프링 WebApplicationContext의 SimpleUrlHandlerMapping 은 모든 요청을 MessageBrokerHandlerAdapter를 통해 스프링이 관리하는 MessageBroker로 전달한다. 기본으로 설정되는 message-broker 태그는 다음과 같은 빈 정의와 동일하다.


<!-- Maps request paths at /* to the BlazeDS MessageBroker -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
        <value>
            /*=_messageBroker
        </value>
    </property>
</bean>

<!-- Dispatches requests mapped to a MessageBroker -->
<bean class="org.springframework.flex.servlet.MessageBrokerHandlerAdapter"/>    
		


BlazeDS services-config.xml의 채널 정의가 반드시 선택한 매핑에 대응해야 한다. 예를 들어, 위 매핑 전략에 대응하는 AMF 채널을 BlazeDS에 다음과 같이 설정할 수 있다. 

 

<channel-definition id="my-amf" class="mx.messaging.channels.AMFChannel">
    <endpoint url="http://{server.name}:{server.port}/{context.root}/messagebroker/amf" 
    	class="flex.messaging.endpoints.AMFEndpoint"/>
    <properties>
        <polling-enabled>false</polling-enabled>
    </properties>
</channel-definition> 		
		

services-config.xml에 커뮤니테이션 채널을 설정하는 더 자세한 방법은  BlazeDS documentation 에서 참조하기 바란다.


2.5. 스프링 MVC 컨트롤러와 플렉스 클라이언트 같이 사용하기


플렉스 기반 클라이언트 뿐 아니라 더 다양한 클라이언트를 지원하는 애플리케이션이 더 흔할 것이다. 예를 들어 RESTful 아키텍처를 구성하여 여러 종류의 클라이언트를 지원할 수 있다. 잠재적으로 플렉스 HTTPService 컴포넌트를 사용하여 RESTful 종점(endpoint)를 구독할 수도 있다. 스프링 MVC의 컨트롤러 모델은 RESTful 종점 같은 것을 만들기 단순하며, 유연한 방법들을 제공한다. 이러한 하이브리드 웹 애플리케이션 시나리오에서는 다른 방식의 매핑 전략이 필요하다.


가장 간단한 방법은 여러 DispatcherServlet과 계층형 애플리케이션 컨텍스트를 사용하는 것이다. 이 방법에서는 주요 애플리케이션 계층(서비스, 보안, 기반시설 지원, 등)을 ContextLoaderListener가 로딩하는 상위 컨텍스트에 두고, 스프링 MVC 컨트롤러들을 그 하위 DispatcherServlet 컨텍스트에 두고, 플렉스 클라이언트와 관련된 모든 것들을 별도의 DispatcherServlet 컨텍스트에 두는 것이다. 이 방법을 적용한 web.xml은 다음과 같다.


<context-param>

    <param-name>contextConfigLocation</param-name>

    <param-value>

        /WEB-INF/spring/*-context.xml

    </param-value>

</context-param>


<listener>

    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>

</listener>


<servlet>

    <servlet-name>flex</servlet-name>

    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <load-on-startup>1</load-on-startup>

</servlet>


<servlet-mapping>

    <servlet-name>flex</servlet-name>

    <url-pattern>/messagebroker/*</url-pattern>

</servlet-mapping>


<servlet>

    <servlet-name>spring-mvc</servlet-name>

    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <load-on-startup>1</load-on-startup>

</servlet>


<servlet-mapping>

    <servlet-name>spring-mvc</servlet-name>

    <url-pattern>/spring/*</url-pattern>

</servlet-mapping> 

 /WEB-INF/spring/ 디렉토리에  -context.xml 로 끝나는 파일 그룹을 묶어서 부모 애플리케이션 컨텍스트를 만든다. 플렉스 관련 하위 컨텍스트는  /WEB-INF/flex-servlet.xml을 사용하고 스프링 MVC 컨트롤러는 WEB-INF/spring-mvc-servlet.xml안에 설정할 것이다. 이 방법은 깔끔하게 관심사를 분리해주며 스프링 2.5+ 애노테이션 컨트롤러가 기본으로 동작하게 해준다.


이 대안이 될 수 있는 방법으로는 단일 DispatcherServlet 컨텍스트를 사용하는 것이다. 이 방법의 단점은 몇 가지 부가적인 설정을 필요로 한다. /spring/* 요처을 DispatcherSerlvet이 처리하고 mapping XML 네임스페이스 설정 태그를 사용해서 /messagebroker/*를 스프링이 관리하는 MessageBroker로 전달하는 것이다. 그런다음 BlazeDS 채널 정의를 적절하게 수정해야 한다. message-broker 태그로 기본 매핑 전략을 다음과 같이 수정할 수 있다.


<flex:message-broker>

    <flex:mapping pattern="/messagebroker/*" />

</flex:message-broker> 


그런다음 BlazeDS의 채널 정의에서 /spring/*을 고려해서 다음과 같이 수정해야 한다. 


<channel-definition id="my-amf" class="mx.messaging.channels.AMFChannel">

    <endpoint url="http://{server.name}:{server.port}/{context.root}/spring/messagebroker/amf" 

    class="flex.messaging.endpoints.AMFEndpoint"/>

    <properties>

        <polling-enabled>false</polling-enabled>

    </properties>

</channel-definition> 


단일 매핑 전략에서는 message-broker 태그가 자동으로 등록해주는 HandlerMapping과 HandlerAdapter가 있기 때문에 스프링 MVC 문서에 따라 스프링 MVC 컨트롤러를 위한 HandlerMapping와 HandlerAdapter을 직접 등록해 줘야 한다.




top


리눅스 시간 맞추기

Linux : 2010.07.23 16:40


맥이나 윈도우는 얼마나 편해 ㅠ.ㅠ 
시간 한번 맞추기가 이렇게 힘들어서야 원...


커맨드만 복사해 놔야지.. 링크가 고장날지도 모르니...

    [root@Zeus ~]# date 
    Fri Aug  3 08:05:03 UTC 2007 
    [root@Zeus ~]# hwclock --show 
    Fri 03 Aug 2007 05:39:07 PM UTC  -0.870183 seconds 
    [root@Zeus ~]# echo $TZ 

    [root@Zeus ~]# export env TZ=KST-09:00:00 
    [root@Zeus ~]# date 
    Fri Aug  3 17:05:45 KST 2007 

    [root@Zeus ~]# date 
    Fri Aug  3 18:05:21 KST 2007 
    [root@Zeus ~]# hwclock --show 
    Fri 03 Aug 2007 06:39:45 PM KST  -0.540050 seconds 
    [root@Zeus ~]# rdate -s time.bora.net 
    [root@Zeus ~]# hwclock --systohc 
    [root@Zeus ~]# date 
    Fri Aug  3 18:06:56 KST 2007 
    [root@Zeus ~]# hwclock --show 
    Fri 03 Aug 2007 06:07:01 PM KST  -0.211481 seconds 

    export env TZ=KST-09:00:00 
    rdate -s time.bora.net 
    hwclock --systohc 


top

TAG 리눅스

[하이버네이트 배치 추가] flush와 clear

Hibernate/etc : 2010.07.23 14:18


배치 작업이라는 것이 DB에서 데이터를 읽어온 다음 뭔가 수정하고 뭔가를 다시 DB에 넣는 작업인데 이런 작업을 하이버네이트로 할 때 조심해야 할 것이 있습니다.

                InvDailyClosing invDailyClosing = new InvDailyClosing();
                invDailyClosing.setDate(today);
                invDailyClosing.setLocation(location);
                invDailyClosing.setItem(item);
                invDailyClosing.setQtyStart(qtyStart);
                invDailyClosing.setInvInList(invInList);
                invDailyClosing.setInvOutList(invOutList);
                invDailyClosing.closing();
                dao.add(invDailyClosing);

이렇게 작성하면 끝난것 같지만 사실 좀 위험한 코드입니다. 만약 저 코드가 for 루프 안에 있고 굉장히 여러번 반복 된다면 언젠가는 MemoryOutOfException을 보게 될 겁니다. "아니 왜?" 라고 하시는 분들이 계실텐데요. 

흠... 하이버네이트 1차 캐시 때문에 그런 현상이 생깁니다. 하이버네이트는 Persistent 상태의 객체를 하이버네이트 Session에 담아둡니다. 여기사 1차 캐시입니다. 캐시니까 저안에 담아놓고 재사용할 수 있습니다. DB에 다녀오는 횟수를 줄일 수 있겠죠. 그런데 언젠가는 이 캐시를 DB와 동기화 시켜야 합니다. 그래야 Session에 담겨있는 객체(Pesistent 객체)를 지지고 볶은 것이 DB에 반영이 되겠죠. 그렇게 DB와 Session 상태를 동기화 시키는것을 Flush라고 하는데.. 또 Persistent 상태라는 것은... 아.. 이런.. 안되겠군요. @_@ 그냥 하이버네이트 완벽 가이드를 읽어주세요.

아무튼 너무 많이 쌓여서 메모리가 부족해지는 상황이 발생하지 않도록 계속해서 Session을 DB에 동기화 시키고 Session을 비워주는게 좋습니다.

하이버네이트 Session의 flush()와 clear()이 바로 그런 용도로 존재하죠. 그래서 

                InvDailyClosing invDailyClosing = new InvDailyClosing();
                invDailyClosing.setDate(today);
                invDailyClosing.setLocation(location);
                invDailyClosing.setItem(item);
                invDailyClosing.setQtyStart(qtyStart);
                invDailyClosing.setInvInList(invInList);
                invDailyClosing.setInvOutList(invOutList);
                invDailyClosing.closing();
                dao.add(invDailyClosing);
             dao.flushAndClear();

이렇게 하이버네이트용 GenrericDao에 flushAndClear()라는걸 만들어 놓고 써주면 됩니다. 주의하실 것은... 반드시 flush 먼저 하고 나서 clear 해야 합니다. 반대로 하면 음식 담긴 쟁반을 서빙도 안하고 설것이 해버리는거나 마찬가지..

top


법치와 민주주의...





배경음악이 좋아서 긁어온 것이지 별다른 의도는 없습니다.
top


주말 강의는 2주 연기

모하니?/Thinking : 2010.07.20 21:05


휴가철이라 그런지 수강생이 10명이 채 안되서 주말 강의를 부득이 하게 2주 연기하게 되었습니다. 스프링 강의 뿐 아니라 아이폰까지도 그런걸 보면 제 잘못은 아닙니다. ㅋㅋ 전 정말 열심히 준비 했고 열심히 강의 했습니다. 물론 부족한 점도 많았지만 앞으로 계속 조금씩 보완해 나가겠습니다. 2주가 연기되서 오히려 좋은 점도 있습니다. 잘하면 토비님 책을 들고 바로 수업을 시작할 수 있을 것 같습니다. 

거럼 다음 강의 때 뵐께요~

top


[Adobe] BlazeDS는 뭔가?

Adobe : 2010.07.20 20:55


http://opensource.adobe.com/wiki/display/blazeds/BlazeDS/

스프링 BlazeDS라는 프로젝트가 있다. 근데 BlazeDS가 뭔지 모른다. 그래서 살펴봤다.

http://opensource.adobe.com/wiki/display/blazeds/Overview

어도비 플랙스나 에어로 클라이어언트 단을 만들고 자바로 만든 서버 단과 연동하는데 필요한 서비스를 제공해준다. 거기다.. XML이나 SOAP 같은 텍스트 기반 서비스 보다 10배나 빠르단다.. 흠.... Push 기능도 구현할 수 있게 해준다는데.. 조금. 구미가 땡기기 시작한다.

주요 서비스는 다음과 같다.

- 리모팅: 서버단의 자바로 만든 서비스를 바로 호출할 수 있게 해준다.
- 메세지 서비스: 배포/구독 방식 메시징으로 실시간으로 데이터를 주고 받는 애플리케이션을 개발할 수 있다.
- 프록시 서비스: 뭔지 감이 잘 안옴.


top

TAG Abode, BlazeDS

[스프링 3 이해와 선택] 2차 강의 진행합니다.

모하니?/Planning : 2010.07.19 15:52


이번주 토요일부터 시작입니다.

http://www.hanbitedu.co.kr/incumbent/shortContent.do?index=1097

강의 준비하느라 홍보를 게을리 했더니 수강생이 몇 분 안되는 듯 합니다. ㅠ.ㅠ
아니면 휴가철이라 그럴지도...

강의는 토비의 스프링 3을 교재로 진행하고, 대략 일정은 다음과 같습니다.

1주차
- 오브젝트와 의존관계
- 테스트
- 템플릿

2주차
- 예외
- 서비스 추상화
- AOP

3주차
- IoC 컨테이너와 DI
- 데이터 엑세스 기술

4주차
- 스프링 웹 기술과 스프링 MVC
- 스프링 @MVC
- 스프링 기타기술

소스코드, 코딩 동영상, PPT 까지 준비 된 상태이고 앞으로는 게임이나 퀴즈 같은 게임 요소를 추가할 생각입니다. 무더운 날씨에도 즐겁게 공부해 봅시다.
top


[토비의 스프링 3] 2차 완독 후기



결국 추천사를 쓸 수 있게 됐습니다. 캬캬캬.
배려해주신 출판사 부사장님과 토비님께 감사드립니다. 잘 쓸께요. 아.. 부담 100배!!!!



----------------지난 이야기-----------------

사실 완독이라고 할 수는 없다. 이번엔 강의 준비를 하면서 다시 봤는데 강의할 부분만 읽었기 때문에 모든 글을 다시 읽진 않았다. 읽으면서 중간 중간 코드와 API 설명이 나온 부분을 유심히 보면서 혹시 소스 코드에 잘못된 것은 없는지 검증하는 작업을 했다. 내가 빅뱅의 쉘든 같은 사람이었다면 완벽하게 리뷰를 할 수 있었을텐데 난 레너드 수준에서 리뷰를 마감했다. 1차 때 보다 훨씬 적운 수의 리뷰를 드렸지만 품질 만큼은 1차때 보다 더 나은 리뷰를 해드린것 같아 다행으로 생각하고 있다. 그래도 분명히 놓친 부분이 있을텐데... 그건 개정판을 내실 때 수정할 수 있도록 출판사에 제보해주면 좋겠다. 아마 별도의 페이지가 있을지 싶다.

한 가지 아쉬움이 남는다. 난 추천사를 못 썼다. 근래 가장 많은 시간과 애정을 쏟아가며 리뷰한 책이고 그 누구보다도 여러 개발자에 이 책을 추천해주고 싶었는데 그럴 수 없었다. 여태까지 본 책 중에 이렇게까지 자발적으로 추천사를 쓰고 싶은 책은 없었는데 조금 안타깝다. 

내가 이 책을 리뷰 한 것은 이 책에 내가 적은 글을 남기고 싶어서도 아니고 돈을 받고 싶어서도 아니고 강의를 하고 싶어서도 아니었다. 돈은 애초에 생각도 안했고, 강의는 이미 리뷰를 시작한 다음에 계획 됐다. 추천사는 이 책을 읽다보니 너무도 쓰고 싶어졌지만 이젠 상관없다. 난 그냥 이 책 내용이 정말 좋았고 많은 도움을 받았다. 나도 어떻게든 이 책이 조금 더 반짝거리게 해주고 싶었다. 또 기나긴 여정에서 고군분투 하시는 토비님에게 조금이나마 힘이 되고 싶었다. 그것으로 됐다.
top


[스프링 3.0] 상속구조에서 @RequestMapping 퀴즈

Spring/3.0 : 2010.07.13 15:09


아무도 안 풀 것 같지만... 자신이 @RM을 얼마나 이해했는지 측정해보기 위해서는 좋은 방법이니까 시간나면 꼭 해보시기 바랍니다.

@RequestMapping("/hier")
public class SuperController {
@RequestMapping("/list")
public String list(Model model){
model.addAttribute("message", "hier super list");
return "/WEB-INF/views/hello.jsp";
}

}

@Controller
public class SubController extends SuperController {
}

1. 이때 /hier/list 요청을 하면 처리가 될까? 

@Controller
public class SubController extends SuperController {
@Override
public String list(Model model){
model.addAttribute("message", "hier sub! list");
return "/WEB-INF/views/hello.jsp";
}
}

SubController 코드를 이렇게 바꿨다. 

2. 이때 /hier/list를 요청했을 때 화면에 찍히는 ${message}의 값은 무엇인가?

@RequestMapping("/hier2")
public class Super2Controller {
}

@Controller
public class Sub2Controller extends Super2Controller {
@RequestMapping("/list")
public String list(Model model){
model.addAttribute("message", "hier list");
return "/WEB-INF/views/hello.jsp";
}

}

3. 이때 /hier2/list를 요청했을 때 핸들러가 실행될까?

public class Super3Controller {
@RequestMapping("/list")
public String list(Model model){
model.addAttribute("message", "hier2 super list");
return "/WEB-INF/views/hello.jsp";
}
}

@Controller
@RequestMapping("/hier3")
public class Sub3Controller extends Super3Controller {

}

4. 이때 /hier3/list 요청이 처리 될까?

@Controller
@RequestMapping("/hier3")
public class Sub3Controller extends Super3Controller {
@Override
@RequestMapping("/list")
public String list(Model model){
model.addAttribute("message", "hier2 sub~! list");
return "/WEB-INF/views/hello.jsp";
}

}

5. 구현체를 이렇게 바꾸면 에러가 날까? 

6. 그렇치 않다면? ${message}의 값은 어떻게 될까?

@RequestMapping("/hier4super")
public class Super4Controller {
@RequestMapping("/all")
public String list(Model model){
model.addAttribute("message", "hier4 super list");
return "/WEB-INF/views/hello.jsp";
}
}

@Controller
@RequestMapping("/hier4")
public class Sub4Controller extends Super4Controller {
@Override
@RequestMapping("/list")
public String list(Model model){
model.addAttribute("message", "hier4 sub list");
return "/WEB-INF/views/hello.jsp";
}

}

7. /hier4suprer/all 이라는 요청은 처리 될까?

8. /hier4/list 라는 요청은 처리 될까?

정답은 토비님 책 또는 이번주 강의에서..

오늘의 퀴즈 2종 세트를 다 맞추시는 분은 @ReuqestMapping 마스터!

top


[스프링 3.0] 클래스-메서드 레벨 @RequestMapping 퀴즈

Spring/3.0 : 2010.07.13 12:12


이미 2.5부터 추가된 기능이고 가장 자주 사용하고 있는 애노테이션 @ReqeustMapping.. 과연 얼마나 알고 있을까? 

public class Book2Controller {
@RequestMapping("/book2/add")
public String book2Add(Model model){
model.addAttribute("message", "book2 add");
return "/WEB-INF/views/hello.jsp";
}

@RequestMapping("/book2/get")
public String book2Get(Model model){
model.addAttribute("message", "book2 get");
return "/WEB-INF/views/hello.jsp";
}
}

1. 이 클래스를 <bean />을 사용해서 빈으로 등록하면 /book2/add 이나 /book2/get 요청 핸들러가 동작할까?

2. 만약 1번에서 false를 선택했다면 위 코드의 @RM이 동작하게 만드는 방법 두가지는 무엇일까?

@Controller
@RequestMapping("/book3")
public class Book3Controller {
@RequestMapping
public String add(Model model){
model.addAttribute("message", "book3 add");
return "/WEB-INF/views/hello.jsp";
}

@RequestMapping
public String get(Model model){
model.addAttribute("message", "book3 get");
return "/WEB-INF/views/hello.jsp";
}
}

3. 이렇게 매핑 했을 때 /book3/add 와 /book3/get은 동작할까?

4. 만약 3번에서 false를 선택했다면 Book3Controller의 @RequestMapping 설정을 어떻게 고치면 동작하게 할 수 있을까? (역시 두가지)

@Controller
@RequestMapping("/book3/*")
public class Book3Controller {
@RequestMapping
public String add(Model model){
model.addAttribute("message", "book3 add");
return "/WEB-INF/views/hello.jsp";
}

@RequestMapping
public String get(Model model){
model.addAttribute("message", "book3 get");
return "/WEB-INF/views/hello.jsp";
}
}

5. 위와 같이 설정했을 때 /book3/a/b/c/add 는 동작할까 안할까?

6. 만약 5번에서 동작하지 않는다고 대답했다면.. Book3Controller의 @RM 설정을 어떻게 고치면 동작하게 될지 적어보자.

정답은.. 토비님 책 또는 이번주 강의에서...
top


네가 있어서 정말 감사하다.

모하니?/Thinking : 2010.07.12 23:32


지난 주말 장모님 댁에 갔을 때 '오늘 하루도 눈을 뜨면 그 자체로 너무 감사하고 고맙다.'라는 말을 들었다. '네'라고 대답은 했지만 사뭇 이해가 되지 않았다. 어떤 뜻인지는 머리로는 이해가 되는데 가슴으로는 와닿질 않았다. 나는 요즘 눈을 뜨면 '오늘은 적어도 이걸 해야되는구나...  아.. 저것까지도 하면 좋겠는데..' 이런 생각을 제일 먼저 한다. '내가 건강한 상태로 지금 오늘을 맞이 할 수 있어서 정말 고맙다.' 이런 생각은 거의 안해본것 같다. 오히려 불만이 더 많았다. '난 왜 이것도 못하지... 난 왜 이것 밖에 안돼지.. 난 왜.. 난 왜.. 난 왜..."  이런 생각만 하고 살아왔다고 해도 과언이 아니다.

그런데, 오늘 문득 성윤이를 만나고 돌아오는 길에 그런 생각이 들었다. '성윤이를 만난건 정말 대단한 행운이구나. 고맙다.'라는 생각이 들었다. 성윤이가 봄싹에서 열심히 활동해 준 것도 고마운 사실이지만 사실 성윤이가 스터디에서 활동한 것 보다는 김성윤이라는 개발자를 알게 된 것 자체가 고맙게 느껴졌다. 정말 치열하게 삶에 도전하고 노력하는 성윤이 같은 개발자가 내 옆에 있었다는 사실이 고맙게 느껴졌다.

이제 몇일이 지나면 온라인으로밖에 볼 수 없는 사이가 되겠지만... 뭐 설마 영영 못보겠어? 다시 볼 그날까지 절대로 너에게 부끄러운 모습이 되지 않도록 유념하며 살겠다. 그러니 너도 잘 살고 있어야 돼. 

고맙다.

나중에 만나서 라이브 코딩으로 한판 겨뤄보자꾸나.. 통키와 타이거처럼.. 말이지..





top


버즈 버그 미투 장애




1. 사진 안나옴
2. 난 '좋아요'를 취소 했는데... 아직도 좋아요 상태.

자기들도 알테니.. 열심히 고쳐주겠지. 어이 구글~ 잘못하면 웨이브 꼴 나는거야... 열심히 해줘...
그나저나 첫날은 8시간을 자라는데.. 그럼 난 무조건 지각인데;; 흠... 10시에 자야되나.. 1시간 남았네;; @_@


미투는 아에 장애다... 장애;;; 흠.. 어감이 별로네. 서비스 "점검중"이 훨씬 좋겠다.  

흠... 미투 가입할때 주민번호를 넣었던가.. 안넣은것 같기도 하고. 암튼 한국 사이트들은 제대로 지켜줄꺼 아니면 주민번호좀 그만 받아라. 주민번호 누출시켰으면 제대로 보상이나 뒷처리를 해주던가... 아! 그것도 무슨 법 때문에 그런거라고 들은것 같은데 잘 기억은 안나네.. 흠...


top

TAG 버즈

[이전글에 이어지는 이야기] 숫자에서 객체로...

모하니?/Coding : 2010.07.07 13:21


... 윗부분 생략 
               double qtyIn = invInDao.getQtyOf(today, item, location);
                // 금일 출고량 조사
                double qtyOut = invOutDao.getQtyOf(today, item, location);
                // 금일 재고량 계산
                double qtyEnd = qtyStart + qtyIn - qtyOut;

                if(isEmpty(qtyStart, qtyIn)){
                    continue;
                }

                InvDailyClosing invDailyClosing = new InvDailyClosing();
                invDailyClosing.setDate(today);
                invDailyClosing.setLocation(location);
                invDailyClosing.setItem(item);
                invDailyClosing.setQtyStart(qtyStart);
                invDailyClosing.setQtyIn(qtyIn);
                invDailyClosing.setQtyOut(qtyOut);
                invDailyClosing.setQtyEnd(qtyEnd);
...아래도 생략

쓰고나서 생각하니 약간 위험한 글을 썼다는 생각에 괜히 불안했는데..  아니나 다를까... 대용량 데이터베이스를 공부하고, 자바 코드로 치우져서 보는거 아니냐, 벽을 세워놓고 한쪽만 보는건 좋치 않다. 라는 글이 달렸었는데 사라졌다.

하아.. 본문에도 적었지만 사실 내가 대용량 DB를 다루는 SQL을 잘 못하는건 사실이다. 근데 어쩌겠는가.. SQL을 하고 싶지 않아서 안하는게 아니라 SQL을 작성할 일이 별로 없는데다, 대용량 데이터는 내가 만지고 싶다고 만질 수 있는 것도 아니다. 난 이럴 때 마다 떠올리며 나를 위로하는 문구가 있는데.. 켄트벡이 쓴 "테스트 주도 개발" 뒷부분 어딘가 보면 그런 말이 있다. 아.. 이런.. 문제의 크기와 해결방법에 관한 거였는데.. 까먹었다. 암튼 0과 1을 떠올리게 하는거였는데 ㅠ.ㅠ

생각해보니, 재고 마감 데이터가 입고와 출고 수량만 들고 있는게 아니라, 해당 마감 데이터에 해당하는 입고 목록과 출고 목록을 들고 있는게 더 유용하겠다는 생각이 들었다. 그렇게 해야 "재고 마감 뷰"에서 해당 마감에 대한 입고 내역과 출고 내역을 보여주기 편할 것 같다. 아니면 마감 데이터를 가지고 해당 마감에 해당하는 입고목록과 출고 목록을 가져오는 기능을 입고DAO와 출고DAO에 추가해줘야 하는데... 글쎄... 흠냐.. 그거 만들기 귀찮아서 기능을 고치고 있는 기분도 들지만, 왠지.. 설계상으론 더 객체 지향적이고 멋진 것 같다.

이런 로직을 반영하려면 숫자보다 객체를 들고 있게 하는게 편하고.. SQL 보다.. 위와 같은 자바 코드를 수정하는게 더 편한데...그건 내가 그렇다는 것이고...  사람에 따라, 상황에 따라, 대용량인지 아닌지에 따라.. 달라질 수 있겠다..

                List<InvIn> invInList = invInDao.listOf(today, item, location);
             List<InvOut> invOutList = invOutDao.listOf(today, item. location);

                if(isEmpty(qtyStart, invInList)){
                    continue;
                }

                InvDailyClosing invDailyClosing = new InvDailyClosing();
                invDailyClosing.setDate(today);
                invDailyClosing.setLocation(location);
                invDailyClosing.setItem(item);
                invDailyClosing.setQtyStart(qtyStart);
                invDailyClosing.setInvInList(invInList);
                invDailyClosing.setInvOutList(invOutList);
                invDailyClosing.closing();

                dao.add(invDailyClosing);

결국은 이렇게 바뀔 것 같다. InvDailyClosing 도메인 객체의 closing() 메소드에서 입고 목록, 출고 목록, 전날 재고를 가지고 금일 재고를 계산하도록...말이다. 물론.. 이정도 쯤이야. SQL로도 간단하게 할 수 있겠다(?). 난 잘 모르겠지만..

top


복잡한 로직은 복잡한 DAO로 직결되는가?

모하니?/Coding : 2010.07.07 10:14


아니지 않을까... 비즈니스 로직이 복잡하다고 해서 SQL이 복잡해 질 필요는 없다.

재고 마감 로직을 생각해보자. 모든 창고에 들어있는 모든 상품들의 전일 재고량, 금일 입고량, 금일 출고량을 가져와서 금일 재고량을 계산한다고 치자. 이걸 가지도 또 상품 카테고리 별로 묶고 창고 별로 묶어서 집계를 내야 하는데.. 일단은 그 전 단계까지만 해보자.

Location - Inventory
Item - Inventory
Item - InvIn
Item - InvOut
Item - InvDailyClosing

이렇게 관계가 맺어져 있으니 SQL로 join을 하던 sub select를 하던 어떻게든 SQL 한방으로 어떤 창고에 있는 어떤 상품의 '전일 재고량', '금일 입고량', '금일 출고량'을 한방에 가져올 수 있을 것 같다.

근데 난 그렇게 하지 않는다. 사실 그렇게 복잡한걸 만들지도 못한다. 만들라면 공부해가면서 만들겠지만 무지 오래 걸릴 것 같다. 그래서 난 그냥 자바 코드로 SQL을 대신한다.

        dao.delelteAllAt(today);

        for(Location location : locationDao.getAll()) {
            // 모든 상품 마다
            for(Item item : itemDao.getAll()){
                // 전일 재고량 조사
                double qtyStart = inventoryDao.getQtyOf(yesterday(today), item, location);
                // 금일 입고량 조사
                double qtyIn = invInDao.getQtyOf(today, item, location);
                // 금일 출고량 조사
                double qtyOut = invOutDao.getQtyOf(today, item, location);
                // 금일 재고량 계산
                double qtyEnd = qtyStart + qtyIn - qtyOut;

               나머진 생략...
            }
        }

즉 이런식으로 짠다. 굵은 글씨 부분은 DAO test를 해서 해당 기능이 내가 원한대로 동작하는지 일일히 확인해다. DBUnit을 사용해서 테스트용 데이터를 넣고 저 기능을 실행해보는 방식이다. 이렇게 구현하니까 상당히 마음이 놓인다. 저걸 만약 한방에 SQL로 구현했다던지.. 테스트 없이 구현했다면.. 글쎄.... 그렇겐 못했을 것 같다.

암튼 이래서.. 복잡한 로직을 처리하는 DAO 코드라 할지라도 단순해진다. 이런걸 디바이드 앤 퀀쿼 라고 하던가.. 몰겠다. 머라하든.

    public double getQtyOf(Date date, Item item, Location location) {
        Object result = getSession().createQuery("select sum(qty) from InvIn where date = :date and item = :item and location = :location group by item")
                .setDate("date", DateUtils.getDateOnly(date))
                .setEntity("item", item)
                .setEntity("location", location)
                .uniqueResult();
        if(result == null)
            return 0.0;
        return (Double) result;
    }

대충 이정도 코드가 생기는데.. 이정도야 뭐.. 초간단 SQL 수준 아닌가..

이렇게 짜면 걱정되는게.. 성능인데... DB에 자주 다녀올수록 더 많은 부하가 생기는건 사실이지만 어차피 복잡한 쿼리 자체도 join 여러번 하면서 부하가 생길테니.. SQL 도사가 아닌 이상 차라리 성능을 조금 포기하고 유지 보수 가능하고 알아보기 쉬운 코드로 나눠서 작성하는게 더 좋치 않을까 싶다.





top


[IBM DW] HTML5 navigator

View/HTML/CSS : 2010.07.06 20:41


http://www.ibm.com/developerworks/opensource/library/x-html5mobile1/index.html
http://www.ibm.com/developerworks/kr/library/x-html5mobile1/ (번역)

HTML5에 대해 궁금하던 찰나 무심코 IBM DW에 들어가봤더니.. 역시나 좋은 글들이 마구 쏟아지고 있었다. 

이 글에서 소개하는  내용은 흔히 GPS라 부르는 기능에 관한 것이다. 그것만 보면 심심할까봐 트위터 API랑 연동해서 접속한 사람 근방의 트위터글을 검색해 주는 애플리케이션을 소개하고 있다.

현재 위치 알아내기

            var gps = navigator.geolocation;
            if (gps) {
                alert("gps enabled");
                gps.getCurrentPosition(searchTwitter,
                        function(error) {
                            alert("Got an error, code: " + error.code + " message: " +
                                    error.message);
                        });
            } else {
                searchTwitter();
            }

첫번째 인자는 성공시 실행할 콜백, 두번째 인자는 실패시 실행할 콜백, 세번째는 옵션인데 생략했군요.

성공시 실행할 콜백에 전달되는 인자는 position

        function searchTwitter(position) {
            alert("searchTwitter position=" + position);
            var query = "http://search.twitter.com/search.json?callback=showResults&q=";
            query += $("kwBox").value;
            if (position) {
                var lat = position.coords.latitude;
                var long = position.coords.longitude;
                query += "&geocode=" + escape(lat + "," + long + ",50mi");
            }
            alert("query=" + query);
            var script = document.createElement("script");
            script.src = query;
            document.getElementsByTagName("head")[0].appendChild(script);
        }

저기서 꺼내서 사용할 값은 주로 coords의 latitude(위도)와 longitude(경도). 여기서 잠깐... 위도 경도가 뭐더라.. 어떤게 가로인지 세로인지 햇갈린다. 적도를 기준으로 상하 각도를 위도. 북극과 남극은 그래서 위도 90. 경도는 영국 그리니치 천문대를 기준으로 좌우 각도.  서울은 북위 37도, 동경 126도.

위치 추적하기

trackerId = gps.watchPosition(function(pos){
var latLng = new google.maps.LatLng(pos.coords.latitude,pos.coords.longitude);
map.setCenter(latLng);
theUser.setPosition(latLng);
showLocation(pos);
});

watchPosition을 사용하면 계속해서 좌표 받아옴.

function stopTracking(){
if (trackerId){
navigator.geolocation.clearWatch(trackerId);
}
}

trackerId를 clearWatch에 넘겨줘숴 종료한다.

트위터 API랑 구글맵 API 사용법도 나오는데... 생략~




'View > HTML/CSS' 카테고리의 다른 글

[IBM DW] HTML5 navigator  (0) 2010.07.06
[CSS] 탭 만들기  (2) 2009.05.26
[CSS] 링크 적용 범위 넓히기, 롤오버 효과 주기  (0) 2009.05.22
[CSS]그라데이션 배경 사용하기  (2) 2009.05.22
XPath Path Expression 문법  (0) 2009.05.15
Chapter 14. Image Replacement  (0) 2008.03.31
Chpater 13. Styling Text  (0) 2008.03.31
Chapter 10. Applying CSS  (0) 2008.03.26
Chapter 9. Minimizing Markup  (0) 2008.03.23
Chapter 8. More Lists  (0) 2008.03.23
Chapter 7. Anchors  (0) 2008.03.23
top


스프링 DAO 3파전

모하니?/Coding : 2010.07.02 13:00


JDBC를 사용하는 부류가 있고, iBatis를 사용하는 부류가 있고, 하이버네이트를 사용하는 부류가 있다. 사실 이보다 더 다양한 영속화 기술들이 있지만.. 이 세 부류로 간추려 보려고 한다. 자 이들은 각각 어떻게 코딩을 하고 있을까.

public interface MemberDao {
void add(Member member);
void update(Member member);
Member get(int id);
List<Member> list();
void delete(int id);

}

이런 인터페이스를 각 부류에서 구현한다고 생각해보자.

아참, 전제는 스프링을 사용한다는 것이다.

1. JDBC

@Repository
public class MemberDaoJdbc implements MemberDao{
@Autowired SimpleJdbcTemplate jdbcTemplate;
public void setMemberMapper(RowMapper<Member> memberMapper) {
this.memberMapper = memberMapper;
}

RowMapper<Member> memberMapper = new RowMapper<Member>(){
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member();
member.setId(rs.getInt("id"));
member.setName(rs.getString("name"));
member.setJoined(rs.getDate("joined"));
return member;
}};
public void add(Member member) {
jdbcTemplate.update("insert into member(id, name, joined) values (?, ?, ?)", 
member.getId(), member.getName(), member.getJoined());
}

public void delete(int id) {
jdbcTemplate.update("delete from member where id = ?", id);
}

public Member get(int id) {
return jdbcTemplate.queryForObject("select * from member where id = ?", memberMapper, id);
}

public List<Member> list() {
return jdbcTemplate.query("select * from member", memberMapper);
}

public void update(Member member) {
jdbcTemplate.update(
"update member set name = :name, joined = :joined where id = :id", 
new BeanPropertySqlParameterSource(member));
}

}

대충 이런 코드가 된다. 스프링이 제공해주는 SimplJdbcTemplate은 멀티 쓰레드 환경에서 공유하면서 사용해도 안전한 객체이다. 따라서 빈으로 등록해놓고 주입받아서 쓴다고 한들 문제될 것이 없다. 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/testContext.xml")
@Transactional
public class MemberDaoJdbcTest {
@Autowired MemberDaoJdbc memberDao;
@Test
public void di(){
assertThat(memberDao, is(notNullValue()));
}
@Test
public void crud(){
Member member = new Member();
member.setId(1);
member.setName("whiteship");
member.setJoined(new Date());
memberDao.add(member);
assertThat(memberDao.list().size(), is(1));
member.setName("기선");
memberDao.update(member);
assertThat(memberDao.get(1).getName(), is("기선"));
memberDao.delete(1);
assertThat(memberDao.list().size(), is(0));
}
}

테스트는 대충 만들었기 때문에 너그럽게 봐주시길...^_^;;;

2. iBatis

스프링에 iBatis의 SqlMapClient를 만들어 주는 팩토리빈을 등록해야 한다.
   
<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="SqlMapConfig.xml" />
</bean>

아이바티스 설정 파일도 추가한다. 저 설정에 보이는 SqlMapConfig.xml이 필요하다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMapConfig      
    PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"      
    "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
<sqlMap resource="sample/ibatis/Member.xml" />
</sqlMapConfig>

그 다음 Member와 관련있는 SQL을 모아둔 Member.xml 파일을 만든다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap      
    PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"      
    "http://ibatis.apache.org/dtd/sql-map-2.dtd">

<sqlMap namespace="Member">
<typeAlias alias="Member" type="sample.domain.Member" />

<delete id="delete" parameterClass="int">
delete from member where id = #id#
</delete>

<insert id="add" parameterClass="Member">
insert into member (id, name, joined) values(#id#, #name#, #joined#)
  </insert>
 
<update id="update" parameterClass="Member">
update member set name = #name#, joined = #joined# where id = #id#
</update>
 
<select id="get" parameterClass="int" resultClass="Member">
select * from member where id = #id#
  </select>

<select id="list" resultClass="Member">
select * from member order by id
  </select>
</sqlMap>

XML 엘리먼트와 어트리뷰트는 별도의 학습이 필요하지 않을만큼 직관적이다.. 이제 DAO 코드를 작성하자.

@Repository
public class MemberDaoIbatis implements MemberDao {
@Autowired SqlMapClientTemplate sqlMapClientTemplate;
public void add(Member member) {
sqlMapClientTemplate.insert("add", member);
}

public void delete(int id) {
sqlMapClientTemplate.delete("delete", id);
}

public Member get(int id) {
return (Member) sqlMapClientTemplate.queryForObject("get", id);
}

@SuppressWarnings("unchecked")
public List<Member> list() {
return sqlMapClientTemplate.queryForList("list");
}

public void update(Member member) {
sqlMapClientTemplate.update("update", member);
}

}

흠 많이 짧아졌다. 그런데.. 전체 코드량은 더 많이 늘은것 같지 않은가? 코드량이 척도의 전부는 아니지만.. 왠지 작업이 줄었다가 보다는 DAO에서 할일을 설정파일로 미뤘다는 느낌이 강하다. 아참 여기서 사용한 SqlMapClientTemplate도 SimleJdbcTemplate과 마찬가지로 빈으로 등록해서 사용해도 별 지장이 없는 쓰레드-안전한 클래스기 때문에 빈으로 등록해놓고 사용했다.

3. Hibernate

이것도 역시 빈을 하나 설정해줘야한다.
 
   <!-- ============================================================= -->
    <!--  Hibernate                                                    -->
    <!-- ============================================================= -->
    <bean id="transactionManager"
          class="org.springframework.orm.hibernate3.HibernateTransactionManager"
          p:sessionFactory-ref="sessionFactory"/>

    <bean id="sessionFactory"
          class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="packagesToScan" value="sample.domain" />
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">${hibernate.dialect}</prop>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.hbm2ddl.auto">update</prop>
            </props>
        </property>
    </bean>

이번건 설정이 제법 길다. 그리고 빈이 하나가 아니라 두갠데.. 그건 나중에 TransactionManager에 대한 글을 올릴 때 설명해야겠다. 아니면 그냥 토비님 책을 보기 바란다. 11장에 자세히 설명해주시고 있다. TransactionManager와 기반 기술의 관계(?)랄까나.. 암튼.. 이제 바로 DAO를 만들 수 있다. 아 아니다 일단 Member 클래스에 애노테이션 부터 추가하자.

@Entity
public class Member {

@Id
int id;

...
}

이렇게 두 군데에 각각 하나씩만 붙여주면 된다. 이제 DAO를 만들자.

@Repository
public class MemberDaoHibernate implements MemberDao{
@Autowired SessionFactory sessionFactory;

public void add(Member member) {
getSession().save(member);
}

public void delete(int id) {
getSession().createQuery("delete from Member where id = ?")
.setInteger(0, id)
.executeUpdate();
}

public Member get(int id) {
return (Member) getCriteria()
.add(Restrictions.eq("id", id))
.uniqueResult();
}

@SuppressWarnings("unchecked")
public List<Member> list() {
return getCriteria().list();
}

public void update(Member member) {
getSession().update(member);
}

private Criteria getCriteria() {
return getSession().createCriteria(Member.class);
}

private Session getSession() {
return sessionFactory.getCurrentSession();
}
}

SQL이 없다. SQL처럼 보이는 HQL(굵은 글씨)만 보인다. 이렇게만 하면 정말 DAO가 구현되는지 의심스럽겠지만 정말 구현이 끝난다. 하지만 하이버네이트 Session과 Criteria API는 약간 학습이 필요하다. 자주 쓰는 API는 몇개 안되기 떄문에 대략 1시간 정도면 익힐 수 있다.

셋 개 다 해본결과.. 난 역시 하이버네이트가 편하다.
top


[Spring Security] http 네임스페이스 쓰려면 필터 이름은 항상 고정(?)

Spring Security/etc : 2010.06.29 17:02


http://static.springsource.org/spring-security/site/docs/3.0.x/reference/appendix-namespace.html#nsa-http-attributes

<filter>
<filter-name>securityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

   <filter-mapping>
       <filter-name>securityFilterChain</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>
    

자 이렇게 필터를 설정하고 

<http>
        <intercept-url pattern="/base/color/mgt" access="ROLE_USER" />
        <intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<form-login login-page="/login" />
<logout logout-success-url="/index" />
<remember-me />
</http>

    <beans:bean id="smdisUserDetailsService" class="smdis.common.security.SmdisUserDetailsService"/>

<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="smdisUserDetailsService"/>
</authentication-manager>

<global-method-security secured-annotations="enabled"
jsr250-annotations="enabled" pre-post-annotations="enabled" />

이렇게 시큐리티 설정을 했다.

잘 돌아갈까?? 안 돌아간다.. 시큐리티 네임스페이스를 사용해서 <http>를 등록하면 FileChainProxy 빈 이름은 항상 springSecurityFilterChain이 된다. 그래서 필터 이름을 springSecurityFilterChain으로 설정해줘야 한다.

뭐.. DelegatingFilterProxy 필터 자체에 targetBeanName 속성을 사용해서 연결할 빈 이름을 설정할 수도 있지만 기본적으로 이 이름은 필터 이름을 따르게 된다. 필터 이름을 바꾸고 targetBeanName을 또 설정해 주느니 그냥 필터 이름을 springSecurityFileterChain으로 하는게 좋겠다.

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
top


전자정부 프레임워크 공통 컴포넌트 실행 성공(?)

모하니?/Coding : 2010.06.25 15:13



이 화면 보기가 너무 힘들다;

OSAF 예제 애플리케이션 띄우는거 보다 100배는 올래거리는것 같다. 

그나마 이 화면에서 다음으로 전개도 안된다. 왜냐면 DB가 세팅되지 않았기 떄문에;;

전자 정부 사이트에서 받은 mysql용 sql 파일들을 돌리다 보면 계속 에러가 난다. 자주 보이는 건 주키가 너무 길다는 에러인데 이건 주키 컬럼 사이즈(보통 메서드 이름이 200으로 잡혀 있었다.)를 줄여가면서 지나갔다. 그런데 두번째 sql 파일의 에러는 도무지 모르겠다; MySQL을 설치한지 오래되서 새 버전으로 올린 담에 해봐야 하는건지.. 그냥 오라클로 해야하는건지... 아흑..

맞다. DB만 만들다고 돌아가는것도 아니다. globals.properties 파일을 C:\Documents and Settings\MyHome\egovProps 이 위치에 넣어줘야 하고 이걸 제대로 쓰려면 그 파일에 있는 각종 설정 내용을 다 손봐줘야 한다.

이거.. 아무래도 내가 너무 무모만 것에 손을 댄것 같다. 덕분에 전자 정부 프레임워크 구조가 어떤지 파악할 수 있었다.

개발 환경쪽에서 '구현도구'라고 되어있는 이클립스를 받아서 전자정부 웹 프로젝트를 만들어야지 필요한 pom.xml이 나오지만 사실상 이건 제대로 된 pom.xml파일이 아닌것 같다. 그래도 일단 이렇게 받는다.

http://www.egovframe.org/wiki/doku.php?id=egovframework:배포_패키지_구성안

여기에서 공통여부 Y로 체크되어 있는 것은 무엇을 클릭하던 똑같이 egovframework-common-1.0.0를 받게 된다. 저 안에 수많은 공통 기능이 들어가 있다. 즉 저것을 넣어서 앱을 실행하면 전자정부 프레임워크 도입을 완수한거나 마찬가지다. 거기에 부가적으로 게시판이나 통계등을 넣고 싶다면 해당 메뉴를 클릭해서 소스를 받은 다음 프로젝트에 추가해줘야 한다. 

이 "공통 컴포넌트"라고 하는 서비스들은 "실행 환경"을 기반으로 하고 있다. 실행 환경 코드는 다시 네가지로 나뉜다. 공통기반, 데이터처리, 연계통합, 화면처리.. 이 중에서 웹 관련 기능이 궁금하다면 "화면 처리" 코드를 받아서 보면 된다.

하지만 안타깝게도 이 "실행 환경"에서 받는 코드들은 pom.xml이 없다. 빌드가 되지 않는다. 아참;; 위에서 설명하려다 까먹었는데.. "개발 환경"에서 자동으로 만들어준 pom.xml은 버리고 "공통 컴포넌트" 중에서도 "공통"에 해당하는 egov~~-common-1.0.0을 받으면 들어있는 pom.xml이 있다. 그걸 쓰면 "공통 컴포넌트"에 필요한 의존성까지 설정되어 있으니 무난히 빌드할 수 있다. 어쨋거나 이 코드들은 그냥 쌩으로 봐야한다. 빌드가 안되니.. 별도의 jar 파일을 만들수도 없다. (원래 안만들어도 된다. jar 파일만 따로 다운 받을 수도 있고, 공통 컴포넌트의 pom.xml에서 의존성으로 알아서 가져오게 되어 있다.) 정... 하고 싶으면 pom.xml 만들고 필요한 의존성 다 붙여주면 되는데.. 글쎄;; 별로..

소스 중에 유일하게 살펴본 코드로는 "화면처리(ptl)"에 들어있는 SimpleUrlAnnotationHandlerMapping이 있는데 이름에서는 추측하기 어려운 일을 해준다. 핸들러 매핑 마다 일괄적용되는 인터셉터가 부담스러워서 그걸 url 리스트를 사용해서 걸러주는 작업이다. 그런데 토비님 책을 보면 스프링 3.0의 <mvc:inteceptor>로 해결이 가능하다. 글로벌한 인터셉터 설정이기 때문에 핸들러 매핑을 기준으로 생각할 필요가 없어졌다. 아마도 저 클래스는 주석에도 적혀있듯이 3.0 가면서 deprecated 될 것 같다.
top


OSAF 2.0 준비중

OSAF : 2010.06.23 17:05


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

Generic 사용 극대화

이전과 마찬가지로 Generic 사용을 극대화해서 DAO, Service, Controller를 만들고 PersistentEnum을 활용하는 GenericPEFormatter도 만들어 뒀고, GenericExcelView까지 있습니다.

REST 스타일 URL 지원

스프링 3.0 최신 버전과 하이버네이트를 사용했으며 URL은 REST 스타일로 만들어 놨습니다. 따라서 GenericController만 상속받으면 기본적인 CRUD 기능을 RESTful URL로 제공해줍니다.

계층 구조

기본적인 계층 구조(DAO, Service, Controller) 형태기 때문에 적응하기 쉬울 겁니다. 스프링 Roo 스타일로 만들까 했지만. AsepctJ를 공부헀었는데도 잘 적응이 안되더라구요.ㅋ

no-iframe

참.. 화면이 이전처럼 iframe으로 나눠진게 아니라 통짜 구조입니다. 화면을 나누는건 JQuery layout 플러그인을 사용했구요.  iframe보다 편합니다.

그리드

그리드도 jqGrid를 사용해서 이전에 쓰던 그리드(뭔지도 기억안남)보다 기능도 많고 더 좋습니다.

상속

단점은.. Spring Roo 처럼 완전히 비침습적인 프레임워크가 아닌지라.. 상속을 사용해야 합니다. DAO, Service, Controller, Enum 들이 OSAF 코드를 상속 받아야 합니다.

코드 생성

코드 생성 기능은 없습니다. 만들라면 만들겠지만 귀찮기도 하고 손으로 직접 만들면서 사용법을 익히는게 좋을 것 같아서 만들지 않았습니다.

바로 시작

프로젝트를 다운받아서 라이브러리 형태로 넣을 필요없이 프로젝트를 받아서 바로 자신의 웹 프로젝트로 사용할 수 있게끔 배포할 계획입니다. 

기본 모델 제공

바로 실행할 수 있는 프로젝트에 Member, Right, Code, CodeCat 같은 기본 도메인을 제공합니다. 자주 쓰는 도메인 코드를 가진 상태에서 시작하세요. Generic류 코드를 어떻게 쓰는지, OSAF 태그파일을 사용해서 화면 코드를 작성하는 방법을 쉽게 볼 수 있습니다.
top

TAG OSAF 2.0

[스프링 3.0 이해와 선택] 2차 교육 공지

모하니?/Planning : 2010.06.23 14:10


http://www.hanbitedu.co.kr/incumbent/shortContent.do?index=1097

벌써 공지가 떴네요. 캬..
1차 교육 끝나자 마자 바로 시작합니다.
2차 강의 할 땐 토비님 책을 가지고 시작할 수 있겠군요.
한결 든든하겠네요.

- 1일차 (7월 24일) 
 1장 오브젝트와 의존관계 
 2장 테스트 
 3장 템플릿 
 4장 예외 

- 2일차 (8월 07일) 
 5장 서비스 추상화 
 6장 AOP 
 7장 스프링 핵심기술 응용 
 8장 스프링이란? 

- 3일차 (8월 14일) 
 9장 스프링 프로젝트 시작하기 
 10장 IoC 컨테이너와 DI 
 11장 데이터 엑세스 기술 

- 4일차 (8월 21일) 
 12장 스프링 웹 기술과 스프링 MVC 
 13장 스프링 @MVC 
 14장 AOP와 LTW 
 15장 콘텍스트 테스트 프레임워크 
 16장 스프링의 기타 기술과 효과적인 학습방법 

1일차와 2일차는 "이해"하는 단계입니다. 이 부분에서 스프링 IoC 컨테이너, 테스트, 템플릿, 트랜잭션 추상화, 이메일 추상화, 스프링 AOP 기능들이 어떻게 만들어졌는지 실습을 통해서 학습하게 됩니다.

3일차와 4일차는 "선택"하는 단계로 실제 웹 애플리케이션을 만들어 가면서 IoC 컨테이너 설정 방법, 연관 관계 설정 방법, 빈 등록 방법 등을 선택하고, DAO 기술을 선택하고 그에 따른 스프링 지원 기술을 사용하며, 트랜잭션 선언 방법도 선택하고 적용합니다. 4일차에서는 @MVC를 사용해서 웹 애플리케이션을 마무리하고 3.0에 추가된 기능들을 학습합니다.


top


[회사일] GenericExcelView 만들기

프로젝트/SLT : 2010.06.23 11:24


현재 업무를 엑셀 파일로 처리하고 있기 때문에 엑셀 파일과의 연동을 필수적입니다. 따라서 시스템에 들어있는 정보를 엑셀로 받아볼 수 있게 하거나 엑셀 정보를 시스템에 올려서 DB에 저장되게 하는 것도 필요합니다.

엑셀 업로드야 엑셀 파일 형식이 도메인 클래스 마다 다를 테고 비즈니스 로직마다 다를테니 일반화하기 힘들지만(뭐 어떤 규칙을 정하면 일반화를 못할 것도 없겠지만..) 엑셀 다운로드 경우에는 일반화하기 좋습니다. 리포트 형식의 엑셀을 원하는게 아니라면 말이죠;; 그런건 여기서 받은 엑셀 파일을 기초데이터로 삼아서 다른 직원보고 편집해서 리포트 만들라고 시키면 될테니 이런 간단한 재고관리 시스템에 무리한 기능을 넣고 싶진 않군요.

어쨋든.. 일반화 시켜봅시다.

<page:mgtpage label="재고 관리">
    <page:search label="재고 검색">
        <s:input label="제품명" path="name" />
        <s:input label="제품번호" path="number" />
        <s:input label="색상" path="colorName" />
        <s:select label="성별" path="sexValue" items="${ref.sexList}" />
        <s:input label="공급업체" path="suppName" />
        <s:input label="창고" path="locationName" />
        <s:date label="날짜" fromPath="dateRange.from" toPath="dateRange.to"/>
    </page:search>
    <script type="text/javascript">
        $(function() {
            $("#smdis-grid").jqGrid({
                colNames:['id', '제품명', '제품번호', '색상', '사이즈', '공급업체', '창고', '수량', '날짜'],
                colModel :[
                    {name:'id', index:'id', width:55, hidden:true},
                    {name:'item.name', index:'item.name', width:90},
                    {name:'item.number', index:'item.number', width:90},
                    {name:'item.colorName', index:'item.color', width:90},
                    {name:'item.size', index:'item.size', width:90},
                    {name:'item.supp.name', index:'item.supp', width:80},
                    {name:'location.name', index:'location.name', width:80},
                    {name:'qty', index:'qty', width:80},
                    {name:'date', index:'date', width:80}
                ]
            });
        });
    </script>
</page:mgtpage>

화면에 보여줄 그리드 정보가 들어있는 mgt.jsp 페이지 소스입니다. 태그 파일을 사용해서 중복 코드를 제거하고 여기서 꼭 다뤄야하는 정보만 남겨뒀습니다. 이 상태에서 엑셀을 일반화 시켜서 만들때 서버쪽으로 넘겨줘야 하는 정보가 들어있습니다. 컬럼명과 각 컬럼 값이 들어있는 path 정보입니다.

화면에 등록된 그리드 정보를 그대로 엑셀로 옮기고 싶다면 그 두 정보를 가져와야 합니다. 구현 과정을 전부다 설명하기는 다소 귀찮군요.. 흠.. 할일도 많으니까 대충 생략하겠습니다.


저기서 버튼을 누르면 컬럼 정보, Path 정보, 검색 매개변수들을 서버쪽으로 넘겨줍니다.

그럼 서버가 받아서 SpEL과 JExcel API와 스프링이 제공해주는 AbstractJExcelView 엑셀뷰를 사용해서 엑셀을 만들어 줍니다.

public class GenericExcelView extends AbstractJExcelView {

    protected void buildExcelDocument(Map<String, Object> model, WritableWorkbook wb, HttpServletRequest req, HttpServletResponse res) throws Exception {
        List<?> list = (List<?>) model.get("list");
        String modelName = (String) model.get("modelName");
        List<String> colNameList = (List<String>) model.get("colNameList");
        List<String> colPathList = (List<String>) model.get("colPathList");

        res.setHeader("Content-Disposition", "attachment; filename=" + modelName + ".xls");
        WritableSheet sheet = wb.createSheet(modelName, 0);

        for(int column = 0 ; column < colNameList.size() ; column++){
            sheet.addCell(new Label(column, 0, colNameList.get(column)));
        }

        ExpressionParser parser = new SpelExpressionParser();
        int row = 1;
        for(Object object : list){
            StandardEvaluationContext context = new StandardEvaluationContext(object);
            for(int column = 0 ; column < colPathList.size() ; column++){
                String path = colPathList.get(column).replace(".", "?.");
                String value = parser.parseExpression(path).getValue(context, String.class);
                sheet.addCell(new Label(column, row, value));
            }
            row++;
        }
    }
}

이걸 사용하도록 GenericController 쪽에도 기능을 추가해줬는데 그거야 뭐 간단하니깐. 생략하겠습니다. 이제 재고관리와 상품관리쪽에 엑셀 업로드를 만들어야겠습니다. 내일할까나;; 흠.


top


[회사일] JPA로 계층구조 매핑하기

프로젝트/SLT : 2010.06.18 15:24


하이버 번역서를 보시면 여러가지 방법이 나와있는데 그 중에서 가장 간단하면서, 성능도 좋고, 다형성까지 살릴 수 있는 방법으로 "계층 구조 당 테이블" 매핑 방법을 소개하고 있습니다.

원래 이 회사에서 다루는 재고가 '신발' 하나였는데 이제 곧 '가방'까지 늘어날 예정인가 봅니다. 고객(울 회사 대표님)과 대화를 하지 않았다면 몰랐을텐데.. 초기에 알았으니 그나마 다행입니다. Item이라는 클래스에 신발 정보 다 넣어놓고 이런 얘기 했다면... @_@ 좀 피곤했을텐데 말이죠. 다행히 테스트용으로 두 세개 밖에 안넣어봤거든요.ㅋ

어쨋든..

Item을 상위클래스로 두고, Shoes와 Bag을 하위 클래스로 설계하고 싶어졌습니다.

그래서 모든 상품에 기본적으로 들어갈만한 속성은 Item에 남겨두고 JPA 계층 구조 매핑을 추가했습니다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
    name = "ITEM_TYPE",
    discriminatorType = DiscriminatorType.STRING
)
public class Item {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column
    @Type(type = "text")
    private String descr;

    @Column(length = 50)
    private String name;

    @Column(length = 50)
    private String number;

    @Column
    private String image;

    @Column
@Type(type="smdis.domain.usertype.SexUserType")
    private Sex sex;

    @ManyToOne
    private Color color;
...
}

그다음 이 클래스를 상속받는 Shoes라는 도메인을 만들었습니다.

@Entity
@DiscriminatorValue("SHO")
public class Shoes extends Item {
    
    @Column(name = "SHO_SIZE")
@Type(type="smdis.domain.usertype.ShoesSizeUserType")
    private ShoesSize size;

    public ShoesSize getSize() {
        return size;
    }

    public void setSize(ShoesSize size) {
        this.size = size;
    }
}

간단하네요. Bag도 추가해봐야겠습니다.



top


[하이버네이트 완벽 가이드] 드뎌.. 하이버 번역서가 나왔습니다.



Java Persistence With HIbernate 번역서가 나왔습니다.
어흑.. 몇년째 번역한건지.. 원래 이 책을 프로 스프링 2.5보다 먼저 번역 하고 있었거든요.
사실 그보다 훨씬 더 전에는 물개선생님께서 번역하고 계셨었는데... ㅋ
하이버에 관심있으신 분들을 한권쯤...ㅋ



top


테스트 주도 개발 : 고품질 쾌속개발을 위한 TDD 실천법과 도구

모하니?/Thinking : 2010.06.15 16:19


http://www.hanb.co.kr/book/look.html?isbn=978-89-7914-726-1


드뎌 수원형 책이 나왔다. 캬~~
좋은 책들이 쏟아지려나보다.
좀있으면 토비님 책도 나오고 재성형 책도 나올텐데.. 

왠지 아는 사람들이 쓴 책을 접하게 되니까 기분이 좋다.
책에 적혀있는 텍스트를 읽으면서 저자분 성격에 비추어 어떠한 생각으로 어떤 기분으로 이 글을 썼는지 짐작하는 재미가 쏠쏠해서 일까...

암튼 왠지.. 난 이 분들을 만나봤다는 것 만으로도 참 기분이 좋다.
나도 언젠간...꼭!

그나저나 이책은 줄여서 뭐라고 불러야 하지...
'고품질 쾌속개발을 위한 TDD 실천법과 도구를 보세요.' 헥헥헥..
'빨간 TDD' 보세요 라고 할까나...
top


[회사일] 다대다 관계 서브 그리드 CRUD 완성

프로젝트/SLT : 2010.06.15 14:17



팝업은 팬시박스를 사용했습니다. 이게 저번에 선택했던 컬러 머시기 보다 더 깔끔하더군요.

http://fancybox.net/home

jqGrid 다중 선택 기능을 사용했는데 아주 깔끔합니다. 그리드도 잘 선택한것 같아요.

URL은 RESTful 하게 만든다고 만들어 봤는데..

    @RequestMapping(value="/{id}/rights", method = RequestMethod.GET)
    public void rights(@PathVariable Integer id, Model model, PageParam pageParam){
        model.addAttribute("list", service.rightListOfMember(id, pageParam));
    }

    @RequestMapping(value="/{id}/rights", method = RequestMethod.POST)
    public @ResponseBody String addRights(@PathVariable("id") Integer id, @RequestParam("ids[]") Integer[] ids){
        service.addRightsToMember(id, ids);
        return "success";
    }

    @RequestMapping(value="/{id}/rights", method = RequestMethod.DELETE)
    public @ResponseBody String deleteRights(@PathVariable("id") Integer id, @RequestParam("ids[]") Integer[] ids){
        service.deleteRightsFromMember(id, ids);
        return "success";
    }

잘 동작합니다. 히든 필드 이용해서 _method에 원하는 RequestMethod 실어서 보내주면 되니깐 아주 간단하네요. HTML5 부터는 이런일 안해도 될려나.. 근데 HTML5 지원하는 브라우저가 있어도 그걸 써야 말이죠;;

지금 상태에서 한가지 굉장히 맘이 불편한 코드가 있는데.. 바로 서비스 코드입니다.

    List<Right> rightListOfMember(Integer memberId, PageParam pageParam);

    void addRightsToMember(Integer memberId, Integer[] rightsIds);

    void deleteRightsFromMember(Integer memberId, Integer[] rightsIds);

별로 객체지향적으로 보이지 않아요.. Request Parameter Oriented 프로그래밍 같지 않나요.@PathVariable로 넘어오는 값에도 분명히 바인딩이 적용될텐데... 컨트롤러에서부터 Member 타입으로 바인딩해서 가져오면 저런 코드가..

    List<Right> rightListOfMember(Member member, PageParam pageParam);

    void addRightsToMember(Member member, Right[] rights);

    void deleteRightsFromMember(Member member, Right[] rights);

이렇게 될텐데 말이죠. 흠.. 뭐좀 먹고 와서 해봐야지.

top


[회사일] 서브 그리드 뿌리기

프로젝트/SLT : 2010.06.14 18:53



계정 정보 뷰 화면에 권한 정보를 뿌리도록 서브 그리드를 추가합니다.

화면 코드에서는 jqGrid를 사용해서 코드를 작성합니다.

    <p class="ui-widget-content">
        <h2 class="smdis-pane-title" id="smdis-grid-title">
            <ul class="smdis-left-icons">
                <li id="smdis-add-button" class="ui-state-default ui-corner-all" title="권한 추가"><span class="ui-icon ui-icon-plusthick"></span></li>
                <li id="smdis-delete-refresh" class="ui-state-default ui-corner-all" title="권한 삭제"><span class="ui-icon ui-icon-minusthick"></span></li>
            </ul>
            ${model.loginId} 권한 정보
        </h2>
        <table id="smdis-sub-grid"></table>
        <div id="smdis-sub-pager"></div>
    </p>
    <script type="text/javascript">
        $(function(){
            $("#smdis-sub-grid").jqGrid({
                colNames:['id', '이름', '설명'],
                colModel :[
                    {name:'id', index:'id', width:55, hidden:true},
                    {name:'name', index:'name', width:100},
                    {name:'descr', index:'descr', width:100}
                ],
                pager: '#smdis-sub-pager',
                url:'${baseUrl}/${model.id}/rights'
            });
        });
    </script>

빨간 부분을 그리는데 이런 코드가 필요했습니다. 아직 버튼에 이벤트를 안달아서;; 좀 더 코드가 늘것 같네요. 일단은 서브 그리드를 그리는 기능만 만들겠습니다.

중요한 저 그리드에서 사용할 URL 핸들러를 만드는 거죠.

@Controller
@RequestMapping("/system/member")
public class MemberController extends GenericController<MemberService, MemberRef, Member, MemberSearchParam> {

    @RequestMapping("/{id}/rights")
    public void rights(@PathVariable int id, Model model, PageParam pageParam){
        model.addAttribute("list", service.rightListOfMember(id, pageParam));
    }

}

MemberController에 핸들러를 추가합니다. 평범해 보이지만 스프링 3.0 컨텐츠 네고 기능을 타고 JSON으로 바껴서 보내지게 됩니다. (물론 브라우저 주소창에 저 URL을 입력하면 JSP 페이지를 찾다가 원하는 페이지가 없다고 404 에러 코드가 보여지겠죠.)

public interface MemberService extends GenericService<Member, MemberSearchParam>{

    List<Right> rightListOfMember(int id, PageParam pageParam);
}

MemberService 인터페이스를 타고.. 

@Service
@Transactional
public class MemberServiceImpl extends GenericServiceImpl<MemberDao, Member, MemberSearchParam> implements MemberService {

    @Autowired RightService rightService;

    @Override
    public List<Right> rightListOfMember(int id, PageParam pageParam) {
        Member member = dao.getById(id);
        return rightService.listOfMember(member, pageParam);
    }
}

MemberServiceImpl에 와서.. id에 해당하는 Member를 꺼내주고 나머진 RightService에 위임합니다.

public interface RightService extends GenericService<Right, RightSearchParam>{
    List<Right> listOfMember(Member member, PageParam pageParam);
}

그럼 RightService 인터페이스를 타고..

@Service
@Transactional
public class RightServiceImpl extends GenericServiceImpl<RightDao, Right, RightSearchParam> implements RightService{

    public List<Right> listOfMember(Member member, PageParam pageParam) {
        pageParam.initCurrentPageInfosWith(member.getRights().size());
        return dao.listOf(member, pageParam);
    }
}

RightServcieImpl에서는 PageParam 값을 설정하고, DB에 다녀와야 할 일은 RightDao로 위임합니다.

public interface RightDao extends GenericDao<Right, RightSearchParam> {
    List<Right> listOf(Member member, PageParam pageParam);
}

그럼 RightDao 인터페이스를 타고..

@Repository
public class RightDaoImpl extends GenericDaoImpl<Right, RightSearchParam> implements RightDao {

    protected void applySearchParam(Criteria c, RightSearchParam rightSearchParam) {
        CriteriaUtils.addOptionalLike(c, "name", rightSearchParam.getName());
    }

    public List<Right> listOf(Member member, PageParam pageParam) {
        Criteria c = getCriteriaOf(Right.class);
        c.createAlias("members", "m");
        c.add(Restrictions.eq("m.id", member.getId()));

        applyPagingParam(c, pageParam);
        applyOrderingParam(c, pageParam);
        
        return c.list();
    }
}

여기서 member가 가지고 있는 권한 목록을 가져옵니다.

public class RightDaoImplTest extends SpringTest {

    @Autowired RightDao rightDao;

    @Test
    public void di(){
        assertThat(rightDao, is(notNullValue()));
    }

    @Test
    public void listOf() throws Exception {
        insertXmlData("testData.xml");

        Member member = new Member();
        member.setId(1);

        PageParam pageParam = new PageParam();
        pageParam.setRows(20);
        pageParam.setPage(0);

        List<Right> rights = rightDao.listOf(member, pageParam);
        assertThat(rights.size(), is(2));

        Right right = (Right)rights.get(0);
        assertThat(right.getId(), is(1));
        assertThat(right.getName(), is("admin"));
    }

}

<dataset>
<member id="1" loginid="whiteship" password="1" email="email@com" name="기선" />

    <rights id="1" name="admin"/>
    <rights id="2" name="user"/>

    <member_rights members_id="1" rights_id="1"/>
    <member_rights members_id="1" rights_id="2"/>
</dataset>

DAO 테스트에서는 원하는 Right를 가져오는지 확인합니다.

    @Test
    public void listOfMember() throws Exception {
        insertXmlData("testData.xml");
        Member member = memberDao.getById(1);

        PageParam pageParam = new PageParam();
        pageParam.setPage(0);
        pageParam.setRows(20);

        List<Right> rs = rightService.listOfMember(member, pageParam);

        assertThat(pageParam.getListSize(), is(2));
        assertThat(pageParam.getTotalPageSize(), is(1));
    }

서비스 테스트에서는 PageParam에 값이 잘 설정되는지 확인했습니다.

이제 잘 될 줄 알고 서버를 켜고 돌려봤더니.. 이런;;;


이런 문제가 생기더군요. 물론 그리드에도 제대로 표시가 되지 않았습니다.

Member <--> Right 양방향 관계로 설정했을 때 쌍방에서 가지고 있는 rights나 members가 비어있더라도 JSON 뷰로 넘어갔을 때 OSIV 필터에 의해서 무조건 값을 읽어오게 됩니다. 실제로 해당 레퍼런스가 아주 null이었으면 가져오려는 시도도 안할텐데 null도 아닙니다. Member나 Right에서 초기화 해준적도 없는데도 null이 아닙니다. 그건 하이버네이트 Lazy loading 하려고 프록시 객체를 만들어 놔서 그런것 같더군요. 어떻게 막아야 하나... 고민을 했습니다.

프로젝션과 DTO로 해결하려고 했으나.. 실패. 

흠.. JSON으로 만들지 않았으면 좋겠는데.. 그런거 없나 찾아보다 @JsonIgnore 발견.
그걸 적용해서 성공했습니다.


휴.. 집에가야지
top


[회사일] Right 도메인 CRUD 구현 도메인 클래스부터 화면까지

프로젝트/SLT : 2010.06.14 16:21


@Entity(name="rights")
public class Right {

    @Id
@GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(length = 100, unique = true)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    private String name;

    @Column
    private String descr;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescr() {
        return descr;
    }

    public void setDescr(String descr) {
        this.descr = descr;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Right right = (Right) o;

        if (name != null ? !name.equals(right.name) : right.name != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return name != null ? name.hashCode() : 0;
    }
}

필드 몇개 추가하고 JPA 애노테이션 추가한 다음 Member 쪽에 Set 타입으로 @ManyToMany 연결.
이때 right는 DB 예약어일 수 있으니 rights라는 이름을 사용하도록 설정 해줌. 
IDEA가 코드 생성 메뉴 상숑해서 hashCode랑 equals 구현. 이때 name 필드만 사용.

자 이제 GenericXXX를 이용해서 CRUD 코드 5분 완성.

public interface RightDao extends GenericDao<Right, RightSearchParam> {
}

@Repository
public class RightDaoImpl extends GenericDaoImpl<Right, RightSearchParam> implements RightDao {

    protected void applySearchParam(Criteria c, RightSearchParam rightSearchParam) {
        CriteriaUtils.addOptionalLike(c, "name", rightSearchParam.getName());
    }
    
}

public interface RightService extends GenericService<Right, RightSearchParam>{
}

@Service
@Transactional
public class RightServiceImpl extends GenericServiceImpl<RightDao, Right, RightSearchParam> implements RightService{

}

@Controller
@RequestMapping("/system/right")
public class RightController extends GenericController<RightService, RightRef, Right, RightSearchParam> {
}

@Component
public class RightRef {
}

public class RightSearchParam {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

이걸로 서버쪽 코딩 끝. 이제 화면 코드는 태그 파일 활용해서 역시 5분 완성.

<page:editpage label="권한 수정">
    <f:input label="권한" path="name" />
    <f:textarea label="설명" path="descr" />
</page:editpage>

<page:mgtpage label="권한 관리">
    <page:search label="권한 검색">
        <s:input label="이름" path="name" />
    </page:search>
    <script type="text/javascript">
        $(function() {
            $("#smdis-grid").jqGrid({
                colNames:['id', '권한명', '설명'],
                colModel :[
                    {name:'id', index:'id', width:55, hidden:true},
                    {name:'name', index:'name', width:100},
                    {name:'descr', index:'descr', width:100}
                ]
            });
        });
    </script>
</page:mgtpage>

<page:newpage label="새 권한">
    <f:input label="이름" path="name"/>
    <f:textarea label="설명" path="descr" />
</page:newpage>

<page:viewpage label="계정 정보">
    <v:text label="id" value="${model.id}"/>
    <v:text label="이름" value="${model.name}"/>
    <v:text label="설명" value="${model.descr}"/>
</page:viewpage>

끝..


도메인 코딩 5분, 서버 코딩 5분, 화면 코딩 5분...

초간단 CRUD 구현하는데 15분.













top


[회사일] DateRange 추가

프로젝트/SLT : 2010.06.14 14:33



오른쪽에 보이는 검색 필드를 MemberSearchingParam에 추가하는데 아무래도 이런 범위 검색 필드는 자주 사용될 것 같더군요.

그래서 별도의 클래스로 분리했습니다.

public class DateRange {

    @DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
    private Date from;

    @DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
    private Date to;

...
}

나머진 요다(?) 조다(?) 라이브러리만 넣어두면 기본 포매터가 동작해서 잘 바인딩 해 줍니다. 아.. 암튼 이번엔 바인딩 얘기가 아니라..  저 클래스 DateRange 얘기입니다.

MemberSearchParam을 MemberDao코드에 적용합니다.

@Repository
public class MemberDaoImpl extends GenericDaoImpl<Member, MemberSearchParam> implements MemberDao {

    protected void applySearchParam(Criteria c, MemberSearchParam searchParam) {
        CriteriaUtils.addOptionalLike(c, "loginId", searchParam.getLoginId());
        CriteriaUtils.addOptionalLike(c, "name", searchParam.getName());
        CriteriaUtils.addOptionalLike(c, "email", searchParam.getEmail());
        CriteriaUtils.addOptionalLike(c, "phoneNumber", searchParam.getPhoneNumber());
        CriteriaUtils.addOptionalLike(c, "jobTiTle", searchParam.getJobTitle());
        CriteriaUtils.addOptionaBetween(c, "birthDay", searchParam.getBirthDayRange());
    }
}

이게 DAO 코드 전부입니다.

유틸 코드로 가야겠네요.

 
   @Test
    public void testOptionalBetween() {
        String dateRangeSearchingFiled = "birthDay";

        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, null);
        verify(c, times(0)).add(any(Criterion.class));

        reset(c);
        DateRange dateRange = new DateRange();
        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, dateRange);
        verify(c, times(0)).add(any(Criterion.class));

        reset(c);
        dateRange.setFrom(new Date());
        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, dateRange);
        verify(c, times(1)).add(Restrictions.ge(any(String.class), dateRange.getFrom()));

        reset(c);
        dateRange.setTo(new Date());
        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, dateRange);
        verify(c, times(1)).add(Restrictions.between(any(String.class), dateRange.getFrom(), dateRange.getTo()));

        reset(c);
        dateRange.setFrom(null);
        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, dateRange);
        verify(c, times(1)).add(Restrictions.le(any(String.class), dateRange.getTo()));
    }

장황해 보이지만 별거 없는 테스트 입니다.

  
 public static void addOptionaBetween(Criteria c, String fieldName, DateRange dateRange) {
        if(dateRange == null)
            return;

        if(dateRange.getFrom() != null && dateRange.getTo() != null){
            c.add(Restrictions.between(fieldName, dateRange.getFrom(), dateRange.getTo()));
        }

        if(dateRange.getFrom() != null && dateRange.getTo() == null){
            c.add(Restrictions.ge(fieldName, dateRange.getFrom()));
        }

        if(dateRange.getFrom() == null && dateRange.getTo() != null){
            c.add(Restrictions.le(fieldName, dateRange.getTo()));
        }
    }

이 코드를 테스트하고 있죠. 그런데 코드가 좀;; Don't Tell, Ask가 생각나더군요. 저런 조건 문을 inline 뭐시기 리팩토링으로 분리할 수도 있지만 그보다 일단 if 조건문에 들어있는 내용이 들어있어야 할 위치가... DateRange가 되어야 할 것 같단 생각이 듭니다.

머 일단 if안에 있는 조건문 들을 extract method 리팩토링으로 빼냅니다. 이런 리팩토링을 인라인 머시기 리팩토링이라고 했던것 같은데 잊어버렸네요. @_@;;

 
   public static void addOptionaBetween(Criteria c, String fieldName, DateRange dateRange) {
        if(dateRange == null)
            return;

        if(hasFromAndTo(dateRange)){
            c.add(Restrictions.between(fieldName, dateRange.getFrom(), dateRange.getTo()));
        }

        if(hasFromOnly(dateRange)){
            c.add(Restrictions.ge(fieldName, dateRange.getFrom()));
        }

        if(hasToOnly(dateRange)){
            c.add(Restrictions.le(fieldName, dateRange.getTo()));
        }
    }

    private static boolean hasToOnly(DateRange dateRange) {
        return dateRange.getFrom() == null && dateRange.getTo() != null;
    }

    private static boolean hasFromOnly(DateRange dateRange) {
        return dateRange.getFrom() != null && dateRange.getTo() == null;
    }

    private static boolean hasFromAndTo(DateRange dateRange) {
        return dateRange.getFrom() != null && dateRange.getTo() != null;
    }

그럼 이렇게 되는데 여기서 아래 세개의 static 메서드를 DateRange 쪽 멤버 메서드로 옯겨줍니다.

public class DateRange {

    @DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
    private Date from;

    @DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
    private Date to;

...

    public boolean hasToOnly(DateRange dateRange) {
        return dateRange.getFrom() == null && dateRange.getTo() != null;
    }

    public boolean hasFromOnly(DateRange dateRange) {
        return dateRange.getFrom() != null && dateRange.getTo() == null;
    }

    public boolean hasFromAndTo(DateRange dateRange) {
        return dateRange.getFrom() != null && dateRange.getTo() != null;
    }
}

그리고 CriteriaUtils 코드를 수정해주면 되죠.

  
 public static void addOptionaBetween(Criteria c, String fieldName, DateRange dateRange) {
        if(dateRange == null)
            return;

        if(dateRange.hasFromAndTo(dateRange)){
            c.add(Restrictions.between(fieldName, dateRange.getFrom(), dateRange.getTo()));
        }

        if(dateRange.hasFromOnly(dateRange)){
            c.add(Restrictions.ge(fieldName, dateRange.getFrom()));
        }

        if(dateRange.hasToOnly(dateRange)){
            c.add(Restrictions.le(fieldName, dateRange.getTo()));
        }
    }

마지막으로 테스트 한번 돌려주면 깔끔하게 끝~









top

TAG DateRange

[회사일] view.jsp 태그파일 적용

프로젝트/SLT : 2010.06.14 12:21


member/view.jsp

<page:viewpage label="계정 정보">
    <v:text label="id" value="${model.id}"/>
    <v:text label="로그인ID" value="${model.loginId}"/>
    <v:text label="이름" value="${model.name}"/>
    <v:text label="이메일" value="${model.email}"/>
</page:viewpage>

code/view.jsp

<page:viewpage label="코드 정보">
    <v:text label="id" value="${model.id}" />
    <v:text label="코드종류" value="${model.cate.descr}" />
    <v:text label="코드값" value="${model.code}" />
    <v:text label="코드명" value="${model.name}" />
    <v:text label="설명" value="${model.descr}" />
</page:viewpage>

흠냐.. 뷰용 태그 파일 네임스페이스는 v로 정했습니다.



자 거럼 이제 찍어낼 준비가 거의 다 된 것 같군요... 흠.. 아니군요. 아직 갈길이 멀군요.
일단 Member에 Date 타입 필드 부터 추가해봐야겠습니다.

다음에는 Right 도메인을 찍어내고, Member랑 다대다 관계 연결해서 뷰까지 또 프레임워크성 코드를 빼내야 하고...

그담엔 시큐리티 적용하고..

그담엔 자동완성 만들고.. 

엑셀 다운/업로드 만들고..

멀구나 멀어..
top


[회사일] new.jsp 태그파일 적용

프로젝트/SLT : 2010.06.14 11:54


code/new.jsp

<page:newpage label="새 코드">
    <f:select label="코드종류" path="cate" items="${ref.codeCateList}" />
    <f:input label="코드값" path="code" />
    <f:input label="코드명" path="name" />
    <f:textarea label="설명" path="descr" />
</page:newpage>

member/new.jsp

<page:newpage label="새 계정">
    <f:input label="로그인 ID" path="loginId"/>
    <f:input label="비밀번호" path="password"/>
    <f:input label="이름" path="name"/>
    <f:input label="이메일" path="email"/>
</page:newpage>



new.jsp 페이지는 edit.jsp랑 비슷했으니까 간단간단 이제 view.jsp 페이지만 남았군요.
top




: 1 : 2 : 3 : 4 : 5 : ··· : 88 :