WordPress Api

Spring boot NestedCondition

Posted by wonwoo on 2018-07-28

오늘 알아볼 내용은 Spring boot 에서 지원해주는 NestedCondition 에서 대해서 알아보도록 하자. Spring boot 에서는 많은 Condition을 지원해 주고 있지만 그중에서 NestedCondition 에 대해서 알아볼 것이다. 사실 Spring boot **Condition 들의 최상위 인터페이스는 Spring boot 에서 지원해주는 인터페이스가 아닌 Spring 에서 지원해주고 있는 인터페이스이다. 그 중 대표적인 Spring의 condition은 우리가 많이 사용하고 있는 ProfileCondition 이니 참고하면 되겠다.
나머지 Spring boot 의 Condition 들은 여기나 다른 블로그 혹은 문서를 참고하면 되겠다.

NestedCondition는 대표적으로 3의 클래스를 갖고 있는데 그 3개 모두 AbstractNestedCondition 클래스를 구현하고 있다. 가장 기본의 되는 클래스이다. 한개씩 살펴보도록 하자.

AnyNestedCondition

AnyNestedCondition 는 어느 한 조건만 만족하면 된다. 예를들어 어떤 빈들이 존재한다거나, 존재 하지 않다거나 등 여러 조건을 만들수 있는데 그 중 하나의 조건만 만족해도 동작한다. 또한 @ConditionalOnBean이 or 조건 에서 and 조건으로 변경되면서 or 조건을 사용할 경우 이 클래스를 이용하면 된다.
일단 한번 코드를 작성해보자.

public class Bar {
}

public class Foo {
}

public class FooService {
}

이런 샘플 코드가 있다고 가정하자. Foo와 Bar는 조건에 넣을 클래스이고 FooService 경우에는 해당 조건이 만족할 때 등록 되는 빈으로 사용할 예정이다.

@Configuration
@Conditional(AnyCondition.class)
public class AnyConfig {

  @Bean
  FooService fooService() {
    return new FooService();
  }

  static class AnyCondition extends AnyNestedCondition {

    public AnyCondition() {
      super(ConfigurationPhase.REGISTER_BEAN);
    }

    @ConditionalOnBean(Foo.class)
    static class FooCondition {

    }

    @ConditionalOnBean(Bar.class)
    static class BarCondition {

    }
  }
}

위와 같이 AnyNestedCondition를 상속받아서 구현?.. 설정하면 된다. ConfigurationPhase 타입의 생성자는 필수이기에 위와 같이 작성해준다. 그리고 나서 해당 조건을 작성하면 된다.

@ConditionalOnBean(Foo.class)
static class FooCondition {

}

이 뜻은 Foo라는 빈이 존재할 경우 해당 설정을 적용하라는 의미이다. 그 아래 Bar 클래스도 동일하다. 위와 같이 작성할 경우엔 FooService 가 빈으로 등록 되지 않는다. 왜냐하면 Foo, Bar 모두 빈으로 등록 되어 있지 않기 때문이다. 만약 FooService를 빈으로 등록 되게 하고 싶다면 아래와 같이 작성해야 한다.

@Component
public class Foo {
}

or 

@Component
public class Bar {
}

Foo, Bar 둘중에 하나만 빈으로 등록 시키면 된다. 어렵지 않다.

NoneNestedConditions

NoneNestedConditions 는 클래스명의 의미와 동일하게 모두 만족하지 않으면 동작한다. 하나라도 만족하면 동작하지 않는다.

@Configuration
@Conditional(NoneCondition.class)
public class NoneConfig {

  @Bean
  FooService fooService() {
    return new FooService();
  }

  static class NoneCondition extends NoneNestedConditions {

    public NoneCondition() {
      super(ConfigurationPhase.REGISTER_BEAN);
    }

    @ConditionalOnBean(Foo.class)
    static class FooCondition {

    }
    @ConditionalOnBean(Bar.class)
    static class BarCondition {

    }
  }
}

AnyNestedCondition와 마찬가지로 NoneNestedConditions를 구현하면 된다. 구현은 AnyNestedCondition와 동일하다.

AllNestedConditions

AllNestedConditions은 해당 조건을 모두 만족해야 한다. 이것 역시 하나라도 만족하지 않으면 동작하지 않는다.

@Configuration
@Conditional(AllCondition.class)
public class AllConfig {

  @Bean
  FooService fooService() {
    return new FooService();
  }

  static class AllCondition extends AllNestedConditions {

    public AllCondition() {
      super(ConfigurationPhase.REGISTER_BEAN);
    }

    @ConditionalOnBean(Foo.class)
    static class FooCondition {

    }

    @ConditionalOnBean(Bar.class)
    static class BarCondition {

    }
  }
}

AllNestedConditions을 구현하면 된다. 쉽다. 딱히 어려운 부분은 없는 것 같다.

여기에서는 ConditionalOnBean만 사용했는데 꼭 그럴필요는 없다. @ConditionalOnMissingBean, @ConditionalOnClass, @ConditionalOnMissingClass@ConditionalOn** 다 동작할 것이다. (하지만 다 해보진 않았다.)

@ConditionalOnProperty 하나만 예제로 만들어 봤다.

static class AnyCondition extends AnyNestedCondition {

  public AnyCondition() {
    super(ConfigurationPhase.REGISTER_BEAN);
  }

  @ConditionalOnBean(Foo.class)
  static class FooCondition {

  }

  @ConditionalOnProperty(prefix = "foo", name = "name")
  static class BarCondition {

  }
}

이와 같이 작성할 경우 foo.name의 키를 갖고 있는 프로퍼티를 작성해주면 된다.

foo.name=wonwoo

그럼 해당 condition을 만족하여 동작한다.

여기서 ConfigurationPhase 라는 enum이 존재하는데 두가지 타입을 제공해준다. PARSE_CONFIGURATION, REGISTER_BEAN 이다. 아주 자세히는 모르겠지만 해당 문서를 보면 이렇다.

PARSE_CONFIGURATION 경우에는 @Configuration 어노테이션을 파싱할떄 조건을 평가한다. (ConfigurationClassParser)
REGISTER_BEAN 경우에는 빈을 추가 할때 조건을 평가한다. (ConfigurationClassBeanDefinitionReader)

음.. 글쎄다 사실 시점이 문제인 것 같다. 언제 조건을 평가하는지를 설정하는 듯하다. 대부분의 경우에는 빈을 추가할 때 시점으로 사용하면 문제 없을 듯 하다. 언제는 예외는 있기에..

PARSE_CONFIGURATION 사용하는 클래스들을 살펴보면 좀 더 알지 않을까 생각된다.

이상으로 오늘은 Spring boot의 NestedCondition 대해서 알아봤다. 좀 더 많은 내용은 문서를 참고하면 되겠다. 사실 문서에 있는지는 모르겠다. 문서를 보고 한게 아니라..

Spring boot 2.0 의 변화

Posted by wonwoo on 2018-06-27

오늘은 간단하게 Spring boot 2.0 의 변화에 대해서 알아보도록 하자. 물론 다 알아볼건 아니고 필자가 필요로하거나 자주 사용할만 것들, 또는 예전에 알아봤던 내용은 살펴보지 않을 것이니 이런게 있구나 정도만 알고 넘어가고 이후 공식문서등을 참고하면 더 좋을 것 같다.

@ConditionalOnBean

@ConditionalOnBean 어노테이션이 AND 조건으로 변경 되었다. 2.0 이전에는 OR 조건이였지만 지금 현재는 AND 조건으로 모두조건이 만족해야 설정된다.

public class ConditionalBean1 {
}

public class ConditionalBean2 {
}

public class Simple {
}

@ConditionalOnBean({ConditionalBean1.class, ConditionalBean2.class})
@Configuration
public class Config {

  private final Logger logger = LoggerFactory.getLogger(this.getClass());
  @Bean
  Simple simple() {
    logger.info("simple test");
    return new Simple();
  }
}

대략 이런 코드가 있다고 가정하자. 위 코드는 일단 simple test란 로그가 찍히지 않는다. 그 이유는 뭐 둘다 빈으로 등록 되지 않았기 때문이다.

@Configuration
public class ConditionalBean1 {
}

ConditionalBean1 클래스만 빈으로 등록해보자. 이 때 2.0 이전 버전에서는 로그가 출력 되지만 2.0 부터는 로그가 출력 되지 않는다. 만약 로그가 출력 되게 원한다면 아래와 같이 모두 bean으로 등록 시켜야 된다.

@Configuration
public class ConditionalBean1 {
}

@Configuration
public class ConditionalBean2 {
}

-parameters

기본적으로 spring boot 2.0의 spring-boot-starter-parent 에는 -parameters 옵션이 추가 되었다.

@RestController
public class TestController {

  @GetMapping("/")
  public String hello() throws NoSuchMethodException {
    Method name = this.getClass().getMethod("name", String.class);
    return name.getParameters()[0].getName();
  }


  public String name(String id) {
    return "wonwoo";
  }
}

위와 같은 코드를 작성할 경우 (파라미터의 변수명을 가져올 때) 굳이 추가적으로 maven에 작성할 필요가 없다.

mvn install 
java -jar target/blabla.jar

1.5 버전에서는 arg0 로 찍히지만 2.0 에서는 id가 출력 된다.

Spring Data Web

나쁘지 않은 설정이 추가 되었다. Spring Data Web의 기본 페이지 사이즈, 파라미터 명, 첫페이지 인덱스 번호 등을 설정하라면 WebMvcConfigurerAdapter 를 상속받은 후에 PageableHandlerMethodArgumentResolver 클래스를 셋팅해줘야 했다. 하지만 이제는 properties 로 가능해졌다. 나쁘지 않다.

spring.data.web.pageable.default-page-size
spring.data.web.pageable.one-indexed-parameters=
spring.data.web.pageable.page-parameter=
spring.data.web.pageable.size-parameter=
spring.data.web.pageable.max-page-size=
spring.data.web.pageable.prefix=
spring.data.web.pageable.qualifier-delimiter=

관련설정은 SpringDataWebAutoConfiguration 클래스를 참고하면 되겠다.

DurationUnit

바인딩 할 때 유용한 어노테이션이 추가되었다. 유용한지는 나중에 알겠지.. java 1.8에 추가된 Duration@ConfigurationProperties에 작성할 수 있다. 아마도 2.0 이전에는 사용할 수 없었다. 하지만 2.0 부터는 Duration을 사용할 수 있다.

@ConfigurationProperties("foo")
public class FooProperties {

  private Duration period;

  public void setPeriod(Duration period) {
    this.period = period;
  }

  public Duration getPeriod() {
    return period;
  }
}

작성 후에 application.properties에 다음과 같이 작성 가능하다.

foo.period=10s

좀 더 나은 방법으로는 @DurationUnit 어노테이션을 사용해서 기본시간대를 지정할 수 있다.

  //..

  @DurationUnit(ChronoUnit.SECONDS)
  private Duration period;

  //..

이후 application.properties에는 10이라는 숫자만 써도 된다.

foo.period=10

http2 지원

Tomcat, Undertow 및 Jetty에서 http2를 지원한다. 하지만 몇가지 주의사항이 있다. 내 기억이 맞다면말이다. java8에서는 기본적으로 http2를 지원하지 않았다. 그래서 추가적인 모듈을 넣어야 한다고 했던 기억이 나고 java9부터는 기본적으로 지원한다.
또한 https를 사용해야지만 http2를 지원한다. spring boot의 기본설정에는.. 물론 커스텀하게 구현해도 될 것 같긴 한데.. 해보진 않았다.

위 그림을 보면 h2라고 설정 보일 것이다.

Property

env endpoint를 보면 origin 이라는 필드가 추가 되었다. 해당하는 프로퍼티의 파일 명과 라인번호 및 해당 컬럼수를 의미 한다.

{
  "name": "applicationConfig: [classpath:/application.properties]",
  "properties": {
    "management.endpoints.web.exposure.include": {
      "value": "*",
      "origin": "class path resource [application.properties]:1:43"
    },
    "foo.period": {
      "value": "10",
      "origin": "class path resource [application.properties]:3:12"
    }
  }
}

사용할 때가 있긴 한가? 흠흠..

이 외에도 엄청나게 많은 변화가 있지만 다 알아볼 수 는 없어 여기까지만 작성하겠다. 예를들어 Kotlin이 폭넓게 지원이 되고 있으며 Reactive Spring Security, Reactive Spring Data, Actuator의 변화 및 추가, Micrometer, 기타 Data 지원, 애니메이션 배너등 여러가지 지원을 많이 해주고 있다.

더 많은 변화를 알고 싶다면 해당 문서를 참고하면 되겠다.

Spring Controller 리턴타입

Posted by wonwoo on 2018-06-23

오늘은 Spring 에서 지원해주는 Controller 리턴타입에 대해서 알아 보도록 하자. 저번에 Spring Controller 파라미터 타입을 작성했는데 나름 인기가 좋아서 리턴타입도 해보려고 한다. 이건 인기가 별로 없을 듯 한데..
어쨌든.. Spring 에서는 많은 리턴타입들을 제공해준다. 대부분 거의 다 알아볼텐데 (필자가 아는 것들은..) 빠진게 있거나 설명이 부족하다면 Spring 문서를 보는 것을 추천한다. 그럼 시작해보자.

String

필자가 가장 자주 사용하며 (뷰템플릿을 사용할 때) 가장 간단한 리턴 타입이다. String 에는 뷰네임을 지정해주면 된다.

@GetMapping("/string")
public String str(Model model) {
  model.addAttribute("user", DATA);
  return "index";
}

Model 안에 데이터를 담고 뷰명을 string 타입으로 작성하면 된다. 그럼 뷰에서는 다음과 같이 해당 코드를 작성하면 된다.

<!DOCTYPE html>
<html>
<body>

<h2>name</h2>
<h3>{{user.name}}</h3>
<h2>email</h2>
<h3>{{user.email}}</h3>

</body>
</html>

뷰템플릿은 mustache를 이용했다. 문법자체는 해당 뷰템플릿을 알아야 한다. 하지만 모델 자체를 받을 때는 비슷하니 다른 뷰템플릿도 동일할 듯 싶다.

ModelAndView

가장 대표적이며 필자도 예전에는 자주 사용한 클래스이다. (왜냐면 옛날 내 사수가 이걸 좋아했기에) 하지만 요즘은 거의 사용하지 않는다. 필자도 바로 위에 작성한 string 타입으로 작성한다.

@GetMapping("/modelview")
public ModelAndView modelview() {
  return new ModelAndView("index").addObject("user", DATA);
}

Model도 파라미터로 받을 필요 없이 ModelAndView 클래스에 addObject 메서드를 이용해서 해당하는 데이터를 담기만 하면 된다. String 타입과 비슷하다. 이것 역시 뷰에 작성할 부분은 동일하다.

//...
<h2>name</h2>
<h3>{{user.name}}</h3>
<h2>email</h2>
<h3>{{user.email}}</h3>
//...

void

Spring 에서는 void 타입도 리턴이 가능하다. void 타입인데 어떻게 view 명을 지정해줄 수 있을까? 딱히 view명을 지정해줄 방법이 보이지 않는데 말이다. Spring은 뷰명을 입력하지 않아도 기본적으로 해당 url을 이용해서 뷰네임을 결정한다. 실제 인터페이스는 RequestToViewNameTranslator 인터페이스이며 DefaultRequestToViewNameTranslator 구현체 하나만을 갖고 있다. 만약 기본적인 뷰명을 변경하고 싶다면 커스텀하게 구현하면 되지 않을까 싶다.

@GetMapping("/void")
public void void(Model model) {
  model.addAttribute("user", DATA);
}

void.html 혹은 그에 따른 뷰템플릿 양식으로 해당파일을 만들면 된다.

//...
<h2>name</h2>
<h3>{{user.name}}</h3>
<h2>email</h2>
<h3>{{user.email}}</h3>
//...

Model Object

여기서 말하는 Model은 Spring에서 제공해주는 Model 클래스가 아니다. 일반 우리가 자주 사용하는 모델 오브젝트를 이야기하는 것이다. Spring 에서는 모델 자체를 리턴해도 된다. 다음과 같이 말이다.

public class Hello {
  private String name;
  private String email;

//...

@GetMapping("/hello")
public Hello hello() {
  return new Hello("wonwoo", "wonwoo@test.com");
}

이 또한 역시 뷰네임을 지정해줄 수 없기에 void와 마찬가지로 RequestToViewNameTranslator 이용해서 뷰네임을 지정한다. 또한, 모델에 대한 명도 조금 다르다 앞에서는 모델명을 지정해주었지만 여기서는 해줄수 없다. (물론 해줄수 있는 방법은 있는데 여기서는 설명하지 않겠다.) 그래서 해당 클래스의 명으로 모델명이 지어진다.

//...
<h2>name</h2>
<h3>{{hello.name}}</h3>
<h2>email</h2>
<h3>{{hello.email}}</h3>
//...

앞에서는 user 라는 모델명을 명시했지만 여기서는 명시하지 않았기에 해당 클래스가 모델명이 된다. Hello으로 클래스로 작성했기에 hello란 모델명으로 작성하면 된다.

Map , 기타 Map(Spring)

모델 오브젝트와 동일하게 Map 형태도 리턴타입으로 가능하다. 하지만 모델 오브젝트와 조금 다른점은 개별로 모델이 등록된다는 것이다. Map이라는 클래스니까 map이라는 모델명이 나올 것 같지만 그렇지 않다.

@GetMapping("/map")
public Map<String, String> map() {
  return DATA;
}

만약 위와 같이 작성했다면 우리는 다음과 같이 뷰를 작성해야 된다.

//...
<h2>name</h2>
<h3>{{name}}</h3>
<h2>email</h2>
<h3>{{email}}</h3>
//...

하지만 그닥 추천하지 않는다고 한다. 그래서 더이상 설명은 생략한다.

@ResponseBody

이것 역시 자주 사용하기에 대부분 다 알 것이라고 생각한다. SPA로 개발을 하거나 ajax로 개발을 할 떄 유용한 어노테이션이다. Http 본문자체를 리턴하며 xml 혹은 json 등 여러 메시지 컨버터를 Spring에서 지원하니 그건 따로 살펴봐야 된다.

@GetMapping("/body")
@ResponseBody
public String body() {
  return "<h2>name</h2>\n" +
      "<h3>wonwoo</h3>\n" +
      "<h2>email</h2>\n" +
      "<h3>wonwoo@test.com</h3>";
}

하지만 요즘은 @ResponseBody 어노테이션을 직접사용하지 않고 @Controller 어노테이션과 @ResponseBody 어노테이션을 메타 어노테이션으로 작성해둔 @RestController를 많이 사용하니 참고하면 되겠다.

Spring5

Spring5에서 지원해주는 reactive streams 또한 리턴타입으로 가능하다. 기본적으로 spring5는 reactor를 지원한다. 왜냐하면 자기들이 만들었으니.. 어쨌든 reactor 에 있는 Mono, Flux으로 리턴타입으로 작성해도 된다.

@GetMapping("/spring5")
public Mono<String> hello() {
  return Mono.just("hello world");
}

여기서는 Mono만 했지만 Flux 또한 가능하다.
또한 reactive streams의 인터페이스인 Publisher을 리턴타입으로 명시해도 동작한다.

@GetMapping("/spring5")
public Publisher<String> hello() {
  return Mono.just("hello world");
}

이 외에도 rxjava1, rxjava2에 있는 Observable, Single, Completable, Flowable, Maybe 등 rxjava1과 rxjava2를 모두 지원하니 적당한 디펜더시를 작성한다면 사용해도 좋다. 또한 지금 현재 버전(5.0.7)에서는 java9에 들어간 reactive streams 인터페이스도 지원하니 자기에게 알맞는 구현체 또는 인터페이스를 사용해도 된다.

Rendering

spring5 에서는 Rendering 인터페이스가 추가되었다. reactive streams를 사용할 때 해당하는 뷰와 데이터를 넣을 수 있는 인터페이스이다.

@GetMapping("/")
public Rendering hi() {
  return Rendering
      .view("index")
      .modelAttribute("user", DATA)
      .build();
}

기존의 Model과 비슷한 생김새이다. 그래서 많이 거부감들지 않게 작성할 수 있다.

오늘은 이렇게 Spring에서 지원해주는 Controller 리턴타입에 대해서 알아봤다. 필자가 모르는 것이 있을 수 있기에 더욱 상세한 내용들은 해당문서를 참고하면 좋겠다. 알면 저도 알려주세요.