설정 간편화

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

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

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

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

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

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

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

꽤 멋지지 않은가? 훗

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

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

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

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

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

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

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


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

데이터 바인딩 간편화

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

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


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

public class Account {

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

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

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

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

}

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

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

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

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

요약

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

출처 : http://whiteship.me/2517

스프링 3.0의 RESTful 기능

- URI 템플릿
@PathVariable 애노테이션으로 URI의 특정 위치의 값을 맵핑해올 수 있음.
@RequestMapping("/hotels/{hotelId}")
public String getHotel(@PathVariable hotelId, ModelMap model) {
  List<Hotel> hotels = hotelService.getHotels();
  model.addAttribute("hotels", hotels);
  return "hotels";
}
이때 /hotels/1 이런 요청이 들어오면 hotelId 매개변수로 1이라는 값을 받아올 수 있음.

두 개 이상 맵핑 할 수도 있고 변수명을 다르게 줄 수도 있음.
@RequestMapping(value="/hotels/{hotel}/bookings/{booking}", method=RequestMethod.GET)
public String getBooking(@PathVariable("hotel") long hotelId, @PathVariable("booking") long bookingId, ModelMap model) {
  Hotel hotel = hotelService.getHotel(hotelId);
  Booking booking = hotel.getBooking(bookingId);
  model.addAttribute("booking", booking);
  return "booking";
}

Ant 스타일 경로를 지정할 수도 있음.
@RequestMapping(value="/hotels/*/bookings/{booking}", method=RequestMethod.GET)
public String getBooking(@PathVariable("booking") long bookingId, ModelMap model) {

}

데이터 바인딩도 사용할 수 있음.
@InitBinder
public void initBinder(WebDataBinder binder) {
  binder.initBeanPropertyAccess();
  SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
  dateFormat.setLenient(false);
  binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}

@RequestMapping("/hotels/{hotel}/dates/{date}")
public void date(@PathVariable("hotel") String hotel, @PathVariable Date date)
                throws IOException {

}
/hotels/1/dates/2008-12-18 이런 요청을 받으면 2008-12-18 이 부분 문자열을 Date 타입으로 바인딩 해 줌.

- Content Negotiation
Accept HTTP 헤더로 뷰를 판단함. 서버 응답은 Content-Type를 통해 뷰를 전달한다. 이런 과정을 content negotiation이라고 한다.

주요 클래스: ContentNegotiatingViewResolver

- Views
RSS 피드를 만들 때 사용할 수 있는 뷰: AbstractAtomFeedView, AbstractRssFeedView
XML 뷰: MarshallingView
JSON 뷰: JacksonJsonView

- HTTP Method Conversion
HTTP에서는 GET, POST, DELETE, PUT을 제공하지만 HTML은 GET, POST만 지원한다. 따라서 POST를 사용하면서 hidden parameter로 Method를 명시하면 해당 Method로 변환해주는 HiddenHttpMethodFilter 제공.
스프링 form 태그에서 이 기능을 지원하기 때문에 form태그의 method에 delete, put도 사용 가능.
<form:form method="delete">
    <p class="submit"><input type="submit" value="Delete Pet"/></p>
</form:form>

@RequestMapping(method = RequestMethod.DELETE)
public String deletePet(@PathVariable int ownerId, @PathVariable int petId) {
    this.clinic.deletePet(petId);
    return "redirect:/owners/" + ownerId;
}

- ETag support
ETga는 HTTP 1.1 호환 응답 헤더로 해당 URL에 있는 컨텐츠가 변경 됐는지 확인할 때 사용.
스프링은 ShallowEtagHeaderFilter를 제공하여 JSP로 랜더링한 결과를 캐싱하고 MD5 해시를 만들고 응답에 ETag를 반환해준다. 다음에도 사용자가 같은 자원을 요청하면 이전에 만든 해시를 If-None-Match 값으로 사용한다. 두 개 해시값을 비교해서 같으면 304(Not Modified)를 반환한다.

차후에 제공할 deep ETag는 모데인 객체나 RDBMS 테이블과 관련되어 있어 좀 더 복잡한데 JPA의 @Version(낙천적-롹킹할 때 사용하는 애노테이션) 기반 또는 ApsectJ의 애스팩트로 지원할 예정~ 이라고 합니다.

출처 : http://whiteship.me/2163


Spring core에서는 AOP를 이용하여 처리내용의 전, 후에 일괄적으로 적용하고 싶은 처리를 추가할수가 있다.
하지만 AOP는 interface를 구현한 곳에만 사용할 수가 있습니다.
그래서 Spring MVC의 Controller에는 상용할수 없죠
자 그럼 만약 로그인 체크를 해야한다면 모든 Controller에 로그인 체크 로직을 넣어줘야할까?. 아니다!
그럼 상위 Controller를 만들고 여기에 로그인 체크로직을 넣고 모든 Controller가 이를 상속 받아 쓰면 어떨까. 그남아 났군..

이런 것을 위해 Spring MVC 에서는 HandlerMapping이 HandlerInterceptor를 이용하해서 Controller가 요청하는 처리의 전과, 후후에 원하는 기능을 수행할수 있도록 해준다고한다.
( 뭐 깊숙한 내용까지 알려면 더 공부를하고, 아니면 그냥 Controller에서도 AOP와 같이 쓸수 있도록 제공해준다고 알자 )

HandlerInterceptor는 세가지 메소드를 정의한다

더보기

- preHandle : 클라이언트의 요청을 컨트롤러에 전달하기 전에 호출됨. false를 리턴하면 다음 내용은 실행하지 않는다.
- postHandle : 클라이언트의 요청을 처리한 뒤에 호출됨. 컨트롤러에서 예외가 발생되면 실행하지 않는다.
- afterCompletion : 클라이언트 요청 처리뒤 클리이언트에 뷰를 통해 응답을 전송한뒤 실행 됨. 뷰를 생설항때 예외가 발생해도 실행된다.

Interceptor의 구현
org.springframework.web.servlet.HandlerInterceptor 를 직접 구현해서 클래스를 작성해도 되지만 이러면 세개의 메소드를 모두 구현해야한다. 즉 preHandle 만 필요해도 postHandle과 afterCompletion 두 메소드를 모두 구현해야한다.
이를위해 Spring에는 HandlerInterceptorAdaptor 클래스를 제공한다.
이는 이미 HandlerInterceptor interface를 구현한 클래스로 위의 세개의 메소드를 이미 구현하고 있어 필요한것만 구현해서 사용하면 된다.

- Interceptor 구현


public class LoginCheckInterceptor extends HandlerInterceptorAdaptor {
    @Override
    public boolean preHandle(HttpServletRequest request,
                                         HttpServletResponse response, 
                                         Object handler) throws Exception{ 
        if ( !isLogin() ) { 
            return false;
        }
        return true;
    }
}


- dispatcher-servlet.xml의Interceptor 설정

<bean id="annotationMapper"
            class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
    <property name="interceptors">
        <list>
            <ref bean="userLoginInterceptor"/>
        </list>
    </property>
    <property name="alwaysUseFullPath" value="true"/>
</bean>

<bean id="userLoginInterceptor" class="com.questionbox.interceptor.UserLoginInterceptor" />

위와 같이 할때는 모든 페이지 URL에서 userLoginInterceptor를 실행하게 된다.

그리고
<list>
   
<ref bean="userLoginInterceptor"/>
    <ref bean="userLoginInterceptor2"/>
    <ref bean="userLoginInterceptor3"/>
</list>

과 같이 여러 인터페이스를 줄수도 있다.

참고 사이트 : http://www.scottmurphy.info/spring_framework_annotation_based_controller_interceptors

'Program > Java' 카테고리의 다른 글

어노테이션을 이용한 설정1 - context:annotation-config  (0) 2009.12.27
Spring3.0 @MVC REST  (0) 2009.12.27
Spring MVC Annotation 기초  (2) 2009.12.27
Annotation (since tiger / 1.5)  (0) 2009.12.27
About JAXB  (0) 2009.12.27
Spring 2.5 버전서부터 추간된 기능중에 Annotation(어노테이션)의 기능이 있다.
실제로 사용해본결과 이전의 xml에서 설정하여 사용하는것보단 훨씬 개발하기 편해진것은 확실하다.
( 솔찍히 Spring은 이번에 처음 써보는것나 마찮가지다. )
아직 많은 사용법을 모르지만  간단하게 정리를 해보고자 한다.

* 참고로 이문서는 어디까지나 어노테이션의 사용에 대한 것이지 Spring에 기본적인 내용은 필자도 잘아지 못한다.
( 필자도 앞으로 공부해나가야할 부분이다. )
즉, 간단하게 이미 spring 2.5 이전 버전을 쓰시는 분들은 그냥 "어노테이션을 이렇게 쓰는구나" 정도로 이해해 주시고
Spring을 안쓰시다 2.5를 처음 쓰시는 분들은 Spring의 이전 버전도 어느정도 지식을 쌓으시는 것이 좋을것으로 판단 된다.

Spring 2.5( 중에서도 Spring MVC) 에서 annotation을 사용하기 위해서는 dispatcher-servlet.xml 에 다음의 설정을 추가해야한다.
그래야만 어노테이션이 적용된 class( @Controller, @Service, @Repository 를 포함한 class )를 로딩할수가 있다.

- dispatcher-servlet.xml -
<context:component-scan base-package="kr.pe.jabsiri" /> 가존 작성중 틀린것


2009-06-26 수정내용
dispactcher-servlet.xml에는 아래와 같이해서 Controller의 어노테이션만 로딩을 하고
<context:component-scan base-package="com.enz.adnad" use-default-filters="false">	<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"></context:include-filter>
</context:component-scan>
applicationContext.xml에는 아래와 같이해서 Controller를 제외한 어노테이션만 로딩을 해야한다.
<context:component-scan base-package="com.enz.adnad">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"></context:exclude-filter>
</context:component-scan>


아래는 dispactcher-servlet.xml 에 설정하는 ViewResolver 로써 /WEB-INF/spring/ 로 시작하고 .jsp 로 끝나는 파일을 찾아 옵니다.
controller에서 return "hello/hello_jabsiri"; 와 같이 하면 /WEB-INF/spring/hello/hello_jabsiri.jsp 파일을 찾습니다.
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/spring/" p:suffix=".jsp">

1. @Controller
위에서 설정한 컴포넌트 스캔을 통하여 로딩된( <bean> tag를 써서 일일이 등록된 class도 )@Controller를 적용한 Class는 DefaultAnnotationHandlerMapping을 통해 컨트롤로 사용된다.
( DefaultAnnotationHandlerMapping을 통해 컨트롤러로 등록이 되는지 어쩐지는 나도 잘 모른다 그냥그렇다고 한다. 즉, 기본적인 Spring에 대한 공부는 각자의 몫이고 필자 또한 나중에 정리를 할 것이다. )

다음은 사용의 예이다.
@Controller
public class HelloJabsiri {
@RequestMapping(value="/hello/hello_jabsiri.jab")
public String helloJabsiri(){
return "hello/hello_jabsiri"; //리턴값은 .jsp 확장자를 뺀 경로
}
}
return 값의 "hello/hello_jabsiri" 는 dispatcher-servlet.xml 에서 정의한viewResolver 의  p:suffix=".jsp" p:prefix="/WEB-INF/spring/" 내용에 해당하는곳에서 jsp를 찾는다.
/WEB-INF/spring/[ return 값].jsp 의 패턴에 해당하는 jsp를 찾는다.

여기서 잠시 주목할 사항은 helloJabsiri() 메소드는 아무런 파라미터도 받지 않는다는 것이다.
맞다 아무것도 안받아도된다.!!
Spring MVC에서 MultiController ( 명칭이 맞나;? )는 파라미터를 받아도되고 않받아도 되고, 받되 순서가 뒤죽 박죽이어도 된다.

다음과 같이 Member bean을 받을수 있는데 이는 html상의 form input type의 id와 Member 객체들의 맴버변수가 같은것을 알아서 매핑시켜 값을 넘겨주기도 한다.

@RequestMapping("member.do")
public String member(Member member, ModelMap modelMap){
modelMap.put("member", member);
return "member/member";
}
이것외에 @RequestParam과 메소드의 파라미터를 받는 방법등은 첨부파일을 받아서 꼭!!! 보길 바란다.


2. @RequestMapping
위의 HelloJabsiri의 Controller에 보면 @RequestMapping(value="/hello/hello_jabsiri.jab") 이 보일 것이다.
이는 @RequestMapping의 value 형태의 url이 들어오면 helloJabsiri method를 호출 하겠다는 것이다.
위의 코드는 다음과 같이 바뀔수 있다.

@Controller
@RequestMapping("/hello/*")
public class HelloJabsiri {
@RequestMapping(value="hello_jabsiri.jab")
public String helloJabsiri(){
return "hello/hello_jabsiri";
}
}

3. @Autowired
@Autowired 어노테이션은 Spring에서 의존관계를 자동으로 설정할때 사용된다.
이 어노테이션은 생성자, 필드, 메서드 세곳에 적용이 가능하다.

아래와 같이 설정해주면 사용할수 있지만 위에서처럼 scan으로 등록했으므로 안해도 된다.
<bean class=org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor />

(필자는 그냥  <context:component-scan base-package="kr.pe.jabsiri" /> 설정후 아무것도 하지 않는다. 뭐 몰라서 그런것도 있다. )

사용 코드는는 다음과 같다.
@Controller
public class HelloJabsiriController {
@Autowired
private HelloJabsiriService helloJabsiriService;
...........
}

@Service("helloJabsiriService")
public class HelloJabsiriServiceImpl implements HelloJabsiriService {
@Autowired
private HelloJabsiriDao helloJabsiriDao;
...........
}
위의 코드는 Controller에서 HelloJabsiriServiceImpl 의 interface인 HelloJabsiriService를 의존관계로 등록 한것이다.
자세히 보면 @Autowired를 적용한 변수명과 @Service 안에있는 String 값이 같다는걸 눈치챌수 있다.

4. @Service("xxxService")
@Service를 적용한 Class는 비지니스 로직이 들어가는 Service로 등록이 된다.
@Service("helloJabsiriService")
public class HelloJabsiriServiceImpl implements HelloJabsiriService {
@Autowired
private HelloJabsiriDao helloJabsiriDao;
public void helloJabsiri() {
System.out.println( "HelloJabsiriServiceImpl :: helloJabsiri()");
helloJabsiriDao.selectHelloJabsiri();
}
}
helloJabsiriDao.selectHelloJabsiri(); 와 같이 @Autowired를 이용한 객체를 이용하여 Dao 객체를 호출한다.

 
5. @Repository("xxxDao")
@Repository를 적용한 Class는 DataBaseAccess를 할수 있는 Dao로 등록된다.
@Repository("helloJabsiriDao")
public class HelloJabsiriDaoImpl implements HelloJabsiriDao {
public void selectHelloJabsiri() {
System.out.println("HelloJabsiriDaoImpl :: selectHelloJabsiri()");
}
}


Controller에 있는 @Autowired는 @Service("xxxService")에 등록된 xxxService와 변수명이 같아야 하며
Service에 있는 @Autowired는 @Repository("xxxDao")에 등로된 xxDao와 변수명이 같아야 한다.



6. @Transactional (이건 추후 작성 하겠음 )

'Program > Java' 카테고리의 다른 글

Spring3.0 @MVC REST  (0) 2009.12.27
Spring MVC - Annotation Base HandlerInterceptor  (0) 2009.12.27
Annotation (since tiger / 1.5)  (0) 2009.12.27
About JAXB  (0) 2009.12.27
StringUtils  (0) 2009.12.22

+ Recent posts