Spring Boot test annotation

오늘 이시간에는 Spring boot의 Test annotation 들을 살펴볼 예정이다. Spring boot 에서는 Test도 쉽게 할 수 있도록 많이 지원해주고 있다.
예전에 Spring Boot 1.4 Test에 보면 필자가 남긴 Spring boot 1.4에 추가된 내용이 있는데 조금 부실한 느낌이 있다. 현재 버전 (1.5.x) 버전으로 좀 더 추가된 내용을 포함해서 다시 살펴보도록 하자.

DataJpaTest

저번 포스팅에서도 언급을 했지만 Jpa를 Test 할수 있게 도와주는 어노테이션이다. 실제로 DataJpaTest 어노테이션을 사용할 경우에는 기본적으로 인메모리 데이터베이스가 존재해야 한다.

@RunWith(SpringRunner.class)
@DataJpaTest
public class SpringDataJpaTest {
 // ..
}

위의 코드는 기본적으로 테스틀 할 수 있는 클래스이다. @DataJpaTest 어노테이션을 사용할 경우에는 Jpa에 필요한 클래스들만 로딩이 되어 좀 더 빠르게 테스트를 할 수 있는 장점이 있다.

@RunWith(SpringRunner.class)
@DataJpaTest
public class SpringDataJpaTest {

  @Autowired
  private TestEntityManager testEntityManager;

  @Autowired
  private ReservationRepository reservationRepository;


  @Test
  public void findByEntityManagerReservationNameTest() {
    Reservation reservation = testEntityManager.persist(new Reservation("wonwoo"));
    Reservation rn = testEntityManager.find(Reservation.class, reservation.getId());
    assertThat(reservation.getReservationName()).isEqualTo(rn.getReservationName());
  }
}

@DataJpaTest를 사용할 경우에 TestEntityManager 클래스가 자동으로 빈으로 등록 된다. 그래서 우리는 위와 같이 TestEntityManager를 주입받아서 사용하면 된다. TestEntityManager는 Spring boot에서 만든 클래스이며 Jpa의 EntityManager와는 다르게 메서드가 많이 없지만 테스트하기에는 불편함없을 정도 있으니 문제는 없을 듯하다.

그러나 만약 실 데이터베이스에 테스트를 하고 싶다면 어떻게 할까? 아주 간단한게 어노테이션의 속성만 주면 메모리 데이터베이스가 아닌 데이터베이스에 테스트를 할 수 있다.

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class SpringDataJpaRealTest {

  @Autowired
  private TestEntityManager testEntityManager;

  @Autowired
  private ReservationRepository reservationRepository;

  //...
}

@AutoConfigureTestDatabase(replace = Replace.NONE) 라는 어노테이션의 속성을 주면 메모리 데이터 베이스가 아닌 실 데이터베이스에 테스트도 가능하다.

DataMongoTest

@DataJpaTest 와 동일하게 MongoDb도 쉽게 테스트할 수 있게 도와주는 어노테이션이다. 이 어노테이션은 Spring 1.5에 추가된 어노테이션이다. 참고로 2.0에는 좀 더 많은 nosql 을 테스트 할 수 있게 도와주는 어노테이션이 많이 추가 되었다. @DataJpaTest 어노테이션과 많이 비슷하므로 바로 코드로 보자.

@RunWith(SpringRunner.class)
@DataMongoTest
public class SpringDataMongoTest {

  @Autowired
  private AccountRepository accountRepository;

  @Autowired
  private MongoTemplate mongoTemplate;

  @Test
  public void findByFirstNameTest() {
    Account account = accountRepository.save(new Account("wonwoo", "lee"));
    Account at = accountRepository.findByFirstName("wonwoo");
    assertThat(account.getFirstName()).isEqualTo(at.getFirstName());
    assertThat(account.getLastName()).isEqualTo(at.getLastName());
  }
}

MongoRepository를 상속받은 인터페이스는 물론이고 MongoTemplate 도 자동으로 빈으로 등록된다. 그래서 아무 설정 없이도 위와 같이 코드를 작성할 수 있다.

JdbcTest

@JdbcTest 이 어노테이션도 @DataMongoTest 어노테이션과 마찬가지로 Spring boot 1.5에 나온 어노테이션이다. 이 어노테이션은 @DataJpaTest 어노테이션의 JPA와 관련된 설정을 제외한 설정으로 동작한다. Hibernate 설정과 JpaRepositories 의 설정을 제외하고 나머지는 동일하다. 사용법도 비슷하니 코드로 보자.

@RunWith(SpringRunner.class)
@JdbcTest
public class SpringJdbcTest {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  @Test
  public void findByReservationTest() {
    jdbcTemplate.update("insert into reservation (reservation_name) values(?)", "wonwoo");

    Reservation reservation = jdbcTemplate.queryForObject("select reservation_name from reservation where reservation_name = ?",
        (resultSet, i) -> new Reservation(resultSet.getString("reservation_name")), "wonwoo");
    assertThat(reservation.getReservationName()).isEqualTo("wonwoo");
  }

}

JdbcTemplate이 자동으로 빈으로 등록되어 우리는 JdbcTemplate를 주입만 받아서 사용하면 된다. 참 쉽게 잘 만들었다.

AutoConfigureRestDocs

테스트를 통해서 restDoc을 뽑을 수 있는 그런 어노테이션이다. 원하는 포맷에 맞게 markdown 혹은 asciidoctor 로 출력 할 수 도 있다.

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
@AutoConfigureRestDocs("target/generated-snippets")
public class UserDocumentationTests {

  @Autowired
  private MockMvc mvc;

  @MockBean
  private UserService userService;

  @Test
  public void getUserDocuments() throws Exception {
    given(userService.findByUser(any()))
        .willReturn(new User("wonwoo", "wonwoo@test.com"));
    this.mvc.perform(get("/users/{name}", "wonwoo").accept(MediaType.APPLICATION_JSON))
        .andDo(print())
        .andDo(document("{method-name}"))
        .andExpect(status().isOk());
  }
}

위와 같이 작성할 경우에 target/generated-snippets 경로 아래에 메서드명 기준으로 doc들이 출력되어 있을 것이다. 기본적으로 아무 설정 하지 않았을 경우에는 asciidoctor 파일들이 출력 된다.

httpie-request.adoc
curl-request.adoc
http-request.adoc
http-response.adoc

기본적으로 4개의 파일들이 출력 된다. 파일들의 내용은 한번씩 해봐서 보길..
만약 markdown으로 파일을 출력 하고 싶다면 아래와 같이 설정을 조금 해줘야 한다.

@TestConfiguration
static class ResultHandlerConfiguration implements RestDocsMockMvcConfigurationCustomizer {

  @Override
  public void customize(MockMvcRestDocumentationConfigurer configurer) {
    configurer.snippets().withTemplateFormat(TemplateFormats.markdown());
  }
}

위와 같이 설정할 경우에는 아까와 달리 adoc 확장자가 아니라 md 확장자로 변경되어 파일로 출력된다. 자세한 내용은 문서를 통해..

JsonTest

JsonTest 어노테이션은 json을 쉽게 serialize, deserialize를 테스트할 수 있도록 도와주는 어노테이션이다. 기본적으로는 BasicJsonTester가 빈으로 등록되며 클래스 패스에 jackson 혹은 gson이 있다면 자동으로 클래스들이 등록되어 사용할 수 있다. BasicJsonTesterserialize 밖에 테스트를 할 수 없는 것 같다. 약간 부족한 느낌이 들어 아마도 잘 사용하지 않을 듯하다.

@RunWith(SpringRunner.class)
@JsonTest
public class UserBasicJsonTests {

  @Autowired
  private BasicJsonTester json;

  @Test
  public void userDeserializeTest() throws Exception {
    assertThat(this.json.from("user.json"))
        .extractingJsonPathStringValue("@.name").isEqualTo("kevin");
  }
}

기본적으로는 이렇게 BasicJsonTester 클래스를 주입받아서 사용할 수 있다. 아무튼 별로다..

@RunWith(SpringRunner.class)
@JsonTest
public class UserJsonTests {

  @Autowired
  private JacksonTester<User> json;

  @Test
  public void userJsonTest() throws Exception {
    User user = new User("kevin", "kevin@test.com");
    assertThat(this.json.write(user)).isEqualToJson("user.json");
  }

  @Test
  public void userSerializeTest() throws Exception {
    User user = new User("kevin", "kevin@test.com");
    assertThat(this.json.write(user)).hasJsonPathStringValue("@.name");
    assertThat(this.json.write(user)).hasJsonPathStringValue("@.email");
    assertThat(this.json.write(user)).extractingJsonPathStringValue("@.name")
        .isEqualTo("kevin");
    assertThat(this.json.write(user)).extractingJsonPathStringValue("@.email")
        .isEqualTo("kevin@test.com");
  }

  @Test
  public void userDeserializeTest() throws Exception {
    String json = "{\"name\": \"kevin\", \"email\" : \"kevin@test.com\"}";
    assertThat(this.json.parse(json))
        .isEqualTo(new User("kevin", "kevin@test.com"));
    assertThat(this.json.parseObject(json).getName()).isEqualTo("kevin");
    assertThat(this.json.parseObject(json).getEmail()).isEqualTo("kevin@test.com");
  }
}

위의 코드는 JacksonTester 을 사용한 테스트 클래스이다. 아주 간편하게 테스트를 할 수 있다. jackson과 gson의 경우에는 serialize, deserialize 모두 테스트를 할 수 있기 때문에 유용한 듯 싶다. 만약 gson 으로 테스트를 하고 싶다면 아래와 같이 JacksonTester 를 GsonTester 변경해주기만 하면 된다.

@RunWith(SpringRunner.class)
@JsonTest
public class UserGsonTests {

  @Autowired
  private GsonTester<User> json;

  //...
}

WebMvcTest

WebMvcTest 어노테이션은 명 그대로 webmvc를 테스트할 수 있는 어노테이션이다. 이 어노테이션은 웹 관련 설정만 Spring이 로딩 한다. 예를들어 @ControllerAdvice, @JsonComponent, WebMvcConfigurer, Filter 등 웹과 관련있는 것들을 설정으로 로딩하므로 만약 로직상에서 다른 설정이 있다하면 제대로 동작하지 않을 수도 있으니 참고하면 되겠다.

사용법도 간단하니 살펴보자.

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTests {

  @Autowired
  private MockMvc mvc;

  @MockBean
  private UserService userService;

  @Test
  public void findBynameTest() throws Exception {
    given(userService.findByUser(any()))
        .willReturn(new User("wonwoo", "wonwoo@test.com"));
    this.mvc.perform(get("/users/{name}", "wonwoo").accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.name", is("wonwoo")))
        .andExpect(jsonPath("$.email", is("wonwoo@test.com")));
  }

  //...
}

@WebMvcTest 어노테이션에 해당하는 Controller 클래스를 넣어주면 된다. 그리고 나서 MockMvc 주입받아 테스트를 진행하면 된다. MockMvc 또한 @WebMvcTest 어노테이션에서 자동으로 설정해주니 따로 설정할 필요가 없다. 저번에도 언급한 내용이라 짧게 하겠다.

RestClientTest

RestClientTest 어노테이션은 외부의 어떤 Rest API를 사용할 때 mock 으로 대체할 수 있는 아주 유용한 테스트 어노테이션이다. 이 또한 사용법은 간단하다. 만약 외부에 어떤 리소스를 가져와야 한다면 아래와 같이 작성할 것이다.

@Component
public class UserService {

  private final RestTemplate restTemplate;

  public UserService (RestTemplateBuilder builder) {
    this.restTemplate = builder.build();
  }

  public User findByUser(String name) {
    return this.restTemplate.getForObject("/users/{name}",
        User.class, name);
  }
}

위의 RestTemplateBuilder 클래스는 Spring boot 1.4부터 추가된 클래스이다. 클래스패스에 라이브러리에 따라 자동으로 ClientHttpRequestFactory 생성해주고 메시지 컨버터도 알맞게 추가해준다.

@RunWith(SpringRunner.class)
@RestClientTest(UserService.class)
public class UserServiceTests {

  @Autowired
  private MockRestServiceServer server;

  @Autowired
  private UserService userService;

  @Test
  public void mockRestServiceTest() throws Exception{
    this.server.expect(
        requestTo("/users/wonwoo"))
        .andRespond(withSuccess(
            new ClassPathResource("user.json", getClass()),
            MediaType.APPLICATION_JSON));
    User user = this.userService.findByUser("wonwoo");
    assertThat(user.getName()).isEqualTo("wonwoo");
    assertThat(user.getEmail()).isEqualTo("wonwoo@test.com");
    this.server.verify();
  }
}

위와 같이 @RestClientTest 어노테이션에 해당하는 클래스를 작성후 테스트를 진행하면 된다. 실제로 API는 호출하지 않고 작성한 user.json 이라는 파일 형태의 json 형식으로 리턴해준다. 단위 테스트를 할 경우에 아주 유용한 어노테이션이다.

이렇게 오늘은 spring boot의 test annotation 에 대해서 살펴봤다. 한번씩 해보면 딱히 어려운 부분은 없는 것 같다. 하지만 익숙해져야 한다.
Spring boot에서는 이렇게 아주 유용한 Test annotation을 지원하니 한번씩 살펴보면 될 듯 싶다. 공식문서에도 잘 나와 있으니 참고하면 되겠다.

위의 코드는 github에 올라가 있으니 한번씩 돌려보도록 하자.

그럼 오늘은 이만!

Spring boot 2.0 Configuration Binding

오늘 알아볼 내용은 아직 릴리즈는 되지 않았지만 조만간 릴리즈될 Spring boot 2.0의 대해서 알아볼 예정이다. 아직 릴리즈전이라 바뀔 가능성이 있어 깊게는 살펴보지 않을 것이다.
또한 혹시나 기존(이전버전)과 동일한 기능을 마치 신기능처럼 이야기하거나 틀린이야기를 할 경우에는 피드백을 주길 바란다.

필자가 Spring boot를 좋아하는 이유중 하나도 Configuration Binding을 아주 손쉽게 해주기 때문이다. 하지만 기존의 버전과 2.0의 버전이 조금 달라졌다. 오늘은 그 내용을 살펴볼 예정이다.

ConfigurationProperties prefix

ConfigurationProperties 어노테이션은 아주 손쉽게 properties를 바인딩 해준다. 기존에는 underscore, camelcase를 prefix에서 지원했지만 이제는 그렇지 못하다. spring boot2.0 부터는 소문자kebab-case만 지원한다.

@ConfigurationProperties("foo_bar")
public class FooProperties {

  private String firstName;

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }
}

만약 위와 같이 사용할 경우 아주 이쁜 에러를 내뱉는다.

***************************
APPLICATION FAILED TO START
***************************

Description:

Configuration property name 'foo_bar' is not valid:

    Invalid characters: '_'
    Bean: commandLineRunner
    Reason: Canonical names should be kebab-case ('-' separated), lowercase alpha-numeric characters and must start with a letter

Action:

Modify 'foo_bar' so that it conforms to the canonical names requirements.

camelcase 역시 마찬가지로 위와 같은 에러가 발생한다. 에러를 발생 시키지 않으려면 다음과 같이 작성해야 한다.

@ConfigurationProperties("foo-bar")
public class FooProperties {
  // ..
}

혹은

@ConfigurationProperties("foobar")
public class FooProperties {
  // ..
}

위와 같이 kebab-case나 혹은 소문자로 이어진 문장만 가능하다. 하지만 속성은 기존과 동일하게 모두다 가능하다.

foo-bar.firstname=wonwoo
foo-bar.first-name=wonwoo
foo-bar.first_name=wonwoo
foo-bar.firstName=wonwoo

Binder

RelaxedPropertyResolver 클래스는 환경정보를 가져오는데 매우 좋은 클래스였다. 하지만 RelaxedPropertyResolver 클래스는 더이상 사용 되지 않는다. 아니 삭제되었다. Deprecated 된것도 아니고 그냥 삭제 해버렸다. 그 대체할 수 있는 클래스는 Binder 클래스이다. 실제로 바인딩 하는 패키지도 거의 대부분 변경되었다.
기존에는 다음과 같이 RelaxedPropertyResolver 사용해서 손쉽게 properties 정보를 가져올 수 있었다.

RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(
        environment, "foo-bar.");
String firstName = resolver.getProperty("firstName");
// ...

하지만 이제는 좀더 타입세이프하게 FooProperties를 바로 가져올 수 있다.

FooProperties fooProperties = Binder.get(environment)
        .bind("foo-bar", Bindable.of(FooProperties.class)).get();

List

기존의 버전에서 List를 사용할 때 다음과 같이 작성해도 문제는 없었다.

@ConfigurationProperties("foo-bar")
public class FooProperties {

  private List<String> url;

  public void setUrl(List<String> url) {
    this.url = url;
  }

  public List<String> getUrl() {
    return url;
  }
}

위와 같이 List를 사용할 때에는 배열의 인덱스와 상관없이 작성하는 것이 가능했다.

foo-test.url[0]=http://localhost:8080
foo-test.url[2]=http://localhost:8081

하지만 2.0 부터는 엔덱스는 순서대로 작성해야 한다. 만약 그렇지 않을 경우 예쁜 에러는 만날 것이다.

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target [Bindable@11a7ba62 type = java.util.List<java.lang.String>, value = 'provided', annotations = array<Annotation>[[empty]]] failed:

    Property: foo-bar.url[2]
    Value: http://localhost:8081
    Origin: class path resource [application.properties]:7:16
    Reason: The elements [foo-bar.url[2]] were left unbound.

Action:

에러를 발생시키지 않으려면 다음과 같이 작성해야 된다.

foo-bar.url[0]=http://localhost:8080
foo-bar.url[1]=http://localhost:8081

spring-boot-properties-migrator

몇일전에 spring-boot-properties-migrator 라는 모듈이 추가 되었다. 이 모듈은 1.x에서 2.0으로 마이그레이션을 할 때 사용하는 모듈로 보인다. 현재 마일스톤7 버전에는 존재하지 않고 스냅샷에만 존재한다.

이 모듈은 마이그레이션할 때 유용한 모듈이다. 변경된 프로퍼티 혹은 삭제된 프로퍼티들을 레포트 해주고 만약 변경된 프로퍼티일 경우에는 실제 런타임시 변경된 프로퍼티의 정보를 변경해준다.

일단 말로는 힘드니 한번 살펴보자.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-properties-migrator</artifactId>
</dependency>

일단 다음과 같이 디펜더시를 받자. 그리고 나서 2.0에서 변경된 프로퍼티를 작성해보자.

server.context-path=/test

실제 이 프로퍼티는 server.servlet.context-path로 변경 되었다. 위와 같이 작성한 뒤에 어플리케이션을 동작시키면 다음과 같은 로그메세지를 리포트 해준다.

2018-01-28 19:50:28.585  WARN 1401 --- [           main] o.s.b.c.p.m.PropertiesMigrationListener  : 
The use of configuration keys that have been renamed was found in the environment:

Property source 'applicationConfig: [classpath:/application.properties]':
    Key: server.context-path
        Line: 9
        Replacement: server.servlet.context-path


Each configuration key has been temporarily mapped to its replacement for your convenience. To silence this warning, please update your configuration to use the new keys.

프로퍼티의 파일명과 그에 대한 라인수 까지 리포트를 해주고 변경된 프로퍼티명도 함께 리포트 해준다. 아주 우용한 모듈이다. 만약 삭제된 프로퍼티라면 log level이 ERROR로 출력 된다.

2018-01-28 19:52:20.571 ERROR 1404 --- [           main] o.s.b.c.p.m.PropertiesMigrationListener  : 
The use of configuration keys that are no longer supported was found in the environment:
...
...

만약 위 모듈과 함께 작성한다면 환경정보에 새로운 프로퍼티명으로 환경정보가 작성되어 있다.

String oldPath = environment.getProperty("server.context-path");
String newPath = environment.getProperty("server.servlet.context-path");

oldPath와 newPath 둘다 모두 동일한 정보가 들어가 있다.

오늘은 이렇게 새로운 Spring boot 2.0에 대해서 조금 알아봤다. 아주 심각한 내용은 아니기에 한번씩 그냥 살펴보는것도 좋은 듯 싶다.
2.0은 언제 나올려나.. 지금 예상은 2월20일로 되어있는데 좀 더 걸릴듯 싶다. 그 때 나오면 좋고.

spring web 비동기

오랜만에 포스팅을 한다. 새해도 거의 보름이 지나가는데 요즘은 포스팅이 뜸했다. 다시 블로그를 열심히 해야 겠다. 물론 될지는 모르겠지만.. 어쨋든 오늘은 Spring에서 지원해주는 web 비동기 기술을 몇가지 살펴보도록 하자.

오늘은 이런 것들이 있다는 것만 알고 넘어가자. 추후에 좀 더 상세하게 살펴볼 수 있으면 그때 살펴보도록 하자. 너무 처음부터 깊게 파고 들면 어려우니.. 이 기술은 요즘 나오는 reactive streams 과 많이 비슷하므로 이 기술 먼저 알고 가면 좋을 듯하다.

실제 이 기술(오늘 말할려고 하는)은 최신 기술이 아니다. 정확히 말하면 대략 5년전 그러니까 spring3.2가 발표되면서 이 기술을 선보였다. 뭐 그건 그렇고 한번 살펴보도록 하자.

Callable

이것은 java 1.5부터 가능한 Callable 인터페이스다. Callable 인터페이스는 추상 메서드가 하나 있는 FunctionalInterface 이다. 해당 타입을 리턴하면 Spring에서 적절하게 리턴값을 만들어 준다.

@GetMapping("/callable")
public Callable<List<Person>> persons(){
  return () -> {
    return generatorPersons.getPersons();
  };
}

이것의 단점은 아무 설정을 할 수 없다. 타임아웃이라던지 어떤 스레드풀을 사용할지 결정할 수 없다.

CompletableFuture

이것은 java 1.8부터 가능한 CompletableFuture 클래스이다. 해당 타입도 마찬가지로 mvc에서 리턴 타입으로 정의하면 Spring에서 적절하게 리턴해준다.

@GetMapping("/future")
public CompletableFuture<List<Person>> future() {
  return CompletableFuture.supplyAsync(() -> generatorPersons.getPersons());
}

supplyAsync 경우에는 Supplier를 파라미터로 받고 있다. 그래서 위와 같이 적절하게 람다를 사용해서 좀 더 나은 코드를 만들 수 있다. CompletableFuture.supplyAsync 경우에는 Executor를 추가로 파라미터로 받고 있다. 그래서 좀 더 나은 설정을 할 수 있다.


@GetMapping("/future") public CompletableFuture<List<Person>> future() { return CompletableFuture.supplyAsync(() -> generatorPersons.getPersons(), this.asyncExecutor); }

ListenableFuture

ListenableFuture는 spring 4.0에 추가된 인터페이스이다. 4.0 이전 버전에서는 사용할 수 없으니 참고하면 되겠다. 실제로 AsyncRestTemplate의 리턴타입은 ListenableFuture로 정의 되어있다. 비동기 적으로 특정한 API를 호출할 때 유용한 AsyncRestTemplate은 리턴 타입 그대로 spring mvc에게 넘겨줘도 된다.

@GetMapping("/async")
public ListenableFuture<ResponseEntity<List<Person>>> persons() {
  return asyncRestTemplate.exchange("http://localhost:8081/persons",
      HttpMethod.GET,
      null,
      new ParameterizedTypeReference<List<Person>>() {});
}

API 통신후에 적절히 사용한다면 매우 유용한 인터페이스이다. 다음 시간이나 혹은 추후에 좀 더 자세히 이야기 해보도록 하고 일단 이정도만 알고 있자.

WebAsyncTask

WebAsyncTask 은 Spring에서 제공해주는 클래스이다. spring 3.2 부터 제공되고 있으니 참고하면 되겠다. 이 클래스는 Callable 인터페어스보다 좀 더 나은 설정을 갖고 있다. 실제로 WebAsyncTask 클래스 안에는 Callable을 사용하고 있으며 타임아웃 설정, executor 등을 설정 할 수 있다.

@GetMapping("/webAsyncTask")
public WebAsyncTask<List<Person>> webAsyncTaskPerson() {
  return new WebAsyncTask<>(() -> generatorPersons.getPersons());
}

위는 기본적인 사용법이다.

@GetMapping("/webAsyncTask")
public WebAsyncTask<List<Person>> webAsyncTaskPerson1() {
  return new WebAsyncTask<>(1000L, this.executor, () -> generatorPersons.getPersons());
}

타임아웃 설정과 Executor를 설정 할 수 있다. Callable 보다 설정할게 좀 더 있으니 Callable 보다는 WebAsyncTask를 사용하길 권장한다.

DeferredResult

DeferredResult 클래스 역시 spring 3.2부터 사용가능 하다. 이건 조금 특이하다. 이건 전혀 다른 스레드에서도 사용가능하다.(물론 위의 것도 전혀 다른 스레드이긴하지만..) 아무튼 설명하기 좀 힘드니 소스부터 보자..

private final Queue<DeferredResult<List<Person>>> personsQueue = new ConcurrentLinkedQueue<>();

@GetMapping("/deferred")
public DeferredResult<List<Person>> persons() {
  DeferredResult<List<Person>> result = new DeferredResult<>();
  personsQueue.add(result);
  return result;
}

@Scheduled(fixedRate = 2000)
public void processQueues() {
  for (DeferredResult<List<Person>> result : this.personsQueue) {
    result.setResult(generatorPersons.getPersons());
    this.personsQueue.remove(result);
  }
}

음 먼저 DeferredResult를 생성해서 큐에 담고 바로 리턴하면 된다. 그리고 나서 전혀 다른스레드에서 setResult를 호출하면 그때 뷰에 전달된다. 아주 특이한 아이이다. 적절하게 잘 쓰면 좋은 클래스인듯 하다.

일단 오늘은 어떤 클래스 혹은 인터페이스들이 Spring mvc가 지원해주는지 알아봤다. 이외에도 몇가지 더 있긴한데 그것까지는 알아보지 않았다. 나중에는 이것저것 테스트도 해보고 어떤 의미를 갖고 있는지도 한번 살펴보도록 하자.

오늘은 이만!