hamcrest 대신 FEST Assert 를 사용해 보자

기본

hamcrest 의 단점 중 하나는 많은 수의 static import 가 필요해 이클립스에서 자동완성 기능을 사용하기 어렵다는 것이죠. Intellij는 메소드 자동완성도 가능하지만 여전히 어떤 이름의 메소드가 있는지 모르거나 해당 매처가 어떤 타입을 지원하는지 모르면 사용하기 어렵긴 매한가지 입니다.

FEST Assert의 경우 빌더 패턴을 통해 이 문제를 매우 운치있게 풀어나갔습니다.

// String specific assertions
assertThat(frodo.getName()).startsWith("Fro").endsWith("do")
                           .isEqualToIgnoringCase("frodo");

// collection specific assertions
assertThat(fellowshipOfTheRing).hasSize(9)
                               .contains(frodo, sam)
                               .excludes(sauron);

hamcrest 나 jUnit assertThat() 과는 다르게 assertThat() 에 검증할 파라미터만 넘기고 매처는 메소드 체이닝으로 뙇! 넘긴 파라미터의 타입에 따라 매처가 뙇! static import 를 딱 하나만 해주면 되는거죠. 괜찮지 않나요? 여기에 2.x 버전 대라 그런지 hamcrest 로 할 수 있는 것 + 타입에 따라 다양하게 제공되는 매처들이 많아 편하네요. 뿐만 아니라 Joda Time, Guava 용 모듈이 따로 있어 최적화된 테스트가 가능합니다.

한 가지 문제점은 FEST Assert 가 기존 jUnit 의 assertThat() 과 같은 이름의 static method 를 사용하기 때문에 같이 사용한다기 보단 대체한다는 생각으로 접근해야 합니다.  그래서 그런지 jUnit 에서 마이그레이션 하는 방법도 만들어 놨더군요.

안 써본 분들은 한 번 써보시길.. 🙂

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();

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로 변경하면 잘 동작합니다!

[번역] WebWork in Action – Appendix WebWork Architecture

기본

WebWork in Action

Appendix: WebWork Architecture

처음 배웠던 챕터1 “웹워크 오버뷰”로 돌아가보자, 웹워크는 두 부분으로 만들어져 있다고 했었다. 웹워크의 근간은 일반적인 커맨드 패턴 프레임워크인 XWork이다. 웹워크는 XWork를 감싼 형태로 MVC 웹 애플리케이션 프레임워크를 구현했다.

프레임워크의 주 컨셉인 액션(Action), 인터셉터(Interceptor), 리절트(Result)는 XWork에 정의돼 있다. 웹워크는 이러한 기본 컨셉을 확장해 웹 애플리케이션 개발을 지원할 수 있도록 한다. 이 부록에선 XWork와 웹워크의 설계에 대해 논의한다.

챕터 1에 XWork와 웹워크를 나눠놨지만 이 부록에서는 XWork와 웹워크의 각 요소에 대해서 깊게 살펴보지 않고 프레임워크의 아키텍처에 대해서 논의할 것이다.

A.1 커맨드 패턴 구현하기

웹워크의 핵심은 커맨드 패턴 구현체란 것이다. 커맨드 패턴은 코드 단위(일반적인 함수나 OOP에서 말하는 메소드)를 커맨드(command) 또는 액션(action)이라고 불리는 클래스에 놓고, 모두 공통된 인터페이스를 구현한다. 그림 A.1에서 보는바와 같이, 액션 인스턴스는 다른 코드에 의해 생성되거나 넘겨져 실행될 수 있는데, 커맨드 인터페이스를 구현했다면 어떤 클래스가 해당 인스턴스를 실행시키는지 알 필요가 없다.

The simplest possible Command pattern implementation

그림 A.1 가장 간단한 커맨드 패턴 구현체

만약 당신이 커맨드를 사용하는 프레임워크를 사용한다면 프레임워크는 액션의 실행을 캡슐화 할 수 있다. 왜냐하면 당신이 프레임워크를 통해 액션을 호출하면, 프레임워크 설정을 통해 해당 호출에 다른 서비스를 추가할 수 있기 때문이다. 예를들어, 커맨드 객체를 저장해 다시 실행하거나 나중에 되돌리기 기능을 구현한다던지, 로그를 위해 저장한다던지, 다른 곳으로 보내 실행되도록 할 수도 있을것이다.

Calling commands through a Command pattern framework allows the client code to be decoupled from the command implementation and lets the framework add other services transparently

그림 A.2 커맨드 패턴 프레임워크를 통해 커맨드를 호출하면, 커맨드 구현체로부터 클라이언트 코드를 분리시켜 프레임워크가 다른 서비스 추가를 쉽게할 수 있습니다.

그림 A.2를 보면 프레임워크를 통해 액션을 실행시키려는 클라이언트는 어떠한 서비스가 호출되는지, 심지어 어떠한 액션 클래스가 실행되는지에 대해서 알아야 할 필요가 없다.

왜냐하면 클라이언트가 직접 클래스를 호출하는 것이 아니라, 프레임워크를 통해 호출이 이루어지기 때문에, 커맨드 클래스의 변경 없이도 프레임워크의 설정을 통해 커맨드의 행동을 바꾸는 것이 가능하기 때문이다. 어떻게 프레임워크 설정을 하는지 그리고 무슨 기능(Service)이 사용가능한지에 대한 구체적인 구현체에 대해서는 챕터 3 “웹워크 세팅하기”에서 논의했었고, 웹워크 액션은 xwork.xml 에서 설정 가능했었다.

A.1.1 커맨드 패턴 기본 기능

웹워크는 다른 커맨드 프레임워크에서는 찾아볼 수 없는 고급 기능 뿐만 아니라 커맨드 패턴 프레임워크의 표준 기능들도 제공한다. 커맨드 패턴 구현체의 표준 기능은 다음을 포함한다:

  • 액션 클래스를 별칭(alias name) 기반으로 실행하게 해서 커맨드 객체 구현체로부터 클라이언트를 분리시킨다. 이것은 xwork.xml 설정 파일에서 할 수 있으며, 챕터3에서 자세히 살펴봤었다.
  • 액션이 실행되기 전, 환경설정 시점(configuration-time)에서 인스턴스에 파라미터 설정이 가능하다. 이 파라미터는 xwork.xml에서 설정 가능하며, 더 자세한 내용은 챕터 3과 챕터 5의 “인터셉터로 기능 추가하기”를 보자.
  • 특정 요청에(request-specific) 필요한 파라미터를 액션이 실행되기 직전에 세팅한다. 이것은 인터셉터를 통해 할 수 있으며 챕터 5에서 논의했었다.
  • 커맨드가 반환하는 리턴코드(return code)와 리절트(Result 인스턴스)간에 매핑을 제공한다. 이것 또한 xwork.xml에서 매핑 설정을 하며, 챕터 3에서 논의했었다.
  • 여러 액션을 조합해 다수의 커맨드를 하나로 묶을 수 있도록 지원한다. 이것은 xwork.xml의 리절트와 인터셉터를 통해 조합할 수 있다. 액션의 구성에 대해선 챕터 7 “리절트 사용하기”에 자세히 나와있다.

A.1.2 커맨드 패턴 고급 기능

웹워크는 다른 커맨드 프레임워크에서는 볼 수 없는 여러 고급 기능들도 제공한다:

  • 액션 인스턴스에 프로퍼티를 세팅할 때, 각각의 클래스와 프로퍼티에 대해 타입 변환을 지원한다. 이 타입 변환은 유연해서 스트럿츠의 String FormBeans나 다른 프레임워크에서 하지 못하는 다양한 타입의 프로퍼티를 가지는 도메인 객체를 사용 가능하게 한다. 타입 변환에 대해선 챕터 12에서 자세히 다루고 있다.
  • 액션 클래스 계층을 기반으로한 메시지 상속 모델로 지역화된 메시지를 지원한다. 국제화 지원에 대해선 챕터 14에서 다루고 있다.
  • 에러 메시지를 클래스 인스턴스와 필드 수준에서 지원한다. 따라서 에러 메시지를 저장했다가 사용자에게 액션이나 각 필드 별로 보여줄 수 있다. 에러 메시지를 다루는 방법은 챕터 4 “웹워크 액션 구현하기”에서 다루었다.
  • 인터셉터 설정으로 액션 인스턴스 실행 전, 후에 기능을 추가할 수 있다. 인터셉터는 xwork.xml에서 설정하며, 챕터 5에서 자세히 논의했다.
  • XML 메타데이터 주도(metadata-driven) 검증 프레임워크는 액션과 도메인 객체의 값을 검증한다. 직접 검증을 정의할 수도 있어, 만들어 둔 코드 외부에서 다양한 사용 시나리오에 따라 다른 검증을 할 수 있다. 검증은 챕터 13에서 다루고 있다.

A.2 액션

웹워크가 동작하는데 있어서 가장 핵심적인 기능은 액션이다. 액션은 섹션 A.1에서 논의했던 커맨드 패턴의 커맨드 객체이며, 웹워크의 구현체다. 액션은 com.opensymphony.xwork.Action 인터페이스를 구현하며, 딱 하나의 메소드를 가진다:

public String execute() throws Exception

이 메소드는 액션 클래스를 실행시키기 위한 기본 엔트리 포인트(default entry point;액션을 실행하기 위해 프레임워크가 호출하는 메소드)이다.

액션 별칭 설정을 하면 execute() 메소드 대신 다른 메소드를 호출할 수도 있으며, 다수의 액션 별칭으로 하나의 액션 클래스에서 다른 메소드를 호출하게 할 수도 있다. 이때, 각 별칭별로 인터셉터나 리절트 등 또한 다르게 설정 가능하다.

com.opensymphony.xwork.ActionSupport 클래는 Action 인터페이스와 여러가지 선택적인 인터페이스를 구현하고 있어, 이를 상속해 자신의 액션 클래스의 베이스 클래스로 사용할 수 있을 것이다. 챕터 4에서, 우리는 액션 클래스를 직접 구현해봤고,  ActionSupport에 의해서 제공되는 것들에 대해서도 살펴봤다.

A.3 인터셉터

웹워크의 인터셉터는 코드를 캡슐화 시켜 액션 실행 전/후에 실행될 수 있게한다. 이것은 부가 기능으로 당신의 액션을 실행할 때 커맨드 패턴 프레임워크에 의해 투명하게(transparently;실제로는 있지만 캡슐화가 돼있어 없는것처럼 보인다는 의미) 제공된다. 인터셉터는 액션 클래스의 외부에 정의되며, 액션에 접근하지는 않고, 액션 실행 환경에서, 부품식으로 조립돼 관심사의 분리(separation of concerns)를 하게된다.

횡단 코드(cross-cutting code)는 액션을 실행할 때 데이터베이스 커넥션이나 트랜잭션 같은 리소스를 설정하거나 액션 실행 후에 뒷처리를 하는 것까지 어떤 것이든 가능하다. 액션 인스턴스에 프로퍼티를 세팅하는 것 같은 주요 기능을 포함한 많은 웹워크 기능들은, 인터셉터로 구현된 것이다. 인터셉터는 당신의 액션에서 많이 사용하게 될 것이다. 하지만 복사-붙이기나 복잡한 클래스 상속구조가 액션에서 자주 보인다고 강제로 적용하는 것 보다는, 액션을 실행할 때 필요한 것을 가져다 쓰는 모듈식으로 접근하자.

이것이 관점 지향 프로그래밍(Aspect-Oriented Programming)같이 들린다고 생각한다면 그것이 맞다. 행위에 대한 아이디어를 AOP에서 가져왔기 때문에 많은 컨셉을 공유한다. 하지만 AOP와는 다르게 어떠한 전처리나 바이트코드 조작은 필요로 하지 않는다. 왜냐면 프레임워크 내부에서 액션 호출을 가로채기 때문에 호출하는 쪽과 호출된 액션을 분리시키기 때문이다. 우리는 챕터 5에서 인터셉터에 대해 자세히 살펴봈으며, 이 부록의 뒤에서 ActionProxy와 ActionInvocation 내부에서 무슨 일이 벌어지는지에 대해 토론할 때, 액션 호출이 발생하는 것에 대해 시퀀스 다이어그램을 검토할 것이다.

A.4 리절트

com.opensymphony.xwork.Result 인터페이스는 액션 실행에 대한 결과물을 나타낸다. 이 리절트는 기본적으로 액션이 실행된 후에 대해 당신이 원하는 어떤 것이라도 될 수 있다. Result 인터페이스는 하나의 메소드 만을 정의한다:

public void execute(ActionInvocation invocation) throws Exception

리절트는 액션 실행으로부터 어떠한 종류의 결과물(웹 페이지 보여주기, 보고서 만들기, 이메일 보내기 등)을 만들어야 할 때 사용될 수 있다. 웹워크는 서블릿 디스패치(렌더링을 위해 JSP에서 사용되던 것), 서블릿 리다이렉트, 벨로시티, 프리마커, JasperReports(PDF, CSV, XML 등을 만들게 해줌), XSLT 렌더링, 그리고 ActionChainResult(현재 액션에서 다른 액션을 연결해 처리하는 것)를 제공한다.

리절트는 xwork.xml 액션에 설정된 결과 코드와 매핑된다. 우리는 이것을 챕터 3에서 자세히 논의했었는데, 지금 당신이 알아야 하는 것은 웹워크는 액션의 execute() 메소드의 결과 코드에 따라 다른 리절트를 실행한다는 것이다. 각각의 결과 코드는 자신과 매핑된 리절트를 실행시킬 것이다. 리절트에 대해선 챕터 7에서 자세히 논의하였다.

A.5 밸류스택

밸류 스택은 XWork와 웹워크 같은 동적인 컨텍스트 주도 환경의 중심이다. 이 객체들의 스택은 표현식(프로퍼티 값을 동적으로 찾는데 사용되며, 평가될 수 있다)에 대해 해당 프로퍼티의 이름을 가지는 객체가 있는지 첫 번째 객체(스택의 꼭대기부터 바닥까지)를 검색한다. 웹워크는 액션이 스택에 푸싱하는 것을 실행하는 동안 밸류 스택을 구성한다.

많은 웹워크 JSP 태그들과 Velocity 매크로는 밸류 스택에 접근해 객체를 집어 넣거나 꺼내올 수 있다. 밸류 스택은 객체 그래프 네비게이션 언어(OGNL)로 만들어졌고 OGNL의 단일 객체 루트 컨셉(single object root concept)을 확장해 다수의 객체 스택(multiple-object stack)을 지원한다. OGNL과 OGNL 그리고 밸류 스택 사이의 인터랙션에 대해서 다음 섹션에서 더 논의할 것이다.

A.5.1 OGNL

OGNL(http://www.opensymphony.com/ognl)은 평가할 수 있는 표현식을 통해 자바빈의 프로퍼티를 세팅하거나 가져올 수 있도록 한다. 또한 OGNL은 static 또는 인스턴스 메소드 실행, 컬렉션 내부 접근 그리고 표현식 재사용을 위한 람다 표현식 같은 고급 표현식 기능을 제공한다.

OGNL은 또한 XWork와 웹워크가 확장한 풍부한 타입 변환 모델을 제공한다. 타입 변환은 챕터 12에서 다루었다.

OGNL 언어의 기초는 단순함이며 90%는 흔히 쓰이는 용법이다. 빈 프로퍼티의 접근하기 위한 기초는 프로퍼티 이름을 사용하는 것이다. 예로, count 라는 표현식은 count 프로퍼티의 getCount() 같은 게터를 찾는 것으로 평가된다. 비슷하게, address.street라는 표현식은 getAddress().getStreet()를 호출하며 프로퍼티를 가져오거나 getAddress().setStreet()를 호출해 세팅할 수도 있다. OGNL의 다른 기능도 비슷하다. 한 예로, hashCode()라는 표현식은 현재 OGNL 컨텍스트에 있는 객체의 hash code 메소드를 호출할 것이다.

우리는 OGNL과 웹워크가 확장한 OGNL 문법에 대해 챕터8에서 자세히 논의했다. 하지만 지금은 예제를 설명하기 위한 용도로 사용됐고 그렇지 않을 경우 커멘트를 표시했다.

A.6 ActionProxy/ActionInvocation

ActionProxy는 액션을 실행하기 위한 클라이언트 코드를 관리하기 위해 제공된다. ActionProxy가 제공되는 이유는 당신이 액션 인스턴스를 스스로 실행하는 대신 프레임워크를 통해 실행을 하면, 인터셉터, 리절트 등과 같은 기타 기능을 캡슐화 할 수 있기 때문이다. 그림 A.3은 ActionProxy와 ActionInvocation, 액션 인스턴스, 인터셉터, 리절트 그리고 ActionContext(다음 섹션에서 다룰것이다) 사이의 관계에 대해 보여주고 있다.

Class diagram of the classes involved in executing an ActionProxy

그림 A.3 ActionProxy 실행과 관련된 클래스들의 클래스 다이어그램

ActionProxy는 현재 액션의 실행 상태를 나타내는 ActionInvocation을 가지고 있다. ActionInvocation은 액션 인스턴스, (순서 상) 적용되어질 인터셉터, 리절트 맵(리턴 코드와 리절트 인스턴스) 그리고 ActionContext(다음 세션에서 더 살펴볼 것이다)를 가진다.

ActionProxy는 ActionProxyFactory static 인스턴스를 사용해 디스패처(웹워크의 ServletDispatcher 또는 FilterDispatcher)에 의해 다음과 같이 생성된다:

ActionProxy proxy = ActionProxyFactory.getFactory().createActionProxy(namespace, actionName, context);

ActionProxy가 context(요청 파라미터, 애플리케이션 맵, 세션 맵, 로케일 그리고 ServletRequest와 ServletResponse를 포함)를 가지고 생성된 뒤, Dispatcher는 ActionProxy를 execute() 메소드를 호출해 실행시킨다. 그림 A.4는 DefaultActionProxy(ActionProxy 인터페이스의 기본 구현체)의 execute() 메소드가 호출되는 과정을 보여주고 있다.

Sequence diagram of the internals of DefaultActionProxy.execute()

그림 A.4 DefaultActionProxy.execute() 시퀀스 다이어그램

ActionProxy는 ActionInvocation(ActionContext에 대해 논의할 때 더 살펴볼 것이다)을 위해 실행 컨텍스트를 세팅하고 ActionInvocation의 invoke() 메소드를 호출한다. ActionInvocation의 invoke() 메소드는 아직 실행되지 않았고, 다음에 실행되어야 하는 인터셉터를 찾아서 intercept()를 호출한다. 인터셉터는 ActionInvocation.invoke()를 다시 호출하기 전에 ActionInvocation을 사용해서 필요한 전처리를 할 수 있다. 이런 재호출(reentrant) 행동은 ActionInvocation을 다소 혼란스럽게 만든다. ActionInvocation을 유지하기 위해선 어떤 인터셉터가 실행됐는지를 알아야하며, 아직 실행이 안된 인터셉터가 남아있다면, intercept()를 호출해서 다음 인터셉터를 실행한다. 만약 더이상 호출할 인터셉터가 남아있지 않다면, 액션 인스턴스가 실행된다. 액션에서 반환된 리턴 코드는 리절트를 찾는데 쓰이며, 찾아낸 리절트를 실행한다. 마침내, invoke() 메소드가 리턴되고, 스택에 있는 마지막 인터셉터에게 제어권이 넘어간다. 인터셉터는 필요한 후처리를 하고 invoke() 메소드로부터 리턴되면 스택에 있는 이전 인터셉터는 필요한 후처리를 한다. 이 작업은 모든 인터셉터가 리턴될 때까지 반복된다. 끝으로, ActionProxy가 몇 가지 정리 작업을 하고 리턴된다.

ActionInvocation이 인터셉터 안으로 넘겨지면서 발생하는 결과 하나는 ActionInvocation의 인터셉터 처리 여부가 다른 인터셉터에 의존된다는 것이고 따라서 다른 인터셉터 처리를 넘겨 액션을 빠르게 실행하는 것도 가능하며, 인터셉터가 액션을 계속 수행할지 선택해 바로 리절트 코드를 리턴할 수도 있다. 이것은 예를 들어, 특정 사용자가 적절한 권한이 없다면 SecurityInterceptor에서 액션의 실행을 막을 수 있도록 허용한다.

ActionProxyFactory/ActionProxy/ActionInvocation 아키텍처 또한 액션 실행을 위한 다른 전략을 허용한다. 예로, 이 아키텍처를 사용해 비동기 액션 처리가 가능하고 필요한 파라미터를 가지고 서버쪽에서 액션을 실행한 다음 사용자를 위해 렌더링 하는 리치 클라이언트를 허용하는 자바 메시지 서비스(JMS) 디스패처 를 만들 수도 있다. 이미 MessageWork 프로젝트가 이를 구현해 최근에 http://dev.java.net에 추가됐다.

A.7 ActionContext

ActionContext는 ThreadLocal Map으로 애플리케이션과 세션 맵, ActionInvocation, 요청 파라미터, 로케일 등의 미리 정의된 값을 가져오거나 저장하는 도우미 역할을 한다. ActionContext는 특정한 ActionInvocation/ActionProxy의 쌍과 연관돼 있으며, 이 쌍은 ActionInvocation이 실행되는 동안의 쓰레드와 연관이 있다.

이전 섹션에서, 우리는 ActionProxy가 ActionInvocation이 호출되기 전 어떠한 상태를 설정하고 호출 후에는 정리한다고 했었다. 이 설정은 ActionContext와 현재 쓰레드 즉, ActionInvocation이 실행되는 동안과 관련이 있다. 자, ActionInvocation이 호출되기 전/후에 ActionProxy가 무엇을 하는지 그림 A.5를 살펴보자.

Sequence diagram of the ActionContext setup and teardown done by DefaultActionProxy before and after invoking the ActionInvocation

그림 A.5 ActionInvocation 호출 전/후 DefaultActionProxy에 의한 ActionContext의 설정과 해체를 나타낸 시퀀스 다이어그램

그림 A.5는 ActionInvocation의 invoke() 메소드 호출 전/후 DefaultActionProxy에 의해 만들어지는 호출의 결과를 보여준다. ActionContext는 ActionInvocation과 ActionProxy가 생성되는 동안 만들어지며 ActionProxy가 실행되기 전까지 연관된 쓰레드에 의해 저장된다. 인터셉터, 액션 그리고 리절트가 실행되는 동안 당신은 ActionContext.getContext() 메소드로 ThreadLocal에서 ActionContext를 가져올 수 있고 현재 실행 컨텍스트를 얻게된다.

ActionContext에는 실행 중인 환경에 대한 중요한 정보들을 담고있다. 이러한 컨텍스트 값들은 ActionProxy가 만들어 지기 전 Dispatcher에 의해 세팅되고 extraContext Map에서 ActionProxyFactory의 createActionProxy()로 넘겨진다. ActionContext에서 발견할 수 있는 프로퍼티들은 다음과 같다:

  • ActionInvocation – getActionInvocation()은 이 ActionContext와 연관된 현재 ActionInvocation에 접근할 수 있게한다.
  • Application – getApplication()은 ServletContext를 사용한 애플레케이션 스코프를 감싸고 있는 java.util.Map 구현체를 넘겨준다. 이 맵을 사용하면 애플리케이션 스코프에서 읽기와 쓰기를 할 수 있다.
  • ConversionErrors – getConversionErrors()는 타입-변환 에러가 난 모든 필드의 이름과 값의 필드 쌍을 가지는 Map을 반환한다. 우리는 이것을 챕터 12에서 자세히 논의했었다.
  • Locale – getLocale은 현재 요청의 로켈을 가져온다. 이 로케일은 지역화된 텍스트 메시지를 찾는데 사용되며, 챕터 14에서 자세히 볼 수 있다.
  • Name – getName()은 현재 액션의 이름을 가져온다. xwork.xml에 정의된 액션의 이름에 대응하며 실행될 액션의 식별자이자 요청에 포함된 정보의 한 부분이다.
  • Parameters – getParameters()는 요청에 포함된 파리미터들의 Map을 반환한다. 이 Map은 String으로 된 파라미터 이름과 String[] 으로 된 값을 가지는데, 그 이유는 HttpServletRequest의 파라미터들은 하나의 파라미터 이름에 여러 값을 가질 수 있기 때문이다.
  • Session – getSession()은 HttpSession 속성을 감싼 java.util.Map 구현체를 반환한다. 이 Map을 사용해 세션에 값을 저장하거나 가져올 수 있다.
  • ValueStack – getValueStack()은 현재 요청의 밸류 스택을 반환한다.

미리 정의된 값에 덧붙여, ActionContext의 put() 그리고 get() 메소드를 사용하면 당신이 사용하고 싶은 값을 메소드의 파라미터로 넘겨줄 필요 없이 ActionContext에 저장했다가 나중에 사용할 수 있다. 예를 들어, 인터셉터가 처리되는 동안 데이터베이스 커넥션과 연관된 작업을 하고 싶을 때 당신은 ActionContext ThreadLocal을 사용할 수 있을 것이다. 이런식으로, 모든 메소드 호출을 할 때 파라미터를 넘겨줄 필요 없이, 어느 코드에서나 데이터베이스 커넥션을 받아올 수 있다. 또한 쓰레드-세이프하기 위해 필요할지 모르는 정적 저장소 관련 문제가 없다. 왜냐하면 ActionContext는 하나의 쓰레드 하고만 연관되기 때문이다.

A.7.1 ThreadLocal 저장소

ActionContext는 ThreadLocal을 구현하고 있다. 따라서 메소드를 호출할 때마다 파라미터를 넘겨줄 필요 없이 항상 사용가능하다. ThreadLocal(java.lang.ThreadLocal)은 Java 1.2에서 추가됐다. ThreadLocal은 각각의 쓰레드에 저장공간을 제공한다. ThreadLocal의 값을 가져오는 게터는 static 메소드가 가능하기 때문에, ThreadLocal의 값을 어디에서나 가져올 수 있으며 메소드 파라미터로 무언가를 넘겨줄 필요가 없다.
Listing A.1은 ActionContext에 있는 ThreadLocal 관련 코드를 보여준다.

ActionContext's ThreadLocal specifit code

Listing A.1 ActionContext에 있는 ThreadLocal 관련 코드

getContext()와 setContext() 메소드를 보면, ThreadLocal의 set(), get() 메소드를 사용한 것을 볼 수 있다. ThreadLocal의 set() 메소드는 ThreadLocal에서 나중에 가져올 수 있게 객체를 저장한다. ThreadLocal get() 메소드는 현재 쓰레드의 ThreadLocal에서 저장된 객체를 가져온다.

한 가지 다른 구현체가 있는데 바로 내부 클래스 ActionContextThreadLocal 이다. 이것은 ThreadLocal의 서브 클래스로 initialValue()를 오버라이드해 디폴트 값을 세팅해 ThreadLocal의 get() 메소드가 처음 호출 될 때 null을 반환하지 않게 한다.

ThreadLocal 사용에 대해 하나 첨부한다: ThreadLocal은 단위 테스트 하기 어렵기로 소문이 나있다. 단위 테스트 전/후에, 셋업과 티어다운에 주의하고, 테스트가 다른 테스트에 간섭하지 않는지 확인하라. 만약 XWork와 웹워크의 단위 테스트를 본다면, 뜻밖의 상호작용으로 인한 어떠한 부작용도 없게끔 노력이 들어간 것을 볼 수 있을 것이다.

A.8 디스패처

Dispatcher는 웹워크 요청의 주 엔트리 포인트이다. 서블릿은 일반적으로 확장자(*.action이나 *.jspa가 웹워크 커뮤니티에서 널리 사용된다)와 매핑된다. Dispatcher는 요청 경로로 어떤 액션이 실행될지 결정하는데 이와 관련해서는 섹션 3.2.2 네임스페이스에서 논의했었다. Dispatcher는 서블릿의 HTTP 요청/응답과 웹워크 그리고 일반적인 커맨드 패턴인 XWork의 액션/리절트 사이의 어댑터 역할을 한다.

Dispatcher는 액션 실행을 위한 컨텍스트(애플리케이션, 세션 그리고 파라미터 스코프의 속성을 감싼 java.util.Map 구현체)를 만든다. 그다음 ActionProxyFactory를 사용해 ActionProxy를 만들거나 요청에 맞는 액션이 없다면 에러를 반환한다. 끝으로, Dispatcher는 ActionProxy를 실행하는데 그 뜻은 앞에서 봤듯이, 관련된 인터셉터, 액션 그리고 액션이 반환하는 리턴 코드와 매핑되는 리절트를 모두 포함한다는 의미다. 따라서 이 실행 결과는 웹 페이지를 보여주는 것일 수도 있고 PDF 문서를 만드는 것일 수도 있다. 또한 Dispatcher는 멀티파트 파일업로드 요청(챕터 4에서 논의)이나 에러 코드를 다루며, 일반적으로 웹워크와 함께 밀접하게 돌아간다.

A.9 요약

핵심은, 웹워크는 커맨드 패턴 구현체(XWork)를 MVC 웹 애플리케이션 프레임워크(웹워크)로 감싸고 있다는 것이다. 프레임워크는 커맨드 실행을 다루며, 호출되는 코드를 구상 액션 클래스로부터 분리시켜 액션 실행 부근에 기능 추가를 할 수 있게 한다. 이러한 기능은 웹워크에서 인터셉터 형태로 제공되며 많은 프레임워크의 핵심 기능을 다룰 수 있다.

이러한 기능을 추가하기 위해, 프레임워크는 ActionProxy/ActionInvocation 쌍에 액션 실행을 캡슐화한다. ActionProxy/ActionInvocation은 액션을 감싸며, 인터셉터와 리절트 그리고 실행되는 동안 각각의 호출을 관리한다. 인터셉터는 순서대로 호출되며 ActionInvocation(다른 인터셉터 포함)의 나머지 부분이 실행되기 전 후에 필요한 것을 무엇이든 할 수 있다. 액션이 실행되는 동안, 액션과 인터셉터 그리고 리절트는 ActionProxy에 의해 관리되고 ActionProxy.execute() 메소드가 실행중인 동안만 사용할 수 있는 ActionContext라고 불리는 ThreadLocal 저장소에 접근할 수 있다.

ActionProxy 인스턴스는 Dispatcher가 static ActionProxyFactory 인스턴스를 사용해 생성한다. Dispatcher는 HTTP 요청을 액션 네임스페이스와 액션 별칭으로 매핑하며, ActionProxyFactory가 ActionProxy를 생성하기 위해  사용된다. 그다음 Dispatcher는 ActionProxy를 실행시키고, 인터셉터 실행, 액션 그리고 리절트(웹 페이지나 PDF 같은)를 처리한다.