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 도 공부하는 중이라 조만간 한번 언급을 할 예정이다. 하지만 너무 어렵..

spring5 와 junit5

저번시간에 이어 오늘도 junit5 포스팅이다. 오늘은 junit5로 Spring을 어떻게 Test 하는지 알아보자. 이미 spring에서 만들어 놓아서 우리는 사용하기만 하면 된다. 일단 아래와 같이 maven 디펜더시를 받자.

<dependencies>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.0.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.0.0.RELEASE</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.0.1</version>
    <scope>test</scope>
  </dependency>
</dependencies>

간단하게 어떻게 하는지만 테스트를 할 예정이라 web과 관련된 설정은 하지 않았다. 그렇다고 웹이라고 해서 크게 다른 건 없다.
테스트를 위해 샘플 코드를 작성해보자.

public interface HelloService {

  String hello();
}

@Configuration
public class Application {

  @Bean
  HelloService helloService() {
    return () -> "hello world";
  }
}

우리는 HelloService 라는 인터페이스를 빈으로 등록 하였다. 샘플 코드이기에 크게 의미는 없다.
저번시간에 배운 ParameterResolver 인터페이스를 구현한 Spring 의 코드가 존재한다. 바로 SpringExtension 클래스이다. 뿐만아니라 여러 인터페이스를 구현했으니 참고해도 되겠다. 우리는 SpringExtension 클래스를 사용해서 테스트를 할 것이다. 일단 한번 작성해보자.

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = Application.class)
class GeneralHelloServiceTests {

  private final HelloService helloService;

  @Autowired
  GeneralHelloServiceTests(HelloService helloService) {
    this.helloService = helloService;
  }

  @Test
  void parametersHello(@Autowired HelloService helloService) {
    assertEquals(helloService.hello(), "hello world");
  }
  @Test
  void constructorHello() {
    assertEquals(this.helloService.hello(), "hello world");
  }

}

junit5의 ExtendWith 어노테이션을 사용해서 우리는 확장할 수 있었다. 또한 기존의 ContextConfiguration 어노테이션으로 컨텍스트를 설정 할 수 있었다. SpringExtension를 사용하면 생성자를 통해 빈을 주입 받을 수 있고 파라미터를 통해서도 빈을 주입 받을 수 있다. 하지만 여기서 주의할 점은 spring 4.3 부터는 일반적인 코드에는 생성자 위에 @Autowired를 붙이지 않아도 자동으로 주입하지만 테스트에서는 그렇지 않다. 무조건 @Autowired를 작성해줘야한다. 물론 파라미터도 마찬가지다. 우리는 Spring에서 미리 만들어진의 junit5 지원으로 아주 심플하게 테스트를 할 수 있게 되었다.

이 보다 좀 더 간단하게 테스트 할 수 있는 방법이다. 간단하다고 하더라도 그냥 한줄 정도? Spring에서 지원해주는 @SpringJUnitConfig 어노테이션은 위의 2개의 애노테이션을 합친 애노테이션이다. 그래서 2줄을 1줄로 바꿀 수 있는 좋은(?) 애노테이션이다. 한번 해보자.

@SpringJUnitConfig(Application.class)
class AnnotationHelloServiceTests {

  private final HelloService helloService;

  @Autowired
  AnnotationHelloServiceTests(HelloService helloService) {
    this.helloService = helloService;
  }

  @Test
  void parametersHello(@Autowired HelloService helloService) {
    assertEquals(helloService.hello(), "hello world");
  }

  @Test
  void constructorHello(@Autowired HelloService helloService) {
    assertEquals(this.helloService.hello(), "hello world");
  }
}

위와 같이 작성해도 기존과 동일한 코드로 동작한다. @SpringJUnitConfig 애노테이션 말고도 web을 지원해주는 @SpringJUnitWebConfig 애노테이션도 존재한다. 이것은 한번씩 해보도록 하자. 그렇다고 크게 어려운건 없고 @WebAppConfiguration 애노테이션 한개가 추가 됐을 뿐이다.

눈치빠른 사람들은 눈치챘겠지만 junit5 또한 메타 애노테이션을 지원한다. Spring과 비슷하게 가고 있는 듯하다. 테스트를 위해 좀 더 코드를 작성해보자.

@Configuration
public class Application {

  @Bean
  HelloService helloService() {
    return () -> "hello world";
  }

  @Profile("default")
  static class TestOrder {

    @Bean
    OrderService orderService() {
      return name -> "hello " + name;
    }
  }
  @Profile("dev")
  static class DevOrder {

    @Bean
    OrderService devOrderService() {
      return name -> "hello dev " + name;
    }
  }
}

아까 위의 코드에서 클래스 2개가 추가 되었다. 하나의 클래스는 Profile이 default 일때 또 다른 한개의 클래스는 dev일 때 동작한다고 가정해보자. 그리고나서 아래와 같은 메타 어노테이션을 만들어보자.

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@SpringJUnitConfig(Application.class)
@ActiveProfiles("dev")
public @interface DevTestConfig {
}

여기에는 @SpringJUnitConfig@ActiveProfiles 애노테이션 두개가 메타 어노테이션으로 정의 되어있다. 이 어노테이션은 dev의 Profile만 적용되는 애노테이션이다. 한번 그렇게 되나 테스트를 해보자.

@DevTestConfig
class DevHelloServiceTest {
  private final OrderService orderService;

  @Autowired
  DevHelloServiceTest(OrderService orderService) {
    this.orderService = orderService;
  }

  @Test
  void hello() {
    assertEquals(orderService.ordered("wonwoo"), "hello dev wonwoo");
  }
}

위에서 보다 시피 dev쪽의 bean은 "hello dev " + name 문자열로 dev라는 문자열이 포함되어 있다. 만약 dev 프로파일이 적용이 되었다면 위의 테스트 코드는 통과해야만 한다. 만약 그렇지 않고 기본 프로파일이 적용된다면 이 테스트는 실패로 돌아간다.
한번 테스트를 돌려보면 이상없이 테스트 코드가 잘 동작하는 것을 볼 수 있다.

우리는 이렇게 junut5로 spring를 테스트하는 코드를 작성해봤다. 그렇게 많이 어려운 내용은 아닌 듯 싶다. 아직 당분간은 회사에서 사용할 일은 없기에 개인적으로만 사용해야겠다.

위의 해당 소스는 여기에 있으니 한번씩 돌려보도록 하자.

Spring boot 2.0 ApplicationContextRunner

오늘은 아직 릴리즈 되지는 않았지만 그래도 큰 변화를 없을 것 같아 이렇게 포스팅을 한다. 그 전에 올해 하반기에 자바진영에 많은 변화가 있을 것으로 예상된다. 그중에 이미 릴리즈 된 Junit5 가 첫 번째 변화이고 두 번째로 자바9(9월 21일), spring5 (9월 28일) spring boot 2.0(11월 20일) 등이 릴리즈 될 예정이다. 하지만 예정은 예정일뿐.. 언제 바뀔지는 모른다. spring5 와 spring boot 2 는 모두 자바 8 이상에서만 작동한다. 그러니 이제는 자바8을 꼭 쓰도록 하자. 그건 그렇고 java EE가 이제는 이클립스재단으로 간다는.. 좋은 소식(?)

아무튼 오늘은 Spring boot 2 에 추가된 ApplicationContextRunner에 대해서 살펴보도록 하자. ApplicationContextRunner의 용도는 테스트로 사용하는 클래스인데 특정한 설정 정보를 불러와서 테스트를 하기에 적합한 클래스이다. 또 한 좀 더 모던한 스타일을 제공하므로 java8의 문법(람다 표현식) 을 사용할 수 도 있고 AssertJ 스타일의 assertions을 제공해 주고 있다. 그나저나 Spring boot가 이제는 AssertJ를 밀고 있는 듯 하다.

ApplicationContextRunner 말고도 WebApplicationContextRunner, ReactiveWebApplicationContextRunner 등이 존재하나 거의 비슷하지 않을까 싶다. 예제를 살펴보면서 해보도록 하자.

public class ApplicationContextRunnerTest {

  private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();

  @Test
  public void fooTest() {
    contextRunner.withUserConfiguration(FooConfiguration.class)
        .run(context -> {
          assertThat(context).hasBean("foo");
          assertThat(context).getBean("foo").isEqualTo("foo");
        });
  }

  @Configuration
  protected static class FooConfiguration {

    @Bean
    public String foo() {
      return "foo";
    }
  }
}

클래스 레벨에 ApplicationContextRunner을 필드로 선언하면 된다. 기본적으로는 AnnotationConfigApplicationContext를 컨테이너로 사용한다. 만약 다른 컨테이너를 사용하고 싶다면 아래와 같이 변경하면 된다.

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(GenericWebApplicationContext::new);

어떠한 특정한 설정을 추가적으로 하고 싶다면 withUserConfiguration() 메서드를 사용해서 configuration 정보를 등록 시킬 수 있다. withUserConfiguration() 메서드 말고 withConfiguration() 메서드로 존재하는데 하는 역할을 동일해 보인다. 실제 Spring boot가 사용하는 패턴을 보면 withConfiguration() 메서드에는 테스트 클래스 전체의 기본설정을 하고 withUserConfiguration() 메서드는 해당하는 테스트에만 설정이 필요한 경우에만 사용하는 것으로 보인긴 하나 굳이 뭐 그럴 필요는 없어 보인다.

예를 들면 다음과 같다.

public class CacheApplicationTests {

  private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
      .withConfiguration(AutoConfigurations.of(CacheAutoConfiguration.class));

  @Test
  public void simpleCacheTest() {
    contextRunner.withUserConfiguration(DefaultConfiguration.class)
        .run(context -> {
          assertThat(context).hasBean("cacheManager");
          assertThat(context).getBean(CacheManager.class).isInstanceOf(ConcurrentMapCacheManager.class);
        });
  }

  @Configuration
  @EnableCaching
  static class DefaultConfiguration {

  }
}

이렇게 테스트 클래스의 기본 설정인 CacheAutoConfiguration을 기본적으로 등록하고 나머지 각각의 테스트에는 그에 맞게 설정 정보를 작성하면 된다. 뭐가 좀 다르긴 하나? 코드로 봐서는 다른 걸 못느끼겠지만..

ApplicationContextRunner에 프로퍼티도 등록하여 테스트 할 수 있는데 withPropertyValues() 메서드를 사용해서 pairs 정보를 String으로 넣으면 된다.

public class CacheApplicationTests {

  private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
      .withConfiguration(AutoConfigurations.of(CacheAutoConfiguration.class));

  @Test
  public void simpleCacheTest() {
    contextRunner.withUserConfiguration(DefaultConfiguration.class)
        .withPropertyValues("spring.cache.type=simple")
        .run(context -> {
          assertThat(context).hasBean("cacheManager");
          assertThat(context).getBean(CacheManager.class).isInstanceOf(ConcurrentMapCacheManager.class);
        });
  }

  @Test
  public void caffeineCacheTest() {
    contextRunner.withUserConfiguration(DefaultConfiguration.class)
        .withPropertyValues("spring.cache.type=caffeine")
        .run(context -> {
          assertThat(context).hasBean("cacheManager");
          assertThat(context).getBean(CacheManager.class).isInstanceOf(CaffeineCacheManager.class);
        });

  }

  @Configuration
  @EnableCaching
  static class DefaultConfiguration {

  }
}

위와 같이 withPropertyValues() 메서드를 사용해서 cache.type을 caffeine으로 등록하게 하였다. 이외에도 withParent(), withClassLoader(), withSystemProperties() 가 존재한다. 아직은 자주 사용될 만한 메서드는 아닌 것 같아서 생략한다. 주로 사용될 메서드는 withUserConfiguration(), withConfiguration(), withPropertyValues() 메서드이기 때문에 이 세가지만 봐도 아직은 괜찮을 것 같다. (물론 필자 생각)

Spring boot는 test에도 관심을 많이 가져준다. boot 1.4 부터 아주 다양하고 사용하기 쉽게 테스트를 도와주는 클래스들이 많으니 1.4부터 차근차근 보는 것을 추천한다. 조만간 나올 spring5 에서 reactive stream 을 지원하니 필자도 공부좀 해놔야 겠다. 당장은 운영에서는 사용할 일이 없더라도 사용할 일이 생긴다면 별 무리 없이 사용하기 위해 봐야겠다. 예전부터 관심은 있었지만 공부를 계속 미루다보니 나올때 공부를 시작하는거 아닐까 싶다.

오늘은 이렇게 Spring boot 2.0에 작은 변화를 살펴봤다. 아주 많은 변화가 있을 예정이지만 언제 다 살펴보지?