@Component
기본 스테레오타입
이 애노테이션을 붙인 클래스는 스프링 빈이 된다.
빈의 기본 행위를 변경하지 않지만, 스피링 차기 버전에서 부가적인 의미를 부여할 것임

@Controller
스프링은 이 스테레오타입 붙은 클래스를 스프링 MVC 지원에서 컨트롤러로 사용한다.

@Repository
이 애노테이션을 가진 클래스는 저장소를 나타낸다.
(예를 들어, DAO 등)
자동 예외 변환을 해 줌

@Service
이 애노테이션은 애플리케이션에서 비즈니스 로직을 구현한 클래스에 붙인다.
빈의 기본 행위를 변경하지 않지만, 스피링 차기 버전에서 부가적인 의미를 부여할 것임


출처 - http://kerrigancap.truesolution.co.kr/ezboard.php?BID=study&GID=root&UID=1996&mode=view


applicationContext를 xml이 아닌 애노테이션으로 지정하는 방법이 있다.

스프링은 기본적으로 4가지 타입의 애노테이션을 지원하며

원하는 경우 확장해서 자신의 애노테이션을 지정할 수 있다.


@Component : 가장 기본적인 애노테이션 (아래 해당 사항이 없는 경우)

@Repository : 데이터 액세스 계층의 DAO또는 리포지토리 클래스 (http://smack.kr/300)

@Service : 서비스 계층의 클래스에 사용

@Controller : 프레젠테이션 계층의 MVC 컨트롤러에 사용


출처 - http://bluesky.thoth.kr/?document_srl=7067643






없으면 에러 낼거야
이건 2.5이전부터 있던건데 그냥 한번 짚고 넘어 갑니다.
@Required 가 붙은 setter 메소드는 반드시 XML 설정에 의존성 삽입이 정의 되어 있어야 합니다.
(@Autowired도 안됩니다.)
XML 설정에 명시적인 의존성 삽입이 없다면 런타임 예외가 던져 집니다.
이 애노테이션을 처리하기 위한 후처리기는 다음과 같습니다.

<bean class="org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor"/>


자동으로 묶어 줄게 
@Autowried 는 타입에 일치하는 bean을 의존성 삽입 해줍니다.
필드에 사용 할 경우 setter가 존재하지 않아도 됩니다.
@Autowried
private Foo foo;

여러개의 인자를 가진 메소드에 사용 할 수 있습니다.
@Autowried
public void setUp(Foo foo, Bar bar) {
    this.foo = foo;
    this.bar = bar;
}

@Autowired의 속성인 required의 기본값이 true이기 때문에 발견되는 bean이 없다면 예외가 던져 집니다.
발견 되는 bean이 없더라도 예외가 던져지지 않기를 원하면 required 속성을 fasle로 설정하면 됩니다.
@Autowried(required=false)
private Foo foo;

일치하는 타입을 찾기 때문에 발견되는 bean이 여러개인 경우 예외가 던져 집니다.
이를 방지 하기 위해 <bean /> 요소의 추가된 속성인 primary를 설정 합니다.
발견되는 bean이 여러개 일지라도 primary가 설정된 bean이 참조 됩니다.
<bean id="foo" class="example.Foo" primary="true" />
<bean id="foo2" class="example.Foo"/>

이 애노테이션을 처리하기 위한 후처리기는 다음과 같습니다.
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>


정확한 이름을 알려줄게 
@Autowired을 사용 할 때 타입으로 bean을 찾기 때문에 여러개의 bean이 발견되면 primary로 해결 했습니다.  하지만 @Qualifier를 이용해 찾으려는 bean의 id를 넘길 수 있습니다.
@Autowried
@Qualifier("foo")
private Foo foo;

필드에만 사용 가능 하기 때문에 메소드에 적용시 다음과 같이 설정 합니다.
@Autowried
public void setUp(@Qualifier("foo") Foo foo, @Qualifier("foo2") Foo foo2) {
    this.foo = foo;
    this.foo2 = foo2;
}

이 애노테이션을 처리하기 위한 후처리기는 @Autowried의 후처리기와 동일 합니다.


JSR-250 애노테이션 지원
-@Resource
-@PostConstruct
-@PreDestroy
위 세가지 애노테이션을 사용하기 위해서는 후처리기를 등록 해야 합니다.
<bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor"/> 

@Resource
JNDI의 리소스 및 Spring 컨테이너의 bean을 찾아 autowring 합니다.
name 속성을 생략 할 경우 해당 필드명으로 찾고 해당 bean이 발견되지 않는다면 타입으로 찾습니다.

필드에 사용 할 경우 setter가 존재하지 않아도 됩니다.
@Resource(name="dataSource")
private DataSource dataSource;

한개의 인자를 가진 setter 메소드만 적용 할 수 있습니다.
@Resource(name="dataSource")
public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
}

타입으로 찾지 않길 원한다면 다음과 같은 설정을 합니다.
<bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor">
    <property name="fallbackToDefaultTypeMatch" value="false"></property>
</bean>

JNDI 리소스만 참조 하려면 다음과 같은 설정을 합니다.
<bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor">
    <property name="alwaysUseJndiLookup" value="true"/>
</bean>

@PostConstruct 와 @PreDestroy
기존에는 생명 주기를 다루는 방법이 2가지가 있었습니다.
1.InitializingBean, DisposableBean을 구현하는 방법
2.XML 설정파일에서 <bean /> 요소의 init-method, destroy-method 속성 설정

2.5에서 추가된 두 애노테이션은 기존의 방식과 동일한 결과를 가져 옵니다.
@PostConstruct
public void init() {
    ...
}
    
@PreDestroy
public void preDestroy() {
    ...
}

만약 3가지 방식 모두 사용할 경우 
우선순위는 애노테이션->XML설정->인터페이스 구현 입니다.


이거 하나면 OK
위에서 나온 애노테이션을 사용하기 위해서는 3가지의 BeanPostProcessor를 등록해야 했습니다.
하지만 이거  하나면 3개가 모두 등록 됩니다.
<context:annotation-config />


알아서 찾아줄게
위에서 나온 애노테이션으로 XML설정을 줄이더라도 bean 선언은 반드시 해야 했습니다.
하지만 component-scan 을 이용하면 bean선언 조차 생략 할 수 있습니다.
component-scan의 대상이 되기 위해서는 스테레오타입 애노테이션을 사용해야 합니다.

4개의 스테레오타입 애노테이션
@Component
- 스테레오타입 애노테이션의 조상 입니다.
@Controller
-Spring MVC에서 컨트롤러로 인식 합니다.
@Service
-역할부여 없이 스캔 대상이 되는데 비즈니스 클래스에 사용하면 될 것 같습니다.
@Repository
-DAO에 사용되며 DB Exception을 DataAccessException으로 변환해 줍니다.

간단한 예제 입니다. 
테스트에 사용될 2개의 클래스
@Component("bar")
public class Bar {
}

@Component("foo")
public class Foo  {
    @Autowired
    private Bar bar;
    
    public Bar getBar() {
        return bar;
    }
}

지정된 패키지의 하위패키지까지 재귀적으로 스테레오타입이 있는 클래스를 찾습니다.
필터를 이용해 제외 검색 대상에서 제외 시킬 수 있으며
<context:annotation-config /> 까지 자동으로 등록됩니다.
<context:component-scan base-package="example" />

테스트는 통과 합니다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"Beans.xml"})
public class ScanTest {
    @Autowired
    private Foo foo;
    
    @Test
    public void fooTest() {
        assertNotNull(foo.getBar());
    }
}

@Scope를 함께 사용 하여 Scope를 설정 할 수 있습니다.
@Scope("request")
@Component("bar")
public class Bar {
}
singleton인 foo에서 request scope인 bar를 사용 하면 어떻게 될 까요?
역시 scope문제가 발생 합니다.

이 문제를 해결하기 위해 <context:component-scan /> 요소의 scoped-proxy 속성이 존재 합니다.
scoped-proxy 속성은 <aop:scoped-poxy/> 요소처럼 WebApplicationContext 에서만 유효하며 
"session", "globalSession", "request" 이외의 scope는 무시 됩니다.

아래 3가지 값을 지정 할 수 있습니다.
no - 디폴트값, proxy를 생성하지 않습니다.
interfaces - JDK Dynamic Proxy를 이용한 Proxy 생성
targetClass - CGLIB를 이용한 Proxy 생성

Bar는 인터페이스 기반이 아니기 때문에 CGLIB를 이용 했습니다.
<context:component-scan base-package="example" scoped-proxy="targetClass"/>

만약 스캔된 bean들 중 @Scope의 값이 "session", "request" 일 때 
scoped-proxy 속성 값이 "targetClass", "interfaces" 가 아닐경우 예외가 던져지고 초기화에 실패 합니다.

component-scan을 이용하면 method-lookup도 불가능 하므로 참조되는 bean이 web-scope가 아니고 
prototype일 경우의 문제는 해결 할수 없을것 같습니다.


출처 - http://jjaeko.tistory.com/20






1. 스프링 2.5 부터 어노테이션 기반 설정 가능
@Component 스테레오타입 애노테이션의 조상입니다.
@Controller Spring MVC에서 컨트롤러로 인식합니다.
@Service 역할부여 없이 스캔 대상이 되는데 비즈니스 클래스에 사용하면 될 것 같습니다.
@Repository DAO에 사용되며 DB Exception을 DataAccessException으로 변환해 줍니다.

2. xml 파일에 <context:component-scan base-package="xxx.yyy.zzz" />와
<context:annotation-config /> 태그를 사용하여 자동 빈 스캔 component-scan 태그 base-package 속성 필수 헤더 스키마 선언

  1. <beans default-autowire="byName"  
  2. xmlns="http://www.springframework.org/schema/beans"  
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  4. xmlns:context="http://www.springframework.org/schema/context"  
  5. xsi:schemaLocation="  
  6. http://www.springframework.org/schema/beans  
  7. http://www.springframework.org/schema/beans/spring-beans.xsd  
  8. http://www.springframework.org/schema/context  
  9. http://www.springframework.org/schema/context/spring-context.xsd">  

장점
XML 설정파일에 대한 설정을 최소화 할 수 있고 단순하다.
코드가 간결해진다.
단점
Java1.5 이상의 환경에서 지원된다.
Annotation에 대한 학습이 필요하다.

4. Annotation 종류
@Scope 설정(prototype, singleton. request, session, globalSession, 기본값은 singleton)
@Autowired = byType
@Resource = byName
@PostConstruct = init-method(어노테이션과 XML 동시 설정 가능. 그럴 경우 어노테이션이 먼저 수행) PostConstruct는 bean이 생성된 이후에 수행해야 할 작업(초기화 작업)들을 선언해주는 역할을 합니다.
@PreDestroy = destroy-method 인스턴스가 삭제되기 전에 수행해야 할 작업들을 선언(destruction callback 기능)해주는 역할을 합니다.

추가내용
어노테이션 관련 글 스프링의 구조는 JSP(View) - Controller(URL mapping) - Service(Business Logic) - Dao(Data Access)
어노테이션이 확실히 좋은 기능임은 틀림없으나 과도한 사용은 득보다 실이 많다는 의견이 많다.
어노테이션을 이용하면 메타정보가 소스코드에 들어가므로 수정될 시 재컴파일이 필요
소스코드가 같이 제공되지 않으면 사용에 제약이 따름
SpringMVC 주요 패턴인 템플릿(Template) 패턴이나 전략(Strategy) 패턴으로 기존 기능을 확장시키기 어려운 점
순수 Spring 컨트롤러에선 xxx-servlet.xml 파일을 통해 url 맵핑이 이루어지는데 어노테이션을 이용할 경우 코드의 양이 방대할수록 계층 구조를 파악하기 힘듬

Struts2는 struts.xml 파일을 통해 url mapping이 이루어지므로 관련 없음

BEA의 EJB 팀 리더이자 OpenJPA 프로젝트의 리더인 패트릭 린스키가 한 말 중 일부분
- 대형 시스템에는 어노테이션이 적합하지 않다. 본격적인 시스템에서는 xml이 필수
- 규모가 있는 시스템일 경우 xml를 단계별로 분리하여 설정을 독립하거나 어노테이션 + xml 사용을 권장

Spring의 아버지 로드 존슨의 어노테이션 도입
- 1.x 버전 당시에는 스프링 사용자들의 어노테이션 DI 요청을 묵살함
- EJB3와 Google Guice의 annotation-driven DI의 인기와 영향력에 의해 Spring2.1에 어노테이션 도입 어노테이션 도입
- 배경에 대한 설명은 "고객과 엔드유저가 원하는 것이 옳은 것이다"라고 말함
- 스프링의 설정은 xml로 충분하다고 고집한 로드 존슨이 닷넷의 어트리뷰트 도입 필요성을 인식하고 가장 먼저 Spring에 어노테이션을 이용한 URL mapping인 @RequsetMapping을 적용

JDK 핵심 개발자이면서 Java5+ 개발에 깊이 관여한 조슈아 블록에 대한 입장
- 어노테이션 사용에 긍정적인 입장 CoC(관습에 의한 설정) 보다는 어노테이션이 낫다고 주장
- 어노테이션 정보(타입)를 얻기위해 사용되는 리플렉션 보다는 인터페이스 사용 선호
- 어노테이션은 타입이 없고 인터페이스는 타입이 있음

결론
시스템 복잡성이 아니라면 어노테이션 사용은 적합하게 쓰이면 코드가 간결해지고 유지보수가 용이해짐
대형 시스템엔 계층 구조가 잘 파악되기 위해서는 xml 사용이 필수
어노테이션은 메타정보가 소스코드에 들어가므로 파악하기 어려움
현재 오픈소스에도 많이 사용되고 계속 발전하는 어노테이션에 주목할 필요성이 보임

출처 - http://sooin01.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98Spring-Annotation






9.Annotation

Spring XML 만을 독립적으로 사용할 경우 때때로 방대하고 복잡한 속성 파일들로 인해 시스템 개발 및 유지보수의 지연을 초래할 가능성이 높아진다. 이러한 문제점을 해결하기 위해 Spring Framework에서는 별도 XML 정의없이도 사용 가능한 annotation 지원에 주력하고 있는 실정이다. Spring 2.0에서는 @Transactional, @Required, @PersistenceConetxt/@PersistenceUnit과 같은 Transaction 관리 또는 Persistence 관리 영역에 대한 annotation들을 지원했다면 Spring 2.5부터는 Bean 또는 Dependency 정의 등과 같이 Spring 속성 정의 XML과 직접적으로 관련된 annotation들을 선보이고 있다. 본 문서에서는 annotation 사용 용도를 Bean Management, Dependency Injection, Life Cycle로 구분하고 각각의 경우에 따른 사용법에 대해 상세히 살펴보도록 한다.

기본적으로, Annotation은 JDK 1.5 이상에서 활용이 가능하며, Spring Container가 Annotation을 인식할 수 있도록 하기 위해서는 속성 정의 XML 파일 내에 다음과 같은 정의가 추가되어야 함에 유의해야 한다.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemalLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                            http://www.springframework.org/schema/context
                            http://www.springframework.org/schema/contxt/spring-context-2.5.xsd">
        <context:annotation-config/>	                            
</beans>

  • XML vs. Annotation

    다음은 특정 서비스를 구성하는 구현 클래스, DAO 클래스, 속성 정의 XML에 대해 XML을 이용하는 경우와 Annotation을 이용하는 경우로 나누어 비교해 본 그림이다.

9.1.Bean Management

Stereotype Annotation을 사용하면 Spring Framework의 컨테이너에 의해 관리되어야 하는 Bean들을 정의할 수 있다. 일반적으로 Parent Stereotype Annotation인 @Component 를 활용하면 모든 Bean에 대한 정의가 가능하다. 그러나 Spring Framework에서는 레이어별로 구성 요소를 구분하여 다음과 같은 Annotation을 사용할 것을 권장하고 있고, 향후 지속적으로 레이어별 특성을 반영할 수 있는 속성들을 추가해 나아갈 예정이다.

  • @Service

    비즈니스 로직을 처리하는 클래스를 정의하는데 사용한다.

  • @Controller

    프리젠테이션 레이어를 구성하는 Controller 클래스를 정의하는데 사용하며, Spring MVC 기반인 경우에 한해 활용 가능하다.

  • @Repository

    데이터 접근 로직을 처리하는 클래스를 정의하는데 사용하며, 퍼시스턴스 레이어에서 발생한 Exception에 대한 Translation이 지원된다.

본 문서에서는 위에서 나열한 annotation을 사용하는 방법에 대해서 자세히 살펴보도록 한다.

9.1.1.Auto Detecting

Stereotype Annotation을 사용하여 Bean을 정의하면 XML에 따로 Bean 정의를 명시하지 않아도 Spring Container가 Bean을 인식하고 관리할 수 있다. 단, 자동 인식이 되기 위해서는 서비스 속성 정의 XML 내에<context:component-scan /> 을 정의해 주어야 한다. 이 설정을 추가하면 Spring Container는 클래스패스 상에 존재하는 클래스들을 스캔하여 Stereotype Annotation이 정의된 클래스들 Bean으로 인식하고 자동으로 등록한다.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemalLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                            http://www.springframework.org/schema/context
                            http://www.springframework.org/schema/contxt/spring-context-2.5.xsd">
        <context:component-scan base-package="anyframe.example" />	                            
</beans>

<context:component-scan />을 정의한 경우 Annotation 인식을 위한 설정 <context:annotation-config/> 을 별도로 추가하지 않아도 된다.

다음은 서비스 레이어의 구성 요소인 ProductServiceImpl 클래스에 대해 @Service라는 Stereotype Annotation을 사용한 예이다.

@Service
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Resource
        MessageSource messageSource;
        @Resource
        ProductDao productDao;
}

위 예제에서는 해당 클래스의 클래스명(소문자로 시작)이 Bean name으로 셋팅되어 해당 Bean을 찾을 때 productServiceImpl 이라는 문자열을 사용해야 한다.

ProductService service = (ProductService) context.getBean("productServiceImpl");       

해당 Annotation에 속성을 부여하면, 원하는 Bean name을 지정하는 것 또한 가능하다.

@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Resource
        MessageSource messageSource;
        @Resource
        ProductDao productDao;
}

이 경우에 해당되는 Bean을 찾기 위해서는 속성으로 정의한 Name을 활용해야 한다.

ProductService service = (ProductService) context.getBean("productService");

9.1.2.Using Filters to customize scanning

<context:component-scan>의 여러 속성들을 이용하면 검색 대상의 범위를 조정하여 자동으로 검색되어 Bean으로 등록되는 클래스들을 filtering 할 수 있다. base-package 속성은 <context:component-scan> 내에 정의 가능한 속성으로 검색 대상 패키지를 정의하는 용도로 사용된다. 이외에도 <context:component-scan>은 하위 element로 <context:include-filter>, <context:exclude-filter>를 가질 수 있는데, 다양한 Filter Type(type)에 해당하는 표현식(expression)을 정의함으로써 이에 해당하는 클래스들을 포함 또는 제외시킬 수가 있다. 다음은 <context:include-filter>, <context:exclude-filter> 사용 예이다.

<context:component-scan base-package="anyframe.example">
    <context:include-filter type="regex" expression=".*Stub.*Repository"/>
    <context:exclude-filter type="annotation"           expression="org.springframework.stereotype.Repository"/>
</context:component-scan>

정의 가능한 Filter Type은 4가지이며, 다음과 같다.

Filter TypeExample Expressions
annotationorg.example.SomeAnnotation
assignableorg.example.SomeClass
regexorg\.example\.Default.*
aspectjorg.example..*Service+

참고

@Controller, @Service, @Repository가 적용된 클래스를 auto detection하는 디폴트 설정을 사용하지 않고자 하는 경우에는 <context:component-scan />태그에 use-default-filters="false" 속성을 추가하면 된다.

9.1.3.Scope Definition

Spring Framework에서는 Bean의 인스턴스 생성 메커니즘에 따라 5가지 Scope 을 제공하는데 이러한 Bean Scope을 정의하기 위해서는 다음과 같이 @Scope을 사용하도록 한다.

@Scope("prototype")
@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Resource
        MessageSource messageSource;
        @Resource
        ProductDao productDao;
}       

9.2.Dependency Injection

특정 Bean의 기능 수행을 위해 다른 Bean을 참조해야 하는 경우 사용하는 Annotation으로는 @Autowired 또는 @Resource가 있다.

  • @Autowired

    Spring Framework에서 지원하는 Dependency 정의 용도의 Annotation으로, Spring Framework에 종속적이긴 하지만 정밀한 Dependency Injection이 필요한 경우에 유용하다.

  • @Resource

    JSR-250 표준 Annotation으로 Spring Framework 2.5.* 부터 지원하는 Annotation이다. 특정 Framework에 종속되지 않은 어플리케이션을 구성하기 위해서는 @Resource를 사용할 것을 권장한다. @Resource를 사용하기 위해서는 클래스패스 내에 jsr250-api.jar 파일이 추가되어야 함에 유의해야 한다.

@Autowired와 @Resource를 사용할 수 있는 위치는 다음과 같이 약간의 차이가 있으므로 필요에 따라 적절히 사용하면 된다.

  • @Autowired : 멤버변수, setter 메소드, 생성자, 일반 메소드에 적용 가능

  • @Resource : 멤버변수, setter 메소드에 적용가능

@Autowired나 @Resource를 멤버변수에 직접 정의하는 경우 별도 setter 메소드는 정의하지 않아도 된다.

9.2.1.@Resource

@Resource annotation은 Bean name을 지정하여 Dependency Injection을 하고자 하는 경우에 사용한다. @Resource는 name이라는 속성을 가지고 있어서, Spring Container가 @Resource로 정의된 요소에 injection하기 위한 Bean을 검색할 때, name 속성에 지정한 이름을 검색할 Bean Name으로 사용한다.

@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Resource
        MessageSource messageSource;
        @Resource (name="productDao")
        ProductDao productDao;

명시적으로 name 속성에 이름을 지정하지 않는 경우, 검색할 Bean Name은 다음과 같은 규칙을 따른다.

  • @Resource가 멤버 변수에 정의되었을 때 : 멤버 변수의 이름

  • @Resource가 setter 메소드에 정의되었을 때 : 해당 setter 메소드의 이름에서 'set'을 제외한 이름(첫 글자는 소문자)

    예) setFoo(...) --> 'foo'

해당하는 Bean Name으로 injection할 Bean을 찾지 못했을 경우에는 @Autowired 처럼 Bean의 type으로 검색한다.

@Resource를 이용하면 BeanFactory, ApplicationContext, ResourceLoader, ApplicationEventPublisher, MessageSource 인터페이스와 하위 인터페이스들을 별도 설정 없이 바로 사용 가능하다.

@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Resource
        ApplicationContext context;
}

9.2.2.@Autowired

서로 다른 Bean 간의 Dependency 정의를 위한 또 다른 Annotation인 @Autowired는 Spring에 종속적이긴 하지만, 적용할 수 있는 위치가 @Resource보다 다양하고, 정밀한 Dependency Injection이 필요한 경우에 유용하다.

다음은 @Autowired를 사용한 예이다.

@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Autowired
        ProductDao productDao;
}

@Autowired 적용 위치 별로 사용 예를 들면 다음과 같다.

  • 생성자 및 멤버 변수

    @Service("productService")
    public class ProductServiceImpl extends GenericServiceImpl<Product, String>
    		implements ProductService {
            @Autowired
            ProductDao productDao;
            MessageSource messageSource;
    		
            @Autowired
            public ProductServiceImpl(MessageSource messageSource) {
                    this.messageSource = messageSource;
            }
    }
    위의 예제와 같이 @Autowired를 사용하면 ProductServiceImpl 클래스가 생성될 때 Spring Container에 의해서 MessageSource 타입의 Bean이 생성자의 argument로 자동으로 injection 된다. 또한 productDao 멤버변수에도 @Autowired가 적용되어 있으므로 ProductDao 타입의 Bean이 자동 injection된다.

  • setter 메소드

    @Service("productService")
    public class ProductServiceImpl extends GenericServiceImpl<Product, String>
    		implements ProductService {
            ProductDao productDao;
            @Autowired
            public void setProductDao(ProductDao productDao) {
                    this.productDao = productDao;
            }
    }
    Spring Container에 의해서 자동으로 setProductDao() 메소드가 호출되어 ProductDao 타입의 Bean이 productDao 멤버변수로 injection된다.

  • 일반 메소드

    @Service("productService")
    public class ProductServiceImpl extends GenericServiceImpl<Product, String>
    		implements ProductService {
            ProductDao productDao;
            MessageSource messageSource;
            @Autowired
            public void prepare(ProductDao productDao, MessageSource messageSource) {
                    this.productDao = productDao;
                    this.messageSource = messageSource;
            }
    }
    @Resource 와는 달리 위의 prepare()와 같은 일반 메소드에도 @Autowired를 적용함으로써 Spring Container에 의한 Dependency Injection 처리를 할 수 있다. 위의 예제에서는 ProductDao 타입의 Bean이 productDao로, MessageSource 타입의 Bean이 messageSource로 injection된다

  • 배열이나 Collection 형태의 멤버변수와 메소드

    @Service("productService")
    public class ProductServiceImpl extends GenericServiceImpl<Product, String>
    		implements ProductService {
            ProductDao productDao;
            @Autowired
            Category[] categories;
    }
    @Service("productService")
    public class ProductServiceImpl extends GenericServiceImpl<Product, String>
    		implements ProductService {
            ProductDao productDao;
            Set<Category> categories;
            @Autowired
            public void setCategories(ProductDao productDao, Set<Category> categories) {
                    this.productDao = productDao;
                    this.categories = categories;
            }
    }
    위 예제 소스의 경우, Spring Container에 등록된 Category 타입의 Bean들이 모두 categories 배열(또는 collection)에 injection된다.

  • Map(key=Bean Name, value=Bean 객체) 형태의 멤버변수와 메소드

    @Service("productService")
    public class ProductServiceImpl extends GenericServiceImpl<Product, String>
    		implements ProductService {
           ProductDao productDao;
           Map<String, Category> categories;
           @Autowired
           public void setCategories(ProductDao productDao, Map<String, Category> categories) {
                    this.productDao = productDao;
                    this.categories = categories;
            }
    }
    위 예제 소스의 경우, Spring Container에 등록된 Category 타입의 Bean들이 Bean name이 key로, Bean 객체가 value인 쌍으로 모두 categories Map에 injection된다.

기본적으로 @Autowired가 적용된 참조 관계는 반드시 해당 빈이 존재해야 하지만, required 속성을 false로 설정하는 경우에는 해당되는 Bean을 찾지 못하더라도 에러가 발생하지 않는다.

@Service
public UserService implements UserService {
        @Autowired(required=false)
        private UserDAO userDAO;
}

또한, @Resource에서 설명했던 바와 같이 @Autowired도 BeanFactory, ApplicationContext, ResourceLoader, ApplicationEventPublisher, MessageSource 인터페이스와 하위 인터페이스들을 별도 설정 없이 바로 사용할 수 있게 해준다.

@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Autowired
        ApplicationContext context;
}

9.2.3.@Qualifier

기본적으로 @Autowired는 type-driven injection 형태로 동작하여, @Autowired가 정의되었을 경우 Spring Container가 해당 Bean을 찾을 때 객체의 type을 기준으로 검색을 한다. 이와 같은 경우 동일한 객체 type의 Bean이 여러 개 검색되었을 때 injection 대상이 되는 Bean을 결정하기 위한 세밀한 제어를 요하는데, 이 때 @Qualifier를 사용할 수 있다.

다음은 @Qualifier를 사용한 예이다.

@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Autowired
        @Qualifier("sports")
        Category sportsCategory;
}

위와 같이 정의하면 "sports"라는 qualifier 속성 값이 정의된 Bean이 sportsCategory 멤버변수로 injection된다.

위의 @Qualifier에 의해 연결될 Bean은 다음과 같이 정의할 수 있다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        				http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
        				
        <context:annotation-config/>

        <bean class="anyframe.example.domain.Category">
                <qualifier value="sportsCategory"/>
                <!-- inject any dependencies required by this bean -->
        </bean>
        <bean class="anyframe.example.domain.Category">
                <qualifier value="livingCategory"/>
                <!-- inject any dependencies required by this bean -->
        </bean>

        <bean id="productService" class="anyframe.example.annotation.sales.service.impl.ProductServiceImpl"/>
</beans>
    	

9.2.4.@Resource vs. @Autowired

@Resource와 @Autowired을 비교하면 다음과 같다.

Annotation@Resource@Autowired
Injection 방식name-matching injectiontype-driven injection
사용가능한 위치멤버변수, setter 메소드멤버변수, setter 메소드, 생성자, 일반 메소드

9.3.LifeCycle Annotation

IoC의 Life Cycle 에서 설명한 바와 같이 Bean의 LifeCycle은 Initializaion ->Activation -> Destruction으로 구성되어 있으며, LifeCycle 메소드를 정의하는 경우 컨테이너 기동시 또는 종료시 필요한 로직을 수행할 수 있게 된다. Bean을 초기화 또는 소멸화 하는 시점에 별도 작업이 필요한 경우 기존에는 InitializingBean과 DesposableBean 인터페이스를 상속하거나, Bean 정의시 명시적으로 초기화 메소드나 소멸화 메소드를 별도로 지정해야 했다. 그러나, 다음과 같은 Annotation을 사용하면 XML 정의 또는 별도 인터페이스 상속없이 Bean의 LifeCycle 관리가 가능해진다.

9.3.1.@PostConstruct

JSR-250 표준 Annotation으로 Bean 초기화시 필요한 작업을 담은 메소드에 대해 정의한다. @PostConstruct를 사용하기 위해서는 클래스패스 내에 jsr250-api.jar 파일이 추가되어 있어야 한다.

@PostConstruct
// 메소드명은 자유롭게 정의할 수 있다.
public void initialize() {
        // ...
}      

9.3.2.@PreDestroy

JSR-250 표준 Annotation으로 Bean 소멸시 필요한 작업을 담은 메소드에 대해 정의한다. @PreDestroy를 사용하기 위해서는 클래스패스 내에 jsr250-api.jar 파일이 추가되어 있어야 한다.

@PreDestroy
// 메소드명은 자유롭게 정의할 수 있다.
public void dispose() {
        // ...
}       

9.3.3.Combining lifecycle mechanisms

앞에서 설명한 바와 같이, Spring 2.5에서 bean lifecycle을 관리할 수 있는 방법은 다음과 같이 세가지가 있다.

  • InitializingBean과 DisposableBean callback 인터페이스 이용

  • 사용자가 작성한 초기화/소멸화 메소드를 XML에서 init-method/destroy-method 속성을 이용하여 정의

  • @PostConstruct와 @PreDestroy annotation 이용

위의 3가지 방법이 동시에 존재하는 경우(예를 들어, 3가지 방법이 각각 정의된 클래스가 Parent-child 관계를 가지는 경우), 실행되는 순서는 다음과 같다.

Initialization 메소드

  1. @PostConstruct를 이용하여 정의한 메소드

  2. InitializingBean 인터페이스의 afterPropertiesSet() 메소드

  3. XML에서 init-method 속성으로 정의된 초기화 메소드

Destroy 메소드

  1. @PreDestroy를 이용하여 정의한 메소드

  2. DisposableBean 인터페이스의 destroy() 메소드

  3. XML에서 destroy-method 속성으로 정의된 소멸화 메소드

9.4.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.annotation.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-tasks-2.0.10.jar 파일이 있어야 한다.)

    표 9.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.annotation.zipDownload
    maven-ant-tasks-2.0.10.jarDownload

출처 - http://dev.anyframejava.org/docs/anyframe/4.0.0/reference/html/ch09.html






Spring 2.5 가 되면서 dependency injection 관련해서 서비스 구성이 무지 편해졌다.
annotation 을 통해서 빈들을 지정된 클래스패스에서 자동으로 인식하게 할수 있게 되었고, 필요자원을 세팅시킬수 있게 되었다. 넘 편리~~

Stereotype-annotation(@Component, @Service, @Repository, @Controller) 을 통해서 자동으로 클래스패스에서 컴포넌트를 인식(auto-detect) 시킬수 있다. @Component는 자동인식이 되는 일반 컴퍼넌트로 다른 녀석들의 부모역할을 한다. @Service는 비즈니스 서비스를 의미하고, @Repository는 Dao 에 적용하면 좋은 녀석인데, DB Exception Translation을 자동으로 해준다(Hibernate DAO를 POJO로 구성해 본 사람은 이 용도가 무지 좋다는 것을 알거다). 마지막으로 @Controller는 웹용으로 MVC 콤퍼넌트로 사용된다.

요 녀석들을 사용해서 간단해게 테스트 해밨다.

public class Delivery {
 private String productName;
 private String destination;
 
 public Delivery(String name, String dest) {
  this.productName = name;
  this.destination = dest;
 }
 public String getProductName() {
  return productName;
 }
 public void setProductName(String productName) {
  this.productName = productName;
 }
 public String getDestination() {
  return destination;
 }
 public void setDestination(String destination) {
  this.destination = destination;
 }
 
 
}

public interface DeliveryDao {
 public List<Delivery> getAll();
}

@Repository
public class DeliveryDaoImpl implements DeliveryDao {
 public List<Delivery> getAll() {
  
  List results = new ArrayList();
  results.add(new Delivery("Apple", "서울"));
  results.add(new Delivery("Melon", "부산"));
  
  return results;
 }
}
제대로 하면 DB 관련된 Dao 여야 하지만 수도 코드로 두개 배송목록을 리턴하도록 했다.

public interface TransportService {
 public List<Delivery> send(String obj);
}

@Service
public class CargoTranportServiceImpl implements TransportService {
 @Autowired
 private DeliveryDao deliveryDao;
 
 public List<Delivery> send(String obj) {
  
  return deliveryDao.getAll();
 }
}
@Autowired 를 사용해서 annotation으로 자원 세팅을 할 수 있다.
JEE 형식의 common annotation도 spring 에서 지원한다. @Resource( name="deliveryDao" ) 처럼 해주면 된다.

context 구성은 다음과 같다.
 <context:component-scan base-package="com.mple.service" />
</beans>

context:component-scan을 해주면 <context:annotation-config /> 설정이 자동으로 된다.

유닛테스트.
public class TransportServiceTest extends
  AbstractDependencyInjectionSpringContextTests {
 @Override
 protected String[] getConfigLocations() {
  return new String[] { "applicationContext.xml" };
 }
 private TransportService transportService;
 public void setTransportService(TransportService transportService) {
  this.transportService = transportService;
 }
 public void testSend() {
  List results = transportService.send("");
  assertEquals(2, results.size());
 }
}


출처 - http://readyset.tistory.com/19






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의 어노테이션만 로딩을 하고
1
2
3
<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><br>
 
</context:component-scan>
applicationContext.xml에는 아래와 같이해서 Controller를 제외한 어노테이션만 로딩을 해야한다.
1
2
3
4
5
6
7
8
<context:component-scan base-package="com.enz.adnad"><br><span id="callbacknestmikyungnettistorycom242716" style="width:1px; height:1px; float:right"><embed allowscriptaccess="always" id="bootstrappermikyungnettistorycom242716" src="http://mikyungnet.tistory.com/plugin/CallBack_bootstrapperSrc?nil_profile=tistory&nil_type=copied_post" width="1" height="1" wmode="transparent" type="application/x-shockwave-flash" enablecontextmenu="false" flashvars="&callbackId=mikyungnettistorycom242716&host=http://mikyungnet.tistory.com&embedCodeSrc=http%3A%2F%2Fmikyungnet.tistory.com%2Fplugin%2FCallBack_bootstrapper%3F%26src%3Dhttp%3A%2F%2Fs1.daumcdn.net%2Fcfs.tistory%2Fv%2F0%2Fblog%2Fplugins%2FCallBack%2Fcallback%26id%3D24%26callbackId%3Dmikyungnettistorycom242716%26destDocId%3Dcallbacknestmikyungnettistorycom242716%26host%3Dhttp%3A%2F%2Fmikyungnet.tistory.com%26float%3Dleft" swliveconnect="true"></span>
 
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"></context:exclude-filter><br>
 
</context:component-scan><br>
 
<br>
<br>
아래는 dispactcher-servlet.xml 에 설정하는 ViewResolver 로써 /WEB-INF/spring/ 로 시작하고 .jsp 로 끝나는 파일을 찾아 옵니다.
controller에서 return "hello/hello_jabsiri"; 와 같이 하면 /WEB-INF/spring/hello/hello_jabsiri.jsp 파일을 찾습니다.
1
<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에 대한 공부는 각자의 몫이고 필자 또한 나중에 정리를 할 것이다. )

다음은 사용의 예이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller<br>
 
public class HelloJabsiri {<br>
 
    @RequestMapping(value="/hello/hello_jabsiri.jab")<br>
 
    public String helloJabsiri(){<br>
 
        return "hello/hello_jabsiri"//리턴값은 .jsp 확장자를 뺀 경로<br>
 
    }<br>
 
}
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 객체들의 맴버변수가 같은것을 알아서 매핑시켜 값을 넘겨주기도 한다.

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller<br>
 
@RequestMapping("/hello/*")<br>
 
public class HelloJabsiri {<br>
 
    @RequestMapping(value="hello_jabsiri.jab")<br>
 
    public String helloJabsiri(){<br>
 
        return "hello/hello_jabsiri";<br>
 
    }<br>
 
}

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

아래와 같이 설정해주면 사용할수 있지만 위에서처럼 scan으로 등록했으므로 안해도 된다.
1
2
3
4
<bean class=org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor /><br>
 
<br>
(필자는 그냥  <context:component-scan base-package="kr.pe.jabsiri" /> 설정후 아무것도 하지 않는다. 뭐 몰라서 그런것도 있다. )<br>

사용 코드는는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Controller<br>
 
public class HelloJabsiriController {<br>
 
    @Autowired<br>
 
    private HelloJabsiriService helloJabsiriService;<br>
 
    ...........<br>
 
}<br>
 
<br>
@Service("helloJabsiriService")<br>
 
public class HelloJabsiriServiceImpl implements HelloJabsiriService {<br>
 
    @Autowired<br>
 
    private HelloJabsiriDao helloJabsiriDao;<br>
 
                ...........<br>
 
}
위의 코드는 Controller에서 HelloJabsiriServiceImpl 의 interface인 HelloJabsiriService를 의존관계로 등록 한것이다.
자세히 보면 @Autowired를 적용한 변수명과 @Service 안에있는 String 값이 같다는걸 눈치챌수 있다.

4. @Service("xxxService")
@Service를 적용한 Class는 비지니스 로직이 들어가는 Service로 등록이 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service("helloJabsiriService")<br>
 
public class HelloJabsiriServiceImpl implements HelloJabsiriService {<br>
 
    @Autowired<br>
 
    private HelloJabsiriDao helloJabsiriDao;<br>
 
    public void helloJabsiri() {<br>
 
        System.out.println( "HelloJabsiriServiceImpl :: helloJabsiri()");<br>
 
        helloJabsiriDao.selectHelloJabsiri();<br>
 
    }<br>
 
}
helloJabsiriDao.selectHelloJabsiri(); 와 같이 @Autowired를 이용한 객체를 이용하여 Dao 객체를 호출한다.

 
5. @Repository("xxxDao")
@Repository를 적용한 Class는 DataBaseAccess를 할수 있는 Dao로 등록된다.
1
2
3
4
5
6
7
8
9
10
11
@Repository("helloJabsiriDao")<br>
 
public class HelloJabsiriDaoImpl implements HelloJabsiriDao {<br>
 
    public void selectHelloJabsiri() {<br>
 
        System.out.println("HelloJabsiriDaoImpl :: selectHelloJabsiri()");<br>
 
    }<br>
 
}


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


출처 - http://mikyungnet.tistory.com/24




















'Framework & Platform > Spring' 카테고리의 다른 글

spring mvc - No mapping found for HTTP request with URI  (0) 2012.05.10
spring - annotation 2  (0) 2012.05.09
Sping MVC - Model  (0) 2012.05.09
Spring - @Controller  (0) 2012.05.09
Spring - @RequestMapping  (0) 2012.05.09
Posted by linuxism
,

스프링 MVC에서 모델(model)이란

컨트롤러에 의해 비즈니스 로직이 수행되고 나면 대체로 사용자에게 반환되어 브라우저에 표시될 정보가 만들어진다. 이런 정보를 모델(model)이라고 한다. 이 정보를 보통 JSP에 해당하는 뷰(view)로 보낸다.

출처 - 스프링 인 액션 3판 p221


@SessionAttributes 가 해주는 기능은 두 가지다. 

첫째, 컨트롤러 메소드가 생성하는 모델정보 중에서 @SessionAttributes 에 지정한 이름과 동일한 것이 있다면 이를 세션에 저장해 준다. 

두 번째로 @SessionAttributes 가 해 주는 일은 @ModelAttribute 가 지정된 파라미터가 있을 때 이 파라미터에 전달해줄 오브젝트를 세션에서 가져오는 것이다.


출처 - 토비의 스프링 3.0


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

Model 객체 전달 방식에 대해서만 말씀 드리면,, 

Controller에서 사용하는 (Servlet)Model 객체를 Service로 전달하는 것은 비추구요.. 

Controller에서 사용하는 (Servlet)Model은 Controller와 View Page 간에 전달용으로 사용하시고, 

Controller - Service - DAO (Repository) 에 두루 걸치는 모델 객체는 별도의 

(Service)Model(또는 DTO)을 

작성해서 사용하시고,, (두 Model의 용도가 다르기 때문에..) 

Controller에서 (Service)Model과 (Servlet)Model을 binding (한쪽 Model의 필요한 속성들을 

다른 쪽 Model에 전달하는 과정)해서 사용하시는 걸 권장드립니다. 

Model binding 관련해서는,, Map의 values들을 전달해주는 라이브러리 등도 있으니 확인해보시기 바랍니다. 

앞에서 설명 드린 방식은 (설계적으로) 원론적이며 이상적으로 권고해드리는 내용이며, 

실제 상황에 따라 업무 효율에 크게 차이가 난다면, 편의적으로 처리하는 것도 가능하지 않을까하는 

의견입니다. ^^;


다른 분들이 말씀하신 것처럼 도메인 계층이 클라이언트 계층(WEB, REST API, 

WS 등)에 의존하는 상황은 금물입니다. 

가능한 서비스를 원자적인 수준으로 잘게 나누고 컨트롤러에서 여러번 호출하 

도록 처리하는 편이 좋겠습니다. 

반드시 한 서비스에서 두 가지 값을 반환해야 한다면 최윤석(mOer)님 말씀처 

럼 DTO를 만들어 이 객체에 담아 반환하는 방법이 자바쪽의 정석이겠죠. 

해당 DTO가 특정 용도로 한번만 사용된다면 DTO를 만드는 것이 부담되고 낭비 

로 느껴지기 때문에 Map, List, 배열 등에 담아 사용하기도 합니다.하지만 이 

럴 경우엔 타입 확인이 안 되는 문제가 있습니다. 담는 값이 모두 같은 타입 

이라면 지네릭을 사용해서 타입 확인이 되도록 할 수 있지만 타입이 다르다면 

컴파일러가 타입을 확인하도록 할 수 없습니다. 

스칼라라는 언어에서는  이럴 때 쓰라고 Tuples라는 특수 목적의 자료구조를 

가지고 있습니다. n개의 객체를 간단하게, 타입 확인도 되면서, 한 묶음으로 

묶어 메서드에 전달하거나 반환할 수 있습니다. 

http://codemonkeyism.com/tuples-scala-goodness/ 

제가 전에 자바용으로 만들어 놓은 것이 있어 좀 전에 github에 올려놨으니 

혹시 필요하면 참고하세요. 

https://gist.github.com/2299564 



출처 - http://www.ksug.org/153


'Framework & Platform > Spring' 카테고리의 다른 글

spring - annotation 2  (0) 2012.05.09
spring - annotation 1  (0) 2012.05.09
Spring - @Controller  (0) 2012.05.09
Spring - @RequestMapping  (0) 2012.05.09
Spring MVC Deprecated API  (0) 2012.05.08
Posted by linuxism
,

Annotation-based Controller

개요

스프링 프레임워크는 2.5 버젼 부터 Java 5+ 이상이면 @Controller(Annotation-based Controller)를 개발할 수 있는 환경을 제공한다.
인터페이스 Controller를 구현한 SimpleFormController, MultiActionController 같은 기존의 계층형(Hierarchy) Controller와의 주요 차이점 및 개선점은 아래와 같다.

  1. 어노테이션을 이용한 설정 : XML 기반으로 설정하던 정보들을 어노테이션을 사용해서 정의한다.
  2. 유연해진 메소드 시그니쳐 : Controller 메소드의 파라미터와 리턴 타입을 좀 더 다양하게 필요에 따라 선택할 수 있다.
  3. POJO-Style의 Controller : Controller 개발시에 특정 인터페이스를 구현 하거나 특정 클래스를 상속해야할 필요가 없다. 하지만, 폼 처리, 다중 액션등 기존의 계층형 Controller가 제공하던 기능들을 여전히 쉽게 구현할 수 있다.

계층형 Controller로 작성된 폼 처리를 @Controller로 구현하는 예도 설명한다.
예제 코드 easycompany의 Controller는 동일한 기능(또한 공통의 Service, DAO, JSP를 사용)을 계층형 Controller와 @Controller로 각각 작성했다.

  • 계층형 Controller - 패키지 com.easycompany.controller.annotation
  • @Controller - 패키지 com.easycompany.controller.hierarchy

설명

어노테이션을 이용한 설정

계층형 Controller들을 사용하면 여러 정보들(요청과 Controller의 매핑 설정 등)을 XML 설정 파일에 명시 해줘야 하는데, 복잡할 뿐 아니라 설정 파일과 코드 사이를 빈번히 이동 해야하는 부담과 번거로움이 될 수 있다.
@MVC는 Controller 코드안에 어노테이션으로 설정함으로써 좀 더 편리하게 MVC 프로그래밍을 할 수 있도록 했다.
@MVC에서 사용하는 주요 어노테이션은 아래와 같다.

이름설명
@Controller해당 클래스가 Controller임을 나타내기 위한 어노테이션
@RequestMapping요청에 대해 어떤 Controller, 어떤 메소드가 처리할지를 맵핑하기 위한 어노테이션
@RequestParamController 메소드의 파라미터와 웹요청 파라미터와 맵핑하기 위한 어노테이션
@ModelAttributeController 메소드의 파라미터나 리턴값을 Model 객체와 바인딩하기 위한 어노테이션
@SessionAttributesModel 객체를 세션에 저장하고 사용하기 위한 어노테이션

@Controller

@MVC에서 Controller를 만들기 위해서는 작성한 클래스에 @Controller를 붙여주면 된다. 특정 클래스를 구현하거나 상속할 필요가 없다.

package com.easycompany.controller.annotation;
 
@Controller
public class LoginController {
   ...
}

앞서 DefaultAnnotationHandlerMapping에서 언급한 대로 <context:component-scan> 태그를 이용해 @Controller들이 있는 패키지를 선언해 주면 된다.
@Controller만 스캔 한다면 include, exclude 등의 필터를 사용하라.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
				http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
 
        <context:component-scan base-package="com.easycompany.controller.annotation" />
 
</beans>

@RequestMapping

@RequestMapping은 요청에 대해 어떤 Controller, 어떤 메소드가 처리할지를 맵핑하기 위한 어노테이션이다. @RequestMapping이 사용하는 속성은 아래와 같다.

이름타입설명
valueString[]URL 값으로 맵핑 조건을 부여한다.
@RequestMapping(value=”/hello.do”) 또는 @RequestMapping(value={”/hello.do”, ”/world.do” })와 같이 표기하며,
기본값이기 때문에 @RequestMapping(”/hello.do”)으로 표기할 수도 있다.
”/myPath/*.do”와 같이 Ant-Style의 패턴매칭을 이용할 수도 있다.
methodRequestMethod[]HTTP Request 메소드값을 맵핑 조건으로 부여한다.
HTTP 요청 메소드값이 일치해야 맵핑이 이루어 지게 한다.
@RequestMapping(method = RequestMethod.POST)같은 형식으로 표기한다.
사용 가능한 메소드는 GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE이다
paramsString[]HTTP Request 파라미터를 맵핑 조건으로 부여한다.
params=“myParam=myValue”이면 HTTP Request URL중에 myParam이라는 파라미터가 있어야 하고 값은 myValue이어야 맵핑한다.
params=“myParam”와 같이 파라미터 이름만으로 조건을 부여할 수도 있고, ”!myParam”하면 myParam이라는 파라미터가 없는 요청 만을 맵핑한다.
@RequestMapping(params={“myParam1=myValue”, “myParam2”, ”!myParam3”})와 같이 조건을 주었다면,
HTTP Request에는 파라미터 myParam1이 myValue값을 가지고 있고, myParam2 파라미터가 있어야 하고, myParam3라는 파라미터는 없어야 한다.

@RequestMapping은 클래스 단위(type level)나 메소드 단위(method level)로 설정할 수 있다.

type level
/hello.do 요청이 오면 HelloController의 hello 메소드가 수행된다.

@Controller
@RequestMapping("/hello.do")
public class HelloController {
 
    @RequestMapping   //type level에서 URL을 정의하고 Controller에 메소드가 하나만 있어도 요청 처리를 담당할 메소드 위에 @RequestMapping 표기를 해야 제대로 맵핑이 된다.
    public String hello(){
	...		
    }
}

method level
/hello.do 요청이 오면 hello 메소드, 
/helloForm.do 요청은 GET 방식이면 helloGet 메소드, POST 방식이면 helloPost 메소드가 수행된다.

@Controller
public class HelloController {	
 
	@RequestMapping(value="/hello.do")
	public String hello(){
		...
	}
 
	@RequestMapping(value="/helloForm.do", method = RequestMethod.GET)
	public String helloGet(){
		...
	}
 
	@RequestMapping(value="/helloForm.do", method = RequestMethod.POST)
	public String helloPost(){
		...
	}	
}

type + method level
둘 다 설정할 수도 있는데, 이 경우엔 type level에 설정한 @RequestMapping의 value(URL)를 method level에서 재정의 할수 없다.
/hello.do 요청시에 GET 방식이면 helloGet 메소드, POST 방식이면 helloPost 메소드가 수행된다.

@Controller
@RequestMapping("/hello.do")
public class HelloController {
 
	@RequestMapping(method = RequestMethod.GET)
	public String helloGet(){
		...
	}
 
	@RequestMapping(method = RequestMethod.POST)
	public String helloPost(){
		...
	}
}

AbstractController 상속받아 구현한 예제 코드 LoginController를 어노테이션 기반의 Controller로 구현해 보겠다. 
기존의 LoginController는 URL /loginProcess.do로 오는 요청의 HTTP 메소드가 POST일때 handleRequestInternal 메소드가 실행되는 Controller였는데, 다음과 같이 구현할 수 있겠다.

package com.easycompany.controller.annotation;
...
@Controller
public class LoginController {
 
	@Autowired
	private LoginService loginService;
 
	@RequestMapping(value = "/loginProcess.do", method = RequestMethod.POST)
	public String login(HttpServletRequest request) {
 
		String id = request.getParameter("id");
		String password = request.getParameter("password");
 
		Account account = (Account) loginService.authenticate(id,password);
 
		if (account != null) {
			request.getSession().setAttribute("UserAccount", account);
			return "redirect:/employeeList.do";
		} else {
			return "login";
		}
	}	
}

위 예제 코드에서 서비스 클래스를 호출하기 위해서 @Autowired가 사용되었는데 자세한 내용은 여기를 참고하라.

@RequestParam

@RequestParam은 Controller 메소드의 파라미터와 웹요청 파라미터와 맵핑하기 위한 어노테이션이다.
관련 속성은 아래와 같다.

이름타입설명
valueString파라미터 이름
requiredboolean해당 파라미터가 반드시 필수 인지 여부. 기본값은 true이다.

아래 코드와 같은 방법으로 사용되는데, 
해당 파라미터가 Request 객체 안에 없을때 그냥 null값을 바인드 하고 싶다면, pageNo 파라미터 처럼 required=false로 명시해야 한다.
name 파라미터는 required가 true이므로, 만일 name 파라미터가 null이면 org.springframework.web.bind.MissingServletRequestParameterException이 발생한다.

@Controller
public class HelloController {
 
    @RequestMapping("/hello.do")
    public String hello(@RequestParam("name") String name, //required 조건이 없으면 기본값은 true, 즉 필수 파라미터 이다. 파라미터 pageNo가 존재하지 않으면 Exception 발생.
			@RequestParam(value="pageNo", required=false) String pageNo){ //파라미터 pageNo가 존재하지 않으면 String pageNo는 null.
	...		
    }
}

위에서 작성한 LoginController의 login 메소드를 보면 파라미터 아이디와 패스워드를 Http Request 객체에서 getParameter 메소드를 이용해 구하는데,
@RequestParam을 사용하면 아래와 같이 변경할수 있다.

package com.easycompany.controller.annotation;
...
@Controller
public class LoginController {
 
	@Autowired
	private LoginService loginService;
 
	@RequestMapping(value = "/loginProcess.do", method = RequestMethod.POST)
	public String login(
			HttpServletRequest request,
			@RequestParam("id") String id,
			@RequestParam("password") String password) {		
 
		Account account = (Account) loginService.authenticate(id,password);
 
		if (account != null) {
			request.getSession().setAttribute("UserAccount", account);
			return "redirect:/employeeList.do";
		} else {
			return "login";
		}
	}
}

@ModelAttribute

@ModelAttribute의 속성은 아래와 같다.

이름타입설명
valueString바인드하려는 Model 속성 이름.

@ModelAttribute는 실제적으로 ModelMap.addAttribute와 같은 기능을 발휘하는데, Controller에서 2가지 방법으로 사용된다.

1.메소드 리턴 데이터와 Model 속성(attribute)의 바인딩. 
메소드에서 비지니스 로직(DB 처리같은)을 처리한 후 결과 데이터를 ModelMap 객체에 저장하는 로직은 일반적으로 자주 발생한다.

...
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public String formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
		Department department = departmentService.getDepartmentInfoById(deptid); //DB에서 부서정보 데이터를 가져온다.
		model.addAttribute("department", department); //데이터를 모델 객체에 저장한다.
		return "modifydepartment";
	}
...

@ModelAttribute를 메소드에 선언하면 해당 메소드의 리턴 데이터가 ModelMap 객체에 저장된다.
위 코드를 아래와 같이 변경할수 있는데, 사용자로 부터 GET방식의 /updateDepartment.do 호출이 들어오면,
formBackingObject 메소드가 실행 되기 전에 DefaultAnnotationHandlerMapping이 org.springframework.web.bind.annotation.support.HandlerMethodInvoker을 이용해서 
(@ModelAttribute가 선언된)getEmployeeInfo를 실행하고, 결과를 ModelMap객체에 저장한다.
결과적으로 getEmployeeInfo 메소드는 ModelMap.addAttribute(“department”, departmentService.getDepartmentInfoById(…)) 작업을 하게 되는것이다.

...
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public String formBackingObject() {
		return "modifydepartment";
	}
 
	@ModelAttribute("department")
	public Department getEmployeeInfo(@RequestParam("deptid") String deptid){
		return departmentService.getDepartmentInfoById(deptid); //DB에서 부서정보 데이터를 가져온다.
	}
	또는
	public @ModelAttribute("department") Department getDepartmentInfoById(@RequestParam("deptid") String deptid){
		return departmentService.getDepartmentInfoById(deptid);
	}
...

2.메소드 파라미터와 Model 속성(attribute)의 바인딩. 
@ModelAttribute는 ModelMap 객체의 특정 속성(attribute) 메소드의 파라미터와 바인딩 할때도 사용될수 있다.
아래와 같이 메소드의 파라미터에 ”@ModelAttribute(“department”) Department department” 선언하면 department에는 (Department)ModelMap.get(“department”) 값이 바인딩된다.
따라서, 아래와 같은 코드라면 formBackingObject 메소드 파라미터 department에는 getDepartmentInfo 메소드가 ModelMap 객체에 저장한 Department 데이터가 들어 있다.

...
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public String formBackingObject(@ModelAttribute("department") Department department) { //department에는 getDepartmentInfo에서 구해온 데이터들이 들어가 있다.
		System.out.println(employee.getEmployeeid());
		System.out.println(employee.getName());
		return "modifydepartment";
	}
 
	@ModelAttribute("department")
	public Department getDepartmentInfo(@RequestParam("deptid") String deptid){
		return departmentService.getDepartmentInfoById(deptid); //DB에서 부서정보 데이터를 가져온다.
	}
...

@SessionAttributes

@SessionAttributes는 model attribute를 session에 저장, 유지할 때 사용하는 어노테이션이다. @SessionAttributes는 클래스 레벨(type level)에서 선언할 수 있다. 관련 속성은 아래와 같다.

이름타입설명
typesClass[]session에 저장하려는 model attribute의 타입
valueString[]session에 저장하려는 model attribute의 이름

유연해진 메소드 시그니쳐

@RequestMapping을 적용한 Controller의 메소드는 아래와 같은 메소드 파라미터와 리턴 타입을 사용할수 있다.
특정 클래스를 확장하거나 인터페이스를 구현해야 하는 제약이 없기 때문에 계층형 Controller 비해 유연한 메소드 시그니쳐를 갖는다.

@Controller의 메소드 파라미터

사용가능한 메소드 파라미터는 아래와 같다.

  • Servlet API - ServletRequest, HttpServletRequest, HttpServletResponse, HttpSession 같은 요청,응답,세션관련 Servlet API들.
  • WebRequest, NativeWebRequest - org.springframework.web.context.request.WebRequest, org.springframework.web.context.request.NativeWebRequest
  • java.util.Locale
  • java.io.InputStream / java.io.Reader
  • java.io.OutputStream / java.io.Writer
  • @RequestParam - HTTP Request의 파라미터와 메소드의 argument를 바인딩하기 위해 사용하는 어노테이션.
  • java.util.Map / org.springframework.ui.Model / org.springframework.ui.ModelMap - 뷰에 전달할 모델데이터들.
  • Command/form 객체 - HTTP Request로 전달된 parameter를 바인딩한 커맨드 객체, @ModelAttribute을 사용하면 alias를 줄수 있다.
  • Errors, BindingResult - org.springframework.validation.Errors / org.springframework.validation.BindingResult 유효성 검사후 결과 데이터를 저장한 객체.
  • SessionStatus - org.springframework.web.bind.support.SessionStatus 세션폼 처리시에 해당 세션을 제거하기 위해 사용된다.

메소드는 임의의 순서대로 파라미터를 사용할수 있다. 단, BindingResult가 메소드의 argument로 사용될 때는 바인딩 할 커맨드 객체가 바로 앞에 와야 한다.

public String updateEmployee(...,@ModelAttribute("employee") Employee employee,			
			BindingResult bindingResult,...) <!-- (O) -->
 
public String updateEmployee(...,BindingResult bindingResult,
                        @ModelAttribute("employee") Employee employee,...) <!-- (X) -->

이 외의 타입을 메소드 파라미터로 사용하려면?
스프링 프레임워크는 위에서 언급한 타입이 아닌 custom arguments도 메소드 파라미터로 사용할 수 있도록 org.springframework.web.bind.support.WebArgumentResolver라는 인터페이스를 제공한다.
WebArgumentResolver를 사용한 예제는 이곳을 참고하라.

@Controller의 메소드 리턴 타입

사용가능한 메소드 리턴 타입은 아래와 같다.

  • ModelAndView - 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 담긴 Model 객체와 View 정보가 담겨 있다.
            @RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public ModelAndView formBackingObject(@RequestParam("deptid") String deptid) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		ModelAndView mav = new ModelAndView("modifydepartment");
    		mav.addObject("department", department);
    		return mav;
    	}
    또는
    	public ModelAndView formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		model.addAttribute("department", department);
    		ModelAndView mav = new ModelAndView("modifydepartment");
    		mav.addAllObjects(model);
    		return mav;
    	}
  • Model(또는 ModelMap) - 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 Model 객체에 담겨 있다.
    View 이름은 RequestToViewNameTranslator가 URL을 이용하여 결정한다. 인터페이스 RequestToViewNameTranslator의 구현클래스인 DefaultRequestToViewNameTranslator가 View 이름을 결정하는 방식은 아래와 같다.
    http://localhost:8080/gamecast/display.html -> display
    http://localhost:8080/gamecast/displayShoppingCart.html -> displayShoppingCart
    http://localhost:8080/gamecast/admin/index.html -> admin/index
    	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public Model formBackingObject(@RequestParam("deptid") String deptid, Model model) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		model.addAttribute("department", department);
    		return model;
    	}
    또는
    	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public Model formBackingObject(@RequestParam("deptid") String deptid) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		Model model = new ExtendedModelMap();
    		model.addAttribute("department", department);
    		return model;
    	}
  • Map - 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 Map 객체에 담겨 있으며, View 이름은 역시 RequestToViewNameTranslator가 결정한다.
    	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public Map formBackingObject(@RequestParam("deptid") String deptid) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		Map model = new HashMap();
    		model.put("department", department);
    		return model;
    	}
    또는 
    	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public Map formBackingObject(@RequestParam("deptid") String deptid, Map model) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		model.put("department", department);
    		return model;
    	}
  • String - 리턴하는 String 값이 곧 View 이름이 된다. 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 Model(또는 ModelMap)에 담겨 있다. 리턴할 Model(또는 ModelMap)객체가 해당 메소드의 argument에 선언되어 있어야 한다.
            <!--(O)-->
    	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public String formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		model.addAttribute("department", department);
    		return "modifydepartment";
    	}
     
            <!--(X)-->
    	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public String formBackingObject(@RequestParam("deptid") String deptid) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		ModelMap model = new ModelMap();
    		model.addAttribute("department", department);
    		return "modifydepartment";
    	}
  • View - View를 리턴한다. 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 Model(또는 ModelMap)에 담겨 있다.
  • void - 메소드가 ServletResponse / HttpServletResponse등을 사용해서 직접 응답을 처리하는 경우. View 이름은 RequestToViewNameTranslator가 결정한다.

POJO-Style의 Controller

@MVC는 Controller 개발시에 특정 인터페이스를 구현 하거나 특정 클래스를 상속해야할 필요가 없다.
Controller의 메소드에서 Servlet API를 반드시 참조하지 않아도 되며, 훨씬 유연해진 메소드 시그니쳐로 개발이 가능하다.
여기서는 SimpleFormController의 폼 처리 액션을 @Controller로 구현함으로써, POJO-Style에 가까워졌지만 기존의 계층형 Controller에서 제공하던 기능들을 여전히 구현할 수 있음을 보이고자 한다.

FormController by SimpleFormController -> @Controller

앞서 SimpleFormController을 설명하면서 예제로 작성된 com.easycompany.controller.hierarchy.UpdateDepartmentController를 @ModelAttribute와 @RequestMapping을 이용해서 같은 기능을 @Controller로 작성해 보겠다.
JSP 소스는 동일한 것을 사용한다. 이곳의 예제 화면 이미지 및 JSP 코드를 참고하라.
기존의 UpdateDepartmentController를 보면 3가지 메소드로 이루어졌다.

  • referenceData - 입력폼에 필요한 참조데이터인 상위부서정보를 가져와서 Map 객체에 저장한다. 이후에 이 Map 객체는 스프링 내부 로직에 의해 ModelMap 객체에 저장된다.
  • formBackingObject - GET 방식 호출일때 초기 입력폼에 들어갈 부서 데이터를 리턴한다. 이 데이터 역시 ModelMap 객체에 저장된다.
  • onSubmit - POST 전송시에 호출되며 폼 전송을 처리한다.
package com.easycompany.controller.hierarchy;
...
 
public class UpdateDepartmentController extends SimpleFormController{
 
	private DepartmentService departmentService;
 
	public void setDepartmentService(DepartmentService departmentService){
		this.departmentService = departmentService;
	}
 
	//상위부서리스트(selectbox)는 부서정보클래스에 없으므로 , 상위부서리스트 데이터를 DB에서 구해서 별도의 참조데이터로 구성한다.
	@Override
	protected Map referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception{
 
		Map referenceMap = new HashMap();
		referenceMap.put("deptInfoOneDepthCategory",departmentService.getDepartmentIdNameList("1"));	//상위부서정보를 가져와서 Map에 담는다.
		return referenceMap;
	}
 
	@Override
	protected Object formBackingObject(HttpServletRequest request) throws Exception {
		if(!isFormSubmission(request)){	// GET 요청이면
			String deptid = request.getParameter("deptid");
			Department department = departmentService.getDepartmentInfoById(deptid);//부서 아이디로 DB를 조회한 결과가 커맨드 객체 반영.
			return department;
		}else{	// POST 요청이면
			//AbstractFormController의 formBackingObject을 호출하면 요청객체의 파라미터와 설정된 커맨드 객체간에 기본적인 데이터 바인딩이 이루어 진다.
			return super.formBackingObject(request);
		}
	}
 
	@Override
	protected ModelAndView onSubmit(HttpServletRequest request,
			HttpServletResponse response, Object command, BindException errors) throws Exception{
 
		Department department = (Department) command;
 
		try {
			departmentService.updateDepartment(department);
		} catch (Exception ex) {
			return showForm(request, response, errors);
		}
 
		return new ModelAndView(getSuccessView(), "department", department);
	}
}

@Controller로 작성된 com.easycompany.controller.annotation.UpdateDepartmentController은 3개의 메소드로 이루어져 있다.
계층형 Controller인 기존의 UpdateDepartmentController와는 달리 각 메소드는 Override 할 필요없기 때문에 메소드 이름은 자유롭게 지을 수 있다.
쉬운 비교를 위해 SimpleFormController과 동일한 메소드 이름을 선택했다.

  • referenceData - 입력폼에 필요한 참조데이터인 상위부서정보를 가져와서 ModelMap에 저장한다.(by @ModelAttribute)
  • formBackingObject - GET 방식 호출일때 처리를 담당한다. 초기 입력폼 구성을 위한 부서데이터를 가져와서 ModelMap에 저장한다.
  • onSubmit - POST 전송시에 호출되며 폼 전송을 처리한다.

(POJO에 가까운) 프레임워크 코드들은 감춰졌고, 보다 직관적으로 비지니스 내용을 표현할 수 있게 되었다고 생각한다.

package com.easycompany.controller.annotation;
 
...
@Controller
public class UpdateDepartmentController {
 
	@Autowired
	private DepartmentService departmentService;
 
	//상위부서리스트(selectbox)는 부서정보클래스에 없으므로 , 상위부서리스트 데이터를 DB에서 구해서 별도의 참조데이터로 구성한다.
	@ModelAttribute("deptInfoOneDepthCategory")
	public Map<String, String> referenceData() {
		return departmentService.getDepartmentIdNameList("1");
	}
 
	// 해당 부서번호의 부서정보 데이터를 불러와 입력폼을 채운다
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public String formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
		Department department = departmentService.getDepartmentInfoById(deptid);
		model.addAttribute("department", department); //form tag의 commandName은 이 attribute name과 일치해야 한다. <form:form commandName="department">.
		return "modifydepartment";
	}
 
	//사용자가 데이터 수정을 끝내고 저장 버튼을 누르면 수정 데이터로 저장을 담당하는 서비스(DB)를 호출한다.
	//저장이 성공하면 부서리스트 페이지로 이동하고 에러가 있으면 다시 입력폼페이지로 이동한다.
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.POST)
	public String onSubmit(@ModelAttribute("department") Department department, BindingResult bindingResult) {
 
		//validation code
		new DepartmentValidator().validate(department, bindingResult);		
		if(bindingResult.hasErrors()){
			return "modifydepartment";
		}
 
		try {
			departmentService.updateDepartment(department);
			return "redirect:/departmentList.do?depth=1";
		} catch (Exception e) {
			e.printStackTrace();
			return "modifydepartment";
		}
	}
}

참고자료

출처 - http://www.egovframe.org/wiki/doku.php?id=egovframework:rte:ptl:annotation-based_controller&s[]=requestmapping


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

19.Annotation based Spring MVC

Spring XML 만을 독립적으로 사용할 경우 때때로 방대하고 복잡한 속성 정의 파일들로 인해 시스템 개발 및 유지보수의 지연을 초래할 가능성이 높아진다. 이러한 문제점을 해결하기 위해 Spring Framework에서는 별도 XML 정의없이도 사용 가능한 annotation 지원에 주력하고 있는 실정이다. SpringMVC기반의 Controller 구현을 위해서 Spring에서 제공하는 annotation의 종류와 그 사용법에 대해서 상세히 살펴보도록 한다. Annotation을 사용하여 SpringMVC기반의 Controller를 작성하면, Controller라는 인터페이스를 상속받거나 그 외 Spring에서 제공하는 Controller 구현체들을 상속받지 않아도 된다. 따라서 Servlet API와는 독립적으로 작성할 수 있다는 장점이 있다. (단, annotation은 JAVA 5 이상에서만 사용가능함에 유의하도록 한다.) 본 문서에서는 annotation에 대한 일반적인 내용보다는, annotation을 사용하여 어떻게 Spring MVC의 각 구성요소들을 구현하는지 알아보도록 한다. Annotation에 대한 보다 자세한 내용은 본 매뉴얼 >> Spring >> Annotation 을 참고하기 바란다.

19.1.Configuration

Annotation을 사용하여 SpringMVC 기반의 웹어플리케이션을 구현하기 위해서는 속성 정의 XML에 추가되어야 할 설정들이 있다.

19.1.1.Handler 설정

@RequestMapping annotation를 처리하는 default 클래스는 다음과 같다.

  • DefaultAnnotationHandlerMapping

  • AnnotationMethodHandlerAdapter

앞에서 BeanNameUrlHandlerMapping이나 SimpleUrlHandleMapping에서 처럼, 위의 DefaultAnnotationHandlerMapping을 사용할 경우에도 Interceptor를 정의할 수 있는데, DefaultAnnotationHandlerMapping에 Interceptor를 정의하면 모든 Request URL이 Interceptor 영향을 받게 되는 불편함이 발생한다. 이 때, 특정 URL에만 Interceptor를 정의하고자 하는 경우에 SelectedAnnotationHandlerMapping 을 사용하여 다음과 같이 설정할 수 있다. common-servlet.xml 파일 예이다.

<bean id="annotationHandlerMapping"
    class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
    <property name="order" value="1" />
    <property name="interceptors" ref="loginInterceptor" />
</bean>

<!-- 특정URL에만 Interceptor를 적용하기 위해 사용
※ 참고 
http://www.scottmurphy.info/spring_framework_annotation_based_controller_interceptors-->
<bean id="selectedAnnotationHandlerMapping"
    class="org.springplugins.web.SelectedAnnotationHandlerMapping">
    <!-- order 값이 작은 것이 우선적으로 적용된다. -->
    <property name="order" value="0" />
    <property name="urls">
        <list>
            <value>/updateCategory.do</value>
        </list>
    </property>
    <property name="interceptors">
        <list>
            <ref bean="authorizationInterceptor" />
        </list>
    </property>
</bean>

19.1.2.Component Scan 설정

@Controller annotation으로 정의된 컨트롤러 클래스를 사용하기 위해서는 <context:component-scan/> 을 속성 정의 XML에 추가해 주어야 한다. <context:component-scan/>에 대한 자세한 내용은 본 매뉴얼 >> Spring >> Annotation 을 참고하기 바란다.

19.1.2.1.Using Filters to customize scanning

<context:component-scan/>은 해당 클래스패스 내에 @Component, @Service, @Repository, @Controller annotation 이 적용된 클래스를 모두 찾아서 Spring 컨테이너가 관리하는 컴포넌트로 등록하도록 하는 설정이다. 이와 같은 디폴트 설정으로 stereotype annotation을 Auto Detecting하여 사용 시, 비즈니스 레이어와 프레젠테이션 레이어에 중복으로 <context:component-scan/>을 설정하는 경우 다음과 같은 문제가 발생할 수 있다.

  • Auto Detecting으로 야기되는 문제점

    • Annotation이 적용된 컴포넌트 클래스가 비즈니스 레이어의 Application Context와 프레젠테이션 레이어의 WebApplication Context에 중복하여 등록된다.

    • 비즈니스 레이어의 Application Context와 프레젠테이션 레이어의 WebApplication Context는 Parent-child 관계이며 일반적으로 AOP 설정은 비즈니스 레이어에서 관리한다.

    • 따라서 Proxy 기반의 Spring AOP는 비즈니스 레이어의 Application Context에 등록된 컴포넌트에만 적용된다.

    • WebApplication Context에 등록된 비즈니스 레이어에 해당하는 컴포넌트는 AOP가 적용되지 않는다.

    • 이로 인해 WebApplication Context에서는 AOP가 적용되지 않은 비즈니스 컴포넌트를 먼저 참조하여 Spring AOP가 동작하지 않을 문제점이 발생한다.

    이와 같은 문제를 방지하기 위해서 비즈니스 레이어(Application Context)에서 관리되어야하는 컴포넌트와 프레젠테이션 레이어(Web Application Context)에서 관리되어야하는 컴포넌트를 구분할 필요가 있다.

    다음은 프레젠테이션 레이어에서 @Controller annotation이 적용된 클래스만 WebApplication Context에 등록하는common-servlet.xml 파일의 설정 예이다.

    <!-- use-default-filters="false"로 설정하고 include-filter를 사용했기 때문에 
          WebApplicationContext에는 stereotype @Contoller Bean 만 등록된다. -->
    <context:component-scan base-package="anyframe.sample.springmvc" 
                                                      use-default-filters="false">
        <context:include-filter type="annotation" 
            expression="org.springframework.stereotype.Controller" />
    </context:component-scan>

    위의 예와 같이 <context:component-scan>하위에 <context:include-filter>나 <context:exclude-filter>를 추가하면 컨테이너에 의해 검색될 대상의 범위를 조정할 수 있다. filter에 대한 자세한 내용은 본 매뉴얼 >> Spring >>Annotation 을 참고 바란다.

19.2.Controller

사용자가 Spring MVC의 컨트롤러를 작성하려면 AbstractController나 SimpleFormController 등 Spring에서 제공하는 컨트롤러 클래스를 상속받아야만 했다. Spring 2.5 이상에서는 다른 클래스를 상속받거나 Servlet API를 사용하지 않아도 annotation을 사용하여 컨트롤러를 구현할 수 있다. 본 문서에서는 annotation을 사용하여 Spring MVC 컨트롤러를 작성하는 방법에 대해서 알아본다.

  • @Controller : 컨트롤러 클래스 정의

  • @RequestMapping : HTTP Request URL을 처리할 컨트롤러 클래스 또는 메소드 정의

  • @RequestParam : HTTP Request에 포함된 파라미터 참조 시 사용

  • @ModelAttribute : HTTP Request에 포함된 파라미터를 Model 객체로 바인딩함, @ModelAttribute의 'name'으로 정의한 Model객체를 다음 View에서 사용 가능

  • @SessionAttributes : Session에 저장할 Model attribute를 정의

19.2.1.@Controller

특정 클래스에 @Controller annotation을 적용하면 다른 클래스를 상속받거나 Servlet API를 사용하지 않아도 해당 클래스가 컨트롤러 역할을 수행하도록 해준다.

다음은 @Controller를 사용하여 작성한 ProductController 클래스 파일의 일부이다.

@Controller
public class ProductController {
    // 중략
}

19.2.2.@RequestMapping

@RequestMapping annotation은 컨트롤러 클래스나 메소드가 특정 HTTP Request URL을 처리하도록 매핑하기 위해서 사용한다. 그래서 클래스 선언부에 @RequestMapping을 적용할 수도 있고(이하 Type-Level), 클래스의 메소드에 @RequestMapping을 적용할 수도 있다(이하 Method-Level). Type-Level의 @RequestMappign에 URL path를 정의한 경우, Method-Level의 @RequestMapping에서는 Type-Level의 URL path를 상속받는다.

@Controller
@RequestMapping("/listProduct.do")
public class ProductController {
    // 중략    
    @RequestMapping
    public ModelAndView list(HttpServletRequest request, ProductSearchVO searchVO) 
                                                                    throws Exception {
        // 중략
        Page resultPage = productService.getPagingList(searchVO);
        
        mnv.addObject("productList", resultPage.getList());
        // 중략

        return mnv;
    }
}

@RequestMapping은 구현하는 컨트롤러 종류에 따라 아래와 같은 방식으로 사용할 수 있다.

  • Form Controller 구현

  • Multi-action Controller 구현

기존에 SimpleFormController와 같은 Controller 클래스를 상속받아서 컨트롤러를 작성할 때는, 상위클래스에 정의된 메소드를 override하여 구현하기 때문에 입력 argument 타입과 return 타입이 이미 정해져있다. 이에 반해 @RequestMapping을 적용하여 작성하는 핸들러 메소드는 다양한 argument 타입과 return 타입을 사용할 수 있다.

19.2.2.1.Form Controller 구현

  • 클래스 선언부에 @RequestMapping을 사용하여 처리할 Request URL Mapping

  • 메소드에는 @RequestMapping의 'method', 'params'와 같은 상세 속성 정보를 정의하여 Request URL의 Mapping을 세분화

위와 같이 작성하면 기존에 SimpleFormController를 상속받아 작성하였던 폼을 처리하는 컨트롤러를 구현할 수 있다. 다음은 폼 처리 컨트롤러를 작성한 EditProductController 의 예이다.

@Controller
@RequestMapping("/product.do")
public class EditProductController {

    @RequestMapping(method = RequestMethod.GET)
    public ModelAndView addProductView() {
        // 중략
        return mnv;
    }

    @RequestMapping(method = RequestMethod.POST)
    public String addProduct(HttpServletRequest request, @ModelAttribute("product")
        Product product, BindingResult result, SessionStatus status) throws Exception {
        // 중략
        return "/listProduct.do";
    }
}

19.2.2.2.Multi-action Controller 구현

@RequestMapping annotation을 사용하여 여러 HTTP Request를 처리할 수 있는 Multi-action 컨트롤러를 구현할 수 있다.

  • 메소드에 Request URL을 Mapping한 @RequestMapping을 정의

다음은 Multi-action 컨트롤러를 구현한ProductController 의 예이다.

@Controller
public class ProductController {

    @RequestMapping("/listProduct.do")
    public ModelAndView getProductList() {
        // 중략
        return mnv;
    }

    @RequestMapping("/getProduct.do")
    public String getProduct(@RequestParam("productNo")
    String productNo, ModelMap model) {
        // 중략
        return "/WEB-INF/jsp/annotation/sales/product/viewProduct.jsp";
    }    
}
        

@RequestMapping annotation에는 다음과 같은 상세 속성 정보를 부여할 수 있다.

nameDescription
value"value='/getProduct.do'"와 같은 형식의 매핑 URI이다. 디폴트 속성이기 때문에 value만 정의하는 경우에는 'value='은 생략할 수 있다. 예 : @RequestMapping(value = {"/addProduct.do", "/updateProduct.do" }) 위의 경우 "/addProduct.do", "/updateProduct.do" 두 URL 모두 처리한다.
methodGET, POST, HEAD 등으로 표현되는 HTTP Request method에 따라 requestMapping을 할 수 있다. 'method=RequestMethod.GET' 형식으로 사용한다. method 값을 정의하지 않는 경우 모든 HTTP Request method에 대해서 처리한다. 예 : @RequestMapping(method = RequestMethod.POST). 이 경우 value 값은 클래스 선언에 정의한 @RequestMapping의 value 값을 상속받는다.
paramsHTTP Request로 들어오는 파라미터 표현이다.'params={"param1=a", "param2", "!myParam"}' 로 다양하게 표현 가능하다. 예 : @RequestMapping(params = {"param1=a", "param2", "!myParam"}) 위의 경우 HTTP Request에 param1과 param2 파라미터가 존재해야하고 param1의 값은 'a'이어야 하며, myParam이라는 파라미터는 존재하지 않아야한다. 또한, value 값은 클래스 선언에 정의한 @RequestMapping의 value 값을 상속받는다.

19.2.2.3.Supported argument types

@RequestMapping을 사용하여 작성하는 핸들러 메소드는 다음과 같은 타입의 입력 argument를 순서에 관계없이 정의할 수 있다. 단, validation results를 입력 argument로 받을 경우에는 해당 command 객체 바로 다음에 위치해야한다.

  • Servlet API의 Request와 Response 객체

    ServletRequest 또는 HttpServletRequest 등을 메소드 내부에서 직접 사용해야 하는 경우

    @RequestMapping(params = "param=add")
    public String addProduct(HttpServletRequest request, Product product
                                             , BindingResult result, SessionStatus status) throws Exception {
        // 중략
        String message = messageSource.getMessage(
           "product.error.exist", new String[] {product.getProductNo() },
            localeResolver.resolveLocale(request));
       }

  • Servlet API의 Session

    HttpSession 객체를 메소드 내부에서 사용하는 경우 예 : user 정보와 같은 global session attribute를 사용할 때

    @RequestMapping("/login.do")
    protected ModelAndView handleRequestInternal(HttpSession session, 
    @RequestParam("userId") String userId) throws Exception {
    	session.setAttribute("userId", userId);
    	return new ModelAndView("/index.jsp");
    }

  • java.util.Locale

    현재 request의 locale을 사용할 경우

    @RequestMapping(params = "param=add")
    public String addProduct(Locale locale, Product product, BindingResult result
                                                                  , SessionStatus status) throws Exception {
        // 중략
        String message = messageSource.getMessage(
        			"product.error.exist", new String[] {product.getProductNo()}, locale);
       }

  • java.io.InputStream 또는 java.io.Reader

    Request의 content를 직접 처리할 경우 (Servlet API가 제공하는 raw InputStream/Reader)

    @RequestMapping(params = "param=add")
    public String addProduct(InputStream is, Product product, BindingResult result
                                                                , SessionStatus status) throws Exception {
            // 중략
            for(int totalRead = 0; totalRead < totalBytes; totalRead += readBytes) {
                readBytes = is.read(binArray, totalRead, totalBytes - totalRead);
                // 중략
            }
        
        // 중략
    }

  • java.io.OutputStream 또는 java.io.Writer

    Response의 content를 직접 처리할 경우(Servlet API가 제공하는 raw OutputStream/Writer)

    @RequestMapping(params = "param=add")
    public String addProduct(OutputStream os, Product product, BindingResult result, SessionStatus status) throws Exception {
        // 중략
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();    
        byte[] content = outStream.toByteArray();
        os.write(content);
        os.flush();
        // 중략
    }

  • @RequestParam annotation이 적용된 argument

    ServletRequest.getParameter(java.lang.String name)와 같은 역할 수행

    @RequestMapping("/deleteProduct.do")
    public String deleteProduct(@RequestParam("productNo") String productNo) {
        productService.deleteProduct(productNo);
        return "/listProduct.do";
    }

  • java.util.Map 또는 org.springframework.ui.Model 또는 org.springframework.ui.ModelMap

    Web View로 데이터를 전달해야 하는 경우 위 타입의 argument를 정의하고, 메소드 내부에서 View로 전달할 데이터를 추가함

    @RequestMapping("/getProduct.do")
    public String getProduct(@RequestParam("productNo") String productNo, Map map) {
        Product product = productService.getProduct(productNo);    
        
        map.put("product", product);
        
        return "/WEB-INF/jsp/annotation/sales/product/viewProduct.jsp";
    }
    @RequestMapping("/getProduct.do")
    public String getProduct(@RequestParam("productNo") String productNo, Model model) {
        Product product = productService.getProduct(productNo);
        
       model.addAttribute("product", product);
        
        return "/WEB-INF/jsp/annotation/sales/product/viewProduct.jsp";
    }
    @RequestMapping("/getProduct.do")
    public String getProduct(@RequestParam("productNo") String productNo, ModelMap modelMap) {
        Product product = productService.getProduct(productNo);
        
       modelMap.addAttribute("product", product);
        return "/WEB-INF/jsp/annotation/sales/product/viewProduct.jsp";
    }
  • Command 또는 form 객체

    HTTP Request로 전달된 parameter를 binding한 객체로 다음 View에서 사용 가능하고 @SessionAttributes를 통해 session에 저장되어 관리될 수 있다. @ModelAttribute annotation을 이용하여 사용자 임의로 이름을 부여할 수 있다.

    @RequestMapping("/addProduct.do")
    public String updateProduct(Product product, SessionStatus status) throws Exception {
        // 여기서 'product'가 Command(/form) 객체이다.
        return "/listProduct.do";
    }

    @RequestMapping("/addProduct.do")
    public String updateProduct(@ModelAttribute("updatedProduct") Product product, 
        SessionStatus status) throws Exception {
        // 여기서 'updatedProduct'라는 이름의 'product'객체가 Command(/form) 객체이다.
        return "/listProduct.do";
    }
  • org.springframework.validation.Errors 또는 org.springframework.validation.BindingResult

    바로 이전의 입력파라미터인 Command 또는 form 객체의 validation 결과 값을 저장하는 객체로 해당 command 또는 form 객체 바로 다음에 위치해야 함에 유의하도록 한다.

    @RequestMapping(params = "param=add")
    public String addProduct(HttpServletRequest request, Product product, BindingResult result
                                                                                 , SessionStatus status) throws Exception {
            
        new ProductValidator().validate(product, result);
        if (result.hasErrors()) {
            return "/WEB-INF/jsp/annotation/sales/product/productForm.jsp";
        } else {
            // 중략
            return "/listProduct.do";
        }
    }

  • org.springframework.web.bind.support.SessionStatus

    폼 처리가 완료되었을 때 status를 처리하기 위해서 argument로 설정. SessionStatus.setComplete()를 호출하면 컨트롤러 클래스에 @SessionAttributes로 정의된 Model객체를 session에서 지우도록 이벤트를 발생시킨다.

    @RequestMapping(params = "param=add")
    public String addProduct(HttpServletRequest request, Product product, BindingResult result
                                                                , SessionStatus status) {
        // 중략
        productService.addProduct(product);
        status.setComplete();
        return "/listProduct.do";
    }

19.2.2.4.Supported return types

@RequestMapping을 이용한 핸들러 메소드는 다음과 같은 리턴타입을 가질 수 있다.

  • ModelAndView 객체

    View와 Model 정보를 모두 포함한 객체를 리턴하는 경우.

    @RequestMapping(params = "param=addView")
    public ModelAndView addProductView() {
        ModelAndView mnv = 
                 new ModelAndView("/WEB-INF/jsp/annotation/sales/product/productForm.jsp");
        mnv.addObject("product", new Product());
        return mnv;
    }

  • Map

    Web View로 전달할 데이터만 리턴하는 경우.

    @RequestMapping("/productList.do")
    public Map getProductList() {
        List productList = productService.getProductList();
        ModelMap map = new ModelMap(productList);//productList가 "productList"라는 이름으로 저장됨.
        return map;
    }

    여기서 View에 대한 정보를 명시적으로 리턴하지는 않았지만, 내부적으로 View name은 RequestToViewNameTranslator에 의해서 입력된 HTTP Request를 이용하여 생성된다. 예를 들어DefaultRequestToViewNameTranslator는 입력된 HTTP Request URI를 변환하여 View name을 다음과 같이 생성한다.

    http://localhost:8080/anyframe-sample/display.do     -> 생성된 View name : 'display'
    http://localhost:8080/anyframe-sample/admin/index.do -> 생성된 View name : 'admin/index'

    위와 같이 자동으로 생성되는 View name에 'jsp/'와 같이 prefix를 붙이거나 '.jsp' 같은 확장자를 덧붙이고자 할 때는 아래와 같이 속정 정의 XML(xxx-servlet.xml)에 추가하면 된다.

    <bean id="viewNameTranslator" 
      class="org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator">
          <property name="prefix" value="jsp/"/>
          <property name="suffix" value=".jsp"/>
    </bean>

  • Model

    Web View로 전달할 데이터만 리턴하는 경우Model 은 Java-5 부터 추가된 인터페이스이다. 기본적으로 ModelMap과 같은 기능을 제공한다. Model 인터페이스의 구현클래스에는 BindingAwareModelMap 와 ExtendedModelMap이 있다. View name은 위에서 설명한 바와 같이 RequestToViewNameTranslator에 의해 내부적으로 생성된다.

    @RequestMapping("/productList.do")
    public Model getProductList() {
        List productList = productService.getProductList();
        ExtendedModelMap map = new ExtendedModelMap();
        map.addAttribute("productList", productList);
        
        return map;
    }

  • String

    View name만 리턴하는 경우.

    @RequestMapping(value = {"/addProduct.do", "/updateProduct.do" })
    public String updateProduct(Product product, SessionStatus status) throws Exception {
    
        // 중략
        
        return "/listProduct.do";
    }

  • void

    메소드 내부에서 직접 HTTP Response를 직접 처리하는 경우. 또는 View name이 RequestToViewNameTranslator에 의해 내부적으로 생성되는 경우

    @RequestMapping("/addView.do")
    public void addView(HttpServletResponse response) {
        // 중략
        // response 직접 처리
    }

    @RequestMapping("/addView.do")
    public void addView() {
        // 중략
        // View name이 DefaultRequestToViewNameTranslator에 의해서 내부적으로 'addView'로 결정됨.
    }

19.2.3.@RequestParam

@RequestParam annotation은 HTTP Request parameter를 컨트롤러 메소드의 argument로 바인딩하는데 사용되며ServletRequest.getParameter(java.lang.String name) 와 같은 역할을 한다. 다음은 @RequestParam annotation의 사용 예이다.

@RequestMapping("/updateProduct.do")
public String updateProduct(@RequestParam("productNo") String productNo,
    @RequestParam("sellAmount") int sellAmount, 
    @RequestParam("realImageFile") MultipartFile picturefile) {
    // 중략
    return "/listProduct.do";
}

@RequestParam을 적용한 파라미터는 반드시 HTTP Request에 존재해야 한다. 그렇지 않은 경우 다음과 같이 org.springframework.web.bind.MissingServletRequestParameterException이 발생한다.

org.springframework.web.bind.MissingServletRequestParameterException:
                         Required java.lang.String parameter 'productNo' is not present

그러나 아래와 같이 @RequestParam의 required 속성을 false로 설정할 경우 HTTP Request에 파라미터가 존재하지 않아도 Exception이 발생하지 않는다.

@RequestMapping("/deleteProduct.do")
public String deleteProduct(@RequestParam(value="productNo", required="false")String productNo){
    // 중략
}

19.2.4.@ModelAttribute

@ModelAttribute는 컨트롤러에서 다음과 같이 두 가지 방법으로 사용할 수 있다.

  • 메소드 자체에 정의

    입력 폼 페이지에서 출력해 줄 reference data를 전달하고자 할 때. SimpleFormController의 referenceData() 메소드와 같은 역할

  • 메소드의 입력 argument에 정의

    메소드의 argument로 입력된 Command 객체에 이름을 부여하고자 할 때.

다음은 위에서 설명한 두가지 방법으로 @ModelAttribute를 사용한 EditProductController 의 예이다.

@Controller
@RequestMapping("/product.do")
public class EditProductController {
    // 메소드 자체에 정의
    @ModelAttribute("categoryList")
    public List populateCategoryList() throws Exception {
        return categoryService.getCategoryList();
    }
    
    // 메소드의 입력 argument에 정의
    @RequestMapping(params = "param=add")
    public String addProduct(@ModelAttribute("updatedProduct")
        Product product, BindingResult result, SessionStatus status) throws Exception {
        // 중략
    }
}

19.2.5.@SessionAttributes

@SessionAttributes는 session에 저장하여 관리할 model attribute를 정의할 때 사용한다. @SessionAttributes에 정의하는 attribute의 이름은 해당 컨트롤러 클래스안에서 사용되는 model attribute의 이름과 같아야 한다.

다음은 @SessionAttributes를 사용하여 session에 저장하여 관리할 model을 정의한 예이다.

@Controller
@RequestMapping("/product.do")
@SessionAttributes(value = {"product", "category"})
public class EditProductController {
    // 중략
}

19.3.Dependency Injection

컨트롤러 클래스에서 기능 수행을 위해 다른 Bean을 참조해야 하는 경우 @Autowired 또는 @Resource annotation을 사용한다. @Resource와 @Autowired annotation에 대한 자세한 설명은 본 매뉴얼 >> Spring >>Annotation 부분을 참고하기 바란다.

다음은 컨트롤러 클래스에서 @Resource annotation을 사용한 EditProductController 의 예이다.

@Controller
@RequestMapping("/product.do")
public class EditProductController {
    @Resource(name = "productService")
    ProductService productService;

    @Resource
    CategoryService categoryService;

    @Resource
    MessageSource messageSource;

    @Resource
    LocaleResolver localeResolver;
    // 중략
}

19.4.Double Submit Prevention

Spring MVC에서는 double submit을 방지하기 위해 AbstractFormController를 제공하고있고, 폼 컨트롤러 구현 시에 사용하는 SimpleFormController 또한 AbstractFormController를 상속받았기 때문에 double submit 방지가 가능하다. XML 기반의 double submit 방지 기능 적용 방법은 본 매뉴얼 >> Spring MVC >> Extension >>Double Submit 부분을 참고한다. 본 문서에서는 Annotation을 사용하여 다른 클래스를 상속받지 않고도 double submit 방지 기능을 구현하는 방법에 대해서 자세히 알아본다.

19.4.1.Annotation을 이용한 Double Submit 방지

annotation을 이용한 Double Submit 방지는 다음과 같은 원리로 구현된다.

  • Double submission을 방지하고자 하는 form 객체를 model로 저장

    다음 예제와 같이 ModelAndView, ModelMap 등을 이용하여 저장한다.

    @RequestMapping(params = "param=addView")
    public ModelAndView addProductView() {
        ModelAndView mnv = 
                  new ModelAndView("/WEB-INF/jsp/annotation/sales/product/productForm.jsp");
        mnv.addObject("product", new Product());
        return mnv;
    }

  • 저장한 model을 @SessionAttributes로 정의

    다음 예제와 같이 컨트롤러 클래스 선언부에 @SessionAttributes("product")로 정의한다.

    @Controller
    @RequestMapping("/product.do")
    @SessionAttributes("product")
    public class EditProductController {
            // 중략
    }

  • 컨트롤러 메소드에서 폼 처리 완료 후 Session status 변경

    @RequestMapping(params = "param=add")
    public String addProduct(HttpServletRequest request, Product product, BindingResult result
                                   , SessionStatus status) throws Exception {
        productService.addProduct(product);
        status.setComplete();
        return "/listProduct.do";    
    }
  • status.setComplete()는 session에서 저장된 model을 삭제하는 이벤트 발생

  • 사용자가 같은 버튼을 여러번 클릭하는 경우와 같이, 여러 thread가 동시에 Session에 접근할 수도 있기 때문에 반드시 AnnotationMethodHandlerAdapter의 synchronizeOnSession 속성을 true로 설정

    <bean id="annotationHandlerAdaptor"
        class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="synchronizeOnSession" value="true" />
    </bean>

  • 따라서, 이후에 다시 submit 요청이 온 경우 session에 저장된 model이 삭제되었기 때문에 아래와 같이 org.springframework.web.HttpSessionRequiredException발생

    org.springframework.web.HttpSessionRequiredException: 
          Session attribute 'dept' required - not found in session

19.5.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.annotation.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-tasks-2.0.10.jar 파일이 있어야 한다.)

    표 19.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.annotation.zipDownload
    maven-ant-tasks-2.0.10.jarDownload


출처 - http://dev.anyframejava.org/docs/anyframe/4.0.0/reference/html/ch19.html







11.Controller

MVC에서 C에 해당하는 컨트롤러는 사용자의 요청을 받아서 어플리케이션에 정의된 적절한 Service를 수행한 후, 그 결과를 다시 View를 통해 사용자에게 보여줄 수 있는 Model 데이터로 변환하는 역할을 담당한다. Spring에서는 이러한 컨트롤러를 특정 API에 종속되지 않고 사용자가 자유롭게 작성할 수 있는 추상적인 구현 방법을 제공하고 있다.

Spring 2.5에서부터 @RequestMapping, @RequestParam, @ModelAttribute 등을 이용한 Annotation 기반의 컨트롤러 개발 방식을 소개했다. Annotation을 사용하여 SpringMVC기반의 컨트롤러를 작성하면, 특정 인터페이스를 상속받거나 특정 클래스를 상속받지 않아도 된다. 또한 Servlet API와도 독립적으로 작성할 수 있다는 장점이 있다. (단, annotation은 JAVA 5 이상에서만 사용가능함에 유의하도록 한다.)

Spring MVC controller hierarchy deprecated

기존에 Spring에서 제공하던 AbstractFormController등의 Form 관련 Class 계층은 Spring 3 부터는 더이상 지원하지 않는다. Spring MVC에서는 @Controller, @RequestMapping 등의 Annotation을 기반으로 컨트롤러를 개발하도록 가이드하고 있다.

11.1.Configuration

컨트롤러 역할을 수행하는 클래스를 정의하기 위해서는 Spring에서 제공하는 Stereotype Annotation 중 @Controller를 사용한다. 이렇게 정의된 컨트롤러 클래스는 XML 설정 파일에 명시적으로 Bean으로 정의하여 Spring Container에 등록할 수도 있지만, 자동으로 검색 및 등록이 가능하게 할 수도 있다. Spring에서는 이를 Autodetection이라고 한다. Stereotype Annotation이 적용된 클래스들에 대한 Autodetection이 이루어 지도록 하기 위해서는 <context:component-scan/> 을 속성 정의 XML에 추가해 주어야 한다. <context:component-scan/>에 대한 자세한 내용은 본 매뉴얼 >> Foundation Plugin >> Spring >> Annotation을 참고하기 바란다.

11.1.1.Using Filters to customize scanning

<context:component-scan/>은 해당 클래스패스 내에 @Component, @Service, @Repository, @Controller가 적용된 클래스를 모두 찾아서 Spring 컨테이너가 관리하는 컴포넌트로 등록하도록 하는 설정이다. 이와 같은 디폴트 동작 방식으로 Autodetection 기능 이용 시, 비즈니스와 프레젠테이션 레이어 간 Bean 정의 XML을 분리하여 관리하면서 <context:component-scan/>을 중복으로 설정하는 경우 다음과 같은 문제가 발생할 수 있다.

  • Autodetection 중복 설정으로 인해 야기되는 문제점

    • Stereotype Annotation이 적용된 클래스가 비즈니스 레이어의 Root WebApplicationContext와 프레젠테이션 레이어 의 WebApplicationContext에 중복하여 등록된다.

    • 비즈니스 레이어의 Root WebApplicationContext와 프레젠테이션 레이어의 WebApplicationContext는 Parent Container - Child Container 관계로 구성된다. Container가 계층 구조를 가질 때, 사용하고자 하는 Bean 검색 순서는 현재 자기 Container가 먼저이고, Bean이 없을 경우 Parent Container가 그 다음이다.

    • 일반적으로 AOP 설정은 비즈니스 레이어에서 관리한다. 따라서 Proxy 기반의 Spring AOP는 비즈니스 레이어의 Root WebApplicationContext에 등록된 Bean에만 적용되고, 프레젠테이션 레이어의 WebApplicationContext에 중복으로 등록된 Bean에는 적용되지 않는다.

    • 결과적으로 프레젠테이션 레이어의 WebApplicationContext에서는 Proxy 기반의 Spring AOP가 적용되지 않은 Bean을 먼저 참조하게 되어 Spring AOP를 사용하여 설정한 기능들이 동작하지 않는 문제점이 발생한다.

    다음은 위의 내용을 그림으로 나타낸 것이다.

    이와 같은 문제를 방지하기 위해서 비즈니스 레이어(Root WebApplicationContext)에서 관리되어야하는 Bean과 프레젠테이션 레이어(Child WebApplicationContext)에서 관리되어야하는 Bean을 구분할 필요가 있다.

    다음은 프레젠테이션 레이어에서 @Controller annotation이 적용된 클래스만 WebApplication Context에 등록하는common-servlet.xml 파일의 설정 예이다.

    <!-- use-default-filters="false"로 설정하고
    include-filter를 사용했기 때문에 이 WebApplicationContext에는 @Contoller가 적용된 클래스만 등록된다. -->
    <context:component-scan base-package="anyframe.sample.springmvc" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

    위의 예와 같이 <context:component-scan>하위에 <context:include-filter> 나 <context:exclude-filter>를 추가하면 컨테이너에 의해 검색될 대상의 범위를 조정할 수 있다. filter에 대한 자세한 내용은 본 매뉴얼 >> Foundation Plugin >> Spring >> Annotation 을 참고 바란다.

11.2.컨트롤러 구현

앞에서 설명했듯이, Spring MVC에서는 요청을 처리하는 컨트롤러를 특정 인터페이스 구현하거나 특정 클래스 상속받아서 구현하지 않아도 된다. @Controller, @RequestMapping 등의 Annotation만을 이용하여 다양한 형태의 컨트롤러를 만들 수 있다. 본 문서에서는 Spring MVC에서 제공하는 Annotation을 사용하여 컨트롤러를 작성하는 방법에 대해서 알아본다.

  • @Controller : 컨트롤러 클래스 정의

  • @RequestMapping : 처리할 HTTP Request URL과 컨트롤러 클래스 또는 메소드 매핑

  • @RequestParam : HTTP Request에 포함된 파라미터 참조 시 사용

  • @RequestHeader : HTTP Request의 Header 값 참조 시 사용

  • @CookieValue : HTTP Cookie 값 참조 시 사용

  • @ModelAttribute : HTTP Request에 포함된 파라미터를 Model 객체로 바인딩함, @ModelAttribute의 'name'으로 정의한 Model객체를 다음 View에서 사용 가능

  • @SessionAttributes : Session에 저장할 Model attribute를 정의

  • @RequestBody/@ResponseBody : 핸들러 메소드가 HTTP Request와 Response의 Body 메세지를 전체를 직접 접근할 경우에 사용 가능. (HttpEntity 객체를 이용하여 HTTP Request나 Response의 Body 메세지나 Header 값을 처리할 수도 있다.)

11.2.1.@Controller

특정 클래스에 @Controller annotation을 적용하면 다른 클래스를 상속받거나 Servlet API를 사용하지 않아도 해당 클래스가 컨트롤러 역할을 수행하도록 정의할 수 있다.

다음은 @Controller를 사용하여 작성한 MovieController 클래스 파일의 일부이다.

@Controller
public class MovieController {
    // 중략
}

11.2.2.@RequestMapping

@RequestMapping annotation은 컨트롤러 클래스나 메소드가 특정 HTTP Request URL을 처리하도록 매핑하기 위해서 사용한다. 그래서 클래스 선언부에 @RequestMapping을 적용할 수도 있고(이하 Type-Level), 클래스의 메소드에 @RequestMapping을 적용할 수도 있다(이하 Method-Level). 예를 들어, Type-Level에 @RequestMapping("/movies")라고 정의하고, Method-Level에 @RequestMapping("/new") 라고 정의하면 @RequestMapping("/new")라고 정의한 메소드가 처리하는 URL 경로는 "/movies/new" 가 된다. @RequestMapping은 "/movies/*.do"와 같은 Ant 스타일 경로 패턴도 지원한다. @RequestMapping에는 URL 경로 외에도 HTTP method나 Request 파라미터 등을 추가하여 처리할 URL의 범위를 줄일 수 있다.

또한, Spring 3 부터 REST 스타일의 Web Application 개발을 위해서 URI templates을 지원하기 시작했다. Spring 3에서 추가된 REST 관련 기능들과 REST Style 웹 어플리케이션 개발에 대한 자세한 내용은 본 매뉴얼 Restweb Plugin을 참고하기 바란다.

다음은 @RequestMapping을 사용하여 처리할 URL을 매핑한 코드예이다.

@Controller
@RequestMapping("/foundationMovie.do")
public class MovieController {
    @RequestMapping(params="method=get")
    public String get(@RequestParam("movieId") String movieId, Model model) throws Exception {
        Movie movie = this.movieService.get(movieId);
        //...
        model.addAttribute(movie);
        return "foundationViewMovie";
    }
}

@RequestMapping annotation에는 다음과 같은 상세 속성 정보를 부여하여 처리할 URL의 범위를 한정지을 수 있다.

nameDescription
value

"value='/getMovie.do'"와 같은 형식의 매핑 URL 값이다. 디폴트 속성이기 때문에 value만 정의하는 경우에는 'value='은 생략할 수 있다.

예 : @RequestMapping(value={"/addMovie.do", "/updateMovie.do" }) 이와 같은 경우 "/addMovie.do", "/updateMovie.do" 두 URL 모두 처리한다.

methodGET, POST, HEAD 등으로 표현되는 HTTP Request method에 따라 requestMapping을 할 수 있다. 'method=RequestMethod.GET' 형식으로 사용한다. method 값을 정의하지 않는 경우 모든 HTTP Request method에 대해서 처리한다. 예 :@RequestMapping(method = RequestMethod.POST). 이 경우 value 값은 클래스 선언에 정의한 @RequestMapping의 value 값을 상속받는다.
params

HTTP Request로 들어오는 파라미터 표현이다.'params={"param1=a", "param2", "!myParam"}' 로 다양하게 표현 가능하다.

예 : @RequestMapping(params = {"param1=a", "param2", "!myParam"})위의 경우 HTTP Request에 param1과 param2 파라미터가 존재해야하고 param1의 값은 'a'이어야하며, myParam이라는 파라미터는 존재하지 않아야한다. 또한, value 값은 클래스 선언에 정의한 @RequestMapping의 value 값을 상속받는다.

headersHTTP Request의 헤더 값이다.'headers="someHader=someValue"', 'headers="someHader"', 'headers="!someHader"' 로 다양하게 표현 가능하다. Accept나 Content-Type 같은 헤더에 대해서 media type 표현 시 '*' 도 지원한다. 예 : @RequestMapping(value="/movie.do", headers="content-type=text/*") 의 경우 HTTP Request에 Content-Type 헤더 값이 "text/html", "text/plain" 모두 매칭이 된다. 또한, Type-Level, Method-Level에서 모두 사용할 수 있는데, Type-Level에 정의된 경우, 하위의 모든 핸들러 메소드에서도 Type-Level에서 정의한 헤더값 제한이 적용된다.

@RequestMapping은 구현하는 컨트롤러 종류에 따라 아래와 같은 방식으로 사용할 수 있다.

  • Form 컨트롤러 구현

  • Multi-action 컨트롤러 구현

기존에 SimpleFormController와 같은 컨트롤러 클래스를 상속받아서 컨트롤러를 작성할 때는, 상위클래스에 정의된 메소드를 override하여 구현하기 때문에 입력 argument 타입과 return 타입이 이미 정해져있다. 이에 반해 @RequestMapping을 적용하여 작성하는 핸들러 메소드는 다양한 argument 타입과 return 타입을 사용할 수 있다.

11.2.2.1.Form 컨트롤러 구현

  • 클래스 선언부에 @RequestMapping을 사용하여 처리할 Request URL Mapping

  • 메소드에는 @RequestMapping의 'method', 'params'와 같은 상세 속성 정보를 정의하여 Request URL의 Mapping을 세분화

위와 같이 작성하면 기존에 SimpleFormController를 상속받아 작성하였던 폼을 처리하는 컨트롤러를 구현할 수 있다. 다음은 폼 처리 컨트롤러를 작성한 EditMovieController 의 예이다.

@Controller
@RequestMapping("/foundationMovie.do")
public class EditMovieController {

    @RequestMapping(method = RequestMethod.GET)
    public String createView() {
        // 중략
        return foundationViewMovie;
    }
    
    @RequestMapping(method = RequestMethod.POST)
    public String addMovie(HttpServletRequest request, @ModelAttribute("movie"),
            Movie movie, BindingResult result, SessionStatus status) throws Exception {
        // 중략
        return "redirect:/foundationMovieFinder.do";
    }
}

11.2.2.2.Multi-action 컨트롤러 구현

@RequestMapping annotation을 사용하여 여러 HTTP Request를 처리할 수 있는 Multi-action 컨트롤러를 구현할 수 있다.

  • 메소드에 처리할 Request URL을 Mapping한 @RequestMapping을 정의

다음은 Multi-action 컨트롤러를 구현한MovieController 의 예이다.

@Controller
public class MovieController {

    @RequestMapping("/deleteMovie.do")
    public ModelAndView delete(@RequestParam("movieId") String movieId) {
        // 중략
        return "redirect:/foundationMovieFinder.do";
    }

    @RequestMapping("/getMovie.do")
    public String get(@RequestParam("movieId") String movieId, ModelMap model) {
        // 중략
        model.addAttribute(movie);

        return "foundationViewMovie";
    }
}

11.2.2.3.Supported argument types

@RequestMapping을 사용하여 작성하는 핸들러 메소드는 다음과 같은 타입의 입력 argument를 순서에 관계없이 정의할 수 있다. 단, validation results를 입력 argument로 받을 경우에는 해당 command 객체 바로 다음에 위치해야한다.

  • Servlet API의 Request와 Response 객체

    ServletRequest 또는 HttpServletRequest 등을 메소드 내부에서 직접 사용해야 하는 경우

    @RequestMapping(params = "param=add")
    public String addMovie(HttpServletRequest request,
            Movie movie, BindingResult result, SessionStatus status)
    		throws Exception {
        // 중략
        String message = messageSource.getMessage(
                                        "movie.error.exist", new String[] {movie.getMovieId()},
                                        localeResolver.resolveLocale(request));
    }

  • Servlet API의 Session

    HttpSession 객체를 메소드 내부에서 사용하는 경우 예 : user 정보와 같은 global session attribute를 사용할 때

    @RequestMapping("/login.do")
    protected ModelAndView handleRequestInternal( HttpSession session,
                @RequestParam("userId") String userId) throws Exception {
        session.setAttribute("userId", userId);
        return new ModelAndView("/index.jsp");
    }

    AnnotationMethodHandlerAdapter의 'synchronizeOnSession' 속성

    Servlet 환경에서 Session 접근은 thread-safe하지 않기 때문에, Session에 저장된 정보에 여러개의 thread가 동시에 접근하여 변경할 가능성이 있는 경우 반드시 AnnotationMethodHandlerAdapter의 "synchronizeOnSession" 속성을 "true"로 셋팅하도록 한다.

  • java.util.Locale

    현재 request의 locale을 사용할 경우

    @RequestMapping(params = "param=add")
    public String addMovie(Locale locale,Movie movie, BindingResult result,
            SessionStatus status) throws Exception {
        // 중략
        String message = messageSource.getMessage(
                                        "movie.error.exist", new String[] {movie.getMovieId()}, 
                                        locale);
    }

  • java.io.InputStream 또는 java.io.Reader

    Request의 content를 직접 처리할 경우 (Servlet API가 제공하는 raw InputStream/Reader)

    @RequestMapping(params = "param=add")
    public String addMovie(InputStream is, Movie movie, BindingResult result
                                                             SessionStatus status) throws Exception {
        // 중략
        for(int totalRead = 0; totalRead < totalBytes; totalRead += readBytes) {
            readBytes = is.read(binArray, totalRead, totalBytes - totalRead);
            // 중략
        }
        // 중략
    }

  • java.io.OutputStream 또는 java.io.Writer

    Response의 content를 직접 처리할 경우 (Servlet API가 제공하는 raw OutputStream/Writer)

    @RequestMapping(params = "param=add")
    public String addMovie(OutputStream os, Movie movie, BindingResult result, 
                                                             SessionStatus status) throws Exception {
        // 중략
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        byte[] content = outStream.toByteArray();
        os.write(content);
        os.flush();
        // 중략
    }

  • @PathVariable annotation이 적용된 argument

    URI template 내의 변수를 핸들러 메소드에서 접근할 경우

    @PathVariable에 대한 자세한 사용 방법은 본 매뉴얼 >> Restweb Plugin >> URI Template 참고

    @RequestMapping(value = "/movies/{movieId}/edit", method = RequestMethod.GET)
    public String get(@PathVariable String movieId, Model model)
            throws Exception {
        Movie movie = this.movieService.get(movieId);
        // 중략
        model.addAttribute(movie);
        return "restwebViewMovie";
    }
  • @RequestParam annotation이 적용된 argument

    ServletRequest.getParameter(java.lang.String name)와 같은 역할 수행

    @RequestMapping(params = "method=remove")
    public String remove(@RequestParam("movieId") String movieId)
    		throws Exception {
        this.movieService.remove(movieId);
        return "redirect:/foundationMovieFinder.do?method=list";
    }
    

  • @RequestHeader annotation이 적용된 argument

    @RequestHeader를 사용하면 Servlet Request HTTP 헤더 값을 핸들러 메소드에서 사용 가능

    @RequestMapping("/displayHeaderInfo")
    @ResponseBody
    public String displayHeaderInfo(@CookieValue("JSESSIONID") String cookie,
    		@RequestHeader("Accept-Encoding") String encoding,
    		@RequestHeader("Accept") String accept) {
        StringBuffer sf = new StringBuffer();
        sf.append("JSESSIONID : " + cookie);
        sf.append("\n");
        sf.append("Accept-Encoding : " + encoding);
        sf.append("\n");
        sf.append("Accept : " + accept);
         
        return sf.toString();
    }

  • @RequestBody annotation이 적용된 argument

    @RequestBody를 사용하면 HTTP Request Body를 핸들러 매소드에서 직접 사용 가능

    HTTP Request Body가 HttpMessageConverter에 의해서 선언한 메소드 argument 타입으로 변환되어 전달됨

    @RequestMapping(value = "/movies/add", method = RequestMethod.POST)
    @ResponseBody
    public String add(@RequestBody Movie movie) throws Exception {
        this.movieService.createMovie(movie);		
        return "/movies/" + movie.getMovieId() + "/edit";
    }

  • HttpEntity<?> 객체

    Servlet request HTTP Header와 Body를 핸들러 메소드에서 접근하기 위해 사용 가능. Request 스트림은 HttpMessageConverter를 통해 entity body로 변환됨.

  • java.util.Map 또는 org.springframework.ui.Model 또는 org.springframework.ui.ModelMap

    Web View로 데이터를 전달해야 하는 경우 위 타입의 argument를 정의하고, 메소드 내부에서 View로 전달할 데이터를 추가함

    @RequestMapping("/getMovie.do")
    public String getMovie(@RequestParam("movieId") String movieId, Map map) {
        Movie movie = movieService.getMovie(movieId);
        map.put("movie", movie);
        return "/WEB-INF/jsp/annotation/sales/movie/viewMovie.jsp";
    }
    @RequestMapping("/getMovie.do")
    public String getMovie(@RequestParam("movieId") String movieId, Model model) {
        Movie movie = movieService.getMovie(movieId);
        model.addAttribute("movie", movie);
        return "/WEB-INF/jsp/annotation/sales/movie/viewMovie.jsp";
    }
    @RequestMapping("/getMovie.do")
    public String getMovie(@RequestParam("movieId") String movieId,  ModelMap modelMap) {
        Movie movie = movieService.getMovie(movieId);
        modelMap.addAttribute("movie", movie);
        return "/WEB-INF/jsp/annotation/sales/movie/viewMovie.jsp";
    }
  • Command 또는 Form 객체

    HTTP Request로 전달된 parameter를 binding한 객체로, 다음 View에서 사용 가능하고 @SessionAttributes를 통해 session에 저장되어 관리될 수 있음. @ModelAttribute annotation을 이용하여 사용자 임의로 이름 부여 가능.

    @RequestMapping("/addMovie.do")
    public String updateMovie(Movie movie, SessionStatus status) throws Exception {
        // 여기서 'movie'가 Command(또는 Form) 객체이다.
        return "/listMovie.do";
    }

    @RequestMapping(params="method=update")
    	public String update(@ModelAttribute("updatedMovie") Movie movie, SessionStatus status) throws Exception {
        // 여기서 'updatedMovie'라는 이름의 'movie'객체가 Command(/form) 객체이다.
        // 중략
        return "redirect:/foundationMovieFinder.do?method=list";
    }
  • org.springframework.validation.Errors 또는 org.springframework.validation.BindingResult

    바로 이전의 입력파라미터인 Command 또는 Form 객체의 validation 결과 값을 저장하는 객체로, 해당 Command 또는 Form 객체 바로 다음에 위치해야 함에 유의

    @RequestMapping(params = "method=create")
    public String create(
            @RequestParam(value="realPosterFile", required=false) MultipartFile posterFile,
            @Valid Movie movie, BindingResult results, SessionStatus status)
                throws Exception {
        if (results.hasErrors()) {
            return "foundationViewMovie";
        }
        
        // 중략
        return "redirect:/foundationMovieFinder.do?method=list";
    }

  • org.springframework.web.bind.support.SessionStatus

    Form 처리가 완료되었을 때 status를 처리하기 위해서 argument로 설정. SessionStatus.setComplete()를 호출 하면 컨트롤러 클래스에 @SessionAttributes로 정의된 Model객체를 session에서 지우도록 이벤트 발생

    @RequestMapping(params = "method=create")
    public String create(
            @RequestParam(value="realPosterFile",required=false) MultipartFile posterFile,
            @Valid Movie movie, BindingResult results, SessionStatus status)
                throws Exception {
        // 중략
        this.movieService.create(movie);
        status.setComplete();
        return "redirect:/foundationMovieFinder.do?method=list";
    }

11.2.2.4.Supported return types

@RequestMapping을 이용한 핸들러 메소드는 다음과 같은 리턴타입을 가질 수 있다.

  • ModelAndView 객체

    View와 Model 정보를 모두 포함한 객체를 리턴하는 경우.

    @RequestMapping(params="param=addView")
    public ModelAndView addMovieView() {
        ModelAndView mnv = new ModelAndView("/WEB-INF/jsp/annotation/sales/movie/movieForm.jsp");
        mnv.addObject("movie", new Movie());
        return mnv;
    }

  • Map

    Web View로 전달할 데이터만 리턴하는 경우.

    @RequestMapping("/movieList.do")
    public Map getMovieList() {
        List movieList = movieService.getMovieList();
        ModelMap map = new ModelMap(movieList);//movieList가 "movieList"라는 이름으로 저장됨.
        return map;
    }

    여기서 View에 대한 정보를 명시적으로 리턴하지는 않았지만, 내부적으로 View 이름은 RequestToViewNameTranslator에 의해서 입력된 HTTP Request를 이용하여 생성된다. 예를 들어 DefaultRequestToViewNameTranslator 는 입력된 HTTP Request URI를 변환하여 View 이름을 다음과 같이 생성한다.

    http://localhost:8080/anyframe-sample/display.do
        -> 생성된 View 이름 : 'display'
    http://localhost:8080/anyframe-sample/admin/index.do 
        -> 생성된 View 이름 : 'admin/index'

    위와 같이 자동으로 생성되는 View 이름에 'jsp/'와 같이 prefix를 붙이거나 '.jsp' 같은 확장자를 덧붙이고자 할 때는 아래와 같이 속정 정의 XML(xxx-servlet.xml)에 추가하면 된다.

    <bean id="viewNameTranslator"
              class="org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator">
        <property name="prefix" value="jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

  • Model

    Web View로 전달할 데이터만 리턴하는 경우 Model 은 Java-5 이상에서 사용할 수 있는 인터페이스이다. 기본적으로 ModelMap과 같은 기능을 제공한다. Model 인터페이스의 구현클래스에는 BindingAwareModelMap 와 ExtendedModelMap 이 있다. View 이름은 위에서 설명한 바와 같이 RequestToViewNameTranslator에 의해 내부적으로 생성된다.

    @RequestMapping("/movieList.do")
    public Model getMovieList() {
        List movieList = movieService.getMovieList();
        ExtendedModelMap map = new ExtendedModelMap();
        map.addAttribute("movieList",movieList);
        return map;
    }

  • String

    View 이름만 리턴하는 경우.

    @RequestMapping(value = {"/addMovie.do", "/updateMovie.do" })
    public String updateMovie(Movie movie, SessionStatus status) 
            throws Exception {
        // 중략
        return"/listMovie.do";
    }

  • void

    메소드 내부에서 직접 HTTP Response를 직접 처리하는 경우. 또는 View 이름이 RequestToViewNameTranslator에 의해 내부적으로 생성되는 경우

     @RequestMapping("/addView.do")
    public void addView(HttpServletResponse response) {
        // 중략
        //response 직접 처리
    }
     @RequestMapping("/addView.do")
    public void addView() {
        // 중략
        // View 이름이 DefaultRequestToViewNameTranslator에 의해서 내부적으로 'addView'로 결정됨.
    }

  • @ResponseBody

    핸들러 메소드의 리턴 객체를 Response HTTP Body로 바로 보내는 경우. HttpMessageConverter를 통해서 리턴 객체가 변환되어 Response로 전달됨.

    @RequestMapping(value = "/welcome", method = RequestMethod.GET)
    @ResponseBody
    public String welcome() {
        return "Welcome!";
    }

  • HttpEntity<?> 또는 ResponseEntity<?>

    Response HTTP의 Body와 Header를 핸들러 메소드에서 접근하기 위해 사용 가능. HttpEntity나 ResponseEntity의 Body는 HttpMessageConverter를 통해 response 스트림으로 변환됨.

    @RequestMapping(value = "/welcome", method = RequestMethod.GET)
    @ResponseBody
    public String welcome() {
        return "Welcome!";
    }

11.2.3.@RequestParam

@RequestParam annotation은 HTTP Request parameter를 컨트롤러 메소드의 argument로 바인딩하는데 사용되며 ServletRequest.getParameter(java.lang.String name) 와 같은 역할을 한다. 다음은 @RequestParam annotation의 사용 예이다.

@RequestMapping("/updateMovie.do")
public String updateMovie(@RequestParam("movieId") String movieId,         
        @RequestParam("sellAmount") int sellAmount, @RequestParam("realImageFile") MultipartFile picturefile) {
    // 중략
    return "/listMovie.do";
}

@RequestParam을 적용한 파라미터는 반드시 HTTP Request에 존재해야 한다. 그렇지 않은 경우 다음과 같이 org.springframework.web.bind.MissingServletRequestParameterException이 발생한다.

 org.springframework.web.bind.MissingServletRequestParameterException:
				
Required java.lang.String parameter 'movieId' is not present

그러나 아래와 같이 @RequestParam의 required 속성을 false로 설정할 경우 HTTP Request에 파라미터가 존재하지 않아도 Exception이 발생하지 않는다.

@RequestMapping("/deleteMovie.do")
public String deleteMovie(@RequestParam(value="movieId", required="false") String movieId){
    // 중략
}

또한 defaultValue 속성을 이용하여 해당 파라미터가 존재하지 않을 경우 사용할 디폴트 값을 정의할 수 있다.

@RequestMapping("/movies.do")
public String findMovies(@RequestParam(value="pageIndex", defaultValue = "1") int pageIndex,
            Movies movies, BindingResult result, Model model) {
    // 중략
}

11.2.4.@RequestBody

@RequestBody annotation은 HTTP Request Body를 컨트롤러 메소드의 argument로 바인딩하는데 사용된다. 다음은 @RequestBody annotation의 사용 예이다.

@RequestMapping(value = "/movies/add", method = RequestMethod.POST)
@ResponseBody
public String add(@RequestBody Movie movie) throws Exception {
    // 중략
}

Request Body의 내용을 메소드의 argument 객체로 전달하기 위해서는 HttpMessageConverter에 의해서 변환이 이루어져야만 한다. HttpMessageConverter는 HTTP Request body와 객체간, 그리고 객체와 HTTP Response body간의 변환을 담당한다. Spring 3 부터 AnnotationMethodHandlerAdapter가 @RequestBody를 지원하고, 다음의 HttpMessageConverter 들을 디폴트로 등록하도록 기능이 확장되었다.

  • ByteArrayHttpMessageConverter : byte 배열로 변환

  • StringHttpMessageConverter : String으로 변환

  • FormHttpMessageConverter : Form 데이터와 MultiValueMap<String, String> 간의 변환

  • SourceHttpMessageConverter : javax.xml.transform.Source로 변환

  • MarshallingHttpMessageConverter : org.springframework.oxm 패키지에서 제공하는 Marshaller와 Unmarshaller를 사용하여 객체와 XML간 변환

  • MappingJacksonHttpMessageConverter : Jackson 라이브러리의 ObjectMapper를 사용해서 객체와 JSON 간의 변환

위와 같은 MessageConverter들이 어플리케이션에서 사용되려면 AnnotationMethodHandlerAdapter에 설정되어 있어야한다. AnnotationMethodHandlerAdapter에 "messageConverters" 속성을 이용하여 설정할 수도 있지만, 앞서 언급했던 <mvc:annotation-driven />만 정의하면 디폴트로 자동으로 등록해준다. MessageConverter에 대한 더 자세한 내용은 본 매뉴얼 >> Restweb Plugin >> HTTP Message Conversion을 참고하기 바란다.

11.2.5.@ResponseBody

@ResponseBody annotation은 핸들러 메소드가 리턴 값을 HTTP Response를 통해서 바로 전달할 경우에 사용할 수 있다. @ResponseBody가 적용되면 Model과 View를 리턴하여 ViewResolver를 통해 View를 찾는 등의 과정들은 거치지 않게 된다.

다음은 @ResponseBody annotation의 사용 예이다.

@RequestMapping(value = "/welcome", method = RequestMethod.GET)
@ResponseBody
public String welcome() {
    return "Welcome!";
}

위에서 설명했던 @RequestBody에서와 같이, 핸들러 메소드의 리턴값은 HttpMessageConverter를 통해 HTTP Response Body로 변환된다.

11.2.6.HttpEntity<?>

HttpEntity는 @RequestBody/@ResponseBody 같이 Request/Response Body 메세지를 처리할 수 있을 뿐만아니라, HTTP Header 값도 함께 다룰 수 있다. 일반적으로 RestTemplate을 사용한 REST 클라이언트를 구현할 때, 편리하게 사용될 수 있다.

@RequestMapping("/handle")
public HttpEntity<String> handle() {
    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("MyResponseHeader", "MyValue");
    return new ResponseEntity<String>("Hello World", responseHeaders);
}

HttpEntity를 사용할 경우에도 역시 Request/Response Body 변환을 위해 HttpMessageConverter가 사용된다.

11.2.7.@ModelAttribute

@ModelAttribute는 컨트롤러에서 다음과 같이 두 가지 방법으로 사용할 수 있다.

  • 메소드 자체에 정의

    입력 폼 페이지에서 출력해 줄 reference data를 전달하고자 할 때. 기존 SimpleFormController의 referenceData() 메소드와 같은 역할

  • 메소드의 입력 argument에 정의

    메소드의 argument로 입력된 Command 객체에 이름을 부여하고자 할 때.

다음은 위에서 설명한 두가지 방법으로 @ModelAttribute를 사용한 예이다.

@Controller
@RequestMapping("/movie.do") 
public class MovieController {
    // 중략
    // 메소드 자체에 정의
    @ModelAttribute("genreList")
	public Collection<Genre> populateGenreList() throws Exception {
		return this.genreService.getDropDownGenreList();
	}

    // 메소드의 입력 argument에 정의
    @RequestMapping(params="method=add")
    public String add(@ModelAttribute("updatedMovie") Movie movie
                                , BindingResult result, SessionStatus status) throws Exception {
        // 중략
    }
}

11.2.8.@SessionAttributes

@SessionAttributes는 Session에 저장하여 관리할 Model Attribute를 정의할 때 사용한다. Session에 저장하고자 하는 Model Attribute의 이름이나 타입을 @SessionAttributes의 속성에 정의해준다.

다음은 @SessionAttributes를 사용하여 Session에 저장하여 관리할 Model을 이름으로 정의한 예이다. 타입으로 정의할 경우 'types'라는 속성을 이용한다.

@Controller
@RequestMapping("/movie.do")
@SessionAttributes(value={"movie","genre"})
public class MovieController {
	// 중략
}

11.2.9.@CookieValue

HTTP Cookie에 저장된 값을 핸들러 메소드에서 사용할 수 있도록 해주는 Annotation이다.

다음은 @CookieValue 사용하여 Cookie 값을 가져와 출력해보는 코드이다.

@RequestMapping("/displayHeaderInfo")
@ResponseBody
public String displayHeaderInfo(@CookieValue("JSESSIONID") String cookie,
		@RequestHeader("Accept-Encoding") String encoding,
		@RequestHeader("Accept") String accept) {
    StringBuffer sf = new StringBuffer();
    sf.append("JSESSIONID : " + cookie);
    sf.append("\n");
    sf.append("Accept-Encoding : " + encoding);
    sf.append("\n");
    sf.append("Accept : " + accept);
     
    return sf.toString();
}

11.2.10.@RequestHeader

HTTP Header에 저장된 값을 핸들러 메소드에서 사용할 수 있도록 해주는 Annotation이다.

위 @CookieValue 예제 코드에서 @RequestHeader가 사용된 모습을 확인할 수 있다.

11.3.Double Form Submission 방지

입력 폼 페이지에서 사용자가 새로 고침 버튼을 클릭하거나, 폼을 Submit하는 버튼을 여러번 클릭할 경우 같은 입력 폼 정보가 서버로 여러번 등록되는 문제가 발생할 수 있다. 이 장에서는 이러한 Double Form Submission을 어떻게 방지할 수 있는지를 알아보도록 하자.

Double Form Submission 방지는 다음과 같은 원리로 구현된다.

  • 반드시 AnnotationMethodHandlerAdapter의 synchronizeOnSession 속성을 true로 설정

    <bean id="annotationHandlerAdaptor"
            class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        property name="synchronizeOnSession" 
        value="true" /
    </bean>

  • Double submission을 방지하고자 하는 Form 객체를 model로 저장

    다음 예제와 같이 ModelAndView, ModelMap 등을 이용하여 저장한다.

    @RequestMapping(params = "param=addView")
    public ModelAndView addMovieView() {
        ModelAndView mnv = 
            new ModelAndView("/WEB-INF/jsp/annotation/sales/movie/movieForm.jsp");
        mnv.addObject("movie", new Movie());
        return mnv;
    }

  • 저장한 model을 @SessionAttributes로 정의

    다음 예제와 같이 컨트롤러 클래스 선언부에 @SessionAttributes("movie")로 정의한다.

    @Controller
    @RequestMapping("/movie.do")
    @SessionAttributes("movie")
    public class EditMovieController {
        // 중략
    }

  • 컨트롤러 메소드에서 폼 처리 완료 후 Session status 변경

    @RequestMapping(params = "param=add")
    public String addMovie(HttpServletRequest request, Movie movie, BindingResult result
            , SessionStatus status) throws Exception {
        movieService.addMovie(movie);
        status.setComplete();
        return "/listMovie.do";    
    }
  • status.setComplete()는 session에서 저장된 model을 삭제하는 이벤트 발생

  • 따라서, 이후에 다시 submit 요청이 온 경우 session에 저장된 model이 삭제되었기 때문에 아래와 같이 org.springframework.web.HttpSessionRequiredException발생

    org.springframework.web.HttpSessionRequiredException: 
           Session attribute 'dept' required - not found in session


출처 - http://dev.anyframejava.org/docs/anyframe/plugin/foundation/4.6.0/reference/html/ch11.html









'Framework & Platform > Spring' 카테고리의 다른 글

spring - annotation 1  (0) 2012.05.09
Sping MVC - Model  (0) 2012.05.09
Spring - @RequestMapping  (0) 2012.05.09
Spring MVC Deprecated API  (0) 2012.05.08
Spring MVC - MultiActionController 사용하기  (0) 2012.05.08
Posted by linuxism
,