Spring Framework 3.2 – Themes And Trends 일부 정리

기본

Spring Framework 3.2 Webinar 인 Themes And Trends 영상을 보고 Spring Framework 3.2 에 대한 내용 중 일부를 정리한 포스트입니다:

Meta 어노테이션

빈을 주입하거나 @Bean 메소드에서 @Autowired, @Value 어노테이션을 메타 어노테이션으로 사용할 수 있습니다. 즉, 커스텀 어노테이션을 만들면서 @Value, @Autowired, 그리고 커스텀 어노테이션을 추가하면 해당 기능이 추가됩니다. 그리고 @Lazy 등을 이용해 기능을 더 추가할 수도 있다고 하네요.

비동기 지원

request.startAsync()

request.startAsync()

코드를 사용해서 명시적으로 비동기 코드를 사용할 수 있습니다. 위와 같이 지정하면..

  • 컨테이너 쓰레드가 종료되고 새로운 쓰레드로 처리되며 완료 후, 다시 컨테이너로 디스패치 됩니다.

java.util.concurrent.Callable<?>

Spring MVC 핸들러에서 Callable<?>을 리턴값으로 사용 가능합니다. Callable을 리턴할 경우 새로운 쓰레드에서 실행됩니다.

@RequestMapping(&amp;quot;…&amp;quot;)
public @ResponseBody Callable callable() {
  return new Callable() {
    @Override
    public String call() throws Exception {
      // Long Running DB job, 3rd party REST API call, etc.
      return &amp;quot;Hello world&amp;quot;;
    }
  }
}

org.springframework.web.*.DeferredResult<?>

Spring MVC가 제어하지 않는 쓰레드를 통해 비동기 처리(JMS, AMQP, Redis 등)를 합니다. 먼저 DeferredResult를 저장하고, 외부 이벤트 발생 시, 실행하게 됩니다. 이때, 스프링의 AsyncTask를 사용할 수도 있습니다:

// save
@RequestMapping(&amp;quot;…&amp;quot;)
public @ResponseBody DeferredResult deferredResult() {
  DeferredResult result = new DeferredResult();
  this.responseBodyQueue.add(result);
}

// process
@Scheduled(fixedRate=2000)
public void processQueues() {
  for (DeferredResult result : this.responseBodyQueue)  {
    result.setResult(&amp;quot;Deferred result&amp;quot;);
    this.responseBodyQueue.remove(result);
  }
}

서버단 롱폴링 처리 프로세스

  1. 브라우저 요청
  2. 핸들러 메소드가 DeferredResult 리턴
  3. 서블릿 컨테이너 종료(응답은 살아있음)
  4. JMS, AMQP, Redis 같은 외부 이벤트 발생
  5. DeferredResult 세팅
  6. 요청이 서블릿 컨테이너로 돌아감
  7. 처리가 다시 진행되고 요청 완료

서블릿 3 에서 비동기 설정

서블릿과 모든 필터에 비동기 지원 플래그 추가
<async-supported>true</async-supported>

<filter-mapping> 자식 엘리먼트인 <disaptcher> 엘리먼트에 ASYNC 추가
<dispatcher>REQUEST, ASYNC, FORWARD</dispatcher>

Java Config 에서 설정을 도와주는 WebApplicationInitializer  추가됨

public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class&amp;lt;?&amp;gt;[] getRootConfigClasses() {
    return new Class&amp;lt;?&amp;gt;[] { RootConfig.class };
  }

  @Override
  protected Class&amp;lt;?&amp;gt;[] getServletConfigClasses() {
    return new Class&amp;lt;?&amp;gt;[] { WebMvcConfig.class };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { &amp;quot;/*&amp;quot; };
  }

  @Override
  protected void customizeRegistration(Dynamic registration) {
    registration.setAsyncSupported(true);
  }

}

컨텐츠 네고시에이션

file extension, Accept header, request parameter, default value, custom strategy 를 기반으로 컨텐츠 네고시에이션이 가능하고, 전략의 우선순위도 설정 할 수 있습니다.

REST 에러 보고

전역 @ExceptionHandler 를 지정할 수 있는 @ControllerAdvice 어노테이션이 추가됐습니다.

@ControllerAdvice
public class GlobalExceptionHandler {

  @ExceptionHandler
  public @ResponseBody String handleBusinessException(BusinessException ex) {
    return &amp;quot;Error details&amp;quot;;
  }

  // May also add globally applicable @InitBinder(e.g. Date) and @ModelAttribute methods
}

Matrix 변수

URI path에 세미콜론을 사용하면.. GET /pets/42;q=21 다음과 같은 코드로 가져올 수 있습니다.

@PathVariable String petId, @MatrixVariable int q

Matrix 변수 설정을 사용하기 위해선 RequestMappingHandlerMapping 의 removeSemicolonContent 프로퍼티의 값을 false 로 지정해야 합니다.(디폴트 true)

TestContext 프레임워크에서 Web Application 지원

// default to &amp;quot;file:src/main/webapp&amp;quot;
@WebAppConfiguration

// detects &amp;quot;WacTests-context.xml&amp;quot; in same package
// or static nested @Configuration class
@ContextConfiguration
public class WacTests {
  //...
}

Spring MVC Test Framework

발표자가 생각하는 좋은 테스트 이디엄(idiom)을 소개해 주고 있습니다.

// Server-side test
@Runwith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(&amp;quot;servlet-context.xml&amp;quot;)
public class SampleTests {
  @Autowired
  private WebApplicationContext wac;
  private MockMvc mvc;

  @Before
  public void setup() {
    this.mvc = webAppContextSetup(this.wac).build();
  }

  @Test
  public void getFoo() throws Exception {
  this.mvc.perform(get(&amp;quot;/foo&amp;quot;).accept(&amp;quot;application/json&amp;quot;))
          .andExpect(status().isOk())
          .andExpect(content().mimeType(&amp;quot;application/json&amp;quot;))
          .andExpect(jsonPath(&amp;quot;$.name&amp;quot;).value(&amp;quot;Lee&amp;quot;));
  }
}

// Client-side test
RestTemplate restTemplate = new RestTemplate();

MockRestServletServer mockServer = MockRestServiceServer.createServer(restTemplate);

mockServer.expect(requestTo(&amp;quot;/greeting&amp;quot;))
    .andRespond(withSuccess(&amp;quot;Hello world&amp;quot;, &amp;quot;text/plain&amp;quot;));

// use RestTemplate …

mockServer.verify();
Advertisements

Spring MVC 3.1에서 ObjectId를 Jackson 매퍼에서 시리얼라이즈 할 때 toString() 하게끔 하기

기본

MongoDB는 컬렉션에 넣는 데이터에 기본적으로 ObjectId를 할당합니다. 일종의 PK라고 볼 수 있죠. Java에서 ObjectId 또한 클래스이고 그 안에 여러가지 필드를 가지고 있는데요. @ResponseBody 어노테이션으로 JacksonMapper를 사용해 ObjectId가 json으로 파싱되면 MongoDB에서 사용하는 문자열 값이 아니라 ObjectId 객체의 필드들이 JSON 형태로 만들어 집니다. 원하는 건 이게 아닌데 말이죠 :-/

그래서 Jackson에서 제공해 주는 @Serializer 어노테이션으로 커스텀 시리얼라이저를 만들어 봤지만 씨알도 안먹히더군요. 그래서 이 포스트를 참고해 CustomObjectMapper 도 만들어 봤지만 <annotation-driven /> 에서 Jackson 매퍼를 빈으로 등록하기 때문에 파라미터로 세팅해 줄수가 없네요.

위 포스팅에선 @PostConstruct 를 사용해 빈으로 등록된 Jackson 매퍼를 찾아서 커스텀 매퍼를 세팅해 주고 있는데요. 위 방법은 Spring 3.1 에서 동작하지 않습니다. 그 이유는 더이상 AnnotationMethodHandlerAdapter를 사용하지 않기 때문이죠. 예제에서 AnnotationMethodHandlerAdapter를 RequestMappingHandlerAdapter로 변경하면 잘 동작합니다!

Spring Test MVC로 컨트롤러 테스트하기

기본

기선이 형의 Spring Test MVC 프로젝트 소개글을 보고 언제 한 번 써봐야겠다 하다가 요새 좀 살펴보고 있습니다.

사실 고백하자면 컨트롤러 테스트 케이스는 작성 해본적이 거의 없는데요.. 이거 테스트 작성할 시간에 그냥 브라우저로 돌려보는게 낫겠다 싶어서요. 그러다가 요새 Spring Test MVC를 써보니 테스트 하기도 쉽고 또 상당히 재미있네요 ㅎㅎ

Maven 디펜던시

아직 정식 릴리즈가 안돼서 메이븐 레파지토리를 추가해 줘야 합니다:

<repository>
	<id>spring-snapshot</id>
	<name>Spring Maven SNAPSHOT Repository</name>
	<url>http://repo.springsource.org/libs-snapshot</url>
</repository>

그리고 디펜던시도 추가해 줘야겠죠:

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-test-mvc</artifactId>
	<version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>

몇 가지 샘플코드

Spring Test MVC는 빌더 패턴 형태로 테스트 템플릿을 만들고 세부적인 것은 다양한 Matchers를 통해 만들어 나가는 식으로 구성됩니다. hamcrest랑 비슷하게요. (그러고보니 hamcrest Matchers를 바로 사용할 수 있는 부분도 본 것 같습니다)
org.springframework.test.web.server.result 패키지를 확인해보면 다양한 Matcher를 볼 수 있죠. 이걸 쓰려고 전부 static import 할 필요는 없고, 다음 2개 클래스만 static import 해줍니다:

import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.*;

Spring MVC의 컨트롤러를 테스트 하기 위해선 먼저 스프링 컨텍스트가 필요한데요. 예전 같았으면 @RunWith, @ContextConfiguration 어노테이션을 붙여서 시작했겠지만 Spring Test MVC는 MockMvcBuilders 클래스를 사용합니다:

MockMvc mockMvc = MockMvcBuilders.xmlConfigSetup(new String[] {"classpath:/applicationContext.xml", "file:web/WEB-INF/spring-servlet.xml"}).build();

XML을 사용하는 방법 외에 @Configuration, WebApplicationContext, 단일 Controller를 사용해 테스트 컨텍스트를 꾸리는 방법도 있습니다. MockMvc를 생성했으면 테스트 할 URL과 파라미터 등을 세팅하면 됩니다:

mockMvc.perform(post("/message/post").param("author", "mOer").param("body", "some contents...").param("title", "some title"))
.andExpect(status().isOk())
.andExpect(redirectedUrl("/list"));</code>

앞에서 static import를 한 이유는 perform() 메소드에 사용하는 get(), post() 등의 메소드가 MockMvcRequestBuilders 클래스에 정의돼 있기 때문입니다. 반대로 다른 Matchers 클래스를 static import 할 필요가 없는 이유는 MockMvcRequestBuilders 클래스 정의된 각각의 메소드가 빌더 형태로 만들어져 있어서 각각의 필요한 Matchers 인스턴스를 리턴하기 때문입니다.

MockMvcResultMatchers 클래스에는 해당 컨트롤러를 호출한 뒤 다양한 검사를 할 수 있는 static 메소드가 정의돼 있습니다.

andExpect() 메소드를 통해서 Assertion을 할 수 있기 때문에 구지 Assert.. 메소드들을 사용할 필요가 없습니다. 또한 andReturn() 메소드를 호출하면 MvcResult가 리턴되는데, 이 안에 ModelAndView 객체가 있어서 실질적으로 어떤 모델이 뷰에 리턴되는지 검증 할 수도 있습니다. 물론, andExpect() 에서도 할 수 있구요:

.andExpect(model().attributeExists("message"));

@PathVariable을 사용하는 것도 문제 없습니다. 만약 @RequestMapping(“/delete/{messageId}”) 이렇게 매핑된 컨트롤러가 있다면, /delete/125 이렇게 테스트 할 수도 있겠지만 그대로 URI Template을 사용하는 방법도 제공해 줍니다:

mockMvc.perform(request(HttpMethod.GET, "/message/delete/{messageId}", 125))

헐.. 테스트하기 엄청 편해요. Spring Test MVC 프로젝트가 아직 정식 릴리즈는 안됐지만 기선이 형 포스팅에는 공식 프로젝트와 합쳐질 거란 말도 있더라구요. 이상 @MVC 테스트의 끝판왕인 Spring Test MVC의 간단한 소개 였습니다 ㅋ

스프링에서 RESTful 적용기

기본

스프링으로 RESTful 스타일의 웹 개발을 하려다 보니 /project/* 또는 /app/* 같이 어떠한 뎁스를 하나 주지 않으면 static 리소스들의 접근도 디스패처 서블릿이 다 먹어 버리는 문제가 있었습니다. 지저분하다고 생각했지만 그냥 쓰고 있었는데, 토비님의 스프링 3.0.4 를 이용해서 UrlRewriteFilter없이 깔끔한 URL을 만들기 포스트를 보고 적용해 보니 더이상 URL에 뎁스를 하나 더 주지 않아도 돼서 너무 좋았습니다.

그런데 기쁨도 잠시 를 추가하고 나니 웹 애플리케이션에 접근이 전혀 안됐습니다. 매핑 정보가 없다는 에러 로그가 계속 나타나더라구요. 분명히 static 리소스들에는 접근이 가능한데 말이죠. 톰캣을 리스타트 해보니 매핑 정보에 디폴트 서블릿이 /** 빼고는 매핑되는게 전혀 없었습니다. 원인을 찾아보니 가 핸들러를 하나 추가하면서 스프링에서 디폴트로 등록해 주는 핸들러나 어탭터가 등록이 안돼서 그렇더군요.

사용하는 핸들러가 DefaultAnnotationHandlerMapping 밖에 없어서 설정 파일에 슥 추가해 주고, 톰캣을 리스타트 시켰더니 로그에 매핑 정보들이 드디어 나타났습니다.

아니 그런데 또 접근이 안되더군요. 원인이 뭘까 생각해 보다가 로그를 가만히 보니 매핑 순서가 디폴트 서블릿이 /** 먼저 잡히고 그 다음 /index 등등이 잡히는게 원인 같아 보였습니다. 설정 파일에서 DefaultAnnotationHandlerMapping을 위로 옮겨보니 맨 마지막에 디폴트 서블릿 매핑이 잡혀서 잘 구동이 됐습니다. 이얏호!

그러다가 문뜩 생각난

구글링 해보니 박성철님의 스프링 설정 이 구체적으로 뭘까?란 포스트가 눈에 들어왔습니다. 읽어보니 @mvc 관련 핸들러, 어탭터를 등록해 주더라구요. 아까의 삽질을 상기 시키며 위에 추가하고 수동으로 등록한 핸들러, 어탭터를 제거하고 톰캣을 띄워보니 잘 돌아갑니다. 아~ 깔끔하구만 ㅋ