[Spring] XML 스키마 기반의 POJO 클래스를 이용한 AOP 구현

XML 스키마 기반의 POJO 클래스를 이용한 AOP 구현


1. XML 스키마 기반 AOP 


XML 스키마를 이용해서 AOP를 구현하는 과정은 다음과 같다.

1 관련 .jar파일을 클래스패스에 추가한다.

2 공통 기능을 제공하는 Advice 클래스를 구현한다.

3 XML 설정 파일에서 <aop:config>를 이용해서 Aspect를 설정한다. Advice를 이떤 Pointcut에 적용할지를 지정하게 된다.


(1) 공통 기능을 제공한 Advice 클래스를 작성

ProfilingAdvice.java

import org.aspectj.lang.ProceedingJoinPoint; 

public class ProfilingAdvice {

// ProfilingAdvice 클래스는 Around Advice를 구현한 클래스이다.

public Object trace(ProceedingJoinPoint joinPoint) throws Throwable {

// trace() 메서드는 ProceedingJoinPoint 타입의 joinpoint 파라미터를 전달받는데, 이 파라미터를 이용해서 Around Advice를 구현할 수 있게 된다.

String signatureString = joinPoint.getSignature().toShortString();

System.out.println(signatureString + " 시작");

long start = System.currentTimeMillis();

try { // Advice가 적용될 대상 객체를 호출 하기 전의 시간을 구해서 대상 객체를 메서드 호출 실행 시간을 출력

Object result = joinPoint.proceed();

return result;

} finally {

long finish = System.currentTimeMillis();

System.out.println(signatureString + " 종료");

// Advice가 적용될 대상 객체를 호출한 후에 시간을 구해서 대상 객체를 메서드 호출 실행 시간을 출력

System.out.println(signatureString + " 실행 시간 : " + (finish - start) + "ms");

}

}

}

ProfilingAdvice 클래스는 Around Advice를 구현한 클래스이며, trace() 메서드는 Advice가 적용될 대상 객체를 호출하기 전과 후에 시간을 구해서 대상 객체의 메서드 호출 실행 시간을 출력한다.


(2) XML 파일에 Advice를 빈으로 등록하고 Aspect를 설정

acQuickStart.xml

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

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

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

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

xsi:schemaLocation="http://www.springframework.org/schema/beans

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

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

            http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

--> XML 스키마를 이용해서 AOP를 구현하려면 aop 네임스페이스를 추가해주어야 한다.


<!-- Advice 클래스를 빈으로 등록 -->

<bean id="performanceTraceAdvice" class="madvirus.spring.chap05.aop.pojo.ProfilingAdvice" />

<!-- Aspect 설정: Advice를 어떤 Pointcut에 적용할 지 설정 -->

<aop:config>

<aop:aspect id="traceAspect" ref="performanceTraceAdvice">

<aop:pointcut id="publicMethodexpression="execution(public * madvirus.spring.chap05..*(..))" />

<!-- excution 명시자는 Advice를 적용할 패키지, 클래스 그리고 메서드를 표현할 때 사용된다. -->

<aop:around pointcut-ref="publicMethod" method="trace" />

</aop:aspect>

</aop:config>

<!-- <aop:config>, <aop:aspect>, <aop:pointcut>, <aop:around> 태그를 이용해서 AOP 설정을 할 수 있다.(aop 네임스페이스를 추가) 

madvirus.spring.chap05 패키지 및 그 하위 패키지에 있는 모든 public 메서드를 Pointcut으로 설정하고, 이들 Pointcut에 Around Advice로 performTraceAdvice 빈 객체의 trace() 메서드를 적용한다. 따라서 writeArticeService 빈의 public 메서드나 articleDao 의 public 메서드가 호출되면 ProfilingAdvice 클래스의 trace() 메서드가 Around Advice로 적용된다.--> 


<bean id="writeArticleService" class="madvirus.spring.chap05.board.service.WriteArticleServiceImpl">

<constructor-arg>

<ref bean="articleDao" />

</constructor-arg>

</bean>


<bean id="articleDao" class="madvirus.spring.chap05.board.dao.MySQLArticleDao" />


<bean id="memberService" class="madvirus.spring.chap05.member.service.MemberServiceImpl" />

</beans>

Advice 클래스를 적용하려면 일단 XML 설정 파일에 Advice 클래스를 빈으로 등록해주어야 한다.


(3) Advice가 Pointcut으로 지정한 메서드에 적용되는 지의 여부를 확인하기 위한 클래스

import madvirus.spring.chap05.board.Article;

import madvirus.spring.chap05.board.service.WriteArticleService;

import madvirus.spring.chap05.member.Member;

import madvirus.spring.chap05.member.service.MemberService;

import org.springframework.context.ApplicationContext;

import org.springframework.context.support.ClassPathXmlApplicationContext;


public class MainQuickStart {


public static void main(String[] args) {

String[] configLocations = new String[] { "acQuickStart.xml" };

ApplicationContext context = new ClassPathXmlApplicationContext(configLocations);


WriteArticleService articleService = (WriteArticleService) context.getBean("writeArticleService");

articleService.write(new Article());


MemberService memberService = context.getBean("memberService", MemberService.class);

memberService.regist(new Member());

}

}

MainQuickStart 클래스는 acQuickStart.xml 파일을 이용해서 ApplicationContext를 생성한 뒤 writeArticleService 빈의 write() 메서드를 호출하고, memberService 빈의 regist() 메서드를 호출하고 있다.

앞서 acQuickStart.xml 설정 파일을 설명할 때 madvirus.spring.chap05 패키지 및 그 하위 패키지에 있는 빈 객체의 public 메서드를 호출하면 ProfilingAdvice 클래스의 trace() 메서드가 Around Advice로 사용된다고 했는데 실제로 실행해보면 다음과 같은 결과가 출력된다.


WriteArticleService.write() 시작

WriteArticleServiceImpl.write() 메서드 실행

ArticleDao.insert() 시작

MySQLArticleDao.insert() 실행

ArticleDao.insert() 종료

ArticleDao.insert(..) 실행시간:0ms

WriteArticleService.write(..): 종료

WriteArticleService.write(..): 실행 시간:0ms

MemberService.regist(..): 시작

MemberServiceImpl.regist(): 메서드 실행

MemberService.regist(..): 종료

MemberService.regist(..): 실행 시간:0ms


위 코드에서 굵게 표시한 부분이 ProfilingAdvice 클래스의 trace() 메서드에서 출력한 내용이다. 실행 결과를 보면 실제 빈 객체의 메서드가 호출되기 전/후로 trace() 메서드에서 실행한 내용이 출력되는 것을 확인할 수 있다. 위 실행결과를 통해서 눈 여겨 볼 부분은 WriteArticleServiceImpl,MySQLServiceImpl 등 클래스를 변경하지 않고(즉, 핵심 로직 코드의 변경 없이) 공통 기능을(즉, 메서드 실행 시간을 출력해주는 기능을) 추가했다는 점이다. 이는 설정 파일만 변경하면 기존 코드의 변경 없이 공통 기능을 추가하거나 변경할 수 있는 AOP의 장점을 잘 보여주고 있다.


2. XML 스키마를 이용한 AOP 설정


AOP를 설정하기 위한 aop 네임스페이스 및 aop 네임스페이스와 관련된 XML 스키마가 추가되었다. aop 네임스페이스와 관련된 XML 스키마는 다음과 같이 <beans> 태그에 명시할 수 있다.

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

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

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

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

xsi:schemaLocation="http://www.springframework.org/schema/beans

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

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

            http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

...

<beans>


aop 네임스페이스와 관련된 XML 스키마를 지정한 뒤에, 다음과 같이 <aop:config> 태그를 사용하여 AOP 관련 정보를 설정할 수 있다.

<bean id="performanceTraceAdvice" class="madvirus.spring.chap05.aop.pojo.ProfilingAdvice" />


<aop:config>

<aop:aspect id="traceAspect1" ref="performanceTraceAdvice">

<aop:pointcut id="publicMethod"

expression="execution(public * madvirus.spring.chap05.board..*(..))" />

<aop:around pointcut-ref="publicMethod" method="trace" />

</aop:aspect>


<bean id="writeArticleService" class="madvirus.spring.chap05.board.service.WriteArticleServiceImpl"/>

위 코드에서 <aop:*>에 해당하는 태그는 다음과 같은 정보를 설정한다.


<aop:config>: AOP 설정 정보임을 나타낸다.

<aop:aspect>: Aspect를 설정한다.

<aop:pointcut>: Pointcut을 설정한다.

<aop:around>: Around Advice를 설정한다. 이외에도 다양한 Advice를 설정할 수 있다.


<aop:aspect> 태그의 ref 속성은 Aspect로서 기능을 제공할 빈을 설정할 때 사용된다. 위 코드의 경우 "performanceTraceAdvice" 빈이 Aspect 기능을 제공한다고 명시하고 있다.

위 코드를 보면 "traceAspect"가 MemberServiceImpl 클래스가 구현한 모든 인터페이스의 public 메서드에 Around Advice로 적용되며, 이때 Aspect의 구현 클래스인 ProfilingAdvice의 trace() 메서드가 

호출된다는 것을 쉽게 유추할 수 있다. XML 스키마 기반의 AOP 설정은 이렇게 설정 파일만 보더라도 어렵지 않게 어떤 코드에 어떤 Aspect가 어떤 타입의 Advice로 적용되는지를 파악할 수 있다.


cp.) Aspect 설정

Aspect 설정에서 <aop:aspect> 태그는 한 개의 Aspect를 설정한다. <aop:aspect> 태그의 ref: 속성에는 공통 기능을 구현하고 있는 빈을 전달한다.

<aop:config>

<aop:aspect id="traceAspect1" ref="performanceTraceAdvice">

<aop:pointcut id="publicMethod" expression="execution(public * madvirus.spring.chap05.board..*(..))" />

<aop:around pointcut-ref="publicMethod" method="trace" />

</aop:aspect>

<aop:aspect id="traceAspect2" ref="performanceTraceAdvice">

<aop:around pointcut="execution(public * madvirus.spring.chap05.member..*(..))" method="trace" />

</aop:aspect>

</aop:config>

Advice를 적용할 Pointcut은 <aop:pointcut> 태그를 이용하여 설정한다. <aop:pointcut> 태그의 id 속성은 Pointcut을 정의하는 AspectJ의 표현식을 입력 받는다.

Advice를 표현하는 태그에는 <aop:around>를 비롯하여 각 타입의 Advice를 정의하기 위해 다양한 태그를 제공하고 있다.


 태 그

 설 명

 <aop:before>

 메서드 실행 전에 적용되는 Advice를 정의한다.

 <aop:returning>

 메서드가 정상적으로 실행된 후에 적용되는 Advice를 정의한다.

 <aop:after-throwing>

 메서드가 예외를 발생시킬 때 적용되는 Advice를 정의한다.

 try-catch 블록에서 catch와 유사하다.

 <aop:after>

 메서드가 정상적으로 실행되는지 또는 예외를 발생시키는지 여부에 상관  없이 적용되는 Advice를 정의한다. try-catch-finally에서 finally 불록과 유사하다.

 <aop:around>

 메서드 호출 이전, 이후, 예외 발생 등 모든 시점에 적용 가능한 Advice를 정의한다.


각 태그는 pointcut 속성 또는 pointcut-ref 속성을 사용하여 Advice가 적용될 Pointcut을 지정한다. pointcut-ref 속성은 <aop:config> 태그를 이용하여 설정한 Pointcut을 참조할 때 사용되며, pointcut 속성은 직접 AspectJ 표현식을 이용하여 Pointcut을 지정할 때에 사용된다. 

Advice의 각 태그는 Pointcut에 포함되는 대상 객체의 메서드가 호출될 때, <aop:aspect> 태그의 ref 속성으로 지정한 빈 객체에서 어떤 메서드를 실행할 지를 지정한다. 예를 들어, 위 코드의 경우 madvirus.spring.chap05.member 패키지 및 그 하위 패키지의 public 메서드가 호출될 때 performTraceAdvice 빈의 trace 메서드가 호출되도록 설정하고 있다.


3. Advice 타입 별 클래스 작성


(1) Before Advice

Before Advice를 사용하려면 <aop:before> 태그를 이용하여 Before Advice를 설정하면 된다.

<aop:config>

...

<aop:aspect id ="loggingAspect" ref="logging">

<aop:before pointcut-ref="publicMethod" method="before" />

</aop:aspect>

</aop:config>


Aspect로 사용될 빈 클래스는 다음과 같이 <aop:before> 태그의 method 속성에 명시한 메서드를 구현해주면 된다.

public void before(){

//대상 객체의 메서드 실행 이전에 적용할 기능 구현

...

}


대상 객체 및 호출되는 메서드에 대한 정보나 전달되는 파라미터에 대한 정보가 필요한 경우 org.aspectj.lang.JoinPoint 타입의 파라미터를 메서드에 전달한다.

public void before(JoinPoint joinPoint){

// 대상 객체의 메서드 실행 이전에 적용할 기능 구현

...

}


Before Advice를 구현한 메서드는 일반적으로 리턴 타입이 void인데, 그 이유는 리턴 값을 갖더라도 실제 Advice의 적용 과정에 아무런 영향이 없기 때문이다.

Before Advice를 위한 메서드 구현 시 주의해야 할 점은 메서드에서 예외를 발생시킬 경우 대상 객체의 메서드가 호출되지 않게 된다는 점이다.

cp.) Before Advice에서 예외를 발생시키면 대상 객체의 메서드가 호출되지 않기 때문에, 메서드를 실행 하기 전에 접근 권한을 검사해서 접근 권한이 없을 경우 예외를 발생시키는 기능을 구현하는 데 Before Advice가 적합하다.


(2) After Returning Advice
After Returning Advice는 대상 객체의 메서드가 정상적으로 실행된 후에 공통 기능을 적용하고 싶을 때 사용되는 Advice로서, 다음과 같이 <aop:after-returning> 태그를 이용하여 After Returning Advice를 설정한다.
<aop:config>
...
<aop:aspect id ="loggingAspect" ref="logging">
<aop:after-returning pointcut-ref="publicMethod" method="afterReturning" />
</aop:aspect>
</aop:config>

Aspect로 사용될 빈 클래스는 아래 코드처럼 <aop:after-returning> 태그에 명시한 메서드를 구현한다.
public void afterReturning(){
// 대상 객체의 메서드가 정상적으로 실행된 이후에 적용할 기능 구현
...
}

Advice를 구현한 메서드에서 리턴 값을 사용하고 싶다면 returning 속성을 사용하여 리턴 값을 전달 받을 파라미터의 이름을 명시해 주면 된다.
<aop:after-returning pointcut-ref="publicMethod" method="afterReturning" returning="ret"/>

Advice를 구현한 메서드는 returning 속성에 명시한 이름을 갖는 파라미터를 이용해서 리턴 값을 전달 받게 된다.
public void afterReturning(Object ret){
// 대상 객체의 메서드가 정상적으로 실행된 이후에 적용할 기능 구현
...
}

만약 리턴 된 객체가 특정 타입인 경우에 한해서 메서드를 실행하고 싶다면 다음과 같이 한정하고 싶은 타입의 파라미터를 사용하면 된다.
pulbic void afterReturning(Article ret){
// 대상 객체의 메서드가 정상적으로 실행된 이후에 적용할 기능 구현
...
}

대상 객체 및 호출되는 매서드에 대한 정보나 전달되는 파라미터에 대한 정보가 필요한 경우 다음과 같이 org.aspectj.lang.JoinPoint를 파라미터로 추가한다.
pulbic void afterReturning(Article ret, Object ret){
// 대상 객체의 메서드가 정상적으로 실행된 이후에 적용할 기능 구현
...
}

cp.) 스프링 AOP <aop:after-returning> 태그의 ret 속성에 명시한 파라미터 이름과 실제 Advice 구현 메서드의 파라미터 이름이 일치하는 지 확인하기 위해 클래스의 디버그 정보를 사용한다.
만약 디버그 모드로 컴파일하지 않았다면 Advice 구현 메서드의 파라미터 개수가 한 개인 경우 해당 파라미터 ret 속성에 명시한 파라미터라고 가정한다.

(3) After Throwing Advice

After Throwing Advice는 대상 객체의 메서드가 예외를 발생시킨 경우에 적용되는 Advice로서 다음과 같이 <aop:config-throwing> 태그를 이용하여 설정한다.

<aop:config>

...

<aop:aspect id ="loggingAspect" ref="logging">

<aop:after-throwing pointcut-ref="publicMethod" method="afterTrowing" />

</aop:aspect>

</aop:config>


Advice 구현 클래스는 다음과 같이 <aop:after-returning> 태그에 명시한 메서드를 구현한다.

public void afterThrowing(){

// 대상 객체의 메서드가 예외를 발생시킨 경우에 적용할 기능 구현

...

}


대상 객체의 메서드가 발생시킨 예외 객체가 필요한 경우 throwing 속성에 예외 객체를 전달받을 파라미터의 이름을 명시하면 된다.

<aop:after-throwing pointcut-ref="publicMethod" method="afterTrowing" throwing="ex"/>


Advice 구현 메서드에서 발생된 예외를 사용하려면 <aop:after-throwing> 태그의 throwing 속성에 명시한 이름을 갖는 파라미터를 추가하면 된다.

public void afterThrowing(Trowable ex){

// 대상 객체의 메서드가 예외를 발생시킨 경우에 적용할 기능 구현

...

}


만약 특정 타입의 예외에 대해서만 처리하고 싶다면, Throwable이나 Exception이 아니라 처리하고 싶은 예외 타입을 파라미터로 지정하면 된다. 예를 들어, 아래 코드는 발생된 예외가 ArticleNotFoundException인 경우에만 호출된다.

public void afterThrowing(AtricleNotFoundException ex){

// 대상 객체의 메서드가 예외를 발생시킨 경우에 적용할 기능 구현

...

}


대상 객체 및 호출되는 메서드에 대한 정보나 전달되는 파라미터에 대한 정보가 필요한 경우 다음과 같이 org.aspectj.lang.JoinPoint를 파라미터로 추가한다.

public void afterThrowing(JoinPoint joinPoint, Exception ex){

// 대상 객체의 메서드가 예외를 발생시킨 경우에 적용할 기능 구현

...

}


(4) After Advice

After Advice는 대상 객체의 메서드가 정상적으로 실행되었는지 아니면 예외를 발생시켰는지의 여부에 상관없이 메서드 실행이 종료된 이후에 적용되는 Advice로서 try-catch-fanally

블록에서 finally 블록에서 finally 블록과 비슷한 기능을 수행한다. 다음과 같이 <aop:after> 태그를 이용하여 After Advice를 설정한다.


<aop:config>

...

<aop:aspect id ="loggingAspect" ref="logging">

<aop:after pointcut-ref="publicMethod" method="afterFinally" />

</aop:aspect>

</aop:config>


Aspect로 사용될 빈 클래스는 다음과 같이 <aop:after> 태그에 명시한 메서드를 구현해주면 된다. 이 때 메서드는 파라미터를 갖지 않는다.

public void afterFinally(){

...

}


대상 객체 및 호출되는 메서드에 대한 정보나 전달되는 파라미터에 대한 정보가 필요한 경우 다음과 같이  org.aspectj.lang.JoinPoint를 파라미터로 명시하면 된다.

public void afterFinally(JoinPoint joinPoint){

...

}


(5) Around Advice

Around Advice는 앞서 살펴 본 Before, After Returning, After Throwing, After Advice를 모두 구현할 수있는 Advice로서, 다음과 같이 <aop:around> 태그를 이용하여 Around Advice를 설정한다.

<bean id ="cache" class="madvirus.spring.chap05.aop.pojo.ArticleCacheAdvice"/>

<aop:config>

...

<aop:aspect id ="cacheAdpect" ref="cache">

<aop:around method="cache" pointcut="execution(public * *..ReadArticleService.*(..))" />

</aop:aspect>

</aop:config>


Around Advice를 구현한 메서드는 org.aspectj.lang.ProceedingJoinPoint를 반드시 첫 번째 파라미터로 지정해야 한다. 그렇지 않을 경우 스프링은 예외를 발생시킨다.

다음 코드는 Around Advice의 구현 예를 보여주고 있다.

package madvirus.spring.chap05.aop.pojo;

import java.util.HashMap;

import java.util.Map;

import madvirus.spring.chap05.board.Article;

import org.aspectj.lang.ProceedingJoinPoint;


public class ArticleCacheAdvice {

private Map<Integer, Article> cache = new HashMap<Integer, Article>();

// 첫 번째 파라미터로 ProceedingJoinPoint를 전달받고 있다. ProceedingJoinPoint의 procced() 메서드를 호출하면 프록시 대상 객체의 실제 메서드를 호출하게 된다.

// 따라서 ProceedingJoinPoint.proceed() 메서드를 호출하기 전과 후에 필요한 작업을 수행할 수 있다.

public Article cache(ProceedingJoinPoint joinPoint) throws Throwable {

Integer id = (Integer) joinPoint.getArgs()[0];

Article article = cache.get(id);

if (article != null) {

System.out.println("[ACA] 캐시에서 Article[" + id + "] 구함");

return article;

}

Article ret = (Article) joinPoint.proceed();

if (ret != null) {

cache.put(id, ret);

System.out.println("[ACA] 캐시에 Article[" + id + "] 추가함");

}

return ret;

}

}

위 코드는 대상 객체의 메서드 호출 전후에 캐시 기능을 삽입하였다. proceed() 메서드를 호출하기 전에 Map에 ID에 해당하는 Article 객체가 존재하는지 검사한다.


[Spring] AOP 개요


AOP 개요


Aspect Oriented Programming, 줄여서 AOP는 문제를 바라보는 관점을 기준으로 프로그래밍하는 기법을 말한다. AOP는 문제를 해결하기 위한 핵심 관심 사항과 전제에 적용되는 공통 관심 사항을 기준으로 프로그래밍 함으로써 공통 모듈을 여러 코드에 쉽게 적용할 수 있도록 도와 준다.


AOP를 구현하는 다양한 방법이 존재하지만, 기본적인 개념은 공통 관심 사항을 구현한 코드를 핵심 로직을 구현한 코드 안에 삽입한다는 것이다.



AOP 기법에서는 핵심 로직을 구현한 코드에서 공통 기능을 직접적으로 호출하지 않는다. 핵심 로직을 구현한 코드를 컴파일 하거나, 컴파일 된 클래스를 로딩하거나, 또는 로딩한 클래스의 객체를 생성할 때 AOP가 적용되어 핵심 로직 구현 코드 안에 공통 기능이 삽입된다.


AOP 프로그래밍에서는 AOP 라이브러리 공통 기능을 알맞게 삽입해주기 때문에 개발자는 게시글 쓰기나 목록 읽기와 같은 핵심 로직을 구현할 때 트랜잭션 적용이나 보안 검사와 같은 공통 기능을 처리하기 위한 코드를 핵심 로직 코드에 삽입할 필요가 없다. 핵심 로직을 구현한 코드에 공통 기능 관련 코드가 포함되어 있지 않기 때문에 적용해야 할 공통 기능이 변경되더라도 핵심 로직을 구현한 코드를 변경할 필요가 없다. 단지, 공통 기능 코드를 변경한 뒤 핵심 로직 구현 코드에 적용만 하면 된다.


1. AOP 용어


(1) Advice: 언제 공통 관심 기능을 핵심 로직에 적용할 지를 정의하고 있다. 예를 들어, '메서드를 호출하기 전'(언제)에 '트랜잭션을 시작한다.'(공통기능)기능을 적용한다는 것을 정의하고 있다.

조인 포인트에 삽입되어져 동작할 수 있는 코드를 '어드바이스'라 한다. 관점으로서 분리되고 실행시 모듈에 위빙된 구체적인 처리를 AOP에서는 Advice라고 한다. Advice를 어디에서 위빙하는지는 뒤에 나오는 PointCut이라는 단위로 정의한다. 또한 Advice가 위빙되는 인스턴스를 '대상객체'라고 한다.


cp.) 스프링의 Advice 타입

- Around Advice: Joinpoint 앞과 뒤에서 실행되는 Adcvice

- Before Advice: Joinpoint 앞에서 실행되는 Advice

- After Returning Advice: Jointpoint 메서드 호출이 정상적으로 종료된 뒤에 실행되는 Advice

- After Throwing Advice: 예외가 던져질 때 실행되는 Advice

- Introduction:  클래스에 인터페이스와 구현을 추가하는 특수한 Advice


(2) JoinPoint: Advice를 적용 가능한 지점을 의미한다. 메서드 호출, 필드 값 변경 등이 Joinpoint에 해당한다.

클래스의 인스턴스 생성 시점', '메소드 호출 시점', '예외 발생 시점'과 같이 어플리케이션을 실행할 때 특정 작업이 시작되는 시점을 '조인포인트'라고 한다. 실행시의 처리 플로우에서 Advice를 위빙하는 포인트를 JointPoint라고 한다. 구체적으로는 메서드 호출이나 예외발생이라는 포인트를 Joinpoint라고 한다.


(3) Pointcut: Joinpoint의 부분 집합으로서 실제로 Advice가 적용되는 Jointpoint를 나타낸다. 스프링에서는 정규 표현식이나 AspectJ 문법을 이용하여 Pointcut을 정의할 수 있다. 여러 개의 조인포인트를 하나로 결합한 것을 포인트 컷이라 한다.

하나 또는 복수의 Jointpoint를 하나로 묶은 것을 Pointcut 이라고 한다. Advice의 위빙 정의는 Pointcut을 대상으로 설정한다. 하나의 Pointcut에는 복수 Advice를 연결할 수 있다. 반대로 하나의 Advice를 복수 Pointcut에 연결하는 것도 가능하다.


(4) Weaving: Advice를 핵심 로직 코드에 적용하는 것을 weaving 이라고 한다. 즉 공통 코드를 핵심 로직 코드에 삽입하는 것이 weaving이다.

어드바이스를 핵심 로직 코드에 삽입하는 것을  위빙이라고 한다.


(5) Aspect: 여러 객체에 공통으로 적용되는 공통 관심 사항을 Aspect라고 한다. 트랜잭션이나 보안 등이 Aspect의 좋은 예이다.

여러 객체에 공통으로 적용되는 공통 관점 사항을 에스펙트라 한다.


(6) Target

핵심 로직을 구현하는 클래스를 말한다.


(7) advisor

어드바이스와 포인트컷을 하나로 묶어 취급한 것을 '어드바이저'라 부른다.

advisor와 Pointcut을 하나로 묶어 다루는 것을 Advisor라고 한다. Advisor는 스프링 AOP에만 있는 것인데, 관점 지향에서 관점을 나타내는 개념이라고 할 수 있다.


2. 세 가지 Weaving 방식

advice를 Weaving하는 방식에는 다음과 같이 세 가지 방식이 존재한다.


(1) 컴파일시에 Weaving 하기

컴파일 시에 코드를 삽입하는 방법은 AspectJ에서 사용하는 방식이다. 컴파일 방식에서는 핵심 로직을 구현한 자바 소스 코드를 컴파일할 때에 알맞은 위치에 공통 코드에 삽입한다.

따라서 AOP가 적용된 클래스 파일이 생성된다. 컴파일 방식을 제공하는 AOP 도구는 공통 코드를 알맞은 위치에 삽입할 수 있도록 도와주는 컴파일러나 IDE를 함께 제공한다.


(2) 클래스 로딩 시에 Weaving 하기

클래스를 로딩할 때에 AOP를 적용할 수도 있다. AOP 라이브러리는 JVM이 클래스를 로딩할 때 클래스 정보를 변경할 수 있는 에이전트를 제공한다. 이 에이전트는 로딩한 클래스의 바이너리 정보를 변경하여 알맞은 위치에 공통 코드를 삽입한 새로운 클래스 바이너리 코드를 사용하도록 한다. 즉 원본 클래스 파일은 변경하지 않고 클래스를 로딩할 때에 JVM이 변경된 바이트 코드를 사용하도록 함으로써 AOP를 적용한다.


(3) 런타임 시에 Weaving 하기

런타임 시에 AOP를 적용할 때에는 소스 코드나 클래스 정보 자체를 변경하지 않는다. 대신 프록시에 이용하여 AOP를 적용한다.

프록시 기반의 AOP는 핵심 로직을 구현한 객체에 직접 접근하는 것이 아니라 중간에 프록시를 생성하여 프록시를 통해서 핵심 로직을 구현한 객체에 접근하게 된다.

이때, 프록시의 핵심 로직을 실행하기 전 또는 후에 공통 기능을 적용하는 방식으로 AOP를 적용하게 된다. 프록시 기반에서는 메서드가 호출될 때에만 Advice를 적용할 수 있기 때문에 필드 값 변경과 같은 Jointpoint에 대해서는 적용할 수 없는 한계가 있다.


4. 프록시를 이용한 AOP 구현

스프링은 프록시를 이용하여 AOP를 구현하고 있다. 스프링은 Aspect의 적용 대상이 되는 객체에 대한 프록시를 만들어 제공하며, 대상 객체를 사용하는 코드는 대상 객체에 직접 접근하기 보다는 프록시를 통해서 간접적으로 접근하게 된다. 이 과정에서 프록시는 공통 기능을 실행한 뒤 대상 객체의 실제 메서드를 호출하거나 또는 대상 객체의 실제 메서드가 호출된 뒤에 공통 기능을 실행하게 된다.

스프링에서 어떤 대상 객체에 대해 AOP를 적용할 지의 여부는 설정 파일을 통해서 지정할 수 있으며, 스프링은 설정 정보를 이용하여 런타임에 대상 객체에 대한 프록시 객체를 생성하게 된다.

프록시 객체를 생성하는 방식은 대상 객체가 인터페이스를 구현하고 있으냐 없느냐 여부에 따라 달라진다.

대상 객체가 인터페이스를 구현하고 있다면, 스프링은 자바 리플렉션 API가 제공하는 java.lang.reflect.Proxy를 이용하여 프록시 객체를 생성한다. 이때 생성된 프록시 객체는 대상 객체와 동일한 인터페이스를 구현하게 되며, 클라이언트는 인터페이스를 통해서 필요한 메서드를 호출하게 된다. 하지만, 인터페이스를 기반으로 프록시 객체를 생성하기 때문에 인터페이스에 정의되어 있지 않은 메서드에 대해서는 AOP가 적용되지 않는 점에 유의해야 한다.

대상 객체가 인터페이스를 구현하고 있지 않다면, 스프링은 CGLIB를 이용하여 클래스에 대한 프록시 객체를 생성한다. CGLIB는 대상 클래스를 상속받아 프록시를 구현한다.

따라서, 대상 클래스가 final인 경우 프록시를 생성할 수 없으며, final인 메서드에 대해서는 AOP를 적용할 수 없게 된다.


[Spring] Locale 처리


Locale 처리


스프링 제공하는 <spring:message> 커스텀 태그는 웹 요청과 관련된 언어 정보를 이용해서 알맞은 언어의 메시지를 출력한다.

웹 브라우저의 언어 설정을 한국어(ko_kr)로 했을 때와 영어(en_us)로 했을 때 <spring:message> 커스텀 태그가 언어에 따라 알맞은 메시지를 출력해 주는 결과 화면을 보여주고 있다.


실제로, 스프링 MVC는 LocaleResolver를 이용해서 웹 요청과 관련된 Locale을 추출하고, 이 Locale 객체를 이용해서 알맞은 언어의 메시지를 선택하게 된다.

본 절에서는 스프링이 제공하는 LocaleResolver를 사용해서 Locale을 변경하는 방법에 대해서 살펴보도록 하겠다.


1. LocaleResolver 인터페이스

org,springframework.web.serlvet.LocaleResolver 인터페이스는 다음과 같이 정의 되어 있다.


package org.springframework.web.servlet;

import java.util.Locale;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


public interface LocaleResolver{

Locale resolveLocale(HttpServletRequest request);

void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale);

}

resolveLocale() 메서드는 요청과 관련된 Locale을 리턴한다. DispatcherServlet은 등록되어 있는 LocaleResolver의 resolveLocale() 메서드를 호출해서 웹 요청을 처리할 때 사용할 Locale을 구한다.

setLocale() 메서드는 Locale을 변경할 때 사용된다. 예를 들어, 쿠키나, 쿠기나 HttpSession에 Locale 정보를 저장할 때에 이 메서드가 사용된다.


2. LocaleResolver의 종류

스프링이 기본적으로 제공하는 LocaleResolver 구현 클래스는 다음과 같다.


※ 스프링이 제공하는 LocaleResolver 구현 클래스

 클래스

 설 명

 AcceptHeaderLocaleResolver

 웹 브라우저가 전송한 Accept-Language 헤더로부터 Locale 선택한다. setLocale() 메서드를 지원  하지 않는다.

 CookieLocaleResolver

 쿠키를 이용해서 Locale 정보를 구한다. setLocale() 메서드는 쿠키에 Locale 정보를 저장한다.

 SessionLocaleResolver

 세션으로부터 Locale 정보를 구한다. setLocale() 메서드는 세션에 Locale 정보를 저장한다.

 FixedLocaleResolver

 웹 요청에 상관없이 특정한 Locale로 설정한다. setLocale() 메서드를 지원하지 않는다.


(1) AcceptHeaderLocaleResolver

LocaleResolver를 별도로 설정하지 않을 경우 AcceptHeaderLocaleResolver를 기본 LocalResolver로 사용한다.

AcceptHeaderLocaleResolver는 AcceptLanguage 헤더로부터 Locale 정보를 추출한다.

헤더로부터 Locale 정보를 추출하기 때문에, setLocale() 메서드를 이용해서 Locale 설정을 변경할 수 없다.


(2) CookieLocaleResolver

CookieLocaleResolver는 쿠키를 이용해서 Locale 정보를 저장한다. setLocale() 메서드를 호출하면 Locale 정보를 담은 쿠키를 생성하고, resolveLocale() 메서드는 쿠기로부터 Locale 정보를 가져와 Locale 정보를

담은 쿠키가 존재하지 않을 경우, defaultLocale 프로퍼티의 값을 Locale로 사용한다. defaultLocale 프로퍼티의 값이 null인 경우에는 Accept-Language 헤더로부터 Locale 정보를 추출한다.

CookieLocaleResolver는 쿠티와 관련해서 별도 설정을 필요로 하지 않지만. 생성할 쿠키 이름, 도메인, 경로 등의 설정을 직접하고 싶다면 프로퍼티에 알맞게 설정해주면 된다.


※ CookieLocaleResolver의 쿠키 설정 관련 프로퍼티

프로퍼티 

설 명  

 cookieName

 사용할 쿠키 이름

 cookieDomain

 쿠키 도메인

 cookiePath

 쿠키 경로, 기본값은 "/"이다.

 cookieMaxAge

 쿠키 유효 시간

 cookieSecure

 보안 쿠키 여부, 기본값은 false 이다.


(3) SessionLocaleResolver

SessionLocaleResolver HttpSessio에 Locale 정보를 저장한다. setLocale() 메서드를 호출하면 Locale 정보를 세션에 저장하고, resolveLocale() 메서드는 세션으로부터 Locale을 가져와 웹 요청의 Locale을 설정한다.

만약 Locale 정보가 세션에 존재하지 않으면, defaultLocale 프로퍼티의 값을 Locale로 사용한다. defaultLocale 프로퍼티의 값이 null인 경우에는 Accept-Language 헤더로부터 Locale 정보를 추출한다.


(4) FixedLocaleResolver

FixedLocaleResolver는 웹 요청에 상관없이 defaultLocale 프로퍼티로 설정한 값을 웹 요청을 위한 Locale로 사용한다. FixedLocaleResolver는 setLocale() 메서드를 지원하지 않는다.

setLocale() 메서드를 호출할 경우 UnsupportedOperationException 예외를 발생시킨다.


3. LocaleResolver를 이용한 Locale 변경

LocaleResolver를 빈으로 등록했다면, 컨트롤러에서 LocaleResolver를 이용해서 Locale을 변경할 수 있게 된다. 예를 들어 다음과 같이 LocaleResolver를 설정했다고 하자.


<bean class="madvirus.spring.chap07.controller.LocaleChangeController">

<property name="localeResolver" ref="localeResolver" />

</bean>

<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver" />


이 경우, 컨트롤러 클래스는 다음과 같이 LocaleResolver의 setLocale() 메서드를 호출해서 클라이언트의 웹 요청을 위한 Locale을 변경할 수 있다.

import org.springframework.web.servlet.LocaleResolver;


@Controller

public class LocaleChangeController {


private LocaleResolver localeResolver;


@RequestMapping("/changeLanguage")

public String change(@RequestParam("lang") String language, HttpServletRequest request, HttpServletResponse response) {

Locale locale = new Locale(language);

localeResolver.setLocale(request, response, locale);

return "redirect:/index.jsp";

}


public void setLocaleResolver(LocaleResolver localeResolver) {

this.localeResolver = localeResolver;

}

}


LocaleResolver를 이용해서 Locale을 변경하면, 이후 요청에 대해서는 지정한 Locale을 이용해서 메시지 등을 로딩하게 된다.

ResolverContextUtils 클래스는 웹 요청과 관련된 LocaleResolver를 구할 수 있는 메서드를 제공하고 있으므로, 위 코드를 다음과 같이 변경할 수도 있다.

@Controller

public class LocaleChangeController2 {


@RequestMapping("/changeLanguage2")

public String change(@RequestParam("lang") String language, HttpServletRequest request, HttpServletResponse response) {

Locale locale = new Locale(language);

LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);

localeResolver.setLocale(request, response, locale);

return "redirect:/index.jsp";

}

}


4. LocaleChangeInterceptor를 이용한 Locale 변경

Locale을 변경하기 위해 별도의 컨트롤러 클래스를 개발한다는 것은 다소 성가신 일이다.

이 경우, 스프링이 제공하는 LocaleChangeInterceptor 클래스를 사용하면 웹 요청 파라미터를 이용해서 손쉽게 Locale을 변경할 수 있다.


LocaleChangeInteceptor 클래스는 handlerInterceptor로서 다음과 같이 HandlerMapping의 intercaptors 프로퍼티에 등록만 하면 설정이 완료된다. 아래 코드는 설정 예이다.

<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"

p:paramName="language" />


<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">

<property name="interceptors">

<list>

<ref bean="localeChangeInterceptor" />

</list>

</property>

</bean>

paramName 프로퍼티는 Locale을 설정할 때 사용할 파라미터 이름을 명시한다. 예를 들어, 위 코드에서는 paramName 프로퍼티의 값으로 language를 설정했는데,

이 경우 language 요청 파라미터를 사용해서 Locale을 변경할 수 있다.


http://localhost:8080/chap07/jsp/login/login.do?language=en

LocaleChangeInterceptor는 paramName 프로퍼티로 설정한 요청 파라미터가 존재할 경우, 파라미터의 값을 이용해서 Locale을 생성한 뒤 LocaleResolver를 이용해서 Locale을 변경한다.

이후, 요청에서는 변경된 Locale이 적용된다.


[Spring] ViewResolver 설정


ViewResolver 설정


뷰 영역 구현

컨트롤러는 최종적으로 결과를 출력할 뷰와 뷰에 전달할 객체를 담고 있는 ModelAndView 객체를 리턴한다.

DispatherServlet은 ViewResolver를 사용하여 결과를 출력할 View 객체를 구하고, 구한 View 객체를 이용하여 내용을 생성한다.


1. 컨트롤러 구현 및 설정 추가

컨트롤러를 구현하려면 먼저 @Contoller 어노테이션을 클래스에 적용한다. 그리고, @RequestMapping 어노테이션을 이용해서 클라이언트의 요청을 처리할 메서드를 지정한다.

package madvirus.spring.chap06.controller;

import java.util.Calendar;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.servlet.ModelAndView;


// @Controller 어노테이션은 해당 클래스가 스프링 MVC의 컨트롤러를 구현한 클래스라는 것을 지정한다. 

@Controller

public class HelloController {


// @RequestMapping 어노테이션은 값으로 지정한 요청 경로를 처리한 메서드를 설정한다. 

// 이 경우 http://host:port[/컨텍스트 경로]/hello.do 요청을 HelloController 클래스의 hello() 메서드가 처리하게 된다.

@RequestMapping("/hello.do")

public ModelAndView hello() {

//ModelAndView는 컨트롤러의 처리 결과를 보여줄 뷰와 뷰에서 출력할 모델을 지정할 때 사용된다.

ModelAndView mav = new ModelAndView();

// 컨트롤러의 처리 결과를 보여줄 뷰의 이름을 'hello'를 지정한다.

mav.setViewName("hello");

// 모델에 'greeting'이라는 이름으로 String 타입의 값을 추가하였다.

mav.addObject("greeting", getGreeting());

return mav;

}

// 스프링은 ModelAndView 뿐만 아니라 String이나 modelMap, 또는 Map과 같은 타입을 이용해서 뷰 이름과 모델 정보를 설정할 수 있도록 하고 있다.

private String getGreeting() {

int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);

if (hour >= 6 && hour <= 10) {

return "좋은 아침입니다.";

} else if (hour >= 12 && hour <= 15) {

return "점심 식사는 하셨나요?";

} else if (hour >= 18 && hour <= 22) {

return "좋은 밤 되세요";

}

return "안녕하세요";

}

}


DispatherServlet은 스프링 컨테이너에서 컨트롤러 객체를 검색하기 때문에 스프링 설정 파일에 컨트롤러를 빈으로 등록해 주어야 한다.

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

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

xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"

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

xsi:schemaLocation="http://www.springframework.org/schema/beans

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

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

        http://www.springframework.org/schema/context/spring-context-3.0.xsd">


<bean id="helloController" class="madvirus.spring.chap06.controller.HelloController" />

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="prefix" value="/WEB-INF/view/" />

<property name="suffix" value=".jsp" />

</bean>

</beans>

스프링 2.5 버전까지는 @Controller 어노테이션이 적용된 클래스를 컨트롤러로 사용하려면 별도의 DefaultAnnoationHandlerMapping과 AnnotationHandlerAdapter를 스프링 설정 파일에 등록해 주어야 한다. 하지만 스프링 3.0부터는 이 두 빈 객체를 기본 구현체로 사용하기 때문에 별도의 설정을 하지 않을 경우 @Controller 어노테이션이 적용된 클래스를 컨트롤러 구현체로 사용할 수 있게 된다.


2. 설정 파일에 ViewResolver 설정 추가

컨트롤러 클래스는 직접 또는 간접적으로 ModelAndView 객체를 생성하게 된다.

예를 들어 앞서 작성한 HelloController 클래스는 다음과 같이 ModelAndView 객체를 생성해서 리턴하였다.


public ModelAndView hello() {

ModelAndView mav = new ModelAndView();

// 뷰이름

mav.setViewName("hello");// setViewName() 메서드를 이용하여 뷰 이름을 지정한 모습

mav.addObject("greeting", getGreeting());

return mav;

}

컨트롤러의 처리 결과를 보여줄 뷰의 이름을 'hello'를 지정하였는데, DispatherServlet은 이 뷰 이름과 매칭되는 뷰 구현체를 찾기 위해 ViewResolver를 사용한다.


JSP를 뷰 기술로 사용할 경우 다음과 같이 InternalResourceViewResolver 구현체를 빈으로 등록해주면 된다.

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="prefix" value="WEB-INF/view"/>

<property name="suffix" value=".jsp/>

// 이는 ViewResolver가 "WEB-INF/view/뷰이름.jsp"를 뷰 JSP로 사용한다는 것을 의미한다. 

// 즉, 앞의 예에서 HelloController는 뷰 이름으로 "hello"를 리턴하므로, 실제로 사용되는 뷰 파일은 "WEB-INF/view/hello.jsp"파일이 된다.

</bean>


InternalResourceViewResolver는 컨트롤러가 지정한 뷰 이름으로부터 실제로 사용될 뷰를 선택하는데, 이 때 컨트롤러가 지정한 뷰 이름 앞뒤로 prefix 프로퍼티와 suffix 프로퍼티를 추가한 값이 실제로 사용될 자원의 경로가 된다.


※ ViewResolver 구현 클래스

스프링 컨트롤러는 뷰에 의존적이지 않다. 컨트롤러는 아래 코드와 같이 결과를 생성할 뷰의 이름만 지정할 뿐이다.

컨트롤러가 지정한 뷰 이름으로부터 응답 결과 화면을 생성하는 View 객체는 ViewResolver가 구한다.

스프링은 몇 가지 ViewResolver 구현 클래스를 제공하고 있는데, 이중 주료 ViewResolver 구현 클래스는 다음과 같다.


3. ViewResolver 인터페이스


package org.springframework.web.servlet;


import java.util.Locale;

public interface ViewResolver{

View resolveViewName(String viewName, Locale locale) throws Exception;

}

ViewResolver는 뷰 이름과 지역화를 위한 Locale을 파라미터로 전달받으며, 매핑되는 View 객체를 리턴한다. 만약, 매핑되는 View 객체가 존재하지 않으면 null을 리턴한다.


4. View 객체

ViewResolver는 응답 결과를 생성할 뷰 객체를 리턴한다. 모든 뷰 클래스는 View 인터페이스를 구현하고 있으며, View 인터페이스는 다음과 같이 정의되어 있다.


public interface View {

    String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";

    String getContentType();

    void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

}


getContentType() 메서드는 "text/html"과 같은 응답 결과의 컨텐츠 타입을 리턴한다. render() 메서드는 실제로 응답 결과를 생성한다. render() 메서드의 첫 번째 파라미터인 model에는

컨트롤러가 리턴한 ModelAndView 객체의 모델 데이터가 전달된다. 각각의 View 객체는 이 모델 데이터로부터 응답 결과를 생성하는데 필요한 정보를 구한다.


'Programing > Spring' 카테고리의 다른 글

[Spring] AOP 개요  (0) 2014.12.22
[Spring] Locale 처리  (0) 2014.12.21
[Spring] ViewResolver 설정  (0) 2014.12.18
[Spring] @RequestBody 어노테이션과 @ReponseBody 어노테이션의 사용  (1) 2014.12.18
[Spring] 파일업로드 처리  (2) 2014.12.18
[Spring] 요청 URI 매칭  (0) 2014.12.18

[Spring] @RequestBody 어노테이션과 @ReponseBody 어노테이션의 사용


@RequestBody 어노테이션과 @ReponseBody 어노테이션의 사용


웹 서비스와 REST 방식이 시스템을 구성하는 주요 요소로 자리 잡으면서 웹 시스템간에 XML이나 JSON 등의 형식으로 데이터를 주고 받는 경우가 증가하고 있다.

이에 따라 스프링 MVC도 클라이언트에서 전송한 XML 데이터나 JSON 또는 기타 데이터를 컨트롤러에서 DOM 객체나 자바 객체로 변환해서 받을 수있는 기능(수신)을 제공하고 있으며,

비슷하게 자바 객체를 XML이나 JSON 또는 기타 형식으로 변환해서 전송할 수 있는 기능(송신)을 제공하고 있다.


@RequestBody 어노테이션과 @ResponseBody 어노테이션은 각각 HTTP 요청 몸체를 자바 객체로 변환하고 자바 객체를 HTTP 응답 몸체로 변환하는 데 사용된다.

@RequestBody 어노테이션을 이용하면 HTTP 요청 몸체를 자바 객체로 전달받을 수 있다. 비슷하게 @ResponseBody 어노테이션을 이용하면 자바 객체를 HTTP 응답 몸체로 전송할 수 있다.


import org.springframework.stereotype.Controller;

import org.springframework.util.MultiValueMap;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.ResponseBody;


@Controller

public class SimpleConverterController {


@RequestMapping(value = "/test/simpleTest.do", method = RequestMethod.GET)

public String simpleTestForm() {

return "test/simpleTestForm";

}


@RequestMapping(method = RequestMethod.POST)

// @RequestBody 어노테이션은 @RequestMapping에 의해 POST 방식으로 전송된 HTTP 요청 데이터를 String 타입의 body 파라미터로 전달된다.(수신)

// 그리고 @ResponseBody 어노테이션이 @RequestMapping 메서드에서 적용되면 해당 메서드의 리턴 값을 HTTP 응답 데이터로 사용한다.

// simpleTest() 메서드의 리턴 값이 String 타입이므로 String 데이터를 HTTP 응답 데이터로 전송한다.(송신)

@ResponseBody

public String simpleTest(@RequestBody String body) {

return body;

}

}


SimpleConverterController에 GET 방식으로 요청이 전달되면 simpleTestForm 뷰가 사용되는데, 이 뷰가 아래와 같은 폼을 생성한다고 해보자

<form method="POST">

이름: <input type="text" name="name" /> <br/>

나이: <input type="text" name="age" />

<input type="submit" />

</form>


이때 HTTP 몸체로 전송되는 데이터는 다음과 같은 형식을 취한다.

name=%C3%D6%B9%FC%B1%D5%$age=34


SimpleConverterController 컨트롤러는 POST 방식으로 요청이 들어오는 경우 @RequestBody 어노테이션이 적용되어 있으므로 그대로 결과 값으로 리턴한다.

그런데, @ResponseBody 어노테이션이 적용되어 있으므로 결과적으로 HTTP 요청 몸체 데이터가 HTTP 응답 몸체로 전송된다.


스프링 MVC는 HttpMessageConverter를 이용해서 자바 객체와 HTTP 요청/응답 몸체 사이의 변환을 처리하는데, @ResponseBody 어노테이션 적용 메서드의 리턴 타입이 String인 경우

HTTP 응답 데이터의 컨텐츠 타입은 "text/plain;charset=ISO-8859-1"이 된다.


1. HttpMessageConverter를 이용한 변환 처리

AnnotationMethodHandlerAdapter 클래스는 @RequestBody 어노테이션이 적용된 파라미터나 @ResponseBody 어노테이션이 적용된 메서드에 대해 HttpMessageConverter를 사용해서 변환을 처리한다. 주요 HttpMessageConverter 구현 클래스는 다음과 같다.


 구현 클래스

 설 명

 ByteArrayHttpMessageConverter(*)

 HTTP 메시지와 byte 배열 사이의 변환을 처리한다. 컨텐츠 타입은  application/octet-stream이다.

 StringHttpMessageConverter(*)

 HTTP 메시지와 String 사이의 변환을 처리한다. 컨텐츠 타입은  text/plain;charset=ISO-8859-1이다.

 FormHttpMessageConverter(*)

 HTML 폼 데이터를 MultiValueMap으로 전달받을 때 사용된다. 지원하는 컨텐  츠 타입은 application-x-www-form-urlencorded이다.

 SourceHttpMessageConverter(*)

 HTTP 메시지와 javax.xml.transform.Source 사이 변환을 처리한다. 컨텐츠 타  입은 application/xml 또는 text/xml이다.

 MarshallingHttpMessageConverter(*)

 스프링의 Marshaller와 unMarshaller를 이용해서 XML HTTP 메시지와 객체 사  이의 변환을 처리한다. 컨텐츠 타입은 application/xml 또는 text/xml이다.

 MappingJacksonHttpMessageConverter(*)

 Jackson 라이브러리를 이용해서 JSON HTTP 메시지와 객체 사이의 변환을 처  리한다. 컨텐츠 타입은 applicaion/json이다.


만약 기본 구현 클래스가 아닌 다른 구현 클래스를 사용하려면 다음과 같이 AnnotationMethodHandlerAdapter에 명시적으로 구현 클래스 목록을 지정해 주어야 한다.

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">

<property name="cacheSeconds" value="0" />

<property name="alwaysUseFullPath" value="true" />

<property name="webBindingInitializer">

<bean class="madvirus.spring.chap06.binder.CustomWebBindingInitializer" />

</property>

<property name="messageConverters">

<list>

<ref bean="byteArrayHttpMessageConverter" />

<ref bean="marshallingHttpMessageConverter" />

<!--<ref bean="stringHttpMessageConverter" />-->

<!--<ref bean="formHttpMessageConverter" />-->

<!--<ref bean="sourceHttpMessageConverter" />-->

<!--<ref bean="jsonHttpMessageConverter" />-->

</list>

</property>

</bean>


<bean id="byteArrayHttpMessageConverter" class="org.springframework.http.converter.ByteArrayHttpMessageConverter" />

<bean id="marshallingHttpMessageConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">

<property name="marshaller" ref="jaxb2Marshaller" />

<property name="unmarshaller" ref="jaxb2Marshaller" />

</bean>


예를 들어, 다음과 같이 JAXB 2의 어노테이션이 적용된 자바 객체가 있다고 하자

public class WriteArticleServiceImpl{@XmlAccessorType(XmlAccessType.FIELD)

@XmlRootElement(name="message-list")

public class GuestMessageList{

@XmlElement(name="message")

private List<GuestMessage> message;

...

}


@XmlAccessorType(XmlAccessType.FIELD)

@XmlType(name="", propOrder={"id", "message", "creationTime"})

public class GuestMessage{

private Integer id;

private String message;

private Date creationTime;

}


컨트롤러에서는 아래 코드와 같이 @ResponseBody 어노테이션을 이용해서 GuestMessageList를 리턴하도록 구현할 수 있을 것이다.

@Controller

public class GuestMessageController {


@RequestMapping(value = "/guestmessage/xml.do", method = RequestMethod.GET)

@ResponseBody

public GuestMessageList listXml() {

GuestMessageList list = ...;

return list;


}

...

}


JAXB2를 이용해서 자바 객체를 XML 응답으로 전송하려면 MarshallingHttpMessageConverter의 marshaller와 unmarshaller로 스프링 OXM 모듈의 Jaxb2Marshaller를 사용하면 된다.

<bean id="marshallingHttpMessageConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">

<property name="marshaller" ref="jaxb2Marshaller" />

<property name="unmarshaller" ref="jaxb2Marshaller" />

</bean>


<bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">

<property name="classesToBeBound">

<list>

<value>madvirus.spring.chap06.model.GuestMessageList</value>

</list>

</property>

</bean>

이제 스프링 MVC는 MarshallingHttpMessageConverter를 이용해서 GuestMessageList 객체를 XML로 변환할 수 있게 된다.


2. Content-Type과 Accept 헤더 기반의 변환 처리

AnnotationMethodHandlerAdapter가 HttpMessageConverter를 이용해서 요청 몸체 데이터를 @RequestBody 어노테이션이 적용된 자바 객체로 변환할 때에는 HTTP 요청 헤더의 Content-Type 헤더에 명시된 미디어 타입(MIME)을 지원하는 HttpMessageConverter 구현체를 선택한다.

예를 들어, 요청 미디어 타입이 application/xml 이고 @RequestBody 어노테이션이 적용된 파라미터가 Source 타입인 경우 SourceHttpMessageConverter가 선택된다.

비슷하게 @ResponseBody 어노테이션을 이용해서 리턴한 객체를 HTTP 응답 객체로 변환할 때에는 HTTP 요청 헤더의 Accept 헤더에 명시된 미디어 타입을 지원하는 HttpMessageConveter 구현체를 선택한다.

예를 들어, Accept 헤더에 명시된 값이 application/json이고 @ResponseBody 어노테이션이 적용된 메서드의 리턴 타입이 자바 객체인 경우

MappingJacksonHttpMessageConverter가 선택된다. 예를 들어, 자바스크립트레서 아래와 같은 코드를 이용해서 Accept 헤더의 값으로 "application/json"을 지정했다고 하자.


<script type="text/javascript">

...

xmlhttp.open("GET","json.do",true);

xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

xmlhttp.setRequestHeader("Accept", "application/json");

xmlhttp.send(null);

...

</script>


이 경우 다음과 같이 @ResponseBody 어노테이션이 적용된 메서드의 리턴 객체를 변환할 때 MappingJacksonHttpMessageConverter를 사용하게 된다.

@RequestMapping(value = "/guestmessage/json.do", method = RequestMethod.GET, headers = "accept=application/json")

@ResponseBody

public GuestMessageList listJson() {

...

}


3. MappingJacksonJsonView를 이용한 JSON 응답 생성

스프링3 버전은 자바 객체를 JSON으로 변환해서 보여주는 뷰 구현 클래스인 MappingJacksonView를 제공하고 있다. 


아래 코드는 MappingJacksonJsonView의 설정 예를 보여주고 있다.

<bean id="viewResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver" />


<bean id="pageJsonReport" class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />


컨트롤러에서는 다음과 같이 뷰 이름으로 MappingJacksonJsonView 타입의 빈을 설정해주면 된다.

@RequestMapping("/pageReport")

public ModelAndView pdfReport() {

List<PageRank> pageRanks = new ArrayList<PageRank>();

pageRanks.add(new PageRank(1, "/bbs/mir2/list"));

pageRanks.add(new PageRank(2, "/bbs/mir3/list"));

pageRanks.add(new PageRank(3, "/bbs/changchun2/list"));

return new ModelAndView("pageReport", "pageRanks", pageRanks);

}


MappingJacksonJsonView는 모델에 저장된 모든 객체를 JSON 형식으로 변환해준다.

{"report":{"pageRanks":["rank":1,"page"."/bbs/mir2/list"},{"rank":2,"page":"/bbs/mir3/list},{"rank":3,"page":"/bbs/changchun2/list"}]}}


예를 들어,  컨틀롤러에서 모델에 다음과 같이 값을 설정했다고 해보자

public String controllerMethod(Model model){

...

model.addAttribute("report", report);

model.addAttribute("summary", summary);

...

}


 컨트롤러에서 위와 같이 모델을 설정한 경우 MappingJacksonJsonView는 다음과 같은 형식의 JSON 응답 결과를 생성한다.

{"report":.... "summary":...}



[Spring] 파일업로드 처리


파일 업로드 처리


인코딩 타입이 Multipart인 경우 파라미터나 업로드한 파일을 구하려면 전송 데이터를 알맞게 처리해 주어야 한다.

스프링은 Multipart 지원 기능을 제공하고 있기 때문에, 이 기능을 이용하면 추가적인 처리없이 Multipart 형식으로 전송된 파라미터와 파일 정보를 쉽게 구할 수 있다.


1. MultipartResolver 설정

Multipart 지원 기능을 이용하려면 먼저 MultipartResolver를 스프링 설정 파일에 등록해 주어야 한다. MultipartResolver는 Multipart 형식으로 데이터가 전송된 경우, 해당 데이터를 스프링 MVC에서 사용할 수 있도록

변환해준다. 예를 들어, @PathVariable 어노테이션을 이용해서 Multipart로 전송된 파라미터와 파일을 사용할 수 있도록 해준다.


스프링이 기본으로 제공하는 MultipartResolver는 CommmnosMultipartResolver이다. CommmnsMultipartResolver는 Commons FileUpload API를 이용해서 Multipart를 처리해준다.

CommmonsMultipartResolver를 MultipartResolver로 사용하려면 다음과 같이 빈 이름으로 "multipartResolver"를 사용해서 등록하면 된다.


<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean>

cf.) DispatcherServlet은 이름이 "multipartResolver"인 빈을 사용하기 때문에 다른 이름을 지정할 경우 MultipartResolver로 사용되지 않는다.


※ CommonsMultipartResolver 클래스의 프로퍼티

 프로퍼티

 타입

설명 

 maxUploadSize

 long

 최대 업로드 가능한 바이트 크기, -1은 제한이 없음을 의미한다. 기본 값은 -1이다.

 maxInMemorysize

 int 

 디스크에 임시 파일을 생성하기 전에 메모리에 보관할 수 있는 최대 바이트 크기, 

 기본 값은 10240 바이트이다.

 defaultEncording

 String 

 요청을 파싱할 때 사용할 캐릭터 인코딩, 지정하지 않은 경  우 HttpServletRequest.setEncording() 메서드로 지정한 캐릭터 셋이 사용된다. 

 아무 값도 없을 경우 ISO-8859-1을 사용한다.


2. @RequestParam 어노테이션을 이용한 업로드 파일 접근

업로드한 파일을 전달받는 첫 번째 방법은 @RequestParam 어노테이션이 적용된 MultipartFile 타입의 파라미터를 사용하는 것이다. 예를 들어, HTML 입력폼이 다음과 같이 작성되어 있다고 해보자

<form action="submitReport1.do" method="post" enctype="multipart/form-data">

학번: <input type="text" name="studentNumber" />

<br/>

리포트파일: <input type="file" name="report" />

<br/>

<input type="submit" />

</form>

위 HTML 코드에서 파일은 report 파라미터를 통해서 전달된다. 


이 경우 다음 코드와 같이 @RequestParam 어노테이션과 MultipartFile 타입의 파라미터를 이용해서 업로드 파일 데이터를 전달받을 수 있다.

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.multipart.MultipartFile;


@Controller

public class ReportSubmissionController {


@RequestMapping(value = "/report/submitReport1.do", method = RequestMethod.POST)

public String submitReport1(

@RequestParam("studentNumber") String studentNumber,

// MultipartFile이 제공하는 메서드를 이용해서 업로드 데이터 접근

@RequestParam("report") MultipartFile report) {

printInfo(studentNumber, report);

return "report/submissionComplete";

}

...

}

MultipartFile 인터페이스는 스프링에서 업로드 한 파일을 표현할 때 사용되는 인터페이스로서, MultipartFile 인터페이스를 이용해서 업로드 한 파일의 이름, 실제 데이터, 파일 크기 등을 구할 수 있다.


3. MultipartHttpServletRequest를 이용한 업로드 파일 접근


업로드한 파일을 전달받는 두 번째 방법은 MultipartHttpServletRequest 인터페이스를 이용하는 것이다.

import org.springframework.web.multipart.MultipartFile;

import org.springframework.web.multipart.MultipartHttpServletRequest;


@Controller

public class ReportSubmissionController {

@RequestMapping(value = "/report/submitReport2.do", method = RequestMethod.POST)

public String submitReport2(MultipartHttpServletRequest request) {

String studentNumber = request.getParameter("studentNumber");

MultipartFile report = request.getFile("report");

printInfo(studentNumber, report);

return "report/submissionComplete";

}

}

MultopartHttpServletRequest 인터페이스는 스프링이 제공하는 인터페이스로서, Multipart 요청이 들어올 때 내부적으로 원본 HttpServletRequest 대신 사용되는 Multipart 요청이 들어올 때 

내부적으로 원본 HttpServletRequest 대신 사용되는 인터페이스이다. MultipartHttpServletRequest 인터페이스는 실제로는 어떤 메서드도 선언하고 있지 않으며,  HttpServletRequest 인터페이스와 MultipartRequest 인터페이스를 상속받고 있다. MultipartHttpServletRequest 인터페이스는 javax.servlet.HttpServletRequest 인터페이스를 상속받기 때문에 웹 요청 정보를 구하기 위한 getParameter()나 getHeader()와 같은 메서드를 사용할 수 있으며, 추가로 MultipartRequest 인터페이스가 제공하는 Multipart 관련 메서드를 사용할 수 있다.


※ MultipartRequest 인터페이스의 파일 관련 주요 메서드

 메서드

설 명 

 Iterator<String> getFileNames()

 업로드 된 파일들의 이름 목록을 제공하는 Iterator를 구한다.

 MultipartFile getfile(String name)

 파라미터 이름이 name이 업로드 파일 정보를 구한다.

 List<MultipartFile> getFiles(String name)

 파라미터 이름이 name인 업로드 파일 정보 목록을 구한다.

 Map<String, MultipartFile> getFileMap()

 파라미터 이름을 키로 파라미터에 해당하는 파일 정보를 값으로 하는 Map을 구한다.


4. 커맨드 객체를 통한 업로드 파일 접근


커맨드 객체를 이용해도 업로드 한 파일을 전달받을 수 있다. 단지 커맨드 클래스에 파라미터와 동일한 이름의 MultipartFile 타입 프로퍼티를 추가해주기만 하면 된다.

예를 들어, 업로드 파일의 파라미터 이름이 "report"인 경우, 다음과 같이 "report" 프로퍼티를 커맨드 클래스에 추가해 주면 된다.


import org.springframework.web.multipart.MultipartFile;


public class ReportCommand {


private String studentNumber;

private MultipartFile report;


public String getStudentNumber() {

return studentNumber;

}


public void setStudentNumber(String studentNumber) {

this.studentNumber = studentNumber;

}


public MultipartFile getReport() {

return report;

}


public void setReport(MultipartFile report) {

this.report = report;

}

}


위 코드와 같이 MultipartFile 타입의 프로퍼티를 커맨드 클래스에 추가해주었다면, @RequestMapping 메서드의 커맨드 객체로 사용함으로써 업로드 파일 정보를 커맨드 객체를 통해서 전달받을 수 있게 된다.

@Controller

public class ReportSubmissionController{

...

@RequestMapping(value="/report/submitReport3.do",medhod = RequestMethod.POST)

public String submitReport3(ReportCommand reportCommand){

...

}

}


5. MultupartFile 인터페이스 사용


org.springframework.web.MutipartFile 인터페이스는 업로드 한 파일 및 파일 데이터를 표현하기 위한 용도로 사용된다.

MultipartFile 인터페이스가 제공하는 주요 메서드는 다음과 같다.


※ MulripartFile 인터페이스의 주요 메서드

 메서드

 설 명

 String getName()

 파라미터 이름을 구한다.

 String getOriginalFilename()

 업로드 한 파일의 이름을 구한다.

 String isEmpty()

 업로드 한 파일이 존재하지 않는 경우 true를 리턴한다.

 long getSize()

 업로드 한 파일의 크기를 구한다.

 byte[ ] getBytes() throws IOExcetion

 업로드 한 파일 데이터를 구한다.

 InputStream getInputStream() throws  IOException

 업로드한 파일 데이터를 읽어노는 InputStream을 구한다. InputStream의 사용  이 끝나면 알맞게 종료해  주어야 한다.

 void transferTo(File dest) throws IOException

 업로드 한 파일 데이터를 지정한 파일에 저장한다.


업로드 한 파일 데이터를 구하는 가장 단순한 방법은 MultipartFile.getByte() 메서드를 이용하는 것이다. 바이트 배열을 구한 뒤에 파일이나 DB등에 저장하면된다.

if(mutipartFile.isEmpty()){

byte[ ] fileData = multipartFile.getBytes();

//byte 배열을 파일/DB/네트워크 등으로 전송

...

}


업로드 한 파일 데이터를 특정 파일로 저장하고 싶다면 MultipartFile.transferTo() 메서드를 사용하는 것이 편리하다.

if(mutipartFile.isEmpty()){

File file = new File(fileName);

multipartFile.transferTo(file);

...

}



[Spring] 요청 URI 매칭


요청 URI 매칭


클라이언트가 요청한 URL과 @RequestMapping 어노테이션의 값을 이용해서 클라이언트의 요청을 어떤 컨트롤러의 어떤 메서드가 처리할 지가 결정된다.

매칭이 어떻게 이루어지는 지 알지 못할 경우 클라이언트의 요청 URL과 매칭되는 메서드를 설정할 수 없기 때문에, 어떻게 매칭이 이루어지는 지 아는 것은 작지만 매우 중요하다.

본 절에서는 매칭과 관련된 기본 내용인 경로와 @RequestMapping 어노테이션 간의 관계 그리고 @PathVariable을 이용한 URI 템플릿에 대해서 살펴보자.


1. 전체 경로와 서블릿 기반 경로 매칭 설정

    <servlet>

        <servlet-name>dispatcher</servlet-name>

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

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

    </servlet>

    

    <servlet-mapping>

        <servlet-name>dispatcher</servlet-name>

        <url-pattern>*.do</url-pattern>

        <url-pattern>/game/*</url-pattern>

    </servlet-mapping>

위 코드는 *.do로 오는 요청과 /game/으로 오는 요청을 dispatcher 서블릿이 처리하도록 설정하고 있다. 이 경우 다음의 두 @RequestMapping은 어떻게 할까?


@RequestMapping("/search/game.do")

public String search(...){

...

}

 /search/game.do 요청은 <url-pattern>이 ".do"인 확장자 기반의 매칭이 되므로 서블릿 경로가 ""가 된다. 따라서, /search/game.do"이므로 요청은 값이 "search/game.do"인 @RequestMapping 어노테이션과 매칭된다.


@RequestMapping("/game/info")

public String info(...){

...

}

info() 메서드는 /game/info요청을 처리하지 않는다. 서블릿 매핑 설정에서 <url-pattern> 의 값을 "/game/*" 으로 설정했기 때문이다.

<url-pattern>의 값으로 디렉터리를 포함한 패턴을 지정하게 되면 서블릿 경로는 "/game"이 되며, 서블릿 경로를 제외한 나머지  경로를 이용해서 @RequestMapping 어노테이션의 값과 매칭 여부를 판단하게 되므로,

실제 비교할 때 사용되는 요청 URI는 "/info"가 된다. @RequestMapping 어노테이션의 값은 "/game/info" 이므로 비교할 요청 URI인 "info"와는 매칭되지 않는다.


코드만 보면 /search/game.do에 대한 요청은 search()메서드가 처리하고 /game/info에 대한 요청은 info() 메서드가 처리한다고 생각하기 쉽다.

하지만 search() 메서드만 /search/game.do에 대한 요청을 처리하고, info() 메서드는 /game/info요청을 처리하지 않는다.


서블릿 경로를 제외한 나머지 경로를 사용하는 이유는 DispatcherServlet이 기본적으로 사용하는 HandlerMapping 구현체(DefalutAnnotationHandlerMapping)와 HandlerAdapter 구현체(AnnotationMethodAdapter)

가 전체 경로를 사용하지 않도록 설정되어 있기 때문이다. 

DispatherServlet은 DispatherServlet이 사용하는 스프링 설정 파일에 HandlerMapping 구현 클래스와 HandlerAdapter 구현 클래스가 빈으로 등록되어 있으면, 기본 구현체를 사용하지 않고 설정 파일에 등록된 구현체를 사용하게 된다.(기본구현체: DefaultAnnotationServlet 과 AnnotationMethodHandlerAdapter)


2. @PathVariable 어노테이션을 이용한 URI 템플릿


RESTful 서비스가 유행하면서 URI를 다음과 같이 REST 방식으로 구성하는 경우가 늘어나고 있다.

- http://somehost/users/madvius

- http://somehost/games

- http://somehost/forun/board1/10

기본에는 http://somehost/users/userinfo?id=madvius와 같이 파라미터를 이용해서 아이디나 이름을 전달받았다면, 이제는 URI에 아이디나 이름 등이 포함되도록 URL을 구성하고 있다.

URI 템플릿을 이용하면 REST 방식의 URL 매칭을 쉽게 처리할 수 있다. URI 템플릿을 사용하는 방법은 매우 간단하며, 다음과 같이 두 가지만 추가로 작업해주면 된다.


- @RequestMapping: 어노테이션의 값으로(템플릿 변수)

- @PathVariable: 어노테이션을 이용해서{템플릿 변수}와 동일한 이름을 갖는 파라미터를 추가한다.


import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;


@Controller

public class CharacterInfoController {

@RequestMapping("/game/users/{userId}/chracters/{chracterId}")

public String characterInfo(@PathVariable String userId/*madvirus*/, @PathVariable int characterId /*1*/, ModelMap model) {

model.addAttribute("userId", userId);

model.addAttribute("characterId", characterId);

return "game/character/info";

}

}

위 코드를 보면 @RequestMapping 어노테이션은 이름이 uesrId와 characterId인 변수를 포함하고 있다. 이들 변수는 @PathVariable 어노테이션이 적용된 동일한 이름을 갖는 파라미터에 매칭된다. 따라서 요청 URI가 "/game/users/madivirus/character/1"이면, characterInfo() 메서드의 userId 파라미터와 chracterId 파라미터의 값은 각각 "madvirus"와 1이 된다.


만약 파라미터 이름과 URI 템플릿의 변수 이름이 동일하지 않다면, 다음과 같이 @PathVariable에 매칭될 템플릿 변수 이름을 지정해주면 된다.

@RequestMapping("/game/users/{userId}")

public String userInfo(@PathVariable("userId") String id, ModelMap model){ //userId != id

...

}


3. @RequestMapping 어노테이션의 추가 설정 방법

@RequestMapping 어노테이션을 클래스와 메서드에 함께 적용할 경우, 메서드에 적용한 @RequestMapping 어노테이션의 값은 클래스에 적용한 @RequestMapping 어노테이션의 값은 클래스에 적용한 @RequestMapping 어노테이션의 값을 기본 경로로 사용하게 된다.


@Controller

@RequestMapping("/game/users/{userId}")

public class CharacterInfoController {


@RequestMapping("/characters/{characterId}")

public String characterInfo(@PathVariable String userId,

@PathVariable int characterId, ModelMap model) {

model.addAttribute("userId", userId);

model.addAttribute("characterId", characterId);

return "game/character/info";

}

}

위 코드에서 characterInfo() 메서드에 적용된 @RequestMapping 어노테이션의 값은 "/chracters/{characterId}"인데, 실제로 매칭되는 값은 클래스에 적용된 @RequestMapping 어노테이션의 값을 포함한

"/game/users/{userId}/chracters/{chracterId}"가 된다.


cf.) @RequestMapping 어노테이션은 Ant 스타일의 패턴을 지원한다. 따라서, "*"나 "**"를 값으로 사용할 수 있다.

- ?: 1개의 문자와 매칭

- *: 0개 이상의 문자와 매칭

- **: 0개 이상의 디렉터리와 매칭



[Spring] 모델 생성하기


모델 생성하기


@RequestMapping 어노테이션이 적용된 메서드의 파라미터의 리턴 타입으로 ModelAndView, Model, ModelMap, Map, 커맨드 객체 등을 이용해서 모델을 뷰에 전달하게 된다.

본 절에서는 컨트롤러가 이들 클래스를 이용해서 모델을 어떻게 뷰에 전달하는 지 살펴보자.


1. 뷰에 전달되는 모델 데이터

@RequestMapping 메서드가 ModelAndView, Model, Map을 리턴하는 경우 이들에 담긴 모델 데이터가 뷰에 전달된다. 또한, 추가로 다음의 항목도 뷰에 함께 모델로 전달된다.

- 커맨드 객체

- @ModelAttribute 어노테이션이 적용된 메서드가 리턴한 객체

- 메서드의 Map, Model, ModelMap 타입의 파라미터를 통해 설정된 모델


import org.springframework.stereotype.Controller;

import org.springframework.ui,ModelMap;

import org.springframework.web.bind.annotation.ModelAttribute;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.servlet.ModelAndView;


@Controller

public class GameSearchController {

private SearchService searchService;

@ModelAttribute("searchTypeList")

  // 이 코드에서 @ModelAttribute 어노테이션이 적용된 referenceSearchTypeList() 메서드가 리턴한 모델 객체는 "searchTypeList"라는 이름으로 뷰에 전달된다.

public List<SearchType> referenceSearchTypeList() {

List<SearchType> options = new ArrayList<SearchType>();

options.add(new SearchType(1, "전체"));

options.add(new SearchType(2, "아이템"));

options.add(new SearchType(3, "캐릭터"));

return options;

}

@RequestMapping("/search/game.do")

public ModelAndView search(@ModelAttribute("command") SearchCommand command, ModelMap model) {

String[ ] queryList = getPopularQueryList();

model.addAttribute("popularQueryList", queryList);

ModelAndView mav = new ModelAndView("search/game");

SearchResult result = searchService.search(command);

mav.addObject("searchResult", result);

return mav;

}

  //@RequestMapping 어노테이션이 적용된 search() 메서드는 파라미터로 커맨드 객체와 ModelMap 객체를 전달받는다. search() 메서드 내부에서는 ModelMap 타입 파라미터인

// model을 이용해서 "popularQueryList"이름의 모델 객체를 뷰에 전달한다. 또한, 내부적으로 ModelAndView 타입의 mav 객체를 만든 뒤 mav를 이용해서 "searchResult"라는 이름의

// 모델 객체를 뷰에 전달한다.

}


이들 모델 객체는 서로 다른 방법으로 생성되었지만, DIspatcherServlet은 이들 모델 객체에 저장된 모델 데이터를 모두 뷰에 전달한다.

따라서 뷰 코드에서는 다음과 같이 이들 모델 객체를 모두 사용할 수 있게 된다.

<body>

인기 키워드: <c:forEach var="popularQuery" items="${popularQueryList}">${popularQuery} </c:forEach>

<form action="game.do">

<select name="type">

<c:forEach var="searchType" items="${searchTypeList}">

<option value="${searchType.code}" <c:if test="${command.type == searchType.code}">selected</c:if>>

${searchType.text}</option>

</c:forEach>

</select>

<input type="text" name="query" value="${command.query}"/>

<input type="submit" value="검색" />

</form>

검색 결과: ${searchResult}

</body>


2. Map, Model, ModelMap을 통한 모델 설정

Map, Model, ModelMap을 이용하면 뷰에 전달할 모델을 생성할 수 있다.


(1) 첫 번째 방법은 이들 세 가지 타입 중 한 가지를 파라미터로 전달받을 것이다.

@RequsetMapping("search1.do")

public String search1(Map model){

model.put("result", searchResult);

}


@RequsetMapping("search2.do")

public String search2(Model model){

model.addAttribute("result", searchResult);

}


@RequsetMapping("search3.do"){

public String search3(ModelMap model){

model.addAttribute("result", searchResult);

}


(2) 두 번째 방법은 Map과 Model을 리턴하는 것이다. 이 둘은 모두 인터페이스이기 때문에 실제로는 인터페이스를 구현한 클래스의 객체를 생성해서 리턴하면 된다. 

@RequestMapping("/search1.do")

public Map<String, Object> search1(){

...

HashMap<String, Object> model = new HashMap<String, Object>();

model.put("result", searchResult);

return model;

}


@RequestMapping("/search2.do")

public Model search2(){

...

Model model = new ExtendedModelMap();

model.addAttribute("result", searchResult);

return model;

}

스프링은 org.springframework.ui.Model 인터페이스의 구현 클래스인 ExtendedModelMap 클래스를 동일한 패키지에 제공하고 있으므로, ExtendedModelMap 클래스를 이용해서 모델을 설정하면 된다.

Map의 경우 많이 사용하는 java.util.HashMap 클래스를 이용해서 모델을 설정한다.


(3) Model 인터페이스의 주요 메서드

org.springframework.ui.Model 인터페이스는 모델을 설정할 수 있도록 다음과 같은 메서드를 제공하고 있다.


1 Model addAttribute(String name, Object value)

value 객체를 name 이름으로 추가한다. 뷰 코드에서는 name으로 지정한 이름을 통해서 value를 사용한다.


2 Model addAttribute(Object value)

value를 추가한다. value의 패키지 이름을 제외한 단순 클래스 이름을 모델 이름으로 사용한다. 이 때 첫 글자는 소문자로 처리한다.

value가 배열이거나 컬렉션인 경우 첫 번째 원소의 클래스 이름 뒤에 "List"를 붙인 걸 모델 이름으로 사용한다.

이 경우에도 클래스 이름의 첫자는 소문자로 처리한다.


3 Model addAllAttribute(Collection<?> values)

addAttribute(Object value) 메서드를 이용해서 컬렉션에 포함된 객체들을 차례대로 추가한다.


4 Model AddAllAttributes(Map<String, ?> attributes)

Map에 초함된 <키, 값>에 대해 키를 모델 이름으로 사용해서 값을 모델로 추가한다.


5 Model mergeAttributes(Map<String, ?> attributes)

Map에 포함된 <키, 값>을 현재 모델에 추가한다. 단, 키와 동일한 이름을 갖는 모델 객체가 존재하지 않는 경우에만 추가된다.


6 boolean containsAttributes(String name)

지정한 이름의 모델 객체를 포함하고 있는 경우 true를 리턴한다.


다음 코드는 컨트롤러 요청 처리 메서드에서 Model 타입의 파라미터를 이용해서 모델을 설정하는 예를 보여주고 있다.

@RequestMapping("/search/game.do")

public String search(SearchCommand command, Model model){

...

model.addAttrubute("searchResult", result);

model.addAttribute("searchTypeList", searchTypeList);

...

}


컨트롤러 요청 처리 메서드에서 Model 타입을 리턴하고 싶다면 Model 인터페이스의 구현 클래스인 org.springframework.ui.ExtendedModelMap 클래스를 사용하면 된다.

@RequestMapping("search/gmae.do")

public Model search(SearchCommand command){

...

Model model = new ExtendedModelMap();

model.addAttribute("searchResult", result);

model.addAttribute("searchTypeList", searchTypeList);

return model;

}


(4) ModelMap 클래스
org.springframework.ui.ModelMap 클래스는 Map의 구현 클래스로서 제공하는, 컨트롤러 메서드의 파라미터를 통해서 전달받는다.

@RequestMapping("search/game.do")
public String search(SearchCommand command, ModelMap model){
...
model.addAttribute("result", searchResult);
...
}
ModelMap 클래스가 제공하는 모델 설정 관련 메서드는 Model 인터페이스와 동일하므로 추가 설명은 하지 않겠다.

3. ModelAndView를 통한 모델 설정

org.springframework.web.servlet.ModelAndView 클래스는 컨트롤러의 처리 결과를 보여 줄 뷰에 전달할 값을 저장하는 용도로 사용되는데, 본 절에서는 ModelAndView 객체를 생성하는 방법을 살펴보자.


(1) modelAndView 객체를 생성하는 방법은 setViewName(String viewName) 메서드를 이용하여 뷰 이름을 설정하고, addObject(String name, Object value) 메서드를 이용하여 뷰에 전달할 값을 추가한다.

@RequestmMapping("/search/game.do")

public ModelAndView search(SearchCommand command) {

ModelAndView mav = new ModelAndView();

mav.setViewName("search/game");

mav.addObject("greeting", getGreeting());

return mav;


(2) Map에 저장된 <키, 값> 쌍 전체를 뷰에 전달할 값으로 ModelAndView 객체에 추가하고 싶다면 다음과 같이 addAllObjects(Map modeMap) 메서드를 사용하면 된다.

Map referenceMap = referenceData();

mav.addAllObjects(referenceData);


(3) 생성자를 사용해서 뷰 이름과 Map을 전달할 수도 있다.

Map referenceMap = referenceData();

return new ModelAndView("search/game", referenceData);


(4) 뷰에 전달할 객체가 한 개 뿐이라면 다음과 같은 생성자를 이용해서 코드 분량을 줄일 수 있을 것이다.

return new ModelAndView("search.game", "result", referenceData");


4. @ModelAttribute 어노테이션을 이용한 모델 데이터 처리


@ModelAttribute 어노테이션을 이용하면 다음의 두 가지 작업을 수행할 수 있다.

1 @RequestMapping 어노테이션이 적용되지 않은 별도 메서드로 모델에 추가될 객체를 생성

2 커맨드 객체의 초기화 작업을 생성


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


(1) 참조 데이터 생성

웹 어플리케이션을 구현하다 보면 동일한 모델 데이터를 두 개 이상의 요청 처리 결과 화면에서 보여주어야 할 때가 있다.

예를 들어, 검색 메인 화면과 검색 결과 화면에서 검색 타입과 인기 검색어를 보여줄 수 있을 것이다. 

이 경우 다음과 같이 이들 공통 모델 데이터를 설정해주는 메서드를 구현한 뒤 요청 처리 메서드에서 호출하도록 구현 할 수 있을 것이다.

private void referenceData(Model model){

...

model.addAttribute("searchTypeList", searchTypeList);

model.addAttribute("popularQueryList", queryList);

}


@RequestMapping("/search/main.do")

public String main(Model model){

referenceData(model);

return "search/main";


@RequestMapping("search/game.do")

public ModelAndView search(

@ModelAttribute("command") SeachCommand command, Model model){

referenceData(model);

ModelAndView mav = new ModelAndView("search/game");

SearchResult result = searchService.search(Command);

mav.addObject("searchResult", result);

return mav;

}


하지만, 위와 같이 구현하려면 다음과 같은 단점이 발생한다.

1 Model이나 ModelMap과 같이 모델 정보를 설정할 때 사용할 타입이 동일해야 한다.

2 각 요청 처리 메서드에 공통으로 사용되는 모델을 설정하기 위한 코드가 중복된다.


@ModelAttribute 어노테이션을 사용하면 이런 단점 없이 두 개 이상의 요청 처리 메서드에서 공통으로 사용되는 모델을 설정할 수 있다.

@ModelAttribute 어노테이션을 메서드에 적용하면 해당 메서드가 생성한 객체가 뷰에 전달된다.

따라서, @ModelAttribute 어노테이션을 사용하면 두 개 이상의 요청 처리 메서드에서 공통으로 사용되는 모델 객체를 생성할 수 있다.

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.ModelAttribute;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.servlet.ModelAndView;


@Controller

public class GameSearchController {

@Autowired

private SearchService searchService;


@ModelAttribute("searchTypeList")

public List<SearchType> referenceSearchTypeList() {

List<SearchType> options = new ArrayList<SearchType>();

options.add(new SearchType(1, "전체"));

options.add(new SearchType(2, "아이템"));

options.add(new SearchType(3, "캐릭터"));

return options;

}


@ModelAttribute("popularQueryList")

public String[] getPopularQueryList() {

return new String[] { "게임", "창천2", "위메이드" };

}


@RequestMapping("/search/main.do")

public String main() {

return "search/main";

}


@RequestMapping("/search/game.do")

public ModelAndView search(@ModelAttribute("command") SearchCommand command) {

ModelAndView mav = new ModelAndView("search/game");

System.out.println("검색어 = " + command.getQuery().toUpperCase());

SearchResult result = searchService.search(command);

mav.addObject("searchResult", result);

return mav;

}

...

}

위 코드에서 main() 메서드와 search() 메서드는 둘 다 referenceSearchTypeList() 메서드와 getPopularQueryList() 메서드를 호출하고 있지 않다.

하지만 reference SearchTypeList() 메서드와 getPopularQueryList() 메서드에는 @ModelAttribute 어노테이션이 적용되어 있으므로 main() 메서드와 search() 메서드가 실행될 때 refereceSearchTypeList() 메서드와 getPopularQueryList() 메서드가 리턴한 객체가 모델로 뷰에 함께 전달된다.

이때 @ModelAttribute 어노테이션의 값이 뷰에 전달되는 객체의 이름으로 사용되므로, 뷰 코드에서는 다음과 같이 @ModelAttribute 어노테이션의 값을 이용해서 모델 객체에 접근할 수 있게 된다.


<body>

인기 키워드: <c:forEach var="popularQuery" items="${popularQueryList}">${popularQuery} </c:forEach>

<form action="game.do">

<select name="type">

<c:forEach var="searchType" items="${searchTypeList}">

<option value="${searchType.code}" <c:if test="${command.type == searchType.code}">selected</c:if>>

${searchType.text}</option>

</c:forEach>

</select>

<input type="text" name="query" value="${command.query}"/>

<input type="submit" value="검색" />

</form>


(2) 커맨트 객체 초기화

@ModelAttribute 어노테이션을 사용하면 커맨드 객체의 초기화 작업을 수행할 수도 있다.

예를 들어, GET 요청시에는 폼의 특정 값을 미리 초기화하고 POST 요청시에는 폼에 입력한 값을 커맨트 객체로 받아야 한다고 해보자.

이런 경우 @ModelAttribute 어노테이션을 사용하면 다음과 같이 GET 요청과 POST 요청에 대해 알맞게 커맨트 객체를 초기화할 수 있다.

아래 코드는 @ModelAttribute 어노테이션의 사용 예를 보여주고 있다.


@Controller

@RequestMapping("/account/create.do")

public class CreateAccountController {


@ModelAttribute("command")

public MemberInfo formBacking(HttpServletRequest request) {

if (request.getMethod().equalsIgnoreCase("GET")) {

MemberInfo mi = new MemberInfo();

Address address = new Address();

address.setZipcode(autoDetectZipcode(request.getRemoteAddr()));

mi.setAddress(address);

return mi;

} else {

return new MemberInfo();

}

}


@RequestMapping(method = RequestMethod.GET)

public String form() {

return "account/creationForm";

}


@RequestMapping(method = RequestMethod.POST)

public String submit(@ModelAttribute("command") MemberInfo memberInfo)

return "account/created";

}

}

위 코드에서 주목할 점은 formBacking() 메서드의 @ModelAttribute 어노테이션값과 submit() 메서드의 파라미터에서 사용된 @ModelAttribute 어노테이션의 값이 "command"로 동일하다는 것이다. submit() 메서드는 POST 요청을 처리하도록 되어 있는데, POST 요청이 전송될 경우 formBacking() 메서드가 생성한 MemberInfo 객체는 "command"라는 이름의 모델 객체로 저장되며, 이렇게 생성된"command" MemberInfo 객체는 submit() 메서드의 동일한 모델 이름을 갖는 커맨드 객체(첫 번째 파라미터)로 전달된다.


@ModelAttribute 어노테이션이 적용된 메서드가 @RequestMapping 어노테이션이 적용된 메서드 보다 먼저 호출되기 때문에, 

커맨드 객체에 대한 초기화 작업이 필요하다면 커맨드 객체와 동일한 이름을 갖는 @ModelAttribute 어노테이션이 적용된 메서드를 이용하면 된다.


(3) @ModelAttribute 어노테이션 적용 메서드에 전달 가능한 파라미터 타입
@ModelAttribute 어노테이션이 적용된 메서드는 @RequestMapping 어노테이션이 적용된 메서드와 동일한 타입의 파라미터를 가질 수 있다. 
앞서 코드에서는 HTTP 메서드를 확인하기 위해 HttpServletRequest를 파라미터로 전달받았다.

@ModelAttribute("command")
public MemberInfo formBacking(HttpServletRequest request) {
if (request.getMethod().equalsIgnoreCase("GET")) {
...
} else {
return new MemberInfo();
}
}
@ModelAttribute 어노테이션이 적용 메서드는 HttpServletRequest 뿐만 아니라 필요에 따라 Locale, @RequestParam 어노테이션 적용 파라미터, @PathVariable 어노테이션 적용 파라미터 등을 이용해서 모델 객체를 생성하는 데 필요한 정보를 구할 수 있다.


'Programing > Spring' 카테고리의 다른 글

[Spring] 파일업로드 처리  (2) 2014.12.18
[Spring] 요청 URI 매칭  (0) 2014.12.18
[Spring] 모델 생성하기  (1) 2014.12.18
[Spring] 뷰 지정  (0) 2014.12.18
[Spring] 컨트롤러 메서드의 파라미터 타입  (2) 2014.12.18
[Spring] HTML 폼과 자바빈 객체  (0) 2014.12.18

[Spring] 뷰 지정


뷰 지정

컨트롤러 처리 메서드는 처리 결과를 보여줄 뷰 이름이나 View 객체를 리턴하고, DispatcherServlet은 뷰 이름이나 View 객체를 이용해서 뷰를 생성하게 된다. 뷰 이름은 직접 또는 자동으로 설정할 수 있다.


1. 뷰 이름 명시적 지정: ModelAndView와 String 리턴 타입

뷰 이름을 명시적으로 리턴하려면 ModelAndView나 String을 리턴해야 한다.


(1) ModelAndView 클래스의 생성자

@RequestMapping("/hello.do")

public ModelAndView index(){

ModelAndView mav = new ModelAndView("hello");

...

return mav;

}


(2) setViewName() 메서드

@RequestMapping("/hello.do")

public ModelAndView index(){

ModelAndView mav = new ModelAndView();

mav.setViewName("hello");

...

return mav;

}



(3)  리턴 타입이 String인 경우

@RequestMapping("/help/main.do")

public String helpMain(ModelMap model){

...

return "help/main";

}

리턴 타입이 ModelAndView가 아니고 String 타입인 경우


2. 뷰 이름 자동 지정

다음의 경우 RequestToViewNameTranslator를 이용해서 URL로부터 뷰 이름을 결정한다.

- 리턴 타입이 Model이나 Map인 경우

- 리턴 타입이 void 이면서 ServletResponse나 HttpServletResponse 타입의 파라미터가 없는 경우


스프링 설정 파일에 RequestToViewNameTranslator 빈이 존재하지 않을 경우 기본적으로 DefalutRequestToViewNameTranslator를 사용한다. 이 클래스는 요청 URI로부터 맨 앞의 슬래시와 확장자를 제외한 나머지 부분을 뷰 이름으로 사용한다. ( 더 정확하게는 전체 경로를 사용하지 않도록 설정한 경우 서블릿 경로를 제외한 나머지 경로가 사용된다.)

@RequestMapping("/search/game2.do")

public Map<String, Object> search(){

HashMap<String, Object> model = new HashMap<String, String>();

...

return model;

}

위 코드는 뷰에 전달된 모델 데이터를 갖고 있는 Map을 리턴하고 있다. 이 경우 RequestToViewNameTranslator를 이용하여 결과를 보여줄 뷰 이름을 결정하게 되는데,

DefaultRequestToViewNameTranslator가 사용될 경우 다음과 같이 URL로부터 뷰 이름이 결정된다.

/search/game2.do --> search/game2


3. 리다이렉트 뷰

뷰 이름에 "redirect:" 접두어를 붙이면, 지정한 페이지로 리다리렉트 된다. 리다이렉트 URL은 다음과 같이 두 가지 방식으로 입력할 수 있다.

- redirect:/bbs/list: 현재 서블릿 컨텍스트에 대한 상대적인 경로로 리다이렉트

- redirect:http://host/bbs/list: 지정한 절대 URL로 리다이렉트


ModelAndView mav = new ModelAndView();

mav.setViewName("redirect:error.do");

return mav;



'Programing > Spring' 카테고리의 다른 글

[Spring] 요청 URI 매칭  (0) 2014.12.18
[Spring] 모델 생성하기  (1) 2014.12.18
[Spring] 뷰 지정  (0) 2014.12.18
[Spring] 컨트롤러 메서드의 파라미터 타입  (2) 2014.12.18
[Spring] HTML 폼과 자바빈 객체  (0) 2014.12.18
[Spring] 자바 코드 기반 설정  (0) 2014.12.17

[Spring] 컨트롤러 메서드의 파라미터 타입


컨트롤러 메서드의 파라미터 타입


컨트롤러 @RequestMapping 어노테이션이 적용된 메서드는 커맨드 클래스뿐만 아니라 HttpServletRequest, HttpSession, Locale 등 웹 어플리케이션과 관련된 다양한 타입의 파라미터를 가질 수 있는데, 전달 가능한 파라미터 타입은 다음과 같다.



1. @RequestParam 어노테이션을 이용한 파라미터 매핑

컨트롤러를 구현하면서 가장 많이 사용되는 어노테이션이 바로 @RequestParam 어노테이션이다. @RequestParam 어노테이션은 HTTP 요청 파라미터를 메서드의 파라미터로 전달받을 때 사용된다. @RequestParam 어노테이션과 HTTP 요청 파라미터의 관계는 다음과 같다.


첫번째 파라미터인 userName은 name 요청 파라미터의 값을 전달 받으며, 두 번째 파라미터인 userEmail 파라미터는 email 파라미터 값을 전달 받는다.

바로, JSP에서 String name = request.getParameter("name"); 과 같다.


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

@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.

...

    }

}

@RequestParam 어노테이션이 적용된 파라미터는 기본적으로 필수 파라미터이다. 따라서 @RequestParam 어노테이션에 명시한 HTTP 요청 파라미터가 존재하지 않을 경우

스프링 MVC는 잘못된 요청을 의미하는 400 응답 코드를 웹 브라우저에 전송한다. 예를 들어 @RequestParam 어노테이션의 별도 설정을 하지 않은 경우, 다음의 URL을 요청하면 400 에러가 발생하게 된다.


http://host/chap06/search/internal.do?query=spring&p=3


@Controller

public class SearchController{

@RequestMapping("/search/internal.do")

public ModelAndView seachInternal(

@RequestParam("query") String query, @RequestParam("p") int pageNumber){

...

}

}

첫 번째 파라미터는 query 요청 파라미터의 값을 전달받으며, 두 번째 파라미터인 pageNumber 파라미터는 p 파라미터의 값을 전달받는다.

@RequestParam 어노테이션이 적용된 파라미터가 String이 아닐 경우 실제 타입에 따라서 알맞게 타입 변환을 수행한다. 예를 들어, pageNumber 파라미터의 타입은 int인데, 이 경우 자동으로 문자열을 int 타입으로 변환해준다. 


만약 아래와 같이 int 타입으로 변환할 수 없는 값인 "a"를 pageNumber 파라미터에 매핑되는 HTTP 요청 파라미터로 전달하면, 스프링 MVC는 잘못된 요청(Bad Request)을 의미하는 400 응답 코드를 웹 브라우저에 전송한다.

http://localhost:8080/chap06/search/internal.do?query=spring&p=a

@RequestParam 어노테이션이 적용된 파라미터는 기본적으로 필수 파라미터이다. 따라서 @RequestParam 어노테이션에 명시한 HTTP 요청 파라미터가 존재하지 않을 경우 스프링 MVC는 잘못된 요청을 의미하는 400 응답코드를 웹 브라우저에 전송한다. 예를 들어 아래와 같이 @RequestParam 어노테이션의 별도 설정을 하지 않은 경우, 다음의 URL을 요청하면 400 에러가 발생하게 된다.


http://localhost:8080/chap06/search/internal.do?query=spring


필수가 아닌 파라미터인 경우 required 속성 값을 false로 지정해 주면 된다. 아래 코드는 적용 예이다. 참고로 required 속성의 기본 값은 true이다.

import orr.springframework.web.bind.annotation.RequestMapping

...

...

@RequestMapping("/search/external.do")

public ModelAndView searchExternal(

@RequestParam(value="query", required=false) String query, @RequestParam(value ="p", required=false) int pageNumber){

System.out.println("query="+query+",pageNumber="+pageNumber);

...

}

필수가 아닌 요청 파라미터의 값이 존재하지 않을 경우 null 값을 할당한다. 그런데, null을 할당할 수 없는 기본 데이터 타입인 경우에는 타입 변환 에러가 발생하게 된다. 예를 들어 , 위 코드에서 "p" 요청 파라미터를 필수가 아닌 파라미터로 설정했는데, 이 상태에서 "p" 요청 파라미터를 지정하지 않은 경우 null을 기본 데이터 타입으로 변환할 수 없다는 예외가 발생하게 된다.


기본 데이터 타입을 사용할 경우 HTTP 요청 파라미터가 존재하지 않으며 기본 값을 할당하는 경우가 많은데, 이런 경우에는 다음과 같이 defaultValue 속성을 이용해서 기본값을 지정할 수 있다.

@RequestMapping("/search/external.do")

public ModelAndView searchExternal(

@RequestParam(value="query", required=false) String query, @RequestParam(value="p", defaultValue="1") int pageNumber){

System.out.println("query="+query+",pageNumber="+pageNumber);

...

}

defaultValue 속성을 이용해서 기본 값을 지정하면 해당 요청 파라미터를 지정하지 않을 경우 defaultValue 속성에 지정한 문자열을 값으로 이용하게 된다. 예를 들어, 위와 같이 "p" 요청 파라미터의 기본 값을 "1"로 지정했다면,

http://localhost:8080.chap06/search/external.do?query=spring

"p" 파라미터가 존재하지 않으므로 기본 값으로 지정한 "1"을 "p" 요청 파라미터의 값으로 사용하게 되고, 따라서 pageNumber 파라미터의 값은 1이 된다.


2. @RequestHeader 어노테이션을 이용한 헤더 맵핑


@RequestHeader 어노테이션을 이용하면 HTTP 요청 헤더의 값을 메서드의 파라미터로 전달받을 수 있다.

import org.springframework.web.bind.annotation.RequestHeader;

import org.springframework.web.bind.annotation.RequestMapping;


@Controller

public class HeaderController {


@RequestMapping("/header/check.do")

public String check(@RequestHeader("Accept-Language") String languageHeader) {

System.out.println(languageHeader);

return "header/pass";

}

}


cf.) 컨트롤러 메서드의 리턴 타입

컨트롤러 메서드는 ModelAndView를 비롯한 몇 가지 리턴 타입을 가질 수 있다.