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가 지원해주는지 알아봤다. 이외에도 몇가지 더 있긴한데 그것까지는 알아보지 않았다. 나중에는 이것저것 테스트도 해보고 어떤 의미를 갖고 있는지도 한번 살펴보도록 하자.

오늘은 이만!

spring boot 2.0 actuator

오늘은 Spring boot 2.0의 actuator에 대해서 살펴보자. 아직 마일스톤 버전이라 바뀔 가능성은 있지만 크게 바뀌지 않을 것 같다. 기존(2.0 이전)의 actuator 와는 구조가 많이 변경되었다. 구조가 변경되었다고 하더라도 우리가 사용하는 것에 대해서는 많은 변화는 없다. 약간의 변화? 일단 spring-boot-actuator 모듈이 분리 되었다. 원래는 spring-boot-actuator 모듈 하나만 있었지만 autoconfigure 모듈이 새로 추가되었다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-actuator-autoconfigure</artifactId>
</dependency>

하지만 우리는 기존과 동일하게 spring-boot-starter-actuator 만 디펜더시 받으면 된다. 그럼 자동으로 actuatoractuator-autoconfigure이 디펜더시 받아진다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

일단 필자가 아는 것만큼 이야기를 해보자.

Endpoint

새로운 @Endpoint 어노테이션이 추가되었다. 기존에는 인터페이스로 되어있던 걸로 기억하는데 어노테이션으로 변경되었다. 새로운 @Endpoint 속성에는 세가지 속성이 있다. id, exposure, defaultEnablement 속성인데 id는 말그대로 id를 뜻하며 endpoint 도 id와 동일하게 지정된다. exposure 은 노출 속성을 의미한다. 두가지 속성이 있는데 JMXWEB이 있다. 아무 설정하지 않았을 경우에는 모두 다 노출되며 특정한(JMX 혹은 WEB) 경우에 노출 시키고 싶다면 JMX 또는 WEB을 지정해주면 된다. defaultEnablement 속성은 기본적으로 enable 시킬 것인가 아니면 disable 시킬것인가 속성이다.

일단 한번 코드로 살펴보자.

@Endpoint(id = "hello")
@Component
public class HelloEndpoint {

  @ReadOperation
  public String hello(String name)  {
    return "hello " + name;
  }

  @WriteOperation
  public String foo(String name) {
    return name;
  }
}

나머지는 위에서 설명을 했으니 넘어가고 여기서 조금 익숙하지 않은 어노테이션이 눈에 띈다. @ReadOperation, @WriteOperation 어노테이션이다.
이 어노테이션은 말그대로 읽기 오퍼레이션이냐 쓰기 오퍼레이션이냐를 정하는 것이다. 그럼 무엇이 다를까? ReadOperation은 http method의 GET에 해당하고 WriteOperation 어노테이션은 POST에 해당한다. 이외에도 @DeleteOperation 어노테이션도 존재한다. 아마도 DELETE 메서드에 해당하지 않을까 생각된다. 아직 테스트는 해보지 않았지만 그러지 않을까?

한번 요청을 해보자.

http http://localhost:8080/application/hello name==wonwoo

hello wonwoo

위와 같이 요청을 했을 경우 hello wonwoo를 볼 수 있을 것이다. 아니다 볼 수 없다. 왜냐하면 기본적으로 몇가지를 제외하고는 모두 disable 되어 있기 때문이다. 루트와 info, status를 제외하고는 모두 비활성화 되어있다. enable 시키기 위해서는 프로퍼티에 다음과 같이 작성하면 된다.

endpoints.hello.web.enabled=true

만약 web을 활성화 시키고 싶다면 위와 같이 작성하면 된다. JMX를 활성화 시키고 싶다면 동일하게 endpoints.hello.jmx.enabled=true 를 작성해주면 된다. 하지만 지금 버그인지 원래그런건지 아니면 필자가 M5 버전이라 그런건지 web만 설정하고 싶어서 저렇게 했는데 jmx도 보인다. 하지만 jmx만 설정하면 web은 동작하지 않는다. 뭐 나중에 다시 확인해봐서 안되면 한번 물어나보지 머..

위와 같이 작성을 했다면 다시 요청을 해보자. 그럼 원하는 대로 hello wonwoo를 볼수 있을 것이다.

하지만 spring boot 에서는 web을 사용할 때 다른 것을 권장하고 있다. @WebEndpointExtension 어노테이션을 사용해서 웹에 특화되게 확장한 어노테이션이라고 생각하면 된다. 다른 건없다. 사용법도 간단하다. 아래와 같이 해당 엔드포인트만 연결해주면 된다.

@WebEndpointExtension(endpoint = HelloEndpoint.class)
@Component
public class HelloWebEndpointExtension {

  private final HelloEndpoint delegate;

  public HelloWebEndpointExtension(HelloEndpoint delegate) {
    this.delegate = delegate;
  }

  @ReadOperation
  public WebEndpointResponse<String> hello(@Nullable String name) {
    return new WebEndpointResponse<>(delegate.hello(name));
  }
}

어렵지 않다. 딱 보기에도 쉽다. 참고로 spring5부터는 Nullable을 지원한다. 옵션값일 경우 spring 에서 제공해주는 Nullable 어노테이션을 사용하면 된다. DI를 받을 때에도 옵션 값이면 Nullable을 사용하면 된다.

private final SomeClass some;

public HelloEndpoint(@Nullable SomeClass some) {
  this.some = some;
}

그리고 Spring boot 만의 Conditional 어노테이션인 ConditionalOnEnabledEndpoint 어노테이션이 추가 되었다. 이것은 endpoints.hello.enabled=false 일 때는 빈으로 동작 시키지 않는 어노테이션이다.

@Bean
@ConditionalOnEnabledEndpoint
public HelloEndpoint helloEndpoint() {
  return new HelloEndpoint();
}

위와 같이 설정 했을 경우 endpoints.hello.enabled=false 이라면 helloEndpoint는 빈으로 등록 시키지 않는다. 이외에도 ConditionalOnEnabledHealthIndicator 어노테이션도 추가 되었다. HealthIndicator는 설정 하는 것 이외에는 변경된 부분은 없다. 기존과 동일하게 HealthIndicator 인터페이스 혹은 AbstractHealthIndicator 추상 클래스를 구현하면 된다.

@Component
public class DummyHealthIndicator implements HealthIndicator {

  @Override
  public Health health() {
    return Health.up().build();
  }

}

HealthIndicator는 우리가 사용하는 것에 대한 변화는 없으므로 넘어가고 마지막으로 Endpoint에 새로 추가된 어노테이션이 하나 더 있다. 그건 @Selector라는 어노테이션인데 간단히 말해서 PathVariable 어노테이션과 동일하다.

@ReadOperation
public WebEndpointResponse<String> selector(@Selector String name) {
  return new WebEndpointResponse<>(delegate.hello(name));
}

위와 같이 설정 했을 경우에 다음과 같이 요청이 가능하다.

http http://localhost:8080/application/hello/wonwoo

hello wonwoo

그럼 동일하게 hello wonwoo라고 응답 받을 수 있다.

안타깝게 현재는 Post로 받을때 바디를 Object로 받을 수 는 없다. 나중에 가능할지는 모르겠지만 현재로는 String, Integer, Long 기타 enum 등으로만 받을 수 있다.

@WriteOperation
public WebEndpointResponse<Person> person(String name) {
  return new WebEndpointResponse<>(new Person(delegate.hello(name)));
}

위와 같이 POST로 설정했을 경우 body로 요청 할 수 있다.

http POST http://localhost:8080/application/hello name=wonwoo

{
    "name": "hello wonwoo"
}

이렇게 actuator 의 Endpoint에 변화에 대해서 알아봤다. 다른 것들은 우리가 사용하는데 있어서 많은 변화는 없었지만 내부적으로 많은 변화가 있었다. 특히 reactive가 추가 됨으로써 구조적으로 많은 변화가 있었다. 우리는 잘 가져다 쓰면 된다. 물론 내부를 아는 것이 휠씬 좋지만 일단 먼저 어떻게 사용하지는 아는 것도 중요하니 잘 가져다 쓰자.

오늘은 이렇게 Spring boot 2.0 actuator에 대해서 살펴봤다. reactive stream 도 공부하는 중이라 조만간 한번 언급을 할 예정이다. 하지만 너무 어렵..