<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발블로그</title>
    <link>https://mkzz.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 5 Jun 2026 01:34:25 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>머룽</managingEditor>
    <item>
      <title>Spring6 @HttpExchange</title>
      <link>https://mkzz.tistory.com/342</link>
      <description>&lt;div class='markdown-body'&gt;&lt;p&gt;작년 말부터 spring에서 6.0대 버전을 개발하고 있다. 현재는 M4(5월기준)까지 나온 상태이고 7월쯤에 M5가 나올 예정이다. 예정대로 진행된다면 올해 말 늦으면 내년초에는 아마  GA 버전이 출시 될 예정이다. 로드맵 기준으로는 올해 10월쯤 예상한다.&lt;/p&gt;

&lt;p&gt;spring6에 몇몇가지의 변화가 있을예정이다. 예를들어 spring6은 java17이 baseline 버전이다. java17을  사용 못한다면 spring5. x 버전 spirng boot 2.x 버전을 사용해야 한다. 아마 당분간 꾸준히 지원할 예정이니 차근차근 마이그레이션해도  상관없다.&lt;/p&gt;

&lt;p&gt;그 중 필자가 제일 관심있어 보이면 기능을 하나 가져왔다. 아직 개발 단계이니 넓은 마음으로 보자. 이런 비슷한 라이브러리는 몇가지 존재 한다. okhttp 로 유명한 Square의 &lt;code&gt;retrofit&lt;/code&gt;와 OpenFeign 의 &lt;code&gt;feign&lt;/code&gt;이 가장 유명하다. 더 있는지는 모르겠지만 android에서는 retrofit를 가장 많이 쓰고 server 쪽에서는 feign을 많이 쓰는것 같다. 아마도 spring cloud 에서 feign을 지원해서 그런거 같다.&lt;/p&gt;

&lt;p&gt;retrofit와 feign을 알고 있다면 대충 어떤 기능인지 알 수 있을 것이다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;java&quot;&gt;public interface GitHubService {
  @GET(&quot;users/{user}/repos&quot;)
  Call&amp;lt;List&amp;lt;Repo&amp;gt;&amp;gt; listRepos(@Path(&quot;user&quot;) String user);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;위는 Retrofit의 sample 코드이다. 위처럼 interface 만으로 Http Api로 변환할 수 있다.&lt;/p&gt;

&lt;h3&gt;기본 사용&lt;/h3&gt;

&lt;p&gt;Retrofit기능처럼 spring6에도 동일한 기능이 추가 되었다. 이제는 retrofit나 feign를 사용하지 않아도 interface만으로 api를 호출 할 수 있다.&lt;/p&gt;

&lt;p&gt;이제 spring 의 기본 사용법을 한번 살펴보자.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;java&quot;&gt;@Bean
GreetingClient gitHubClient() {
    return HttpServiceProxyFactory.builder(new WebClientAdapter(WebClient.builder().build()))
            .build()
            .createClient(GreetingClient.class);
}

@HttpExchange(value = &quot;http://localhost:8080&quot;, contentType = &quot;text/html&quot;)
interface GreetingClient {

    @GetExchange(&quot;/greeting&quot;)
    Flux&amp;lt;Greeting&amp;gt; greetings();

    @GetExchange(&quot;/greeting/{message}&quot;)
    Mono&amp;lt;Greeting&amp;gt; greetings(@PathVariable String message);

    @PostExchange(value = &quot;/greeting&quot;, contentType = &quot;application/json&quot;)
    Mono&amp;lt;Greeting&amp;gt; greetings(@RequestBody Greeting greeting);
}

@EventListener
public void start(ApplicationStartedEvent ignore) {
    GreetingClient greetingClient = gitHubClient();
    greetingClient.greetings(new Greeting(&quot;hello&quot;))
            .then(greetingClient.greetings(&quot;hello&quot;))
            .doOnNext(System.out::println)
            .flatMapMany(__ -&amp;amp;gt; greetingClient.greetings())
            .subscribe(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;기본  사용법은 위와 같다. &lt;code&gt;@HttpExchange&lt;/code&gt;, &lt;code&gt;@GetExchange&lt;/code&gt;, &lt;code&gt;@PostExchange&lt;/code&gt; ... 등으로 설정하여 GET, POST, PUT, DELETE 메서드를 만들 수 있다. (http method 다 존재한다.)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GreetingClient&lt;/code&gt;의 proxy 객체를 만들어 bean으로 등록하면 설정은 끝이다. 간단하다. &lt;code&gt;HttpServiceProxyFactory&lt;/code&gt; 클래스에는 몇가지 메서드가 존재한다.
&lt;code&gt;addCustomResolver&lt;/code&gt;는 custom한 parameter를 만들떄 사용하면 된다. 
&lt;code&gt;setReactiveAdapterRegistry&lt;/code&gt;는 spring이 지원해주지 않는 다른 비동기 라이브러리를 사용할때 설정하면 된다. 
&lt;code&gt;setConversionService&lt;/code&gt; 추가 적으로 데이터 변환이 있을 경우 사용하면 된다. 기본 설정은 &lt;code&gt;DefaultConversionService&lt;/code&gt;을 사용한다. &lt;code&gt;setBlockTimeout&lt;/code&gt;은 block 메서드를 사용할경우 timeout을 지정할 수 있다.&lt;/p&gt;

&lt;p&gt;물론 아마 더 메서드가 추가 될 예정인듯 하다.&lt;/p&gt;

&lt;h3&gt;HttpExchange&lt;/h3&gt;

&lt;p&gt;다음은 &lt;code&gt;HttpExchange&lt;/code&gt; 대해서 알아보자. 사실 @RequestMapping과 사용법은 거의 동일하다. url, method, contentType, accept이 존재 한다. 이건 다 아는 내용이니 설명하지 않겠다. 메서드 별로 사용할 수 있도록 @GetExchange, @PostExchange, @DeleteExchange 등 다 존재 하니 한번 살펴보면 되겠다. 어려운 내용은 없다.&lt;/p&gt;

&lt;p&gt;또한 다음과 같이 해도 무방하다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;java&quot;&gt;@Bean
GreetingClient gitHubClient() {
    return HttpServiceProxyFactory.builder(new WebClientAdapter(WebClient.builder().baseUrl(&quot;http://localhost:8080&quot;).build()))
            .build()
            .createClient(GreetingClient.class);
}

interface GreetingClient {

    @GetExchange(&quot;/greeting&quot;)
    Flux&amp;lt;Greeting&amp;gt; greetings();

    @GetExchange(&quot;/greeting/{message}&quot;)
    Mono&amp;lt;Greeting&amp;gt; greetings(@PathVariable String message);

    @PostExchange(value = &quot;/greeting&quot;, contentType = &quot;application/json&quot;)
    Mono&amp;lt;Greeting&amp;gt; greetings(@RequestBody Greeting greeting);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;위와 같이 baseUrl을 미리 설정해서 사용해도 된다.&lt;/p&gt;

&lt;h3&gt;리턴타입&lt;/h3&gt;

&lt;p&gt;다음은 리턴타입에 대해 알아보자. 여러 리턴타입을 지원한다. 기본적으로 spring6가 지원하는 비동기 타입은 reactor, rxjava3, mutiny, coroutines이 존재한다. 해당 타입들은 다 사용할 수 있다.&lt;/p&gt;

&lt;p&gt;동기타입도 지원한다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;java&quot;&gt;interface GreetingClient {

    @GetExchange(&quot;/greeting&quot;)
    List&amp;lt;Greeting&amp;gt; greetings();

    @GetExchange(&quot;/greeting/{message}&quot;)
    Greeting greetings(@PathVariable String message);

    @PostExchange(value = &quot;/greeting&quot;, contentType = &quot;application/json&quot;)
    Greeting greetings(@RequestBody Greeting greeting);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;위와 같이 작성하면 동기타입으로도 작성할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ResponseEntity&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;Optional&amp;lt;T&amp;gt;&lt;/code&gt; 타입도 지원한다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;java&quot;&gt;interface GreetingClient {

    @GetExchange(&quot;/greeting/{message}&quot;)
    Optional&amp;lt;Greeting&amp;gt; greetingsOptional(@PathVariable String message);

    @PostExchange(value = &quot;/greeting&quot;, contentType = &quot;application/json&quot;)
    ResponseEntity&amp;lt;Greeting&amp;gt; greetings(@RequestBody Greeting greeting);

    // asynchronous

    @GetExchange(&quot;/greeting/{message}&quot;)
    Mono&amp;lt;ResponseEntity&amp;lt;Greeting&amp;gt;&amp;gt; greetings(@PathVariable String message);

    @GetExchange(&quot;/greeting&quot;)
    Mono&amp;lt;ResponseEntity&amp;lt;Flux&amp;lt;Greeting&amp;gt;&amp;gt;&amp;gt; greetings();

}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;파라미터 타입&lt;/h3&gt;

&lt;p&gt;현재는 @RequestBody, @PathVariable, @RequestHeader, @RequestParam, @CookieValue, HttpMethod, URI가 존재 한다.&lt;/p&gt;

&lt;p&gt;어노테이션기반의 Spring webmvc이나 webflux를 사용한적이 있다면 다 아는 내용이다. 어노테이션들은 다 아는 내용이니  예제만 간단히 보자.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;java&quot;&gt;interface GreetingClient {

    @PostExchange(value = &quot;/greeting&quot;)
    Greeting greetings(@RequestBody Greeting greeting);

    @GetExchange(&quot;/greeting/{message}&quot;)
    Greeting greetingsOptional(@PathVariable String message);

    @GetExchange(value = &quot;/greeting&quot;)
    Greeting greetingsHeader(@RequestHeader String foo);

    @GetExchange(value = &quot;/greeting&quot;)
    Greeting greetingsParam(@RequestParam String foo);

    @GetExchange(value = &quot;/greeting&quot;)
    Greeting greetingsCookie(@CookieValue String foo);


}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;String만 작성했지만 Map&amp;lt;K,V&gt;도 가능하다.&lt;/p&gt;

&lt;p&gt;동적으로 method 나 uri을 변경하고 싶을때 HttpMethod, URI을 사용하면 된다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;interface GreetingClient {

    @PostExchange(value = &quot;/greeting&quot;)
    Greeting greetings(HttpMethod method);

    @PostExchange(value = &quot;/foo?&quot;)
    Greeting greetings(URI uri);

}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt; 이제 막 개발을 시작하는 단계라 완벽하지는 않다. 아직 버그가 종종 있다. 지금 현재는 리턴타입이 Optional은 동작하지 않는다. (수정해서 pr을 날렸다) 스냅샷 버전으로 해야 동작한다. 아직 M5가 안나온 관계로..&lt;/p&gt;

&lt;p&gt;또한 coroutines 의 &lt;code&gt;suspend&lt;/code&gt; function 도 현재는 동작하지 않는다. 이거 역시 pr날렸는데 머지 될지는 모르겠다. 
안타깝게 아직은 spring boot 쪽에선 움직임이 없는 것 같다. 조만간 움직임이 있지 않을까 싶다.&lt;/p&gt;

&lt;p&gt;이렇게 오늘은 spring6에 나올 @HttpExchange에 대해서 알아봤다. 아직은 개발단계라서 언제 무엇이 변경 될지 모른다. 기능적으로는 변경 되지 않을 것 같아 보이는데.. 뭐 클래스명이나 메서드명 정도는 변경될수도 있을 것 같다.&lt;/p&gt;

&lt;p&gt;오늘은 이만!&lt;/p&gt;
&lt;/div&gt;</description>
      <author>머룽</author>
      <guid isPermaLink="true">https://mkzz.tistory.com/342</guid>
      <comments>https://mkzz.tistory.com/342#entry342comment</comments>
      <pubDate>Sun, 23 Apr 2023 14:06:22 +0900</pubDate>
    </item>
    <item>
      <title>Java Features</title>
      <link>https://mkzz.tistory.com/341</link>
      <description>&lt;div class='markdown-body'&gt;요즘들어 Kotlin만 사용해서 Java의 새로운 문법? 기능들에 관심이 없었다. 그래서 오늘은 자바11~17까지의  문법적인 변화에 대해 알아보도록 하자. 대부분 문법적인 부분들만 살펴볼 예정이니 자세한 내용들은 해당 공식문서를 살펴보면 좋겠다.

&lt;h3&gt;Local-Variable Syntax for Lambda Parameters&lt;/h3&gt;

Java 10부터 지역변수에 &lt;code&gt;var&lt;/code&gt; 키워드를 통해 타입추론을 할 수 있었다. 그런데 Lambda 표현식의 변수엔 사용 불가 했다. 하지만 이제는 Lambda expression에서도 변수에 var 키워드를 사용가능하다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Stream.iterate(1, (var number) -&amp;gt; number + 1).limit(10).collect(Collectors.toList())
&lt;/code&gt;&lt;/pre&gt;

만약 변수가 2개이상일 경우엔 혼합해서 사용하면 안된다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;BiConsumer&amp;lt;Integer, Integer&amp;gt; foo = (var b1, Integer b2) -&amp;gt; {};
&lt;/code&gt;&lt;/pre&gt;

해당 코드는 올바르지 않다.

&lt;h3&gt;http client&lt;/h3&gt;

java.net 의 HttpClient가 java 11부터 정식으로 표준화 되었다. java9에서 인큐베이팅 된 후 두번의 업그레이드 후 출시 되었다. 기존의 &lt;code&gt;HttpURLConnection&lt;/code&gt;들은 사용하기 어렵고 사용자 친화적이지 않다. 그래서 다른 써드 라이브러리들을 많이 사용해왔다. 그러나 이제는 조금 편리하고 사용자 친화적인 &lt;code&gt;HttpClient&lt;/code&gt;를 사용하면 된다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) throws Exception {

    var httpClient = HttpClient.newBuilder()
            .build();
    var httpRequest = HttpRequest.newBuilder()
            .GET()
            .uri(URI.create(&quot;https://httpstat.us/200&quot;))
            .build();
    var httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
    System.out.println(httpResponse.body());

}
&lt;/code&gt;&lt;/pre&gt;

근데 사용할 일이 있나 모르겠다.

&lt;h3&gt;Pattern Matching for instanceof&lt;/h3&gt;

java 14부터 진행되고 있는 instanceof Pattern Matching 이다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;if (records instanceof Records) {
    var r = (Records) records;
    // use r 
}
&lt;/code&gt;&lt;/pre&gt;

이전에는 위와 같이 instanceof로 비교한 후 해당 타입으로 캐스팅을 했다. 어쩌면 불필요한 코드였을지도 모른다. 그러나 이제는 그럴필요 없다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (records instanceof Records r) {
    // use r 
} else {
    // cant use r
}
&lt;/code&gt;&lt;/pre&gt;

이제는 해당 타입의 캐스팅하지 않고 바로 사용할 수 있다. 만약 해당 조건에 만족하지 않으면 사용할 수 없다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (!(records instanceof Records r)) {
    // cant use r
} else {
    // use r 
}
&lt;/code&gt;&lt;/pre&gt;

해당 변수로 조건문을 추가 할 수도 있다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (records instanceof Records r &amp;amp;&amp;amp; r.name.length() &amp;gt; 10) {
  // use r
}
&lt;/code&gt;&lt;/pre&gt;

편리한 기능이 추가 되었다.

&lt;h3&gt;Switch Expressions and Pattern Matching&lt;/h3&gt;

java 12 부터 Switch Expressions을 사용할 수 있고 java17 부터 Pattern Matching을 사용할 수 있다.

이전에 switch문은 실수의 여지도 크며 불필요한 코드가 많았다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void getTransportPrint(Transport transport) {

        switch (transport) {
            case BUS:
                System.out.println(&quot;take a bus&quot;);
                break;
            case TAXI:
                System.out.println(&quot;take a taxi&quot;);
                break;
            case SUBWAY:
                System.out.println(&quot;take a subway&quot;);
                break;
            default:
                System.out.println(&quot;walking&quot;);
                break;

        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;code&gt;break&lt;/code&gt;키웓드를 실수로 넣지 않으면 버그가 나오기 싶다. 또한 break키워드는 불필요한 코드일지도 모른다.
이제는 간단한 표현식으로 바꿀수도 있다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void getTransportPrint(Transport transport) {

    switch (transport) {
        case BUS -&amp;gt; System.out.println(&quot;take a bus&quot;);
        case TAXI -&amp;gt; System.out.println(&quot;take a taxi&quot;);
        case SUBWAY -&amp;gt; System.out.println(&quot;take a subway&quot;);
        default -&amp;gt; System.out.println(&quot;walking&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;

실수의 여지도 적으며 코드도 간결해졌다. 물론 case에 여러 조건도 가능하다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;switch (transport) {
    case BUS, TAXI -&amp;gt; System.out.println(&quot;take a bus and taxi&quot;);
    case SUBWAY -&amp;gt; System.out.println(&quot;take a subway&quot;);
    default -&amp;gt; System.out.println(&quot;walking&quot;);
}
&lt;/code&gt;&lt;/pre&gt;

표현식이라 리턴도 받을 수 있다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static int getTransportNumber(Transport transport) {
    return switch (transport) {
        case BUS, TAXI -&amp;gt; 1;
        case SUBWAY -&amp;gt; 2;
    };
}
&lt;/code&gt;&lt;/pre&gt;

추가적으로 &lt;code&gt;yield&lt;/code&gt; 키워드도 추가되었다. case문에 추가적인 코드가 들어갈때 사용하면 된다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static int getTransportOrdinal(Transport transport) {
    return switch (transport) {
        case BUS -&amp;gt; 0;
        case TAXI -&amp;gt; {
            var ordinal = transport.ordinal();
            // bla bla            
            yield 1200;
        }
        case SUBWAY -&amp;gt; {
            // bla bla
            yield 8000;
        }
    };
}
&lt;/code&gt;&lt;/pre&gt;

이전에는 타입체크를 할 경우 if문과 instanceof를 사용해 체크를 했다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static double getTypeNumberSwitch(Object o) {
    var result = 0d;
    if (o instanceof Number) {
        var i = (Integer) o;
        result = i.doubleValue();
    } else if (o instanceof String) {
        var s = (String) o;
        result = Double.parseDouble(s);
    }
    return result;
}
&lt;/code&gt;&lt;/pre&gt;

불필요한 코드들도 많고 result에 계속 어싸인을 하고 있어 실수의 여지도 크다. 이제는 그럴 필요 없다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static double getTypeNumberSwitch(Object o) {
    return switch (o) {
        case Number i -&amp;gt; i.doubleValue();
        case String s -&amp;gt; Double.parseDouble(s);
        default -&amp;gt; 0;
    };
}
&lt;/code&gt;&lt;/pre&gt;

간단하면서 한눈에 알아 볼수 있는 코드가 되었다.

&lt;h3&gt;Text Blocks&lt;/h3&gt;

java 13 부터 나온 기능이다. 다른언어는 진작에 있었긴 한데 그래도 나오니 한결 나은거 같다. 멀티 라인의 string을 손쉽게 작성할 수 있다.
이전에는 멀티라인의 string 컨트롤하기 힘들었다. 예를들어 json을 예쁘게 만들고 싶다면 아래와 같이 해야 했다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;final var text = &quot;{\
&quot; +
        &quot;\\t\\&quot;name\\&quot; : \\&quot;wonwoo\\&quot;,\
&quot; +
        &quot;\\t\\&quot;address\\&quot;: \\&quot;Carson, CA, 90746\\&quot;\
&quot; +
        &quot;}&quot;;
System.out.println(text);
&lt;/code&gt;&lt;/pre&gt;

보기만 해도 힘들다. 만들긴 더 힘들다. 그러나 이제는 손쉽게 만들수 있다.

&lt;pre&gt;&lt;code&gt;final var text = &quot;&quot;&quot;
        {
            &quot;name&quot; : &quot;wonwoo&quot;,
            &quot;address&quot;: &quot;Carson, CA, 90746&quot;
        }
        &quot;&quot;&quot;;
&lt;/code&gt;&lt;/pre&gt;

마지막 행으로 공백이 결정되므로 마지막행의 위치가 중요하다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;final var text = &quot;&quot;&quot;
            {
                &quot;name&quot; : &quot;wonwoo&quot;,
                &quot;address&quot;: &quot;Carson, CA, 90746&quot;
            }
        &quot;&quot;&quot;;
&lt;/code&gt;&lt;/pre&gt;

위와 같이 작성했다면 아래와 같이 출력된다.

&lt;pre&gt;&lt;code&gt;|    {
|        &quot;name&quot; : &quot;wonwoo&quot;,
|        &quot;address&quot;: &quot;Carson, CA, 90746&quot;
|    }
&lt;/code&gt;&lt;/pre&gt;

&lt;code&gt;|&lt;/code&gt; 해당 표시로 여백을 표시하였다.

만약 text block 에는 여러줄을 표시하고 한줄로 출력이 되게 하고 싶다면 &lt;code&gt;\\&lt;/code&gt;(역슬래스)를 추가적으로 넣으면 된다.

&lt;pre&gt;&lt;code&gt;final var text = &quot;&quot;&quot;
        Lorem ipsum dolor sit amet, consectetur adipiscing \\
        elit, sed do eiusmod tempor incididunt ut labore \\
        et dolore magna aliqua.\\
        &quot;&quot;&quot;;
&lt;/code&gt;&lt;/pre&gt;

출력시에는 한줄로 표기 된다.

여러 이스케이프문자들이 있는데 이건 문서를 찾아보자!

아쉬운게 하나 있는데 &lt;code&gt;interpolation&lt;/code&gt;이 없다. 잠깐 보기에는  향후에 추가 될 수도 있다고 하니 기다려보자.
아쉬운대로 몇가지 방법으로 해결 할 수 있다. &lt;code&gt;replace&lt;/code&gt;, &lt;code&gt;String.format&lt;/code&gt;와 text block을 지원하기 위해 추가된 &lt;code&gt;String.formatted&lt;/code&gt;을 사용하면 된다.
replace와 String.format은 이전부터 있었으니 생략하고

&lt;pre&gt;&lt;code&gt;var text = &quot;&quot;&quot;
        red
        green
        blue
        %s
        &quot;&quot;&quot;.formatted(&quot;white&quot;);
&lt;/code&gt;&lt;/pre&gt;

내부적으로는 String.format과 동일하다.

&lt;h3&gt;Record&lt;/h3&gt;

java 14부터 추가된  Record는 값 타입을 쉽게 표현할 수 있는 기능이다. 자바는 쓸때없이 너무 장황하다라는 말이 많다. 값 타입의 클래스를 한번 만드려면 생성자, 접근자, equals, hashCode, toString, getter 등 너무 불필요하게 코드들을 작성해야 한다.

&lt;code&gt;Record&lt;/code&gt;를 사용하면 이제 그럴 필요 없다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public record Records(String name, String address) {
}
&lt;/code&gt;&lt;/pre&gt;

이제는 위와 같이만 해도 생성자, 접근자, equals, hashCode, toString, getter? 등이 만들어 진다.

추가적으로 생성자를 작성할 수 있다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Records(String name) {
    this(name, &quot;1528 Fillmore st, San Francisco&quot;);
}
&lt;/code&gt;&lt;/pre&gt;

Compact 생성자를 만들수 있다. Compact생성자는 지루한 변수할당없이 로직에 집중할 수 있도록 도와준다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;record Records(String name, String address) {

    public Records {
        if (name == null) {
            throw new IllegalArgumentException(&quot;name must be not null!&quot;);
        }
        if (address == null) {
            throw new IllegalArgumentException(&quot;addrss must be not null!&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

예를들어 위의 코드는 name 과 address의 대한 유효성을 체크하는 부분이다. 
혹은 다음과 같이 변수할당없이 로직에만 집중할 수 있게 도와주기도 한다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;record Rational(int num, int denom) {
    Rational {
        int gcd = gcd(num, denom);
        num /= gcd;
        denom /= gcd;
    }
}
&lt;/code&gt;&lt;/pre&gt;

이는 다음과 같은 코드이다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;record Rational(int num, int denom) {
    Rational(int num, int demon) {
        // Normalization
        int gcd = gcd(num, denom);
        num /= gcd;
        denom /= gcd;
        // Initialization
        this.num = num;
        this.denom = denom;
    }
}
&lt;/code&gt;&lt;/pre&gt;

몇가지 제약조건들이 있다. 예를들어 extends절이 없고, final class 이며 abstract도 할 수 없다. 더 많은 제약 조건이 있으니 문서를 참고하면 되겠다.

&lt;h3&gt;Sealed Class&lt;/h3&gt;

kotlin의 Sealed 클래스랑 문법만 다르지 거의 동일하다. Pattern Matching에도 사용할 수 있다. 예제부터 보자.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;sealed interface TransportSealed permits BusSealed, TaxiSealed {

}

sealed class BusSealed implements TransportSealed {

    public String getBus() {
        return &quot;take a bus&quot;;
    }
}

final class TaxiSealed implements TransportSealed {

    public String getTaxi() {
        return &quot;take a taxi&quot;;
    }
}

final class ExpressBus extends BusSealed {

    @Override
    public String getBus() {
        return super.getBus() + &quot; Express&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;

sealed class 는 sealed 키워드를 통해 만들수 있으니 봉인된 클래스를 permits 키워드를 통해 선언 할 수 있다. permits에 없는 클래스는 사용할 수 없다.
봉인된 클래스에는 &lt;code&gt;non-sealed&lt;/code&gt;, &lt;code&gt;sealed&lt;/code&gt; 와 &lt;code&gt;final&lt;/code&gt;, 세 키워드중에 하나를 선택해야만 한다. 
non-sealed 은 상속이 가능하다.
final 은 상속이 불가하다.
sealed은 하위 클래스가 있어야 한다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;static void getTransport(TransportSealed transportSealed) {

    switch (transportSealed) {
        case ExpressBus expressBus -&amp;gt; System.out.println(expressBus.getBus());
        case BusSealed bus -&amp;gt; System.out.println(bus.getBus());
        case TaxiSealed taxi -&amp;gt; System.out.println(taxi.getTaxi());
    }
}
&lt;/code&gt;&lt;/pre&gt;

위와 같이 switch을 이용해서 Pattern Matching도 할 수 있다. switch 문에 봉인된 클래스가 하나라도 없으면 컴파일에러가 발생한다.
또한 record 클래스에도 사용할수 있으니 참고하면 되겠다.

&lt;h3&gt;Miscellaneous&lt;/h3&gt;

&lt;h4&gt;Helpful NullPointerExceptions&lt;/h4&gt;

NullPointException의 message가 좀 더 명확하게 나온다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static void lowerCase(String str) {
    System.out.println(str.toLowerCase());
}

String str = null;
//Null Point Exception
lowerCase(str);
&lt;/code&gt;&lt;/pre&gt;

위와 같은 코드가 있을 경우 정확히 어디에서 에러가 발생한지 메시지로 통해 확인할 수 있다.

&lt;pre&gt;&lt;code&gt;Exception in thread &quot;main&quot; java.lang.NullPointerException: Cannot invoke &quot;String.toLowerCase()&quot; because &quot;str&quot; is null
&lt;/code&gt;&lt;/pre&gt;

사실 위의 코드는 굳이 메시지 말고도 라인만 봐도 명확하다. 하지만 좀 더 복잡한 경우에는 도움이 많이 될 것 같다. 아래와 같이 말이다.

&lt;pre&gt;&lt;code&gt;lowerCase(a.b.c.i);
&lt;/code&gt;&lt;/pre&gt;

만약 b가 null이라면 메시지는 다음과 같다.

&lt;pre&gt;&lt;code&gt;Exception in thread &quot;main&quot; java.lang.NullPointerException: Cannot read field &quot;c&quot; because &quot;a.b&quot; is null
&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;toList&lt;/h4&gt;

Strem을 자주 사용한다면 많이 사용되는 terminal operation &lt;code&gt;.collect(Collectors.toList())&lt;/code&gt;을 간편하게 줄일 수 있다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Stream.of(1, 2, 3, 4, 5).toList();
&lt;/code&gt;&lt;/pre&gt;

귀찮게 collect를 사용하지 않아도 바로 List로 만들 수 있다.

필자가 알아본건 여기까지다. 물론 더 많은 내용이 있으니 공식문서들을 잘 살펴보면 되겠다. 사용법들을 알았으니 좀 더 각자 딥하게 들어가는 것도 좋겠다. 
필자는 언젠가 다시 자바를 사용할 수 있으니 그때 더 딥하게 알아보자.&lt;/div&gt;</description>
      <author>머룽</author>
      <guid isPermaLink="true">https://mkzz.tistory.com/341</guid>
      <comments>https://mkzz.tistory.com/341#entry341comment</comments>
      <pubDate>Sun, 23 Apr 2023 14:06:21 +0900</pubDate>
    </item>
    <item>
      <title>Reactor 써보기 (3)</title>
      <link>https://mkzz.tistory.com/340</link>
      <description>&lt;div class='markdown-body'&gt;Reactor를 거의 일년만에 다시 작성한다. 요즘 영 귀찮아서 블로그를 잘 안썼더니 올해 처음으로 작성한다.

&lt;h2&gt;Flux&lt;/h2&gt;

&lt;h3&gt;just, fromIterable, fromStream, range&lt;/h3&gt;

just는 Mono 에서도 배웠다. 동일하게 Flux에서도 just를 통해 Flux를 생성할 수 있다.

&lt;pre&gt;&lt;code&gt;@Test
void fluxJustTest() {
    Flux.just(1, 2, 3, 4, 5)
            .subscribe(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `flux just test`() {\r
    Flux.just(1, 2, 3, 4, 5)\r
            .subscribe {\r
                println(it)\r
            }\r
}\r
[/kotlin]

Mono 와는 조금 다르게 가변인자 파라미터를 받는다. 0 부터 N까지의 스트림을 만들수 있기 때문이다. 실행해보면 1 ~ 5까지 숫자가 출력되는 것을 볼수 있다.
여러개의 엘리먼트를 생성할 수 있으니 &lt;code&gt;Iterable&lt;/code&gt;를 받아 생성할 수 도 있다.

&lt;pre&gt;&lt;code&gt;@Test
void fluxFromIterableTest() {
    Flux.fromIterable(List.of(1, 2, 3, 4, 5))
            .subscribe(System.out::println);

}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `flux fromIterable test`() {\r
    listOf(1, 2, 3, 4, 5).toFlux()\r
            .subscribe {\r
                println(it)\r
            }\r
}\r
[/kotlin]

또한 java8의 &lt;code&gt;Stream&lt;/code&gt;을 받아 생성할 수 도 있다.

&lt;pre&gt;&lt;code&gt;@Test
void fluxStreamTest() {
    Flux.fromStream(Stream.of(1, 2, 3, 4, 5))
            .subscribe(System.out::println);

}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `flux stream test`() {\r
      Stream.of(1, 2, 3, 4, 5).toFlux()\r
            .subscribe {\r
                println(it)\r
            }\r
}\r
[/kotlin]

위 처럼 작성해도 1부터5까지의 숫자가 출력된다. 
어떤 범위를 표현하고 싶다면 &lt;code&gt;range&lt;/code&gt;를 사용하면 된다.

&lt;pre&gt;&lt;code&gt;@Test
void fluxRangeTest() {
    Flux.range(1, 10)
            .subscribe(System.out::println);
    }
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `flux range test`() {\r
    Flux.range(1, 10)\r
            .subscribe {\r
                println(it)\r
            }\r
}\r
\r
[/kotlin]

첫번쨰 파라미터는 시작값이고 두번째 파라미터는 갯수를 의미한다. 
위 내용을 실행해보면 1부터 10까지 숫자가 출력된다.

여기까지는 가장 기본적인 Flux의 생성법을 알아봤다. 대부분 위의 내용을 알고 있을 듯 싶다. 구글링해보면 대부분 위의 내용이 잘 표현되고 있으니 말이다. 
우리는 좀 더 많은 내용을 다뤄보기로 하자.

&lt;h3&gt;concat, merge&lt;/h3&gt;

&lt;code&gt;concat&lt;/code&gt;과 &lt;code&gt;merge&lt;/code&gt;은 거의 동일하게 여러 Publisher를 합쳐 내보낸다. 
하지만 조금 다른 부분이 있는데 concat은 순서대로 동작하며 이전 Publisher 먼저 구독하고 완료된 후에 다음 Publisher 가 동작한다.

&lt;pre&gt;&lt;code&gt;@Test
void fluxConcatTest() throws InterruptedException {
    Flux.concat(Flux.just(1, 2, 3, 4).delayElements(Duration.ofSeconds(1)), Flux.just(5, 6, 7, 8))
            .subscribe(System.out::println);
    TimeUnit.SECONDS.sleep(5);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `flux concat test`() {\r
    Flux.concat(Flux.just(1, 2, 3, 4).delayElements(Duration.ofSeconds(1)), Flux.just(5, 6, 7, 8))\r
            .subscribe {\r
                println(it)\r
            }\r
\r
    TimeUnit.SECONDS.sleep(5)\r
}\r
[/kotlin]

위의 코드는 1 ~ 4까지 1초간격으로 출력되고 5 ~ 6까지는 이전 Publisher(1~4까지)가 완료된후에 한꺼번에 출력된다.

merge의 경우에는 순서가 없으며 다른 Publisher의 영향도 없다.

&lt;pre&gt;&lt;code&gt;@Test
void fluxMergeTest() throws InterruptedException{
    Flux.merge(Flux.just(1, 2, 3, 4).delayElements(Duration.ofSeconds(1)), Flux.just(5, 6, 7, 8))
            .subscribe(System.out::println);
    TimeUnit.SECONDS.sleep(5);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `flux merge test`() {\r
    Flux.merge(Flux.just(1, 2, 3, 4).delayElements(Duration.ofSeconds(1)), Flux.just(5, 6, 7, 8))\r
            .subscribe {\r
                println(it)\r
            }\r
\r
    TimeUnit.SECONDS.sleep(5)\r
}\r
[/kotlin]

위는 첫번째 Publisher와 상관없이 두번째 Publisher 동시에 구독한다. 첫번째 Publisher에게 delay를 주어서 5~8까지 먼저 출력 후 1~4가 다음에 출력된다. 
만약 순서가 중요하다면 concat을 순서가 상관없다면 merge를 이용하면 된다.

concat은 순서대로 구독을한다.

만약 Publisher의 순서가 상관없고 마지막 시퀀스만 순서대로 방출하려면 어떻게 해야 될까? 
Publisher가 빨라도 상관없다면 &lt;code&gt;mergeSequential&lt;/code&gt;를 사용하면 된다. Publisher는 동시에 구독하지만 마지막 시퀀스는 구독 순서대로 방출한다.

&lt;blockquote&gt;
  동시라고 표현했지만 구독은 순서대로 구독한다. 그 차이가 거의 동시이기에 그렇게 표현했다.
&lt;/blockquote&gt;

&lt;h3&gt;mergeSequential&lt;/h3&gt;

&lt;code&gt;concat&lt;/code&gt; 과 &lt;code&gt;merge&lt;/code&gt;를 조금 합쳐놓은듯한 느낌이다. Publisher는 순서와 상관없이 모두 동시에 구독하며 마지막 시퀀스는 구독 순서대로 방출한다.

[kotlin]\r
@Test\r
fun `flux mergeSequential test`() {\r
    Flux.mergeSequential(\r
            Flux.just(1, 2, 3, 4).delayElements(Duration.ofSeconds(1))\r
                    .doOnNext { println(&quot;first : $it&quot;) },\r
            Flux.just(5, 6, 7, 8)\r
                    .doOnNext { println(&quot;second : $it&quot;) })\r
            .subscribe {\r
                println(it)\r
            }\r
\r
    TimeUnit.SECONDS.sleep(5)\r
}\r
[/kotlin]

위의 코드를 실행하면 어떻게 출력될까? 한번 &lt;code&gt;merge&lt;/code&gt;경우에도 어떻게 출력될지 상상해보도록 하자.
Publisher는 순서와 상관없이 동시에 구독하지만 마지막 시퀀스는 순서를 보장한다.
그래서 5~8까지 doOnNext의 로그가 출력되고 1~4까지의 doOnNext와 마지막 시퀀스가 방출된다.

&lt;pre&gt;&lt;code&gt;second : 5
second : 6
second : 7
second : 8
first : 1
1
first : 2
2
first : 3
3
first : 4
4
5
6
7
8
&lt;/code&gt;&lt;/pre&gt;

위는 &lt;code&gt;mergeSequential&lt;/code&gt;의 결과이다. 마지막 시퀀스는 순서가 보장되었다.

&lt;h3&gt;mergeOrdered&lt;/h3&gt;

merge의 종류도 많다. 사실 이건 언제 사용할지도 감이 잘 오지 않는다. 뭐 하다보면 생길지도 모르겠지만.. 
엘리먼트 순서와 관련이 있다. 일단 기본은 오름차순인데 내림차순으로 변경도 할수 있다.
Publisher들의 엘리먼트들 순서대로 가장 작은 엘리먼트를 비교하여 방출한다. 말이 어렵구만. 한번 예제를 보자.

[kotlin]\r
@Test\r
fun `flux mergeOrdered test`() {\r
    Flux.mergeOrdered(Flux.just(9, 6, 11, 3), Flux.just(2, 10, 1, 4))\r
            .subscribe {\r
                println(it)\r
            }\r
}\r
[/kotlin]

예를들어 위의 두개의 Publisher 실행한다고 해보자.
9-2 를 비교하여 작은 엘리먼트를 방출한다.
2가 방출되었으니 같은 Publisher에 있는 2 다음의 10과 9를 비교하여 작은 엘리먼트인 9를 방출한다.
다음 9가 방출되었으니 6과 10을 비교하여 작은 엘리먼트인 6이 방출된다.
6이 방출되었으니 같은 Publisher에 있는 11과 10을 비교하여 작은 엘리먼트인 10을 방출한다.
이런식으로 모든 엘리먼트를 방출한다.

&lt;pre&gt;&lt;code&gt;2
9
6
10
1
4
11
3
&lt;/code&gt;&lt;/pre&gt;

물론 필자는 숫자로 했지만 Comparable를 구현한 클래스라면 뭐든 가능하다.

&lt;h3&gt;combineLatest&lt;/h3&gt;

이거 역시 말로 설명하기 힘들다. 메서드명과 같이 마지막으로 결합되는 엘리먼트를 방출하는 역할을 하는 메서드이다.
먼저 예제를 보자.

&lt;pre&gt;&lt;code&gt;@Test
void fluxCombineLatestTest() throws InterruptedException {
    Flux.combineLatest(Flux.just(1, 2, 3).delayElements(Duration.ofMillis(80)), Flux.just(4, 5, 6).delayElements(Duration.ofMillis(100)), (a, b) -&amp;gt; a + &quot;, &quot; + b)
            .subscribe(System.out::println);
    TimeUnit.SECONDS.sleep(5);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `flux combineLatest test`() {\r
    Flux.combineLatest(Flux.just(1, 2, 3).delayElements(Duration.ofMillis(80)), Flux.just(4, 5, 6).delayElements(Duration.ofMillis(100))) { a, b -&gt;\r
        &quot;$a, $b&quot;\r
    }.subscribe {\r
        println(it)\r
    }\r
    TimeUnit.SECONDS.sleep(5)\r
}\r
[/kotlin]

위에서 말했다시피 마지막으로 결합되는 즉시 방출한다.

예를들어 첫번째 Publisher는 &lt;code&gt;80밀리세컨드&lt;/code&gt;로 delay를 시켰고 두번째 Publisher는 &lt;code&gt;100밀리세컨드&lt;/code&gt;로 delay를 주었다.

1이 80ms 뒤에 방출되었지만 4가 20ms동안 더 대기를 해야되므로 100ms 뒤에 1, 4 가 출력된다.
그 후 2는 60ms 뒤에 방출되므로 2, 4 가 출력된다.
다음은 40ms 뒤에 두번째 Publisher에 있는 5가 방출 되어 2, 5가 출력된다.
이런식으로 마지막으로 결합되는 즉시 방출한다. 시원찮지만 대충 그림을 보면 아래와 같다.

&lt;pre&gt;&lt;code&gt;     80ms 100ms   160ms  200ms   240ms
======== 1 ======== 2 ====   ==== 3 
========== 4 ======   ==== 5 ====   ====== 6

========== 1 ====== 2 ==== 2 ==== 3 ====== 3
           4        4      5      5        6

&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;generate&lt;/h3&gt;

generate는 신호를 차례대로 생성하는 메서드이다. 일단 코드를 보자.

&lt;pre&gt;&lt;code&gt;@Test
void fluxGenerateTest() {
    Flux.generate(() -&amp;gt; 1, (number, sink) -&amp;gt; {
        if (number == 10) {
            sink.complete();
        } else {
            sink.next(number);
        }
        return number + 1;
    }).subscribe(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `flux generate test`() {\r
    Flux.generate({ 1 }, { number, sink: SynchronousSink&lt;Int&gt; -&gt;\r
        if (number == 10) {\r
            sink.complete()\r
        } else {\r
            sink.next(number)\r
        }\r
        number + 1\r
    }).subscribe {\r
        println(it)\r
    }\r
}\r
[/kotlin]

위의 코드는 1 부터 9까지 생성하는 Flux 이다. 첫번째 파라미터는 최초값이며 두번째 파라미터는 BiFunction을 받고 있으며 현재 상태를 사용하여 신호를 보낸후 다음 상태의 값을 반환한다. 내부 상태값을 갖고 있으니 상태에 맞게 next, complete, error 신호를 발생시키면 된다.
만약 무한으로 신호를 보내고 싶다면 complete, error 신호를 보내지 않으면 된다.

&lt;h3&gt;generate vs create&lt;/h3&gt;

Flux 의 create는 설명하지 않았지만 Mono의 create와 사용법은 동일하기에 생략했다. (멀티스레드를 제외한 기능은 동일하다)
generate와 create의 차이점을 살펴보자.

&lt;ol&gt;
&lt;li&gt;generate는 내부에 상태값이 있으며 create는 상태값이 없다.&lt;/li&gt;
&lt;li&gt;generate는 next를 한번만 호출해야되는 반면 create는 여러번 호출해도 된다.&lt;/li&gt;
&lt;li&gt;generate 동기식프로그램만 가능하지만 create는 비동기 멀티스레드 프로그래밍이 가능하다.&lt;/li&gt;
&lt;/ol&gt;

두 메서드를 비교를 했지만 쓰임새가 전혀 다르다. 오히려 &lt;code&gt;create&lt;/code&gt;와 &lt;code&gt;push&lt;/code&gt;를 비교하는게 좀 더 나아 보인다.

좀 더 자세한 내용은 문서를 보는 것을 추천한다. 그래야 이해가 더 빠를 것 같다.
또한 실제로 직접 사용해보고 예제 코드들을 살펴봐야 좀 더 도움이 될 것이다.

오늘 이렇게 Flux 생성에 대해서 알아봤다. 좀 더 많은 사용법이 있지만 이전글 Mono 생성하기에서 겹치는 부분도 있고 필자가 써보지 않았던 메서드들을 작성하지 않았다.

다음시간에는 Flux 오퍼레이터에 대해 알아보도록 하자.&lt;/div&gt;</description>
      <author>머룽</author>
      <guid isPermaLink="true">https://mkzz.tistory.com/340</guid>
      <comments>https://mkzz.tistory.com/340#entry340comment</comments>
      <pubDate>Sun, 23 Apr 2023 14:06:20 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 2.4 Config file processing</title>
      <link>https://mkzz.tistory.com/339</link>
      <description>&lt;div class='markdown-body'&gt;Spring boot 2.4-m2 버전이 저번달(8/14)에 릴리즈 되었다. 그러기엔 이글이 좀 늦은 듯 하다. 한달이나 지나서야..원..
Spring Boot 의 m2 버전에서 여러 추가 기능이 있지만 제일 큰 변화는 아마도 Config file processing 처리하는 방법이 아닐까 싶다. 
나중에 정식 릴리즈가 되면 다른 특징들도 알아보고 오늘은 Config file processing 관한 변화의 특징들을 알아보도록 하자.

spring boot 2.3 까지 Kubernetes 지원을 열심히 하고 있다. 하지만 그 중에 할 수 없었던 부분이 volume mounted configuration라는 기능이다. (사실 필자도 잘 모름) Kubernetes 에서 인기있는 기능이라고 하니.. 하지만 이 기능을 지원하기 위해서는 &lt;code&gt;ConfigFileApplicationListener&lt;/code&gt; 클래스를 수정해야만 했다고 한다.

개발을 하다보면 때때로 변경하기 어려운 코드들이 존재한다. 사실 이건 어느 누구에게나 닥칠 수 있는 상황이라고 생각한다. 그 중 &lt;code&gt;ConfigFileApplicationListener&lt;/code&gt;는 변경하기 어려운 코드중 하나라고 판단 되었다고 한다. 사실 ConfigFileApplicationListener 클래스는 코드를 잘못 작성하거나 혹은 테스트 코드 누락이 된 것이 아니라 기능을 추가 하면서 그 클래스가 할 수 있는 일을 다했다고 판단 되었다.

&lt;h3&gt;ConfigFileApplicationListener 문제&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;spring.profiles: local
foo.username: userb
foo.password: userb
---
spring.profiles: !dev
spring.profiles.include: local
foo.username: userc
&lt;/code&gt;&lt;/pre&gt;

위와 같이 설정후에 &lt;code&gt;--spring.profiles.active=prod&lt;/code&gt; 로 실행하는 경우 어떤 값들이 나와야 할까? 조금 애매한 부분이 많다. &lt;code&gt;prod&lt;/code&gt;으로 프로파일을 설정했으니 &lt;code&gt;!dev&lt;/code&gt; 프로파일이 동작할텐데 foo.username 과 foo.password 값은 어떤 값이 될까? include가 local 로 되어 있으니 local 프로파일을 오버라이딩 할까? 그럼 foo.password 의 값은 뭐가 될까? 사실 돌려보면 다음과 같다.

&lt;pre&gt;&lt;code&gt;userc
null
&lt;/code&gt;&lt;/pre&gt;

근데 이게 맞는 걸까? 필자도 잘 모르겠다. 사실 이건 프로파일을처리 할 때 활성화 되지 않았다. (prod 와 정확한 매칭되는게 없어) 그래서 local 이라는 프로파일을 포함시키지 못한다. 이러한 처리는 자주 논란이 되었다고 한다. 문제가 제기 되면 그 문제를 해결 할때마다 다른 문제들이 생기곤 한다. 그래서 Spring boot 쪽에선 이와 같은 문제를 해결하고자 다시 재정의 했다.

&lt;ul&gt;
&lt;li&gt;문서의 정렬&lt;/li&gt;
&lt;li&gt;프로파일에 더이상 추가 프로파일 작성 불가&lt;/li&gt;
&lt;/ul&gt;

Spring boot 에선 위와 같이 새롭게 정의 하였다.

&lt;h3&gt;문서 정렬&lt;/h3&gt;

문서 정렬은 아주 간단하다. 하위 문서가 상위 문서를 오버라이딩한다.

&lt;pre&gt;&lt;code&gt;spring.profiles: test
foo.username: wonwoo
foo.password: wonwoo123
---
spring.profiles: dev
foo.username: fidel
foo.password: fidel123
---
foo.username: overridden-wonwoo
&lt;/code&gt;&lt;/pre&gt;

위와 같이 문서가 작성 되고 &lt;code&gt;--spring.profiles.active=test&lt;/code&gt;로 실행을 한다면 어떤 결과가 나올까? spring2.3 이전버전과 spring 2.4 버전에선 다른 결과가 출력 된다.

2.3

&lt;pre&gt;&lt;code&gt;wonwoo
wonwoo123
&lt;/code&gt;&lt;/pre&gt;

2.4

&lt;pre&gt;&lt;code&gt;overridden-wonwoo
wonwoo123
&lt;/code&gt;&lt;/pre&gt;

위와 같이 하위 문서가 상위 문서를 재정의 하도록 변경 되었다. 추후에 2.4로 마이그레이션 할 때 주의할 점이다.

&lt;h3&gt;멀티 프로퍼티&lt;/h3&gt;

yaml을 파일과 비슷한 기능이다. yaml 파일은 &lt;code&gt;---&lt;/code&gt;구분자를 통해 한 파일에 여러 프로파일을 작성 할 수 있었다. 이제는 properties 파일도 가능하게 지원한다. (필자는 프로퍼티파일을 더 선호에서 좋은 기능같다.)

&lt;pre&gt;&lt;code&gt;spring.profiles=test
foo.username=wonwoo
foo.password=wonwoo123
#---
spring.profiles=dev
foo.username=fidel
foo.password=fidel123
&lt;/code&gt;&lt;/pre&gt;

프로퍼티파일은 &lt;code&gt;#---&lt;/code&gt; 구분자를 통해 구분할 수 있다. 이제는 여러 파일로 나누지 않고도 한 파일에 환경별로 분리 할 수 있어 좋은 것 같다. 하지만 아직 IDEA 에선 빨간줄이..

&lt;h3&gt;spring.config.activate.on-profile&lt;/h3&gt;

&lt;code&gt;spring.profiles&lt;/code&gt;에서 &lt;code&gt;spring.config.activate.on-profile&lt;/code&gt;로 변경 되었다.
아직은 spring.profiles 을 사용할 수는 있지만 deprecated 되어 있으니 참고하면 되겠다.

&lt;pre&gt;&lt;code&gt;spring.config.activate.on-profile=test
foo.username=wonwoo
foo.password=wonwoo123
#---
spring.config.activate.on-profile=dev
foo.username=fidel
foo.password=fidel123
&lt;/code&gt;&lt;/pre&gt;

위와 같이 작성하면 기존의 spring.profiles 과 동일한 효과를 얻을 수 있다.

하지만 여기서 조금 주의할 점이 있다.

만약 &lt;code&gt;spring.config.activate.on-profile&lt;/code&gt; 설정과 &lt;code&gt;spring.profiles&lt;/code&gt; 설정을 함께 사용하면 안된다.

&lt;pre&gt;&lt;code&gt;spring.config.activate.on-profile=test
spring.profiles=dev
foo.username=wonwoo
foo.password=wonwoo123
&lt;/code&gt;&lt;/pre&gt;

위와 같이 사용할 경우에는 에러가 발생한다. 하지만 아래와 같은 경우는 상관없다.

&lt;pre&gt;&lt;code&gt;spring.config.activate.on-profile=test
foo.username=wonwoo
foo.password=wonwoo123
#---
spring.profiles=dev
foo.username=fidel
foo.password=fidel123
&lt;/code&gt;&lt;/pre&gt;

각 환경별로는 다르게 사용해도 에러가 나지 않는다. 하지만 일관성있게 사용하는 것이 좋아보인다.

&lt;h3&gt;프로파일에 더이상 추가 프로파일 작성 불가&lt;/h3&gt;

이제는 정의된 프로파일에 더 이상 추가 프로파일을 작성 할 수 없다. 다음 설정 파일을 보자.

&lt;pre&gt;&lt;code&gt;spring.config.activate.on-profile=test
spring.profiles.include=dev
foo.username=wonwoo
foo.password=wonwoo123
#---
spring.config.activate.on-profile=dev
foo.username=fidel
foo.password=fidel123
&lt;/code&gt;&lt;/pre&gt;

이전에는 위와 같이 특정 프로파일에 추가적으로 프로파일을 포함시킬 수 있었다. 하지만 이제는 더이상 추가적으로 프로파일을 포함 할 수 없다. 위와 같이 작성한다면 에러가 발생한다.

spring.profiles 을 사용해도 마찬가지다.
좀 더 빠르게 이해가 되며 쉽게 관리 할 수 있다는 장점이 있을 것 같다. 허나 여러 종류의 프로파일들을 함께 사용하고 싶을 때는 어떻게 할까? 
Spring boot 의 새기능인 Profiles Groups 기능을 사용하면 된다.

&lt;h3&gt;Profiles Groups&lt;/h3&gt;

이제는 Profiles을 group 을 지정할 수 있다. 여러 프로파일들을 한꺼번에 그룹지어 하나의 프로파일로 만들수 있다.

&lt;pre&gt;&lt;code&gt;spring.config.activate.on-profile=test
foo.username=wonwoo
foo.password=wonwoo123
#---
spring.config.activate.on-profile=dev
foo.username=fidel
foo.password=fidel123
#---
spring.profiles.group.testdev=test,dev

&lt;/code&gt;&lt;/pre&gt;

위와 같이 test, dev 을 그룹지어 하나의 프로파일을 만들 수 있다. &lt;code&gt;include&lt;/code&gt; 기능보다 좀 더 쉽게 추론할 수 있을 듯 싶다. 
또 한 @Configuration 사용해 @Profile 설정을 할 떄 유용하다.

&lt;pre&gt;&lt;code&gt;@Profile(&quot;devDb&quot;)
@Configuration
class DevDataBase {

}

@Profile(&quot;devMessage&quot;)
@Configuration
class DevMessage {

}

spring.profiles.group.dev=devDb,devMessage
&lt;/code&gt;&lt;/pre&gt;

이와 같이 &lt;code&gt;dev&lt;/code&gt; 프로파일에 message 설정과 db 설정을 한꺼번에 넣을 수 있다. 테스트할 때도 좋은 기능같고 다른 설정으로 배포할 경우에도 좋은 기능 같다.

&lt;h3&gt;Import Properties&lt;/h3&gt;

이전 Spring boot는 application.properties, application.yml 이외에 추가적인 설정파일을 가져오는데 &lt;code&gt;spring.config.additional-location&lt;/code&gt; 사용하여 가져올 수 있었으나 application.properties 로 작성시에는 가져오지 못하고 환경변수나 인자로 넘거야 했었다. 또 한 파일 유형이 너무 제한적이다.

어쩄든 이 불편함을 수정하기 위해 새로운 설정으로 추가적인 파일들을 추가할 수 있다.

&lt;pre&gt;&lt;code&gt;spring.config.import=classpath:/test/test.properties
spring.config.import=file:/test/test.properties
spring.config.import=configtree:/test/test.properties
&lt;/code&gt;&lt;/pre&gt;

위와 같이 &lt;code&gt;spring.config.import&lt;/code&gt;를 이용해서 추가적인 파일들을 가져올 수 있다. 클래스패스, 파일, 혹은 configtree(Kubernetes 에서 사용하는 듯) 를 prefix를 사용해 다양한 유형들을 설정할 수 있다.

이 뿐만이 아니라 다른 파일 유형들도 적용할 수 있다. 예를 들어 추후에는 archaius, vault, zookeeper 와 같이 외부 설정을 불러 올 수 있도록 확장가능하다.

&lt;pre&gt;&lt;code&gt;archaius://
vault://
zookeeper://
&lt;/code&gt;&lt;/pre&gt;

대략 위와 같은 모양이 되겠다.

만약 import할 대상의 파일이 없을 경우 에러가 발생한다. 그래서 파일이 없더라도 에러가 발생하지 않고 무시할 수 있는 기능이 있다. 그러기 위해서는 다음과 같이 &lt;code&gt;optional:&lt;/code&gt; 을 prefix로 지정하면 된다.

&lt;pre&gt;&lt;code&gt;spring.config.import=optional:classpath:/test/test.properties
spring.config.import=optional:file:/test/test.properties
spring.config.import=optional:configtree:/test/test.properties

&lt;/code&gt;&lt;/pre&gt;

위와 같이 &lt;code&gt;optional:&lt;/code&gt; 사용하면 파일이 없더라도 에러가 나지 않는다. 뿐만아니라 기존에 사용하고 있던 &lt;code&gt;spring.config.additional-location&lt;/code&gt; 설정도 동일하게 optional을 사용할 수 있다.

만약 모든 항목에 optional 처럼 기능을 작동하고 싶다면 &lt;code&gt;spring.config.on-location-not-found=ignore&lt;/code&gt;, 혹은 &lt;code&gt;SpringApplication.setDefaultProperties(…​)&lt;/code&gt; 메서드를 사용해서 속성을 넣을 수 있다.

&lt;pre&gt;&lt;code&gt;spring.config.import=classpath:/test/test.properties
&lt;/code&gt;&lt;/pre&gt;

&lt;code&gt;ignore&lt;/code&gt; 설정 후 위와 같이 &lt;code&gt;optional:&lt;/code&gt;를 제거해도 파일이 없다는 에러는 발생하지 않는다.

&lt;h3&gt;레거시 사용&lt;/h3&gt;

기존의 레거시(ConfigFileApplicationListener)를 사용해서 기존과 동일한 프로파일 설정들을 사용할 수 있다. 위의 내용이 아직 불편하다면 굳이 사용할 필요 없이 기존 설정과 동일하게 사용할 수 있다.

&lt;pre&gt;&lt;code&gt;spring.config.use-legacy-processing=true
&lt;/code&gt;&lt;/pre&gt;

&lt;code&gt;spring.config.use-legacy-processing&lt;/code&gt; 를 사용하면 기존 프로파일 설정(ConfigFileApplicationListener) 그대로 사용할 수 있다. 하지만 천천히 조금은 익숙해져야 되지 않을까 싶다.

오늘은 이렇게 spring boot 2.4의 새로운 config file processing 대해서 알아봤다. 만약에 추후에 2.4로 마이그레이션 할 때 조금 눈여겨 봐야할 특징들이다. 아직은 레거시도 지원하기에 급하지 않겠지만 Spring boot 특성상 바로 다음 메이저 업그레이드할 때 삭제 될 것 같다. Spring boot 는 대부분 deprecated 시킨 버전 바로 다음 버전에 대부분 삭제했다. 아마도 2.5 에는 삭제 되지 않을까 생각된다.&lt;/div&gt;</description>
      <author>머룽</author>
      <guid isPermaLink="true">https://mkzz.tistory.com/339</guid>
      <comments>https://mkzz.tistory.com/339#entry339comment</comments>
      <pubDate>Sun, 23 Apr 2023 14:06:18 +0900</pubDate>
    </item>
    <item>
      <title>Reactor 써보기 (2)</title>
      <link>https://mkzz.tistory.com/338</link>
      <description>&lt;div class='markdown-body'&gt;오늘은 저번시간에 이어서 Reactor 써보기 (2)를 준비했다.

저번시간에는 Mono를 만드는 것을 배웠다. 필자도 아직까지는 써보지 않은 것들도 많이 존재한다. 대부분 쓰는 것들만 자주 쓰기에..

오늘은 저번시간에 이어서 Mono 오퍼레이터를 알아보도록 하자. 물론 다 알아보진 못할 거 같고(워낙 많아서..) 자주 사용될만한 것들 위주로 살펴 볼 예정이니 여기에 없는 것들은 문서를 찾아보면 되겠다.

&lt;h3&gt;map, flatMap, flatMapMany, filter&lt;/h3&gt;

사실 이것들은 java8 에 나온 Stream API의 map 과 flatMap 과 사용법은 동일하다. 흔히 함수형 프로그래밍에서 말하는 functor, monad 라 하는 것들 처럼 의미하는 바도 동일하다. 이 부분을 더 알고 싶다면 함수형 프로그래밍을 공부하면 되겠다. 사실 필자도 잘..

이 오퍼레이션은 대부분 다 알고 있을거라 생각되기 때문에 간단한 사용법만 보고 넘어가자!

&lt;pre&gt;&lt;code&gt;@Test
public void mapTest() {
    Mono.just(&quot;hello&quot;)
            .map(it -&amp;gt; it + &quot; world&quot;)
            .subscribe(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
@Test\r
fun `map test`() {\r
    Mono.just(&quot;hello&quot;)\r
        .map {\r
            &quot;$it world&quot;\r
        }.subscribe {\r
            println(it)\r
        }\r
}\r
\r
[/kotlin]

&lt;pre&gt;&lt;code&gt;@Test
public void flatMapTest() {
    Mono.just(&quot;hello&quot;)
            .flatMap(it -&amp;gt; Mono.just(it + &quot; world&quot;))
            .subscribe(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
@Test\r
fun `flat map test`() {\r
    Mono.just(&quot;hello&quot;)\r
        .flatMap {\r
            Mono.just(&quot;$it world&quot;)\r
        }.subscribe {\r
            println(it)\r
        }\r
\r
}\r
\r
[/kotlin]

&lt;pre&gt;&lt;code&gt;@Test
void filterTest() {
    Mono.just(&quot;filter&quot;)
            .filter(it -&amp;gt; it.equals(&quot;filter&quot;))
            .subscribe(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
@Test\r
fun `filter test`() {\r
    Mono.just(&quot;filter&quot;)\r
        .filter {\r
            it == &quot;filter&quot;\r
        }.subscribe {\r
            println(it)\r
        }\r
}\r
\r
[/kotlin]

사실 flatMapMany는 java의 Stream API 에는 존재하지는 않지만 flatMap과 동일하다.

&lt;pre&gt;&lt;code&gt;public final &amp;lt;R&amp;gt; Mono&amp;lt;R&amp;gt; flatMap(Function&amp;lt;? super T, ? extends Mono&amp;lt;? extends R&amp;gt;&amp;gt; transformer)

public final &amp;lt;R&amp;gt; Flux&amp;lt;R&amp;gt; flatMapMany(Function&amp;lt;? super T, ? extends Publisher&amp;lt;? extends R&amp;gt;&amp;gt; mapper)

&lt;/code&gt;&lt;/pre&gt;

Type 이 Mono 냐 Publisher냐의 차이이며 리턴타입도 Flux로 리턴한다. 
flatMapMany 일경우에는 Mono, Flux 혹은 다른 reactive streams api 타입도 가능하다.

&lt;pre&gt;&lt;code&gt;@Test
public void flatMapManyTest() {
    Mono.just(&quot;hello&quot;)
            .flatMapMany(it -&amp;gt; Flux.just(&quot;$it world&quot;, &quot;$it world!!&quot;))
            .subscribe(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
@Test\r
fun `flat map many test`() {\r
    Mono.just(&quot;hello&quot;)\r
        .flatMapMany {\r
            Flux.just(&quot;$it world&quot;, &quot;$it world!!&quot;)\r
        }.subscribe {\r
            println(it)\r
        }\r
}\r
\r
[/kotlin]

java8의 Stream API를 사용했다면 그리 어려운 내용은 아닌 듯 싶다.
사실 이것들은 굉장히 자주 사용하는 메서드 중 하나다. 이것만 알아도? 대부분 무난하게 개발도 가능할 듯 싶다. 
하지만 reactor 에서는 좀 더 우아한 방법의 메서드들이 존재하니 살펴보자.

&lt;h3&gt;zipWhen&lt;/h3&gt;

위에서 간단하게 맛보기로 아마? 기존의 알던 오퍼레이터를 알아봤지만 이제부터는 reactor 만의 메서드를 알아보도록 하자.

이것은 필자가 실제로 map, flatMap 만큼 자주 사용하는 메서드중 하나다. flatMap가 사용법은 동일하지만 onNext 방출이 다르다. zip 이란 메서드에 걸맞게 묶어서 결과를 받을 수 있다.

&lt;pre&gt;&lt;code&gt;@Test
public void zipWhenTest() {
    Mono.just(&quot;hello&quot;)
            .zipWhen(it -&amp;gt; Mono.just(it + &quot; world&quot;))
            .subscribe((Tuple2&amp;lt;String, String&amp;gt; it) -&amp;gt; System.out.println(it.getT1() + &quot;, &quot; + it.getT2()));
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `zip when test`() {\r
    Mono.just(&quot;hello&quot;)\r
        .zipWhen {\r
            Mono.just(&quot;$it world&quot;)\r
        }.subscribe { it: Tuple2&lt;String, String&gt; -&gt;\r
            println(&quot;${it.t1}, ${it.t2}&quot;)\r
        }\r
}\r
\r
[/kotlin]

필자는 타입을 보여주기 위함이지 생략해도 문제 없다. 
출력은 다음과 같을 것이다.

&lt;pre&gt;&lt;code&gt;hello, hello world
&lt;/code&gt;&lt;/pre&gt;

쉽게 생각해서 처음 Mono를 onNext 신호를 받을 수 있다. 만약 처음 만든 Mono 를 onNext 에서 사용해야 된다면 &lt;code&gt;zipWhen&lt;/code&gt; 메서드를 사용하면 된다.

&lt;h3&gt;zipWith&lt;/h3&gt;

zipWith 메서드는 사실 저번에 배운 Mono.zip 과 동작 방식은 동일하다. 이 역시 모노들이 각각 동작한다. 일단 예제를 살펴보자.

&lt;pre&gt;&lt;code&gt;@Test
public void zipWithTest() {
    Mono.just(&quot;hello&quot;)
            .zipWith(Mono.just(&quot;world&quot;))
            .subscribe((Tuple2&amp;lt;String, String&amp;gt; it) -&amp;gt; System.out.println(it.getT1() + &quot;, &quot; + it.getT2()));
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `zip with test`() {\r
\r
    Mono.just(&quot;hello&quot;)\r
        .zipWith(Mono.just(&quot;world&quot;))\r
        .subscribe { it: Tuple2&lt;String, String&gt; -&gt;\r
            println(&quot;${it.t1}, ${it.t2}&quot;)\r
        }\r
}\r
\r
[/kotlin]

zip 과 동일하게 onNext 신호는 동일하다. 실제로 내부적으로는 Mono.zip 을 사용한다. 사실 필자는 이 메서드는 잘 사용하지 않는다. 그냥 Mono.zip() 을 더욱 선호하는 편이긴 하다. 만약 aggregating 하는 Mono 들이 여러개 늘어 난다면 사실 코드 보기가 좀 지저분해 보일 수도 있을 거 같아 그냥 zip 메서드를 사용한다. 사용하고 싶다면 위 예제처럼 두개의 모노만을 묶는다면 사용해도 괜찮을 듯 싶다.

[kotlin]\r
@Test\r
fun `zip with test`() {\r
\r
    Mono.just(&quot;hello&quot;)\r
        .zipWith(Mono.just(&quot;world&quot;)).zipWith(Mono.just(&quot;!!!&quot;))\r
        .subscribe { it: Tuple2&lt;Tuple2&lt;String, String&gt;, String&gt; -&gt;\r
            println(&quot;${it.t1.t1} ${it.t1.t2}${it.t2}&quot;)\r
        }\r
}\r
\r
[/kotlin]

만약 Mono 가 여러개라면 위와 같은 코드가 될 것 같다. 만약 더 많은 모노가 있다면 onNext 타입이 정말 복잡할 듯 싶다.

&lt;h3&gt;concatWith&lt;/h3&gt;

이것은 메서드명 그대로 연결하는 메서드이다. 연결후 Flux 타입으로 리턴한다. 파라미터는 꼭 Mono타입이 아니더라도 가능하다.

&lt;pre&gt;&lt;code&gt;public final Flux&amp;lt;T&amp;gt; concatWith(Publisher&amp;lt;? extends T&amp;gt; other)
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code&gt;@Test
public void concatWithTest() {
    Mono.just(&quot;hello&quot;)
            .concatWith(Mono.just(&quot;world&quot;))
            .subscribe(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `concat with test`() {\r
    Mono.just(&quot;hello&quot;)\r
        .concatWith(Mono.just(&quot;world&quot;))\r
        .subscribe {\r
            println(it)\r
        }\r
}\r
\r
[/kotlin]

출력은 다음과 같다

&lt;pre&gt;&lt;code&gt;hello
world
&lt;/code&gt;&lt;/pre&gt;

딱히 어려운 부분은 없다.

이 메서드는 순차적으로 진행되기 때문에 만약 병렬로 진행해도 된다면 아래의 mergeWith를 사용하면 된다.

&lt;h3&gt;mergeWith&lt;/h3&gt;

&lt;code&gt;concatWith&lt;/code&gt; 와 사용법은 동일하지만 이 메서드는 순차적이지 않다. concatWith 달리 각각 실행 후 합쳐서 onNext를 방출한다. 순차적으로 실행시킬 필요가 없을 경우 이 메서드를 사용하면 된다. 그렇기 때문에 순서는 보장하지 않는다. 이 역시 합치는 기능이므로 Flux 를 리턴한다.

&lt;pre&gt;&lt;code&gt;public final Flux&amp;lt;T&amp;gt; mergeWith(Publisher&amp;lt;? extends T&amp;gt; other)
&lt;/code&gt;&lt;/pre&gt;

이 역시 파라미터 타입은 꼭 Mono 일 필요는 없다.

&lt;pre&gt;&lt;code&gt;@Test
public void mergeWithTest() {
    Mono.just(&quot;hello&quot;)
            .mergeWith(Mono.just(&quot;world&quot;))
            .subscribe(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `merge with test` () {\r
    Mono.just(&quot;hello&quot;)\r
        .mergeWith(Mono.just(&quot;world&quot;))\r
        .subscribe {\r
            println(it)\r
        }\r
}\r
[/kotlin]

필자의 경우 종종 mergeWith 는 사용했다. 아쉽게도 concatWith는 사용한적이 없다. 언젠가 필요할 때가 있겠지..

&lt;h3&gt;then&lt;/h3&gt;

then 오퍼레이션은 몇가지 존재하는데 대부분 비슷하다. 조금씩 다르니 참고하면 되겠다. 
아무 파라미터가 없는 경우 &lt;code&gt;then()&lt;/code&gt;은 리턴 값은 Void 타입이다. 즉, onNext는 방출하지 않는 다는 뜻과 같다. onNext만 방출하지 않지 에러와 완료신호는 방출한다.

&lt;pre&gt;&lt;code&gt;@Test
fun `then test`() {
    Mono.just(&quot;then&quot;).then()
        .subscribe({ println(it) }, {println(&quot;error&quot;)}) { println(&quot;complete&quot;) }
}

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `then test`() {\r
    Mono.just(&quot;then&quot;).then()\r
        .subscribe({ println(it) }, {}) { println(&quot;complete&quot;) }\r
}\r
\r
[/kotlin]

출력 결과는 보면 complete 이외엔 아무것도 출력 되지 않는다. 만약 error 가 방출 될경우에는 &lt;code&gt;error&lt;/code&gt; 메시지가 출력 된다. 
이것은 종종 운영에서도 사용했다. 예를들어 message 를 publisher 한다거나 혹은 consumer 할 때 주로 사용했다. 혹은 spring 의 &lt;code&gt;EventListener&lt;/code&gt;를 사용할 때도 사용했다. 종종 쓸일이 있으니 알아두면 좋을 것 같다.

then 메서드에 파라미터를 받는 메스드가 존재한다. 메서드 형태는 다음과 같다.

&lt;pre&gt;&lt;code&gt;public final &amp;lt;V&amp;gt; Mono&amp;lt;V&amp;gt; then(Mono&amp;lt;V&amp;gt; other)
&lt;/code&gt;&lt;/pre&gt;

해당 모노를 완료시킨후에 다른 모노를 연결하는 오퍼레이터이다. 이는 순차적이다. 이 메서드는 onNext를 방출 할 수 있다.

&lt;pre&gt;&lt;code&gt;@Test
public void thenTest() {
    Mono.just(&quot;then1&quot;).then(Mono.just(&quot;then2&quot;))
            .subscribe(System.out::println, (e) -&amp;gt; System.out.println(&quot;error&quot;), () -&amp;gt; System.out.println(&quot;complete&quot;));
}


&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `then test`() {\r
    Mono.just(&quot;then1&quot;).then(Mono.just(&quot;then2&quot;))\r
        .subscribe({ println(it) }, {println(&quot;error&quot;)}) { println(&quot;complete&quot;) }\r
}\r
\r
[/kotlin]

위의 결과는 &lt;code&gt;then2&lt;/code&gt; 와 &lt;code&gt;complete&lt;/code&gt; 가 출력된다. 만약 기존 모노가 &lt;code&gt;empty&lt;/code&gt; 일지 라도 다음 모노는 실행 시킨다.

&lt;pre&gt;&lt;code&gt;@Test
public void thenTest() {
    Mono.empty().then(Mono.just(&quot;then2&quot;))
            .subscribe(System.out::println, (e) -&amp;gt; System.out.println(&quot;error&quot;), () -&amp;gt; System.out.println(&quot;complete&quot;));
}

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `then test`() {\r
    Mono.empty&lt;String&gt;().then(Mono.just(&quot;then2&quot;))\r
        .subscribe({ println(it) }, {println(&quot;error&quot;)}) { println(&quot;complete&quot;) }\r
}\r
\r
[/kotlin]

위처럼 작성해도 동일하게 동작한다. 이 메서드 역시 운영에서 종종사용했다. 
더 많은 메서드들이 있긴한데 간단하게 설명만 하고 넘어가도록 하자.

&lt;code&gt;thenEmpty&lt;/code&gt; 는 Void 타입을 받는 Publisher이다. 즉, 실행만 시키고 onNext는 방출하지 않는다는 뜻이다. 내부적으로는 then(Mono&lt;V&gt; other) 메서드를 호출한다.
&lt;code&gt;thenReturn&lt;/code&gt;은 Publisher 타입이 아닌 T 타입을 받는 메서드이다. 내부적으로는 then(Mono&lt;V&gt; other)를 호출한다.
&lt;code&gt;thenMany&lt;/code&gt;는 Publisher 타입을 받는 메서드이다. then(Mono&lt;V&gt; other)랑 비슷해보이지만 리턴 타입은 Flux를 리턴한다.

&lt;h3&gt;handle&lt;/h3&gt;

handle 메서드는 좀 더 우아하게 onNext, 에러 혹은 완료 신호를 줄 수 있다. 글 보다 코드를 보는게 이해가 빠를 듯 싶다.

&lt;pre&gt;&lt;code&gt;@Test
public void handleTest() {

    Mono.just(&quot;bar&quot;)
            .handle((it, sink) -&amp;gt; {
                if (it.equals(&quot;foo&quot;)) {
                    sink.next(it);
                } else if (it.equals(&quot;bar&quot;)) {
                    sink.error(new NullPointerException());
                } else {
                    sink.complete();
                }
            }).subscribe(System.out::println, (e) -&amp;gt; System.out.println(&quot;error&quot;), () -&amp;gt; System.out.println(&quot;complete&quot;));

}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `handle test`() {\r
    Mono.just(&quot;bar&quot;)\r
        .handle { it, sink: SynchronousSink&lt;String&gt; -&gt;\r
            if (it == &quot;foo&quot;) {\r
                sink.next(it)\r
            } else if (it == &quot;bar&quot;) {\r
                sink.error(NullPointerException())\r
            } else {\r
                sink.complete()\r
            }\r
        }.subscribe({ println(it) }, { println(&quot;error&quot;) }, { println(&quot;complete&quot;) })\r
}\r
[/kotlin]

위와 같이 특정 조건에 의해 next, error, complete 신호를 보낼 수 있다. 꼭 해당 타입과 sink의 제네릭 타입이 같지 않아도 된다. 
foo, bar 혹은 그외의 것들을 넣어 한번씩 테스트해보는게 이해가 더욱 빠를 듯 싶다. 이 역시 운영에서 아주 많이 사용한다.

&lt;h3&gt;cast, ofType&lt;/h3&gt;

cast 혹은 ofType을 써서 형 변환을 할 수 있다. cast 와 ofType 두 메서드 모두 타입 캐스팅을 할 수 있지만 조금 다르다. cast는 타입캐스팅 할 때 형이 맞지 않다면 에러가 발생하지만 ofType 의 경우에는 에러가 나지 않고 무시된다. ofType은 내부적으로 filter를 사용해 무시한다.

&lt;pre&gt;&lt;code&gt;@Test
public void castTest() {
    Mono.just(1)
            .cast(Number.class)
            .subscribe(System.out::println, (e) -&amp;gt; System.out.println(&quot;error&quot;), () -&amp;gt; System.out.println(&quot;complete&quot;));

    Mono.just(1)
            .ofType(Number.class)
            .subscribe(System.out::println, (e) -&amp;gt; System.out.println(&quot;error&quot;), () -&amp;gt; System.out.println(&quot;complete&quot;));
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `cast test`() {\r
\r
    Mono.just(1)\r
        .cast&lt;Number&gt;()\r
        .subscribe({\r
            println(it) }, { println(it) }, { println(&quot;complete&quot;) })\r
\r
    Mono.just(1)\r
        .ofType&lt;Number&gt;()\r
        .subscribe({ println(it) }, { println(it) }, { println(&quot;complete&quot;) })\r
}\r
[/kotlin]

둘다 출력 되는건 동일하다. 형변환이 잘 되었을 땐 동일하게 동작하지만 형변환이 안될 경우에는 조금 다르게 동작한다.

&lt;pre&gt;&lt;code&gt;@Test
public void castTest() {
    Mono.just(1)
            .cast(String.class)
            .subscribe(System.out::println, (e) -&amp;gt; System.out.println(&quot;error&quot;), () -&amp;gt; System.out.println(&quot;complete&quot;));

    Mono.just(1)
            .ofType(String.class)
            .subscribe(System.out::println, (e) -&amp;gt; System.out.println(&quot;error&quot;), () -&amp;gt; System.out.println(&quot;complete&quot;));
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `cast test`() {\r
\r
    Mono.just(1)\r
        .cast&lt;String&gt;()\r
        .subscribe({\r
            println(it) }, { println(it) }, { println(&quot;complete&quot;) })\r
\r
    Mono.just(1)\r
        .ofType&lt;String&gt;()\r
        .subscribe({ println(it) }, { println(it) }, { println(&quot;complete&quot;) })\r
}\r
[/kotlin]

위와 같이 형 변환이 안됐을 시에는 cast 경우 에러가 출력 되며 ofType 일 경우에는 complete 만 출력 된다.

&lt;h3&gt;filterWhen&lt;/h3&gt;

filter의 비동기 버전이다. filter는 다들 알다시피 걸러내는 작업을 하는 메서드이다. filter와 동일한 동작을 하니 예제만 보고 넘어가자.

&lt;pre&gt;&lt;code&gt;@Test
public void filterWhenTest() {
    Mono.just(&quot;filterWhen&quot;)
            .filterWhen((it) -&amp;gt; Mono.just(true))
            .subscribe(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `filter when test`() {\r
    Mono.just(&quot;filterWhen&quot;)\r
        .filterWhen {\r
            Mono.just(true)\r
        }.subscribe {\r
            println(it)\r
        }\r
}\r
[/kotlin]

filter 와 동일한 동작은 하니 그리 어렵진 않다. 사실 이건 운영에서 써본적은 없다.

&lt;h3&gt;repeat&lt;/h3&gt;

메서드명 그대로 반복하여 구독한다. repeat()는 여러 메서드가 존재한다. 예를들어 repeat() 만 사용할 경우에는 무한정으로 구독하는 시스템이다. 실제로 필요한지는 잘모르겠다. 특정 조건에 만족하면 반복하는 메서드도 존재한다.

&lt;pre&gt;&lt;code&gt;public final Flux&amp;lt;T&amp;gt; repeat(BooleanSupplier predicate)
&lt;/code&gt;&lt;/pre&gt;

또 한 원하는 만큼 반복도 가능하다.

&lt;pre&gt;&lt;code&gt;@Test
public void repeatTest() {
    Mono.just(&quot;repeat&quot;)
            .repeat(2)
            .subscribe(System.out::println);
}

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `repeat test`() {\r
    Mono.just(&quot;repeat&quot;)\r
        .repeat(2)\r
        .subscribe {\r
            println(it)\r
        }\r
}\r
[/kotlin]

위의 소스는 2번을 더 반복한다는 뜻이다. 실제로 3번의 구독이 이루어진다. 만약 0으로 지정을 했다면 구독은 한번만 이루어진다. 
이외에도 사실 repeat** 메서드가 많긴하지만 필자는 사용한적이 없다. 만약 사용한다면 테스트할때 사용하지 않나 싶다. 운영에서 사용할일은 극히 드물 것 같다.

&lt;h3&gt;doOnSubscribe, doOnRequest, doOnNext, doOnCancel, doOnTerminate, doAfterTerminate, doOnSuccess, doOnError, doFinally, doOnEach&lt;/h3&gt;

doOn** 시리즈는 trigger에 해당한다. 로직상으로는 많이 사용하지 않겠지만 로그를 찍을 때 사용하면 적절할 듯 싶다. 필자도 대부분 로그를 찍을 때 사용했다. 물론 다른 경우에도 사용하겠지만 아직까지는 그런일은 없었다. 
일단 먼저 사용법 부터 보자.

&lt;pre&gt;&lt;code&gt;@Test
void doOnSeries() {
    Mono.just(&quot;doOnSeries&quot;)
            .doOnSubscribe(it -&amp;gt; System.out.println(&quot;doOnSubscribe&quot;))
            .doOnRequest(it -&amp;gt; System.out.println(&quot;doOnRequest&quot;))
            .doOnNext(it -&amp;gt; System.out.println(&quot;doOnNext&quot;))
            .doOnEach(it -&amp;gt; System.out.println(&quot;doOnEach&quot;))
            .doOnCancel(() -&amp;gt; System.out.println(&quot;doOnCancel&quot;))
            .doAfterTerminate(() -&amp;gt; System.out.println(&quot;doAfterTerminate&quot;))
            .doOnTerminate(() -&amp;gt; System.out.println(&quot;doOnTerminate&quot;))
            .doOnSuccess(it -&amp;gt; System.out.println(&quot;doOnSuccess&quot;))
            .doOnError(it -&amp;gt; System.out.println(&quot;doOnError&quot;))
            .doFinally(it -&amp;gt; System.out.println(&quot;doFinally&quot;))
            .subscribe();
}

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `do on series`() {\r
    Mono.just(&quot;doOnSeries&quot;)\r
        .doOnSubscribe {\r
            println(&quot;doOnSubscribe&quot;)\r
        }.doOnRequest {\r
            println(&quot;doOnRequest&quot;)\r
        }.doOnNext {\r
            println(&quot;doOnNext&quot;)\r
        }.doOnEach {\r
            println(&quot;doOnEach&quot;)\r
        }.doOnCancel {\r
            println(&quot;doOnCancel&quot;)\r
        }.doAfterTerminate {\r
            println(&quot;doAfterTerminate&quot;)\r
        }.doOnTerminate {\r
            println(&quot;doOnTerminate&quot;)\r
        }.doOnSuccess {\r
            println(&quot;doOnSuccess&quot;)\r
        }.doOnError {\r
            println(&quot;doOnError&quot;)\r
        }.doFinally {\r
            println(&quot;doFinally&quot;) \r
        }\r
        .subscribe()\r
}\r
[/kotlin]

사용법은 위와 같이 간단하다. 각 메서드 별로 알아보자. 
&lt;code&gt;doOnSubscribe&lt;/code&gt; 메서드는 구독이 시작 될 트리거 된다. 위 메서드 중 가장 먼저 실행 된다. 파라미터로는 Subscription 가 넘어 온다.
&lt;code&gt;doOnRequest&lt;/code&gt; 메서드는 요청 받을 떄 트리거 된다. 기본적으로 파라미터는 Long의 Long.MAX_VALUE 값이 넘어온다.
&lt;code&gt;doOnNext&lt;/code&gt;는 성공적으로 데이터가 방출 될 때 트리거 된다. 파라미터로는 해당 T 타입이 넘어 온다.
&lt;code&gt;doOnCancel&lt;/code&gt; 메서드는 구독이 취소 됐을 때 넘어오는 이벤트다 파라미터로는 아무것도 넘어오지 않는다.
&lt;code&gt;doOnTerminate&lt;/code&gt; 메서드는 완료 혹은 에러가 났을 떄 트리거 된다. 이벤트시기는 완료, 에러 이벤트 전에 트리거 된다. 이 역시 파라미터로는 아무것도 넘어오지 않는다.
&lt;code&gt;doAfterTerminate&lt;/code&gt; 메서드는 doOnTerminate 와 동일하게 트리거 되지만 시점이 조금 다르다. 완료, 에러 이벤트 후에 트리거 된다. 역시 파라미터는 없다.
&lt;code&gt;doOnSuccess&lt;/code&gt; 완료 되면 트리거 된다. 파라미터는 해당 T 타입이 넘어 온다.
&lt;code&gt;doOnError&lt;/code&gt; 에러가 났을 때 트리거 된다. 파라미터로는 Throwable 가 넘어 온다.
&lt;code&gt;doFinally&lt;/code&gt; 에러, 취소, 완료 될 때 트리거 된다. 모노가 종료되면 무슨 일이든 트리거 된다. 파라미터로는 SignalType이 넘어와 종료 이벤트 타입을 받을 수 있다. 자바의 try catch finally의 finally 과 동일한 느낌이다.
&lt;code&gt;doOnEach&lt;/code&gt; 메서드는 데이터를 방출 할 때, 혹은 완료 에러가 발생했을 때의 고급 이벤트다. 파라미터로 Signal&lt;T&gt; 이 넘어온다. 이 신호에는 context 도 포함되어 있다. 주로 모니터링으로 사용한다고 한다. 위의 코드에서는 doOnEach 가 두번 출력된다. 데이터를 방출 할때 한번, 완료 되었을 때 한번.

&lt;h3&gt;retry&lt;/h3&gt;

메서드명 그대로 에러가 발생하였을 때 재시도 하는 메서드이다. 이 또한 많은 메서드가 존재한다. 기본 retry() 메서드는 무한정(Long.MAX_VALUE 사실 무한은 아닌 듯)으로 재시도를 한다. 
retry(long) 은 retry 횟수를 지정할 수 있다. 만약 2로 지정하였을 경우는 2번을 재시도 한다. 
retry(Predicate) 은 특정 조건에 만족하면 재시도를 시도 한다.

&lt;pre&gt;&lt;code&gt;@Test
void retryTest() {
    Mono.just(&quot;retry&quot;)
            .handle((it, sink) -&amp;gt; {
                System.out.println(it);
                sink.error(new NullPointerException());
            }).retry(2)
            .subscribe();
}


&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `retry test`() {\r
    Mono.just(&quot;retry&quot;)\r
        .handle { it, sink: SynchronousSink&lt;String&gt; -&gt;\r
            println(it)\r
            sink.error(NullPointerException())\r
        }.retry(2)\r
        .subscribe()\r
}\r
[/kotlin]
위의 코드는 재시도를 2번 시도하는 코드이다. 출력은 retry 가 3번 출력된다. 한번은 처음 구독했을 때와 두번은 retry를 시도했을 때 총 3번 출력 된다.

&lt;h3&gt;retryBackoff&lt;/h3&gt;

retryBackoff 는 기본 retry 보다 조금 우아하게 재시도를 할 수 있다, backoff 설정를 할 수 있어 특정 시간만큼 Duration을 줄 수 있다. 이 역시 메서드는 많다.

&lt;pre&gt;&lt;code&gt;public final Mono&amp;lt;T&amp;gt; retryBackoff(long numRetries, Duration firstBackoff, Duration maxBackoff, double jitterFactor)
&lt;/code&gt;&lt;/pre&gt;

하지만 위의 메서드를 설명하면 대부분의 메서드는 설명된다.

위의 retry 는 사실 큰 도움은 되지 않을 수 있다. 대기시간도 없어 실패하면 바로 다시 재시도를 하기 때문이다. 만약 동시에 모든 호출이 실패하면 바로 또 모든 호출을 동시에 날리기 때문이다. 어쩌면 운영에서는 retryBackoff 메서드를 쓰는게 좀 더 많은 도움이 되지 않을까 싶다.

retryBackoff의 첫번째 파라미터인 numRetries는 retry와 동일하게 재시도 횟수를 지정하면 된다. 
두 번째 파라미터인 firstBackoff는 첫 번째 재시도 할 때의 대기 시간이다. 
세 번째 파라미터 maxBackoff는 최대로 해당 시간만큼 대기 할 수 있다는 뜻 이다. 그러기 때문에 firstBackoff 보다는 작으면 안된다. 
jitterFactor는 위에서 말했던 것 처럼 동시에 모든 호출이 실패하면 또 다시 모든 호출은 동시에 재시도 하기 때문에 이를 분산시키기 위한 값이다. 최소값은 0.0 이며 최대값은 1.0 이다. 지정 하지 않을 경우에는 0.5가 기본값이다.

궁금해할진 모르겠지만 해당 알고리즘은 다음과 같다. &lt;code&gt;i&lt;/code&gt;는 재시도 한 값이다.

&lt;pre&gt;&lt;code&gt;nextBackoff = firstbackoff * 2^i
jitterOffset = (nextBackoff * 100 * jitterFactor) / 100
lowBound = max(firstbackoff - nextBackoff, -jitterOffset)
highBound = min(maxBackoff - nextBackoff, jitterOffset)
jitter = random (lowBound, highBound)
backoff = nextBackoff + jitter
&lt;/code&gt;&lt;/pre&gt;

사용법은 대략 다음과 같다.

&lt;pre&gt;&lt;code&gt;@Test
void retryBackoffTest() throws InterruptedException {
    Mono.just(&quot;retry&quot;)
            .handle((it, sink) -&amp;gt; sink.error(new NullPointerException()))
            .retryBackoff(2, Duration.ofSeconds(3), Duration.ofSeconds(10), 0.5)
            .subscribe();
    sleep()
}


&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `retry backoff test`() {\r
    Mono.just(&quot;retry&quot;)\r
        .handle { it, sink: SynchronousSink&lt;String&gt; -&gt;\r
            sink.error(NullPointerException())\r
        }.retryBackoff(2, Duration.ofSeconds(3), Duration.ofSeconds(10), 0.5)\r
        .subscribe()\r
    sleep()\r
}\r
[/kotlin]

retryBackoff 는 다른 쓰레드로 돌리기 때문에 보여주기 위해 sleep를 줬다.

&lt;h3&gt;delayElement&lt;/h3&gt;

사실 Mono의 이 메서드는 테스트에서 사용했지 운영에선 사용해본적이 없다. 메서드명 그대로 delay 를 줄 수 있는 메서드이다. 
사용법 부터 살펴보자.

&lt;pre&gt;&lt;code&gt;@Test
void delayElement() throws InterruptedException {
    Mono.just(&quot;delayElement&quot;)
            .delayElement(Duration.ofSeconds(3))
            .doOnNext(System.out::println)
            .subscribe();
    sleep
}

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `delay element`() {\r
    Mono.just(&quot;delayElement&quot;)\r
        .delayElement(Duration.ofSeconds(3))\r
        .doOnNext {\r
            println(it)\r
        }\r
        .subscribe()\r
    sleep\r
}\r
[/kotlin]

이 역시 다른 스레드로 돌아가기 때문에 sleep 을 줬다. delayElement은 해당 시간만큼 onNext 방출을 지연시킨다. 실행해보면 3초 후에 &lt;code&gt;delayElement&lt;/code&gt; 가 출력된다.

&lt;h3&gt;onErrorResume, onErrorReturn, onErrorMap, onErrorContinue&lt;/h3&gt;

오늘 마지막으로 살펴볼 아이는 onError** 시리즈이다. 이 역시 여러 종류의 메서드들이 있지만 기본적인 내용은 살펴보고 나머지는 해당 메서드를 살펴보는 게 좋다. 그리 어렵지 않은 내용이니 말이다.

&lt;code&gt;onErrorResume&lt;/code&gt; 메서드는 에러가 발생하였을 경우 다른 Mono로 대체 할 수 있다.

&lt;pre&gt;&lt;code&gt;@Test
void onErrorResumeTest() {
    Mono.just(&quot;onErrorResume&quot;)
            .handle((__, sink) -&amp;gt; sink.error(new NullPointerException()))
            .onErrorResume(it -&amp;gt; Mono.just(&quot;foo&quot;))
            .subscribe(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `on error resume test`() {\r
    Mono.just(&quot;onErrorResume&quot;)\r
        .handle { _, sink: SynchronousSink&lt;String&gt; -&gt;\r
            sink.error(NullPointerException())\r
        }.onErrorResume {\r
            Mono.just(&quot;foo&quot;)\r
        }.subscribe {\r
            println(it)\r
        }\r
}\r
[/kotlin]

위의 코드는 &lt;code&gt;foo&lt;/code&gt;가 출력 되는 것을 볼 수 있다.

&lt;code&gt;onErrorReturn&lt;/code&gt;은 에러가 발생했을 경우 다른 값으로 대체 할 수 있다.

&lt;pre&gt;&lt;code&gt;@Test
void onErrorReturnTest() {
    Mono.just(&quot;onErrorReturn&quot;)
            .handle((__, sink) -&amp;gt; sink.error(new NullPointerException()))
            .onErrorReturn(&quot;foo&quot;)
            .subscribe(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `on error return test`() {\r
    Mono.just(&quot;onErrorReturn&quot;)\r
        .handle { _, sink: SynchronousSink&lt;String&gt; -&gt;\r
            sink.error(NullPointerException())\r
        }.onErrorReturn(&quot;foo&quot;)\r
        .subscribe {\r
            println(it)\r
        }\r
}\r
[/kotlin]

이 역시 &lt;code&gt;foo&lt;/code&gt;가 출력 된다.

&lt;code&gt;onErrorMap&lt;/code&gt; 다른 에러로 변환할 수 있다.

&lt;pre&gt;&lt;code&gt;@Test
void onErrorMapTest() {
    Mono.just(&quot;onErrorMap&quot;)
            .handle((__, sink) -&amp;gt; sink.error(new NullPointerException()))
            .onErrorMap(it -&amp;gt; new IllegalArgumentException())
            .subscribe(System.out::println, System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `on error map test`() {\r
    Mono.just(&quot;onErrorMap&quot;)\r
        .handle { _, sink: SynchronousSink&lt;String&gt; -&gt;\r
            sink.error(NullPointerException())\r
        }.onErrorMap {\r
            IllegalArgumentException()\r
        }\r
        .subscribe ({ println(it) }) { e -&gt; println(e)}\r
}\r
[/kotlin]

위 처럼 에러를 변환하고 싶다면 사용하면 된다.

&lt;code&gt;onErrorContinue&lt;/code&gt;는 연산 과정에서 오류가 발생하였을 때 복구하는 메서드이다. 넘어오는 파라미터는 에러와 해당 object이 넘어온다.

&lt;pre&gt;&lt;code&gt;@Test
void onErrorContinueTest() {
    Mono.just(&quot;onErrorContinue&quot;)
            .handle((__, sink) -&amp;gt; sink.error(new NullPointerException()))
            .onErrorContinue((it, object) -&amp;gt; { })
            .subscribe(System.out::println, System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
@Test\r
fun `on error continue test`() {\r
    Mono.just(&quot;onErrorContinue&quot;)\r
        .handle { _, sink: SynchronousSink&lt;String&gt; -&gt;\r
            sink.error(NullPointerException())\r
        }.onErrorContinue { e, any -&gt; }\r
        .subscribe({ println(it) }) { e -&gt; println(e) }\r
\r
}\r
[/kotlin]

위는 오류가 복구되어 기존의 있던 값 onErrorContinue가 출력 된다.

오늘은 여기까지 배워보도록 하자. 우리가 공부한 이외에도 훨씬 더 많은 메서드가 존재한다. 필자도 대부분 사용하는 것만 사용하기에 필자도 모르는 메서드가 많다. 물론 언젠가는 사용할 날이 오겠지.. 그럼 그때 다시 공부하는 걸루..

오늘은 이렇게 Mono 의 오퍼레이터에 대해서 알아봤다. 사실 위의 메서드만 알아도 어느정도는 충분한 개발을 할 수 있다고 믿는다. 물론 다 알면 좋겠지만.. 만약 여기에 나오지 않은 오퍼레이터들은 나중에 Flux 시간에 나올 수 도 있으니 참고하면 되겠다.
다음시간에는 Flux에 대해 알아보도록 하자.&lt;/div&gt;</description>
      <author>머룽</author>
      <guid isPermaLink="true">https://mkzz.tistory.com/338</guid>
      <comments>https://mkzz.tistory.com/338#entry338comment</comments>
      <pubDate>Sun, 23 Apr 2023 14:06:16 +0900</pubDate>
    </item>
    <item>
      <title>Reactor 써보기 (1)</title>
      <link>https://mkzz.tistory.com/337</link>
      <description>&lt;div class='markdown-body'&gt;오랜만에 글을 쓰니 좀 어색하다. 요 근래 계속 글을 안썼더니 말이다.

요즘 필자는 회사에서 Spring Webflux를 사용하고 있다. 그래서 좀 더 잘 사용해보자라는 의미에서 Reactor 를 공부해보도록 하자.
하지만 여기에선 Reactive Streams 에 대해 개념적으로는 설명하지 않겠다. 이미 다른 블로그에 좋은 글들이 많으니 그걸 보고 개념을 이해하면 좋겠다.

&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reactive-streams/reactive-streams-jvm&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;reactive-streams-jvm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=8fenTR3KOJo&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;토비의 봄 TV 5회 스프링 리액티브 프로그래밍 (1) - Reactive Streams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alwayspr.tistory.com/44&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Spring WebFlux는 어떻게 적은 리소스로 많은 트래픽을 감당할까?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://brunch.co.kr/@springboot/152&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Project Reactor 1.리액티브 프로그래밍&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

위의 글들은 한번 읽어보면 좋을 것 같다. 토비느님의 방송 역시 함께 보면 개념적으로 더욱 이해가 빨리 될 듯 싶다. 꽤 많은 시리즈가 있으니 시간 날 때 짬짬이 보면 도움이 확실이 된다.

사실 Reactive Streams 구현체인 Reactor나 RxJava2나 사용법은 대부분 비슷하다. 같은 개발자가 만든건 아니지만 여러 회사들이 협업을 하면서 만든거니 아무래도 비슷할 수 밖에 없을 것도 같다. 아마도 구현체들 대부분 사용법은 비슷하지 않을까 싶다?

필자는 아무래도 거의 대부분 Spring을 사용하기에 Reactor로 먼저 접하게 되었다. 안드로이드에서는 Reactor 보다는 RxJava를 더 선호하고 많이 쓰는 것 같다. 하지만 상관없다. Spring 에선 RxJava 나 Reactor 나 혹은 다른 Reactive Streams 구현 된 어느 것을 써도 좋다. 이건 조만간 다시 한번 다뤄보기로 하겠다.

그럼 한번 어떤 메서드들이 있는지 주로 사용될만한 메서드 위주로 한번 살펴보도록 하자.

일단 모노 부터 만들어보자!

&lt;h2&gt;Mono&lt;/h2&gt;

&lt;image src=&quot;https://projectreactor.io/docs/core/release/api/reactor/core/publisher/doc-files/marbles/mono.svg&quot; /&gt;

위의 그림은 reactor Mono 에 대한 그림이다. 사실 처음 보는 개발자분들이라면 이게 뭔가 싶기도 하다. (필자도 가끔 뭔가 싶기도 하다.)
위의 그림은 마블다이어그램이라 하는데 해당 오퍼레이션들이 어떤 행위를 하는지 나타낸 그림이다. 
쉽게 생각하면 왼쪽에서 오른쪽으로의 흐름을 나타내며 오버레이터를 통해 어떤 결과가 어떤식으로 나오고 있는지 생각하면 될듯 싶다.

Mono 는 Reactive Streams 의 구현체로 0 또는 1의 스트림을 만들 수 있다. 나중에 배울 Flux도 마찬가지로 Reactive Streams의 구현체이며 0 부터 N 까지의 스트림을 만들 수 있으니 참고 하면 되겠다.

아주 쉽게 비교를 하자면 java8에 나온 Optional 과 Stream 으로 비교할 수 있을 것 같다. 
Optional 은 비어있거나 값을 가지고 있고 Stream 은 0 ~ N 까지의 연속된 요소들을 의미한다.

이렇게 비교하면 좀 더 접근하기 쉬울 것 같아 비교를 해봤다.

그럼 이제 본격적으로 모노를 만들어 보자!

kotlin, java 모두 예제에 넣어봤다.

&lt;h3&gt;just&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;Mono&amp;lt;String&amp;gt; just = Mono.just(&quot;hello reactor&quot;);

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
val just = &quot;hello reactor&quot;.toMono()\r
\r
[/kotlin]

&lt;blockquote&gt;
  코틀린의 경우 원래 reactor 에 기본 확장확함수가 있었는데 Deprecated 되고 &lt;code&gt;extensions&lt;/code&gt; 디펜더시를 추가 해야 된다.
&lt;/blockquote&gt;

아주 기본적인 사용법이다. 모노를 만드는 가장 쉬운 방법이다. 자바의 Optional과 조금 비슷해 보인다.

&lt;h3&gt;fromSupplier&lt;/h3&gt;

만약 지연된 처리가 필요하다면 fromSupplier 을 사용하면 된다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;Mono&amp;lt;String&amp;gt; fromSupplier = Mono.fromSupplier(() -&amp;gt; &quot;hello reactor&quot;);

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
val fromSupplier = { &quot;hello reactor&quot; }.toMono()\r
\r
[/kotlin]

from** 으로 시작하는 메서드는 다양하다. Callable, CompletionStage, Runnable, Future 등 여러 메서드들이 있으니 필요에 따라 사용하면 되겠다.
from** 메서드는 기본 값으로 사용하거나 fallback 으로 운영에서도 종종 사용하는 편이다.

&lt;h3&gt;error&lt;/h3&gt;

에러를 만드는 방법이다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;Mono&amp;lt;String&amp;gt; error = Mono.error(new NullPointerException());
Mono&amp;lt;String&amp;gt; errorSupplier = Mono.error(NullPointerException::new);

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
val error : Mono&lt;String&gt; = NullPointerException().toMono()\r
val errorSupplier = { NullPointerException() }.toMono()\r
\r
[/kotlin]

구독할 때 에러를 방출하여 종료한다. 이것 역시 운영에서 종종 사용하는 편이다.

&lt;h3&gt;empty, never&lt;/h3&gt;

빈 모노와 무기한으로 실행되는 모노를 만든다. 사실 &lt;code&gt;never&lt;/code&gt;는 사용한 적이 단 한번도 없다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;Mono&amp;lt;String&amp;gt; empty = Mono.empty();
Mono&amp;lt;String&amp;gt; never = Mono.never();

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
val empty = Mono.empty&lt;String&gt;()\r
val never = Mono.never&lt;String&gt;()\r
\r
[/kotlin]

해당 메서드는 데이터를 방출하지 않는다. 사실상 아무 것도 하지 않는다. empty 는 완료신호는 오지만 never 경우는 무기한으로 실행되므로 오류, 완료 등 어떠한 신호도 오지 않는다.
&lt;code&gt;never&lt;/code&gt; 는 필요에 따라 테스트할 경우 사용한다 하는데 필자는 그런 경우가 없어 사용한 적은 없다.

아주 기본적인 모노를 만드는 경우를 살펴봤다. 아직 모노만 만드는데 반도 못온 느낌이다. 이러다 오늘은 모노만 만들다 끝나겠는걸..

&lt;h3&gt;zip&lt;/h3&gt;

모노를 만드는 메서드 중에 필자가 아마 가장많이 쓰지 않나 싶다. 물론 각자가 다 다르겠지만 필자의 경우 각각의 Mono 들을 &lt;code&gt;aggregating&lt;/code&gt; 하는 경우가 많았다. 아마 가장 많이는 사용하지 않더라도 프로젝트에 reactor 를 사용한다면 꼭 한번을 쓸 일이 있을 듯 하다.

방금도 이야기 했지만 모노들을 aggregating 하는 역할을 한다. 모노들이 각각 동작하므로 여러 모노들을 한꺼번에 동작하게 만들 때 유용하게 쓰인다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;Mono&amp;lt;String&amp;gt; zip = Mono.zip(Mono.just(&quot;foo&quot;), Mono.just(&quot;bar&quot;));
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
val zip = Mono.zip(Mono.just(&quot;foo&quot;), Mono.just(&quot;bar&quot;))\r
\r
[/kotlin]

기본 사용법은 위와 같다. 필자가 말한대로 모노들이 각각 동작하는지는 테스트해보자.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;@Test
void zipTest() throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(1);

    Mono.zip(testMethod1(), testMethod2())
            .subscribe(it -&amp;gt; { }, (throwable) -&amp;gt; { },
                    countDownLatch::countDown);
    countDownLatch.await();
}

private Mono&amp;lt;String&amp;gt; testMethod1() {
    return Mono.just(&quot;foo&quot;)
            .delayElement(Duration.ofSeconds(1))
            .doOnNext(System.out::println);
}

private Mono&amp;lt;String&amp;gt; testMethod2() {
    return Mono.just(&quot;bar&quot;)
            .doOnNext(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
@Test\r
fun `zip test`() {\r
    val countDownLatch = CountDownLatch(1)\r
\r
    Mono.zip(testMethod1(), testMethod2())\r
        .subscribe({}, {}, {\r
            countDownLatch.countDown()\r
        })\r
\r
    countDownLatch.await()\r
}\r
\r
private fun testMethod1(): Mono&lt;String&gt; {\r
    return Mono.just(&quot;foo&quot;)\r
        .delayElement(Duration.ofSeconds(1))\r
        .doOnNext {\r
            println(it)\r
        }\r
}\r
\r
private fun testMethod2(): Mono&lt;String&gt; {\r
    return Mono.just(&quot;bar&quot;)\r
        .doOnNext {\r
            println(it)\r
        }\r
}\r
\r
[/kotlin]

위의 메서드들 중에 하나는 delay 를 주었다. 만약 차례대로 실행이 되어야 한다면 &lt;code&gt;foo&lt;/code&gt; 가 출력된 후에 &lt;code&gt;bar&lt;/code&gt; 출력 되어야 한다. 하지만 그렇지 않다. 실행시키지마자 &lt;code&gt;bar&lt;/code&gt;가 출력되고 1초후 &lt;code&gt;foo&lt;/code&gt;가 실행 된다.

위와 같이 zip의 파라미터가 2개 일 경우에는 Tuple2&amp;lt;T1, T2&gt; 로 생산 된다. zip으로 8개 까지 가능하며 그 후로는 Iterable 타입으로 넘겨야한다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;Mono.zip(testMethod1(), testMethod2())
        .subscribe((Tuple2&amp;lt;String,String&amp;gt; it)  -&amp;gt; { });

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
Mono.zip(testMethod1(), testMethod2())\r
    .subscribe { it: Tuple2&lt;String, String&gt; -&gt; }\r
\r
[/kotlin]

필자는 어떤 타입인지 보여주기 위함이지 타입은 제거해도 좋다. 참고로 zip의 하나라도 empty 거나 오류를 방출하면 즉시 종료된다. empty 경우엔 onNext도 방출하지 않는다.

&lt;h3&gt;when&lt;/h3&gt;

when 은 zip 과 유사하지만 onNext 를 방출하지 않는다. 단지 각각의 모노를 독립적으로 실행 시킬때만 사용하면 된다. 이 역시 종종 사용할 경우가 있다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;@Test
void whenTest() throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(1);

    Mono.when(testMethod1(), testMethod2())
            .subscribe(it -&amp;gt; { }, (throwable) -&amp;gt; { },
                    countDownLatch::countDown);
    countDownLatch.await();
}

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
@Test\r
fun `when test`() {\r
    val countDownLatch = CountDownLatch(1)\r
\r
    Mono.`when`(testMethod1(), testMethod2())\r
        .subscribe({}, {}, {\r
            countDownLatch.countDown()\r
        })\r
    countDownLatch.await()\r
}\r
\r
[/kotlin]

이 역시 &lt;code&gt;zip&lt;/code&gt;과 동일하게 오류를 방출하면 즉시 종료 된다.

&lt;h3&gt;delay&lt;/h3&gt;

메스드명 그대로 delay를 줄 수 있는 모노를 만들 수 있다. 해당 Duration 만큼 지연된 후에 onNext를 방출한다. 방출되는 Long 의 값은 0 이다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;@Test
void delayTest() throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(1);

    Mono.delay(Duration.ofSeconds(1))
            .doOnNext(System.out::println)
            .subscribe(it -&amp;gt; { }, (throwable) -&amp;gt; { },
                    countDownLatch::countDown);
    countDownLatch.await();

}

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
@Test\r
fun `delay test`() {\r
    val countDownLatch = CountDownLatch(1)\r
\r
    Mono.delay(Duration.ofSeconds(1))\r
        .doOnNext {\r
            println(it)\r
        }.subscribe({}, {}, { countDownLatch.countDown() })\r
\r
    countDownLatch.await()\r
}\r
\r
[/kotlin]

위의 코드는 1초 후에 onNext 로 방출한다.

&lt;h3&gt;defer&lt;/h3&gt;

defer 메서드는 fromSupplier 와 비슷하게 지연된 Mono 처리를 하고 싶다면 해당 메서드를 이용하면 된다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;Mono&amp;lt;String&amp;gt; defer = Mono.defer(() -&amp;gt; Mono.just(&quot;foo&quot;));

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
val defer = Mono.defer { Mono.just(&quot;foo&quot;) }\r
\r
[/kotlin]

음 간단한 예 로는 아직 배우진 않았지만 &lt;code&gt;switchIfEmpty&lt;/code&gt; 에 아주 적합할 수 있다. 만약 모노가 비었을 때 해당 메서드를 사용하여 다른 모노로 대체할 수 있는 fallback 메서드이다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;Mono.just(&quot;foo&quot;)
    .switchIfEmpty(Mono.just(&quot;bar&quot;))
    .subscribe();

}

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
Mono.just(&quot;foo&quot;)\r
    .switchIfEmpty(Mono.just(&quot;bar&quot;))\r
    .subscribe()\r
\r
[/kotlin]

만약 위와 같은 코드가 있다면 모노가 비어있지 않았음에도 불구하고 Mono.just(&quot;bar&quot;)를 매번 호출 한다. 사실 위와 같은 코드라면 많은 상관은 없지만 만약 다른 무거운 작업을 한다 가정하면 사실 불필요한 작업을 더 하는 꼴이 된다. 좀 더 우아하게 이 때 defer 메서드를 사용하면 된다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;Mono.just(&quot;foo&quot;)
        .switchIfEmpty(Mono.defer(() -&amp;gt; Mono.just(&quot;bar&quot;)))
        .subscribe();
}

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
Mono.just(&quot;foo&quot;)\r
    .switchIfEmpty { Mono.just(&quot;bar&quot;) }\r
    .subscribe()\r
[/kotlin]

위와 같이 코드를 작성한다면 Mono.just(&quot;bar&quot;)는 정말로 모노가 비어있을 때만 실행 된다.

&lt;h3&gt;from&lt;/h3&gt;

reactive streams API 의 Publisher 타입을 Mono 로 바꿀 수 있다. 1개 이상의 스트림일 경우 (예 : Flux) 첫번째 onNext 만 방출 되며 종료 된다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;Mono.from(Flux.just(1,2,3,4,5))
        .subscribe(System.out::println);

&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
Mono.from(Flux.just(1, 2, 3, 4, 5))\r
    .subscribe { println(it) }\r
[/kotlin]

결과는 1만 방출 되며 종료 된다. 
꼭 Reactor 만 되는 것은 아니다 Publisher 타입을 구현한 것이라면 해당 메서드를 사용할 수 있다.
아래는 RxJava 를 사용한 예제이다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;Mono.from(Single.just(&quot;bar&quot;).toFlowable())
        .subscribe(System.out::println);
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
Mono.from(Single.just(&quot;bar&quot;).toFlowable())\r
    .subscribe {\r
        println(it)\r
    }\r
[/kotlin]

동일하게 bar 가 방출 되며 종료 된다.

&lt;h3&gt;***DelayError&lt;/h3&gt;

에러를 지연시키며 모든 예외가 결합되서 에러를 발생시킨다. 에러가 나더라도 zip 의 모든 Mono 를 실행 시킨다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;@Test
void delayErrorTest() throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(1);

    Mono.zipDelayError(testMethod1(), Mono.error(new NullPointerException()), testMethod1(), Mono.error(new IllegalArgumentException()))
            .subscribe((it) -&amp;gt; {},
                    System.out::println,
                    countDownLatch::countDown);
    countDownLatch.await();
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
@Test\r
fun `delay error test`() {\r
    val countDownLatch = CountDownLatch(1)\r
\r
    Mono.zipDelayError(testMethod1(), Mono.error&lt;NullPointerException&gt;(NullPointerException()), testMethod1(), Mono.error&lt;IllegalArgumentException&gt;(IllegalArgumentException()))\r
        .subscribe({ }, {\r
            println(it)\r
            countDownLatch.countDown()\r
        }, {\r
            countDownLatch.countDown()\r
        })\r
\r
    countDownLatch.await()\r
}\r
[/kotlin]

위와 같은 코드가 있다면 모든 zip의 Mono 를 실행하고 나머지 에러를 결합해서 보여준다.
출력 결과는 다음과 같다.

&lt;pre&gt;&lt;code&gt;foo
foo
reactor.core.Exceptions$CompositeException: Multiple exceptions
&lt;/code&gt;&lt;/pre&gt;

만약 zip을 사용했다면 &lt;code&gt;NullPointerException&lt;/code&gt; 에러만 방출 한다.

여기에선 zip 만 설명했지만 when 도 동일하다.

&lt;h3&gt;create&lt;/h3&gt;

Listener 혹은 callback 기반의 모노를 만들 수 있다. 예를 들어 비동기 콜백 코드를 모노로 만들 수 있다 생각하면 되겠다.
말보다는 코드를 보면 훨씬 이해가 빠를듯 싶다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;@Test
void createTest() {

    Mono.create(sink -&amp;gt; {
        client.async(request, new Listener() {
            @Override
            public void onFailure(Exception e) {
                sink.error(e);
            }

            @Override
            public void onResponse(Response response) {
                sink.success(response);
            }
        });
    });
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
@Test\r
fun `create test`() {\r
    Mono.create&lt;Response&gt; {\r
        client.async(request, object : Listener {\r
            override fun response(response: Response) {\r
                it.success(response)\r
            }\r
\r
            override fun failure(e: Exception) {\r
                it.error(e)\r
            }\r
        })\r
    }\r
}\r
[/kotlin]

위와 같이 비동기 콜백 코드는 모노 형태로 바꾸어 사용할 수 있다. 
만약 리스너 해제 및 자원 해제는 &lt;code&gt;onDispose&lt;/code&gt; 메서드를 사용해서 처리 할 수 있으며 취소 시그널을 받고 싶다면 &lt;code&gt;onCancel&lt;/code&gt; 메서드를 사용하면 된다. 만약 다른 라이브러리를 쓰는데 비동기 콜백 코드로 작성되어 있다면 쉽게 모노 바꿀 수 있어 좋다. 필자도 종종 운영에서 사용했다.

&lt;h3&gt;using&lt;/h3&gt;

이 메서드는 사실 사용해보지 않았다. 그리고 사용할 일도 없었던거 같았다. 마블 다이어그램도 복잡하다. 
이 메서드는 외부 자원을 스트리밍하고 해제하는 역할을 한다. 아마 가장 많이 사용할 곳은 파일을 읽고 쓰고 하는곳이 아닐까 싶다. 혹은 socket을 열고 닫고 하는? 대략 그런 부분에서 많이 사용될 듯 싶다. 
사실 필자도 써본적이 없어 그냥 간단한 사용법만 가져왔다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;br /&gt;@Test
void usingTest() {
    Mono.using(() -&amp;gt; AsynchronousFileChannel.open(Paths.get(path), StandardOpenOption.READ),
            it -&amp;gt; Mono.create(sink -&amp;gt; it.read(buffer, 0, buffer, new CompletionHandler&amp;lt;Integer, ByteBuffer&amp;gt;() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    sink.success(attachment);
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    sink.error(exc);
                }
            })), it -&amp;gt; {
                try {
                    it.close();
                } catch (IOException e) {
                }
            });
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
@Test\r
fun `using test`() {\r
    Mono.using({ AsynchronousFileChannel.open(Paths.get(path), StandardOpenOption.READ) }, {\r
        Mono.create&lt;ByteBuffer&gt; { sink -&gt;\r
            it.read(buffer, 0, buffer, object : CompletionHandler&lt;Int, ByteBuffer&gt; {\r
                override fun completed(result: Int, attachment: ByteBuffer) {\r
                    sink.success(attachment)\r
                }\r
\r
                override fun failed(exc: Throwable, attachment: ByteBuffer) {\r
                    sink.error(exc)\r
                }\r
            })\r
        }\r
    }, { it.close() })\r
\r
}\r
[/kotlin]

조금 복잡해 보이긴해도 그닥 어려운 내용은 아니다. 만약 좀 더 궁금하다면 &lt;code&gt;using&lt;/code&gt;를 사용하는 코드를 참고 하면 되겠다.

&lt;h3&gt;usingWhen&lt;/h3&gt;

이 것 역시 &lt;code&gt;using&lt;/code&gt;과 동일하게 사용해본적이 없다. using 과 사용법은 거의 동일하나 타입이 &lt;code&gt;Publisher&lt;/code&gt; 타입이다. 사용곳은 아마 주로 트랜잭션 처리에 사용가능 할 듯 하다.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
void usingWhenTest() {
    Mono&amp;lt;String&amp;gt; data = Mono.just(&quot;foo&quot;);
    Mono.usingWhen(data, it -&amp;gt;
            Mono.just(it),
            it -&amp;gt; transition.commit(),
            (it, error) -&amp;gt;  transition.rollback(),
            it-&amp;gt; transition.rollback())
}
&lt;/code&gt;&lt;/pre&gt;

[kotlin]\r
\r
@Test\r
fun `using when test`() {\r
    val data = Mono.just(&quot;foo&quot;)\r
    Mono.usingWhen(data, Function&lt;String, Mono&lt;out String&gt;&gt; { Mono.just(it) },\r
        Function { transition.commit() },\r
        BiFunction { it, error -&gt; transition.rollback() },\r
        Function { transition.rollback() })\r
}\r
[/kotlin]

예제가 영 시원찮다. 나중에 좀 더 나온 샘플 코드가 생각나거나 필자가 사용할 일이 있다면 좀 더 구체적으로 남기겠다.

오늘은 중간에도 말했다시피 모노를 만드는 것으로 끝이났다. 이렇게 보니까 모노로 만들 수 있는 메서드가 생각 보다 많은 것 같다. 오버로딩 된 메서드들은 따로 설명하지 않아도 행위 자체는 동일 하기에 필요하다면 문서를 보는 것을 추천한다.

다음 편은 이어서 Mono 의 오퍼레이터에 대해서 살펴보도록 하자.&lt;/div&gt;</description>
      <author>머룽</author>
      <guid isPermaLink="true">https://mkzz.tistory.com/337</guid>
      <comments>https://mkzz.tistory.com/337#entry337comment</comments>
      <pubDate>Sun, 23 Apr 2023 14:06:15 +0900</pubDate>
    </item>
    <item>
      <title>Spring boot 2.2</title>
      <link>https://mkzz.tistory.com/336</link>
      <description>&lt;div class='markdown-body'&gt;오늘은 좀 늦은감이 있지만 그래도 spring boot 2.2 의 변화에 대해서 알아보도록 하자. 물론 예전에 틈틈이 특정부분은 관련해서도 남기긴 했지만 정리하는 의미에서 다시 한번 살펴보도록 하자. 물론 이것도 필지가 자주 사용할 것들 혹은 자주 사용하는 것들만 정리하니 나머지는 해당 문서를 참고하면 되겠다.

&lt;h3&gt;Spring Framework 5.2&lt;/h3&gt;

알다시피 Spring Framework 5.2로 업그레이드 되었다. 관련해서는 해당 문서를 찾아보면 더 좋을 듯 싶다. 
해당 문서는 &lt;a href=&quot;https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-5.x#upgrading-to-version-52&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;여기&lt;/a&gt;를 살펴보자.

&lt;h3&gt;JMX now disabled by default&lt;/h3&gt;

JMX는 더 이상 기본적으로 비활성화 되어있다. 만약 이 기능을 사용하고 싶다면 &lt;code&gt;spring.jmx.enabled=true&lt;/code&gt;를 사용하여 활성화 시킬 수 있다.
사실 이 기능은 많은 사용자가 사용하고 있지 않다고 판단되고 생각보다 많은 리소스를 필요로 하기 때문에 이와 같은 결정을 내린 것이라고 한다.

&lt;h3&gt;Jakarta EE dependencies&lt;/h3&gt;

다들 알다시피 java ee는 몇년전에 이클립스 재단에 넘어갔다. 그러면서 아마도 네임스페이스를 변경작업을 진행했을 것이다. 스프링부트도 그에 맞게 &lt;code&gt;javax.&lt;/code&gt; 에서 &lt;code&gt;jakarta.&lt;/code&gt;를 사용할 수 있게 디펜더시가 추가 되었다. 아직은 &lt;code&gt;javax&lt;/code&gt;의 디펜더시가 존재하지만 추후에는 사라질 예정이니 만약 직접적으로 디펜더시를 받고 있다면 마이그레이션 하는게 좋다.

&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;jakarta-activation.version&amp;gt;1.2.1&amp;lt;/jakarta-activation.version&amp;gt;
&amp;lt;jakarta-annotation.version&amp;gt;1.3.5&amp;lt;/jakarta-annotation.version&amp;gt;
&amp;lt;jakarta-jms.version&amp;gt;2.0.3&amp;lt;/jakarta-jms.version&amp;gt;
&amp;lt;jakarta-json.version&amp;gt;1.1.6&amp;lt;/jakarta-json.version&amp;gt;
&amp;lt;jakarta-json-bind.version&amp;gt;1.0.2&amp;lt;/jakarta-json-bind.version&amp;gt;
&amp;lt;jakarta-mail.version&amp;gt;1.6.4&amp;lt;/jakarta-mail.version&amp;gt;

...

&lt;/code&gt;&lt;/pre&gt;

위는 디펜더시 관리하는 부분의 일부를 가져왔다. 
또한 &lt;code&gt;com.sun.mail:javax.mail&lt;/code&gt; 는 &lt;code&gt;com.sun.mail:jakarta.mail&lt;/code&gt; 변경이 되었고, &lt;code&gt;org.glassfish:javax.el&lt;/code&gt;는 &lt;code&gt;org.glassfish:jakarta.el&lt;/code&gt;로 artifact ID 가 변경 되었으므로 이것 역시 직접적으로 선언해서 사용한다면 마이그레이션하는게 정신건강에 좋을 듯 싶다.

&lt;h3&gt;JUnit 5&lt;/h3&gt;

기본적으로 spring boot starter test 에는 junit5가 디폴트로 설정되어 있다. 또한 &lt;code&gt;vintage engine&lt;/code&gt; 도 포함되어 있으니 junit4를 이용해도 좋다. 만약 spring boot 2.2로 마이그레이션을 한다 한들 문제는 없다. 점진적으로 junit5로 업그레이드도 가능하다. 왜냐하면 동일한 모듈에서 junit5 와 junit4를 혼합하여 테스트를 작성해도 아무 문제가 없다.

허나 주의할 점이 하나 있다. junit4의 listener 기능을 사용한다면 위의 junit5의 모듈들을 사용할 수 없다.

&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;configuration&amp;gt;
    &amp;lt;properties&amp;gt;
        &amp;lt;property&amp;gt;
            &amp;lt;name&amp;gt;listener&amp;lt;/name&amp;gt;
            &amp;lt;value&amp;gt;com.example.CustomRunListener&amp;lt;/value&amp;gt;
        &amp;lt;/property&amp;gt;
    &amp;lt;/properties&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

만약 위의 같은 기능을 현재 사용하고 있다면 &lt;code&gt;vintage engine&lt;/code&gt;을 사용할 수 없으니 junit4를 직접적으로 선언하여 사용해야 한다.

&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependencies&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
        &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
        &amp;lt;exclusions&amp;gt;
            &amp;lt;exclusion&amp;gt;
                &amp;lt;groupId&amp;gt;org.junit.jupiter&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;junit-jupiter&amp;lt;/artifactId&amp;gt;
            &amp;lt;/exclusion&amp;gt;
            &amp;lt;exclusion&amp;gt;
                &amp;lt;groupId&amp;gt;org.junit.vintage&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;junit-vintage-engine&amp;lt;/artifactId&amp;gt;
            &amp;lt;/exclusion&amp;gt;
            &amp;lt;exclusion&amp;gt;
                &amp;lt;groupId&amp;gt;org.mockito&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;mockito-junit-jupiter&amp;lt;/artifactId&amp;gt;
            &amp;lt;/exclusion&amp;gt;
        &amp;lt;/exclusions&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;4.12&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Elasticsearch &amp;amp; Reactive Elasticsearch Auto-configuration&lt;/h3&gt;

Elasticsearch 관련해 조금 변경 사항이 있다. Elasticsearch 쪽의 &lt;code&gt;TransportClient&lt;/code&gt;가 7.0부터 Deprecated 되어 아마도 추후에는 해당 관련 설정은 다 삭제 될 것으로 보인다. 클러스터 노드를 설정할 수 있는 &lt;code&gt;spring.data.elasticsearch.cluster-nodes&lt;/code&gt; 와 &lt;code&gt;spring.data.elasticsearch.properties&lt;/code&gt; 프로퍼티도 Deprecated 되었다.

jest 또한 Deprecated 되어 이제는 &lt;code&gt;RestHighLevelClient&lt;/code&gt;를 권장하는 듯 하다.

Reactive Elasticsearch 자동설정이 추가 되었다. 해당 설정은 &lt;code&gt;spring.data.elasticsearch.client.reactive.&lt;/code&gt; 들을 이용하면 된다. 해당 설정후에 &lt;code&gt;ReactiveElasticsearchClient&lt;/code&gt;를 직접사용해도 되고, Spring 스럽게(?) 만든 &lt;code&gt;ReactiveElasticsearchOperations&lt;/code&gt; 을 이용해도 좋다.

또한 &lt;code&gt;ReactiveElasticsearchRepository&lt;/code&gt;을 이용하여 Spring data 스럽게(?) 만든 repository도 이용할 수 있다.

&lt;pre&gt;&lt;code&gt;public class FooController {

    private final ReactiveElasticsearchClient reactiveElasticsearchClient;
    private final ReactiveElasticsearchOperations reactiveElasticsearchOperations;

    public FooController(ReactiveElasticsearchClient reactiveElasticsearchClient,
                         ReactiveElasticsearchOperations reactiveElasticsearchOperations) {
        this.reactiveElasticsearchClient = reactiveElasticsearchClient;
        this.reactiveElasticsearchOperations = reactiveElasticsearchOperations;
    }

    // ...
}

public interface FooRepository implements ReactiveElasticsearchRepository&amp;lt;Foo, String&amp;gt; {

    //...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Hibernate Dialect&lt;/h3&gt;

예전에는 아무 설정 하지 않았을 경우 Dialect 를 spring boot 가 결정하곤 했다. 하지만 이제는 spring boot가 결정하지 않고 해당 JPA 컨테이너가 결정하기로 변경하였다. 사실 하이버네이트 말곤 다른 구현체들은 잘 모르겠다. 테스트도 해보지 않아서.. 만약 동작하지 않는다면 명시적으로 선언하면 된다.

&lt;h3&gt;Actuator HTTP Trace and Auditing are disabled by default&lt;/h3&gt;

Actuator HTTP Trace 와 Auditing 기본적으로 활성화가 되지 않는다. 기본적으로는 인 메모리를 사용하기 때문에 불필요한 메모리를 사용하고 클러스터의 친화적이지 않기 때문에 비활성으로 기본값을 변경하였다. 만약 운영에서 사용하려면 Spring Cloud Sleuth 또는 그와 유사한 어떠한 것을 사용해도 좋다.

혹시나 간단한 테스트를 하기 위해 인메모리라도 사용하고 싶다면 다음과 같이 설정하면 된다.

&lt;pre&gt;&lt;code&gt;@Bean
public InMemoryHttpTraceRepository traceRepository() {
    return new InMemoryHttpTraceRepository();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Logback max history&lt;/h3&gt;

로그백 기본 max history가 0일에서 7일로 변경되었다. 만약 그 값을 변경하고 싶다면 &lt;code&gt;logging.file.max-history&lt;/code&gt; 프로퍼티를 사용해 변경하면 된다.

&lt;h3&gt;Sample projects renamed and relocated&lt;/h3&gt;

Sample projects 들의 이름(smoke)과 소스 재배치가 되었다. 궁금하다면 github을 찾아 보면 된다.

&lt;h3&gt;Java 13 support&lt;/h3&gt;

Spring Boot 2.2는 Java 13에 지원한다. 또한 Java 8 및 11도 여전히 지원하고 있다.

&lt;h3&gt;Lazy initialization&lt;/h3&gt;

모든 빈들을 초기화를 지연 시킬수 있다. &lt;code&gt;spring.main.lazy-initialization&lt;/code&gt; 이용해서 모든 빈들의 생성을 지연시킬 수 있는 옵션이다. 여기서 주의할 점은 http 첫 요청이 조금 느리고, startup시 에러가 발생한 코드가 들어있다면 startup시 찾지 못할 수도 있다. 사실 http 첫 요청이 느린건 괜찮지만 후자인 startup시 에러가 발생하지 않는다면 조금 크리티컬한 부분일 수 도 있다. 아마도 운영환경에선 사용하지 않는 것을 추천하고 싶다.

만약 때때로 특정 클래스는 초기화를 지연시킬 필요가 없거나 그러고 싶지 않은 경우도 있을 것이다. 그럴때는 &lt;code&gt;@Lazy(false)&lt;/code&gt;어노테이션을 해당 빈에 설정하면 된다.

&lt;pre&gt;&lt;code&gt;@Bean
@Lazy(false)
public Filters filters() {
    return new Filters();
}
&lt;/code&gt;&lt;/pre&gt;

위의 경우는 초기화 지연을 하지 않는다. 만약 개발자가 직접 컨트롤 할 수 없는 클래스들은 &lt;code&gt;LazyInitializationExcludeFilter&lt;/code&gt;를 이용해서 제외 시키면 된다.

&lt;pre&gt;&lt;code&gt;@Bean
static LazyInitializationExcludeFilter integrationLazyInitExcludeFilter() {
    return LazyInitializationExcludeFilter.forBeanTypes(Filters.class);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Spring Data Moore&lt;/h3&gt;

Spring Boot 2.2는 Spring Data Moore(spring data 2.2) 를 제공한다. 사실 이부분은 추후에 다룰 예정이니 그때 글을 참고하면 될 것같다.

&lt;h3&gt;@ConfigurationProperties scanning&lt;/h3&gt;

@ConfigurationProperties 어노테이션을 스캐닝 할 수 있는 어노테이션이 추가 되었다. spring boot 2.2 이전엔 &lt;code&gt;@ConfigurationProperties&lt;/code&gt; 어노테이션을 직접사용할 경우에 @Component 나 @EnableConfigurationProperties 어노테이션을 사용했지만 이제는 그럴 필요 없다. &lt;code&gt;@ConfigurationPropertiesScan&lt;/code&gt; 어노테이션을 사용하면 @ConfigurationProperties 어노테이션이 달린 클래스들은 모두 스캔하므로 추가적인 작업을 할 필요 없다.

&lt;pre&gt;&lt;code&gt;@EnableConfigurationProperties(FooProperties.class)
@Configuration
public class Config {
}

or 

@Component
@ConfigurationProperties(&quot;foo&quot;)
public class FooProperties {
}

&lt;/code&gt;&lt;/pre&gt;

예전에는 위와같이 작업을 했다면 이제는 그럴 필요 없이 아래와 같이 작업을 하면 된다.

&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@ConfigurationPropertiesScan
public class Application {
   //.. main 
}
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
  참고로 spring 2.2.0 에서는 @SpringBootApplication 어노테이션에 @ConfigurationPropertiesScan 어노테이션이 메타 어노테이션으로 존재했지만 spring boot 2.2.1에서는 버그로 인해 제거 되었다. 그 이유가 궁금하다면 &lt;a href=&quot;https://github.com/spring-projects/spring-boot/issues/18674&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;여기&lt;/a&gt;를 살펴보면 된다.
&lt;/blockquote&gt;

&lt;h3&gt;Immutable @ConfigurationProperties binding&lt;/h3&gt;

불변의 @ConfigurationProperties가 추가 되었다. 코틀린을 좀 더 위한거겠지? 어떻게 사용하는지 보자.

&lt;pre&gt;&lt;code&gt;@ConfigurationProperties(&quot;http&quot;)
@ConstructorBinding
public class BarProperties {

    private final String url;
    private final Duration timeout;
    private final LocalDate date;

    public BarProperties(String url, @DefaultValue(&quot;10s&quot;) Duration timeout,
                         @DateTimeFormat(pattern = &quot;yyyyMMdd&quot;) LocalDate date) {
        this.url = url;
        this.timeout = timeout;
        this.date = date;
    }

    public Duration getTimeout() {
        return timeout;
    }

    public String getUrl() {
        return url;
    }

    public LocalDate getDate() {
        return date;
    }
}

&lt;/code&gt;&lt;/pre&gt;

위와 같이 @ConstructorBinding 어노테이션을 이용해 생성자 바인딩을 한다고 알려줘야 한다. 그런후엔 생성자로 파라미터들을 받을 수 있다. &lt;code&gt;@DefaultValue&lt;/code&gt; 어노테이션을 이용해 기본값을 넣을 수도 있고 &lt;code&gt;@DateTimeFormat&lt;/code&gt; 어노테이션을 이용해 date format도 지정할 수 있다.

&lt;pre&gt;&lt;code&gt;http.url=http://localhost
http.date=20191111
&lt;/code&gt;&lt;/pre&gt;

코틀린 코드도 한번 보자.

[kotlin]\r
@ConfigurationProperties(&quot;http&quot;)\r
@ConstructorBinding\r
data class BarProperties(\r
    val url: String,\r
    val timeout: Duration = Duration.ofSeconds(10),\r
    @DateTimeFormat(pattern = &quot;yyyyMMdd&quot;)\r
    val date: LocalDate\r
)\r
[/kotlin]

더 심플한 코드가 되었다. 이래서 코틀린을 써야.. 아무튼 좀 더 코틀린스럽게(?) 코드가 되었다.

&lt;h3&gt;RSocket Support&lt;/h3&gt;

Rsocket을 지원하기 시작했다. Rsocket이 뭔지 궁금한 분들을 &lt;a href=&quot;http://rsocket.io&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;여기&lt;/a&gt;를 참고하면 되겠다. 
&lt;code&gt;spring-boot-starter-rsocket&lt;/code&gt;인 starter 를 디펜더시 받으면 자동설정이 동작한다. &lt;code&gt;spring.rsocket.server.&lt;/code&gt; 프로퍼티들을 이용해서 설정할 수 있으니 참고하면 되겠다.
CBOR과 JSON을 사용하여 인코딩 디코딩 설정을 자동으로 구성해주고 있다. 또한 &lt;code&gt;spring-security-rsocket&lt;/code&gt;가 클래스패스에 있을 경우 시큐리티도 자동으로 설정 된다.
아주 간단하게 샘플 코드만 보도록 하고 나머지는 해당 문서를 보는 것을 추천한다.

&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-rsocket&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;

@Controller
public class RSocketController {

    @MessageMapping(&quot;foo.{name}&quot;)
    public Mono&amp;lt;Foo&amp;gt; foo(@DestinationVariable String name) {
        return Mono.just(new Foo(name));
    }
}

public class Foo {

    private final String name;

    public Foo(@JsonProperty(&quot;name&quot;) String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

@SpringBootTest(properties = &quot;spring.rsocket.server.port=0&quot;)
class RSocketControllerTests {

    @LocalRSocketServerPort
    private int port;

    private final RSocketRequester.Builder builder;

    @Autowired
    RSocketControllerTests(RSocketRequester.Builder builder) {
        this.builder = builder;
    }

    @Test
    void rsocketTest() {
        RSocketRequester requester = this.builder
                .connectTcp(&quot;localhost&quot;, this.port).block(Duration.ofSeconds(10));
        Mono&amp;lt;Foo&amp;gt; result = requester.route(&quot;foo.wonwoo&quot;).retrieveMono(Foo.class);
        StepVerifier.create(result)
                .assertNext(it -&amp;gt; Assertions.assertThat(it.getName()).isEqualTo(&quot;wonwoo&quot;))
                .verifyComplete();
    }
}

&lt;/code&gt;&lt;/pre&gt;

음 딱히 어려운 부분은 없다. 우리가 자주 사용하는 web Controller와 비슷한 느낌이라 많은 거부감은 없는 듯 싶다.

&lt;h3&gt;RestTemplateBuilder request customisation&lt;/h3&gt;

RestTemplateBuilder 에 몇가지 추가 되었다. 기본헤더를 넣을 수 있거나 request를 커스텀할 수 있는 기능이다.

&lt;pre&gt;&lt;code&gt;public RestTemplateBuilder defaultHeader(String name, String... values)

public RestTemplateBuilder requestCustomizers(RestTemplateRequestCustomizer&amp;lt;?&amp;gt;... requestCustomizers)

&lt;/code&gt;&lt;/pre&gt;

요청 정보 커스터마이저 메서드는 좀 더 있지만 비슷한 맥략이라 생략했다.

&lt;pre&gt;&lt;code&gt;private final RestTemplateBuilder builder;

public TestController(RestTemplateBuilder builder) {
    this.builder = builder.defaultHeader(&quot;foo&quot;, &quot;bar&quot;).requestCustomizers(request -&amp;gt; ...).build();
}

&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Plain text support for Thread dump endpoint&lt;/h3&gt;

actuator endpoint 중 하나인 Thread dump를 Plain text 내려받을 수 있다. 기존에는 json만 존재했지만 이제는 Plain text도 추가 되어 &lt;a href=&quot;https://github.com/irockel/tda&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Thread Dump Analyzer&lt;/a&gt; 와 
&lt;a href=&quot;https://fastthread.io&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;https://fastthread.io&lt;/a&gt; 여기에서 비쥬얼라이징 할 수 있으니 참고하면 되겠다. 사실 테스트는 fastthread 여기에서만 해봤다.

&lt;h3&gt;Qualifier for Spring Batch datasource&lt;/h3&gt;

여러 데이터 소스가 있는 경우 &lt;code&gt;@BatchDataSource&lt;/code&gt;어노테이션으로 spring batch에서 사용하는 datasource를 표시할 수 있다.

&lt;h3&gt;Health indicator groups&lt;/h3&gt;

Health indicator를 그룹핑하여 사용할 수 있다. 쿠버네티스의 liveness, readiness 프로브의 대해 다른 상태를 표시할 수 있다는 그런내용?..

&lt;pre&gt;&lt;code&gt;management.endpoint.health.group.foo.include=db,redis
&lt;/code&gt;&lt;/pre&gt;

위와 같이 설정했다면 엔드포인트 &lt;code&gt;/actuator/health/foo&lt;/code&gt;를 실행 할 수 있다. 그러면 db와 redis만 Health indicator에 포함되어 체크한다. 만약 redis를 포함하고 싶지 않다면 redis를 제외 시키면 된다.

&lt;pre&gt;&lt;code&gt;management.endpoint.health.group.foo.include=db
&lt;/code&gt;&lt;/pre&gt;

이외에도 좀 더 많은 프로퍼티가 존재하니 참고하면 되겠다.

&lt;pre&gt;&lt;code&gt;management.endpoint.health.group.foo.show-details=
management.endpoint.health.group.foo.roles=
management.endpoint.health.group.foo.exclude=
management.endpoint.health.group.foo.show-components=

&lt;/code&gt;&lt;/pre&gt;

하나만 제외하고 나머지 프로퍼티들은 원래 있던 기능이니 해당 문서를 살펴보길 추천한다. &lt;code&gt;show-components&lt;/code&gt; 바로 밑에 설명하겠다.

&lt;h3&gt;Health Endpoint component detail&lt;/h3&gt;

Health Endpoint 의 component detail을 볼 수 있는 기능이 생겼다. 위에서 잠깐 언급했지만 그 프로퍼티는 &lt;code&gt;show-components&lt;/code&gt;를 이용하면 된다. 이것 역시 &lt;code&gt;show-details&lt;/code&gt;과 동일하게 NEVER, WHEN_AUTHORIZED, ALWAYS 등 3가지 타입이 존재한다. 
NEVER는 표시 하지 않겠다는 의미, WHEN_AUTHORIZED 인증 후에 표시 하겠다는 의미, ALWAYS 항상 표시 하겠다는 의미를 갖고 있다.

이 기능은 세부정보와는 다르게 컴포넌트들의 Health 정보의 상태만 보여주는 기능이다.

&lt;pre&gt;&lt;code&gt;http :8080/actuator/health

{
    &quot;components&quot;: {
        &quot;db&quot;: {
            &quot;status&quot;: &quot;UP&quot;
        },
        &quot;diskSpace&quot;: {
            &quot;status&quot;: &quot;UP&quot;
        },
        &quot;ping&quot;: {
            &quot;status&quot;: &quot;UP&quot;
        }
    },
    &quot;status&quot;: &quot;UP&quot;
}
&lt;/code&gt;&lt;/pre&gt;

show-details과는 다르게 각 컴포넌트 별 &lt;code&gt;status&lt;/code&gt;만 출력 되는 것을 볼 수 있다.

오늘은 위와 같이 spring boot 2.2에 대해서 알아봤다. 사실 더 많은 내용이 있긴 한데 필자가 잘 쓰지 않거나 이해 되지 않은 내용은 작성하지 않았으니 꼭 해당 문서를 통해 확인하길 바란다.&lt;/div&gt;</description>
      <author>머룽</author>
      <guid isPermaLink="true">https://mkzz.tistory.com/336</guid>
      <comments>https://mkzz.tistory.com/336#entry336comment</comments>
      <pubDate>Sun, 23 Apr 2023 14:06:13 +0900</pubDate>
    </item>
    <item>
      <title>Spring WebClient</title>
      <link>https://mkzz.tistory.com/335</link>
      <description>&lt;div class='markdown-body'&gt;오늘은 Spring의 &lt;code&gt;WebClient&lt;/code&gt;의 사용법에 대해서 몇가지 알아보도록 하자. 사용 API만 살펴 볼 예정이므로 reactive streams(reactor..) 들의 개념과 사용법은 다른 블로그를 살펴보길 바란다. reactive streams 대한 내용을 알고 보면 좋지만 몰라도 코드를 보는데는 문제가 없을 듯 하다.

WebClient는 Spring5 에 추가된 인터페이스다. spring5 이전에는 비동기 클라이언트로 &lt;code&gt;AsyncRestTemplate&lt;/code&gt;를 사용을 했지만 spring5 부터는 Deprecated 되어 있다. 만약 spring5 이후 버전을 사용한다면 AsyncRestTemplate 보다는 WebClient 사용하는 것을 추천한다. 아직 spring 5.2(현재기준) 에서 AsyncRestTemplate 도 존재하긴 한다.

&lt;h3&gt;기본 문법&lt;/h3&gt;

기본적으로 사용방법은 아주 간단하다. WebClient 인터페이스의 static 메서드인 &lt;code&gt;create()&lt;/code&gt;를 사용해서 &lt;code&gt;WebClient&lt;/code&gt; 를 생성하면 된다. 한번 살펴보자.

&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
void test1() {

    WebClient webClient = WebClient.create(&quot;http://localhost:8080&quot;);
    Mono&amp;lt;String&amp;gt; hello = webClient.get()
            .uri(&quot;/sample?name={name}&quot;, &quot;wonwoo&quot;)
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext(&quot;hello wonwoo!&quot;)
            .verifyComplete();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;code&gt;create()&lt;/code&gt;는 두가지 메서드가 있는데 baseUrl를 지정해주는 것과 그렇지 않은 것 두가지가 존재한다. 원하는 API를 사용하면 되겠다.

&lt;pre&gt;&lt;code&gt;@Test
void test1() {

    WebClient webClient = WebClient.create();
    Mono&amp;lt;String&amp;gt; hello = webClient.get()
            .uri(&quot;http://localhost:8080/sample?name={name}&quot;, &quot;wonwoo&quot;)
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext(&quot;hello wonwoo!&quot;)
            .verifyComplete();
}
&lt;/code&gt;&lt;/pre&gt;

API 자체도 명확하다. get(), post(), put(), patch(), 등 http method들을 모두 정의되어 있다.

&lt;pre&gt;&lt;code&gt;webClient.get()
  .///
webClient.post()
  .///
webClient.put()
  .///
webClient.method(HttpMethod.GET)
  .///
&lt;/code&gt;&lt;/pre&gt;

또는 위와 같이 HttpMethod를 지정할 수 있다.

uri 또한 여러 메서드가 존재한다. 단순하게 string 으로 uri을 만들 수 도 있고 queryParam, pathVariable 등 명확하게 uri을 만들 수 도 있다. 위의 코드를  사실 &lt;code&gt;RestTemplate&lt;/code&gt; 클래스를 자주 사용했다면 익숙한 문법일 수 있다.

&lt;pre&gt;&lt;code&gt;@Test
void test1_3() {

    WebClient webClient = WebClient.create();
    Mono&amp;lt;String&amp;gt; hello = webClient.get()
            .uri(&quot;http://localhost:8080/sample?name=wonwoo&quot;)
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext(&quot;hello wonwoo!&quot;)
            .verifyComplete();
}


@Test
void test1_3() {

    WebClient webClient = WebClient.create(&quot;http://localhost:8080&quot;);
    Mono&amp;lt;String&amp;gt; hello = webClient.get()
            .uri(&quot;/sample?name={name}&quot;, Map.of(&quot;name&quot;, &quot;wonwoo&quot;))
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext(&quot;hello wonwoo!&quot;)
            .verifyComplete();
}

@Test
void test1_3() {

    WebClient webClient = WebClient.create(&quot;http://localhost:8080&quot;);
    Mono&amp;lt;String&amp;gt; hello = webClient.get()
            .uri(&quot;/sample?name={name}&quot;, &quot;wonwoo&quot;)
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext(&quot;hello wonwoo!&quot;)
            .verifyComplete();
}

@Test
void test1_3() {

    WebClient webClient = WebClient.create(&quot;http://localhost:8080&quot;);
    Mono&amp;lt;String&amp;gt; hello = webClient.get()
            .uri(it -&amp;gt; it.path(&quot;/sample&quot;)
                    .queryParam(&quot;name&quot;, &quot;wonwoo&quot;)
                    .build()
            ).retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext(&quot;hello wonwoo!&quot;)
            .verifyComplete();
}
&lt;/code&gt;&lt;/pre&gt;

위와 같이 여러 방법이 존재하니 각자가 원하는 어떤것을 사용해도 좋다. 마지막 &lt;code&gt;S uri(Function&amp;lt;UriBuilder, URI&amp;gt; uriFunction)&lt;/code&gt; API는 좀 더 세세하게 컨트롤 할 수 있으니 세세하게 컨트롤 할 일이 있다면 이 API를 사용하는게 좋다.

다음은 &lt;code&gt;retrieve()&lt;/code&gt; 메서드인데 이 메서드는 request를 만들고 http request를 보내는 역할을 한다. 이 메서드 말고 &lt;code&gt;exchange()&lt;/code&gt;가 존재하는데 약간 다르다. 사실 API만 살짝 다를뿐이지 retrieve() 내부에선 exchange() 메서드를 호출한다.

retrieve() 메서드는 &lt;code&gt;ResponseSpec&lt;/code&gt; 타입을 리턴하고 exchange() 메서드는 &lt;code&gt;Mono&amp;lt;ClientResponse&amp;gt;&lt;/code&gt; 를 리턴하고 있다.

&lt;pre&gt;&lt;code&gt;@Test
void test2() {

    WebClient webClient = WebClient.create(&quot;http://localhost:8080&quot;);

    Mono&amp;lt;String&amp;gt; hello = webClient.get()
            .uri(&quot;/sample?name={name}&quot;, &quot;wonwoo&quot;)
            .exchange()
            .flatMap(it -&amp;gt; it.bodyToMono(String.class));

    StepVerifier.create(hello)
            .expectNext(&quot;hello wonwoo!&quot;)
            .verifyComplete();
}
&lt;/code&gt;&lt;/pre&gt;

위의 test1_3 메서드와 test2 메서드는 동일한 동작을 한다. 위에서 말했다시피 exchange() 메서드는 ClientResponse를 사용해 좀 더 세세한 컨트롤을 하면 된다.

&lt;pre&gt;&lt;code&gt;@Test
void test2() {

    WebClient webClient = WebClient.create(&quot;http://localhost:8080&quot;);

    Mono&amp;lt;String&amp;gt; hello = webClient.get()
            .uri(&quot;/sample1?name={name}&quot;, &quot;wonwoo&quot;)
            .exchange()
            .flatMap(it -&amp;gt; {
                if(it.statusCode() == HttpStatus.NOT_FOUND) {
                    throw new NotFoundException(&quot;....&quot;);
                }
                return it.bodyToMono(String.class);
            });

    StepVerifier.create(hello)
            .verifyError(NotFoundException.class);
}
&lt;/code&gt;&lt;/pre&gt;

이렇게 기본문법에 대해서 알아봤다. 그리 어려운 내용도 없는 듯 하다. 좀 더 범용성있게 사용하려면 아직은 부족하다. 좀 더 살펴보자.

&lt;h3&gt;formData 와 message&lt;/h3&gt;

위에서 알아보지 않은게 있는데 바로 post나 put 기타 http 메서드에 자주 사용하는 formdata 와 message에 대해서 알아보자. 
만약 formData 로 server에 보낸다면 다음과 같이 작성하면 된다.

&lt;pre&gt;&lt;code&gt;import static org.springframework.web.reactive.function.BodyInserters.fromFormData;

@Test
void test3() {

    WebClient webClient = WebClient.create(&quot;http://localhost:8080&quot;);
    Mono&amp;lt;String&amp;gt; hello = webClient.post()
            .uri(&quot;/sample&quot;)
            .body(fromFormData(&quot;name&quot;, &quot;wonwoo&quot;))
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext(&quot;wonwoo&quot;)
            .verifyComplete();
}

&lt;/code&gt;&lt;/pre&gt;

&lt;code&gt;fromFormData&lt;/code&gt;란 static 메서드를 사용해서 전달하면 된다. 만약 좀 더 많은 내용이 있다면 &lt;code&gt;.with(key, value)&lt;/code&gt; 메서드를 체이닝해 사용하면 된다.

&lt;pre&gt;&lt;code&gt;.body(fromFormData(&quot;name&quot;, &quot;wonwoo&quot;).with(&quot;foo&quot;,&quot;bar&quot;).with(&quot;...&quot;,&quot;...&quot;))
&lt;/code&gt;&lt;/pre&gt;

또는 &lt;code&gt;MultiValueMap&lt;/code&gt;를 이용해서 fromFormData에 넣어도 된다.

이번엔 message에 대해서 알아보자. 일반적으로 json, xml 기타 message로 보낼때 유용하다. 한번 살펴보자.

&lt;pre&gt;&lt;code&gt;@Test
void test3_1() {

    WebClient webClient = WebClient.create(&quot;http://localhost:8080&quot;);
    Mono&amp;lt;String&amp;gt; hello = webClient.put()
            .uri(&quot;/sample&quot;)
            .bodyValue(new Sample(&quot;wonwoo&quot;))
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext(&quot;wonwoo&quot;)
            .verifyComplete();
}

&lt;/code&gt;&lt;/pre&gt;

위와같이 &lt;code&gt;bodyValue&lt;/code&gt;를 이용해서 message를 전달할 수 있다. 참고로 spring 5.2 이전버전에선 &lt;code&gt;syncBody&lt;/code&gt;를 이용해야 한다. spring 5.2에선 syncBody는 Deprecated 되었다.

만약 전달하는 message가 Publisher 타입일 수 도 있다. 그럼 다음과 같이 작성하면 된다.

&lt;pre&gt;&lt;code&gt;@Test
void test3_2() {

    WebClient webClient = WebClient.create(&quot;http://localhost:8080&quot;);
    Mono&amp;lt;String&amp;gt; hello = webClient.put()
            .uri(&quot;/sample&quot;)
            .body(Mono.just(new Sample(&quot;wonwoo&quot;)), Sample.class)
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext(&quot;wonwoo&quot;)
            .verifyComplete();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;filter&lt;/h3&gt;

filter이용하면 client를 호출하기전에 인터셉터해서 추가적인 작업을 할 수 있다. 예를들면 로그를 출력 할 수도 있고 헤더정보 혹은 인증정보를 넣어 호출 할 수 있다.

필터를 사용하려면 &lt;code&gt;ExchangeFilterFunction&lt;/code&gt; 인터페이스를 구현하면 된다. 추상 메서드는 하나뿐이라 람다를 이용해도 좋다.

&lt;pre&gt;&lt;code&gt;@Test
void test4() {

    WebClient webClient = WebClient.builder().filter((request, next) -&amp;gt; next.exchange(ClientRequest.from(request)
            .header(&quot;foo&quot;, &quot;bar&quot;).build())).baseUrl(&quot;http://localhost:8080&quot;)
            .build();

    Mono&amp;lt;String&amp;gt; hello = webClient.get()
            .uri(&quot;/sample?name={name}&quot;, &quot;wonwoo&quot;)
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext(&quot;hello wonwoo!&quot;)
            .verifyComplete();

}
&lt;/code&gt;&lt;/pre&gt;

위의 코드는 헤더 정보 를 추가하는 코드이다. 또한 한번에 여러 필터를 적용할 수 도 있다.

&lt;pre&gt;&lt;code&gt;WebClient.builder().filters(exchangeFilterFunctions -&amp;gt;
        exchangeFilterFunctions.add(0, (request, next) -&amp;gt; {
            return next.exchange(request);
        }));
&lt;/code&gt;&lt;/pre&gt;

위의 코드는 0번째에 해당 필터를 삽입하는 코드이다. 물론 filter를 계속 체이닝해서 써도 상관 없다.

&lt;h3&gt;ClientHttpConnector&lt;/h3&gt;

현재 spring에서는 reactive http client가 2개밖에 존재하지 않는다. netty와 jetty이다. 사실 spring을 사용한다면 그냥 netty를 사용하는게 정신건강에 좋을 듯 싶다.

&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.eclipse.jetty&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jetty-reactive-httpclient&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

위와 같이 jetty reactive httpclient를 먼저 디펜더시 받은 후에 다음과 같이 작성하면 된다.

&lt;pre&gt;&lt;code&gt;@Test
void test5() {
    WebClient webClient = WebClient.builder().clientConnector(new JettyClientHttpConnector())
            .baseUrl(&quot;http://localhost:8080&quot;).build();

    Mono&amp;lt;String&amp;gt; hello = webClient.get()
            .uri(&quot;/sample?name={name}&quot;, &quot;wonwoo&quot;)
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext(&quot;hello wonwoo!&quot;)
            .verifyComplete();
}
&lt;/code&gt;&lt;/pre&gt;

추가 적인 설정은 &lt;code&gt;JettyClientHttpConnector&lt;/code&gt; 클래스와 &lt;code&gt;org.eclipse.jetty.client.HttpClient&lt;/code&gt; 클래스를 살펴보면 되겠다.

&lt;h3&gt;default values&lt;/h3&gt;

만약 기본으로 헤더정보 쿠키정보등 값을 지정하고 싶다면 다음과 같이 작성하면 된다.

&lt;pre&gt;&lt;code&gt;@Test
void test6() {

    WebClient webClient = WebClient.builder().baseUrl(&quot;http://localhost:8080&quot;)
            .defaultHeader(&quot;foo&quot;, &quot;bar&quot;)
            .defaultCookie(&quot;foo&quot;, &quot;BAR&quot;)
            .defaultRequest(it -&amp;gt; it.header(&quot;test&quot;, &quot;sample&quot;)).build();

    Mono&amp;lt;String&amp;gt; hello = webClient.get()
            .uri(&quot;/sample?name={name}&quot;, &quot;wonwoo&quot;)
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext(&quot;hello wonwoo!&quot;)
            .verifyComplete();

}
&lt;/code&gt;&lt;/pre&gt;

위와 같이 작성하면 기본으로 위와 같은 값이 같이 전송된다. &lt;code&gt;defaultRequest()&lt;/code&gt; 메서드를 사용하면 좀더 세세하게 컨트롤이 가능하니 참고하면 되겠다.

&lt;h3&gt;retrieve&lt;/h3&gt;

위에서 잠깐 언급한 retrieve 메서드를 이용하는 경우에 보다 상세한 에러 코드들을 컨트롤 할 수 있다. 원한다면 사용해도 좋다.

&lt;pre&gt;&lt;code&gt;@Test
void test7() {
    WebClient webClient = WebClient.create(&quot;http://localhost:8080&quot;);

    Mono&amp;lt;String&amp;gt; hello = webClient.get()
            .uri(&quot;/sample1?name={name}&quot;, &quot;wonwoo&quot;)
            .retrieve()
            .onStatus(HttpStatus::is4xxClientError, __ -&amp;gt; Mono.error(new IllegalArgumentException(&quot;4xx&quot;)))
            .onStatus(HttpStatus::is5xxServerError, __ -&amp;gt; Mono.error(new IllegalArgumentException(&quot;5xx&quot;)))
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .verifyErrorMessage(&quot;4xx&quot;);

} 
&lt;/code&gt;&lt;/pre&gt;

onStatus 메서드를 이용해서 해당 코드를 작성후에 Mono type의 exception을 던지면 된다. 위의 코드는 4xx 에러 코드일땐 4xx라는 메시지를 던지고 5xx 에러 코드일 땐 5xx라는 메세지를 던진다는 코드이다.

onStatus() 메서드 말고도 onRawStatus() 메서드도 존재한다. 이것은 위와 같이 HttpStatus 코드가 아닌 int로된 코드를 리턴한다.

&lt;pre&gt;&lt;code&gt;@Test
void test8() {
    WebClient webClient = WebClient.create(&quot;http://localhost:8080&quot;);

    Mono&amp;lt;String&amp;gt; hello = webClient.get()
            .uri(&quot;/sample?name={name}&quot;, &quot;wonwoo&quot;)
            .retrieve()
            .onRawStatus(it -&amp;gt; it == 400, __ -&amp;gt; Mono.error(new IllegalArgumentException(&quot;aaa&quot;)))
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .verifyErrorMessage(&quot;400&quot;);
}
&lt;/code&gt;&lt;/pre&gt;

이렇게 기본문법과 사용법에 대해서 알아봤다. 물론 좀 더 많은 메서드들이 있지만 필자가 자주 사용할만한 API 위주로 살펴봤다. 다른 궁금한 점이 있다면 해당 문서를 찾아보길 추천한다.

마지막으로 Spring boot를 사용할 때에 WebClient는 어떻게 사용해야 될까? 사실 기본적인 설정은 되어있다. 그래서 아주 쉽고 간단하게 사용할 수 있다.

&lt;h3&gt;spring boot&lt;/h3&gt;

spring boot 를 사용할 때는 WebClient.Builder 인터페이스가 기본적으로 bean으로 등록 되어있다. 그래서 우리는 이걸 사용하면 된다.

&lt;pre&gt;&lt;code&gt;@RestController
public class WebClientController {

    private final WebClient webClient;

    public WebClientController(WebClient.Builder builder) {
        this.webClient = builder.baseUrl(&quot;http://localhost:9999&quot;).build();
    }

    @GetMapping(&quot;/users&quot;)
    public Mono&amp;lt;String&amp;gt; findByName() {
        return webClient.get()
                .uri(&quot;/users&quot;)
                .retrieve()
                .bodyToMono(String.class);
    }
}

&lt;/code&gt;&lt;/pre&gt;

딱히 설명할 것도 없다. 만약 필터나 default values 가 필요하면 위에서 했던 그 방법을 그대로 이용하면 된다.

&lt;pre&gt;&lt;code&gt;public WebClientController(WebClient.Builder builder) {
    this.webClient = builder
            .filter((request, next) -&amp;gt; next.exchange(request))
            .defaultHeader(&quot;foo&quot;, &quot;bar&quot;)
            .baseUrl(&quot;http://localhost:8080&quot;)
            .build();
}
&lt;/code&gt;&lt;/pre&gt;

그리고 만약 전역적으로 커스텀할 코드들이 있다면 &lt;code&gt;WebClientCustomizer&lt;/code&gt; 인터페이스를 이용해서 커스텀할 수 있다.

&lt;pre&gt;&lt;code&gt;@Bean
WebClientCustomizer webClientCustomizer() {
    return builder -&amp;gt; builder.filter((request, next) -&amp;gt; next.exchange(request));
}
&lt;/code&gt;&lt;/pre&gt;

위와 같이 WebClientCustomizer 빈으로 등록하여 커스터마이징하면 된다.

번외로 kotlin 코드도 한번 살펴보자.

[kotlin]\r
fun user(name: String): Flux&lt;User&gt; {\r
    return this.webClient.get()\r
        .uri(&quot;/user/{name}&quot;, name)\r
        .body(Mono.just(&quot;foo&quot;))\r
        .retrieve()\r
        .bodyToFlux()\r
}\r
[/kotlin]

&lt;code&gt;bodyToFlux()&lt;/code&gt;, &lt;code&gt;bodyToMono()&lt;/code&gt;, &lt;code&gt;body()&lt;/code&gt;, &lt;code&gt;awaitExchange()&lt;/code&gt;, &lt;code&gt;bodyToFlow()&lt;/code&gt;, &lt;code&gt;awaitBody()&lt;/code&gt; 등 확장함수로 된 함수들이 몇가지 존재하니 참고하면 되겠다. 몇가지는 코루틴 관련 확장함수이다.

오늘은 이렇게 Spring의 WebClient에 대해서 살펴봤다. 이정도만 알아도 사용하기엔 충분할 듯 싶다. Spring 5에서 Non blocking http client를 사용한다면 꼭 WebClient를 사용하도록 하자!&lt;/div&gt;</description>
      <author>머룽</author>
      <guid isPermaLink="true">https://mkzz.tistory.com/335</guid>
      <comments>https://mkzz.tistory.com/335#entry335comment</comments>
      <pubDate>Sun, 23 Apr 2023 14:06:12 +0900</pubDate>
    </item>
    <item>
      <title>Spring WebFlux HandlerMethodArgumentResolver</title>
      <link>https://mkzz.tistory.com/334</link>
      <description>&lt;div class='markdown-body'&gt;오늘은 Spring WebFlux의 &lt;code&gt;HandlerMethodArgumentResolver&lt;/code&gt;에 대해서 알아보도록 하자.

사실 WebFlux 이전에 WebMvc에도 동일한 기능이 존재한다. 인터페이스명까지 동일하니 거부감은 사실 없다. 기존의 mvc의 기능과 동일은 하나 WebFlux API에 맞춰진 형태라 생각하면 된다. 어떤 기능인지는 &lt;a href=&quot;http://wonwoo.ml/index.php/post/1092&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;여기&lt;/a&gt;를 참고해도 되고 다른 블로그 혹은 문서를 살펴봐도 좋다.

WebMvc 클래스는 &lt;code&gt;org.springframework.web.method.support.HandlerMethodArgumentResolver&lt;/code&gt;와 같고 WebFlux의 클래스는 &lt;code&gt;org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver&lt;/code&gt; 이와 같다.

&lt;h2&gt;HandlerMethodArgumentResolverSupport&lt;/h2&gt;

위의 내용을 알아보기 전에 WebMvc에는 존재 하지 않지만 WebFlux에 존재하는 클래스인 &lt;code&gt;HandlerMethodArgumentResolverSupport&lt;/code&gt; 를 살펴보자. HandlerMethodArgumentResolverSupport 에는 protected 메서드가 3개 존재하는데 &lt;code&gt;checkParameterType&lt;/code&gt;, &lt;code&gt;checkParameterTypeNoReactiveWrapper&lt;/code&gt;, &lt;code&gt;checkAnnotatedParamNoReactiveWrapper&lt;/code&gt; 라는 메서드이다.

어떤 일들은 하는지 한번살펴보자. 일반적으로는 &lt;code&gt;HandlerMethodArgumentResolver.supportsParameter&lt;/code&gt; 메서드에서 많이 사용하고 있다.  물론 위의 메서드를 사용하지 않아도 된다.

&lt;h3&gt;checkParameterType&lt;/h3&gt;

&lt;code&gt;checkParameterType&lt;/code&gt; 메서드는 reactive 랩퍼 클래스를 허용한다는 메서드이다. 한마디로 파라미터에 reactive 랩퍼 클래스를 사용해도 된다는 의미를 갖고 있다.

사용법은 아래와 같다.

&lt;pre&gt;&lt;code&gt;@Override
public boolean supportsParameter(MethodParameter parameter) {
    return checkParameterType(parameter, UserInfo.class::isAssignableFrom);
}

&lt;/code&gt;&lt;/pre&gt;

두번째 파라미터는 &lt;code&gt;Predicate&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt;&lt;/code&gt;로 해당 조건이 만족하면 true를 리턴하고 그렇지 않으면 false를 리턴한다. 
UserInfo 라는 클래스를 파라미터로 허용한다는 뜻인데 그게 만약 reactive 클래스라도 허용한다는 뜻이다.

&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/&quot;)
public Mono&amp;lt;Message&amp;gt; message(Mono&amp;lt;UserInfo&amp;gt; userInfo) {

    //...
}

@GetMapping(&quot;/&quot;)
public Mono&amp;lt;Message&amp;gt; message(UserInfo userInfo) {

     //...
}
&lt;/code&gt;&lt;/pre&gt;

위와 같이 &lt;code&gt;Mono&amp;lt;UserInfo&amp;gt;&lt;/code&gt; 와 &lt;code&gt;UserInfo&lt;/code&gt; 둘다 가능하다는 뜻이다.

&lt;h3&gt;checkParameterTypeNoReactiveWrapper&lt;/h3&gt;

checkParameterType 메서드와는 다르게 reactive 스타일을 허용하지 않는다. 
사용법은 다음과 같다.

&lt;pre&gt;&lt;code&gt;@Override
public boolean supportsParameter(MethodParameter parameter) {
    return checkParameterTypeNoReactiveWrapper(parameter, UserInfo.class::isAssignableFrom);
}
&lt;/code&gt;&lt;/pre&gt;

두번째 파라미터는 이 또한 &lt;code&gt;Predicate&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt;&lt;/code&gt;로 해당 조건이 만족하면 true를 리턴하고 그렇지 않으면 false를 리턴한다.

&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/&quot;)
public Mono&amp;lt;Message&amp;gt; message(Mono&amp;lt;UserInfo&amp;gt; userInfo) {

    //...
}

&lt;/code&gt;&lt;/pre&gt;

만약 위와 같이 작성했지만 파라미터를 reactive 스타일로 받는다음 에러가 발생한다.

&lt;pre&gt;&lt;code&gt;FooHandlerMethodArgumentResolver does not support reactive type wrapper: reactor.core.publisher.Mono&amp;lt;ml.wonwoo.example.UserInfo&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

위의 메서드를 사용할 경우에는 아래와 같이 사용해야 된다.

&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/&quot;)
public Mono&amp;lt;Message&amp;gt; message(UserInfo userInfo) {

    //...
}


&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;checkAnnotatedParamNoReactiveWrapper&lt;/h3&gt;

위의 두 내용은 Type과 관련있었지만 이 메서드는 애노테이션과 관련이 있다. 이 메서드는 리액티브 스타일을 허용 하지 않는다.

사용법은 다음과 같다.

&lt;pre&gt;&lt;code&gt;@Override
public boolean supportsParameter(MethodParameter parameter) {
    return checkAnnotatedParamNoReactiveWrapper(parameter, CurrentUser.class,
            (annotation , clazz) -&amp;gt; !UserInfo.class.isAssignableFrom(clazz));
}
&lt;/code&gt;&lt;/pre&gt;

두번째 파라미터는 &lt;code&gt;BiPredicate&amp;lt;A, Class&amp;lt;?&amp;gt;&amp;gt;&lt;/code&gt;로 해당 조건이 만족하면 true를 리턴하고 그렇지 않으면 false를 리턴한다.

위와 같이 작성하면 아래와 같이 파라미터로 사용가능하다.

&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/&quot;)
public Mono&amp;lt;Message&amp;gt; message(@CurrentUser User user) {

     //...
}
&lt;/code&gt;&lt;/pre&gt;

근데 왜 checkAnnotatedParamReactiveWrapper 메서드는 만들지 않았을까?

어쨌든 WebFlux에서는 &lt;code&gt;HandlerMethodArgumentResolverSupport&lt;/code&gt; 대부분 상속받아서 구현하고 있으니 HandlerMethodArgumentResolverSupport 상속받아서 구현해도 되며 &lt;code&gt;HandlerMethodArgumentResolver&lt;/code&gt; 를 직접 구현해도 상관없다.

하지만 필자는 HandlerMethodArgumentResolverSupport 상속받아서 구현했다.

&lt;h2&gt;resolveArgument&lt;/h2&gt;

위에서는 어떤 파라미터로 받을지 체크하는 부분이였다면 &lt;code&gt;resolveArgument&lt;/code&gt;는 해당 파라미터에 대한 구현을 직접해줘야 한다.

만약 reactive 스타일로 파라미터로 받지 않는다면 예전처럼 webmvc 구현해도 되지만 그렇지 않고 reactive 스타일도 지원한다면 추가적으로 조금 구현해야 되는게 있다.

&lt;blockquote&gt;
  참고로 동기적으로 구현하고 싶다면 SyncHandlerMethodArgumentResolver 를 구현하면 된다.
&lt;/blockquote&gt;

해당 파라미터가 reactive 스타일의 파라미터인지 알아야 한다. 하지만 이것보다 중요한건 대부분 reactor를 사용하겠지만 만약 다른 Reactive Streams API(rxjava1, rxjava2, jdkFlow) 혹은 coroutine 을 사용한다면 그에 맞게 변환을 해줘야 한다. 그 클래스가 ReactiveAdapterRegistry 이며 자세한건 문서를 찾아보자.

&lt;blockquote&gt;
  참고로 HandlerMethodArgumentResolverSupport 의 메서드들도 ReactiveAdapterRegistry를 사용한다.
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code&gt;@Override
public Mono&amp;lt;Object&amp;gt; resolveArgument(MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {

    ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);

    ReactiveAdapter adapter = (resolvableType != null ? getAdapterRegistry().getAdapter(resolvableType.resolve()) : null);

    Mono&amp;lt;UserInfo&amp;gt; userMono = Mono.just(new UserInfo(&quot;wonwoo&quot;, &quot;wonwoo@test.com&quot;));

    return userMono
            .map(userInfo -&amp;gt; adapter != null ? adapter.fromPublisher(userMono) : userInfo);
}
&lt;/code&gt;&lt;/pre&gt;

위와 같이 해당 타입의 &lt;code&gt;ReactiveAdapter&lt;/code&gt;를 가져와 Publisher 하면 된다. 아주 간단하다. 크게 복잡한 내용은 없는 것 같다. 
그럼 다음과 같이 사용가능하다.

&lt;pre&gt;&lt;code&gt;//reactor
@GetMapping(&quot;/&quot;)
public Mono&amp;lt;UserInfo&amp;gt; hello(Mono&amp;lt;UserInfo&amp;gt; userInfo) {

    return userInfo;
}

@GetMapping(&quot;/&quot;)
public Flux&amp;lt;UserInfo&amp;gt; hello(Flux&amp;lt;UserInfo&amp;gt; userInfo) {

    return userInfo;
}

//rxjava 1,2 
@GetMapping(&quot;/&quot;)
public Single&amp;lt;UserInfo&amp;gt; hello(Single&amp;lt;UserInfo&amp;gt; userInfo) {

    return userInfo;
}

@GetMapping(&quot;/&quot;)
public Observable&amp;lt;UserInfo&amp;gt; hello(Observable&amp;lt;UserInfo&amp;gt; userInfo) {

    return userInfo;
}

@GetMapping(&quot;/&quot;)
public Maybe&amp;lt;UserInfo&amp;gt; hello(Maybe&amp;lt;UserInfo&amp;gt; userInfo) {

    return userInfo;
}

@GetMapping(&quot;/&quot;)
public Flowable&amp;lt;UserInfo&amp;gt; hello(Flowable&amp;lt;UserInfo&amp;gt; userInfo) {

    return userInfo;
}


//jdk
@GetMapping(&quot;/&quot;)
public Flow.Publisher&amp;lt;UserInfo&amp;gt; hello(Flow.Publisher&amp;lt;UserInfo&amp;gt; userInfo) {

    return userInfo;
}
&lt;/code&gt;&lt;/pre&gt;

rxjava는 잘 몰라서 그냥 테스트만 해봤다.

오늘은 이렇게 Spring WebFlux의 &lt;code&gt;HandlerMethodArgumentResolver&lt;/code&gt; 대해서 알아봤다. 만약 사용할 일이 있다면 언젠든지 사용해도 좋다. 어려운 내용이 아니므로 한번씩 해보는 것도 나쁘지 않아 보인다.

WebFlux도 공부할게 많다.&lt;/div&gt;</description>
      <author>머룽</author>
      <guid isPermaLink="true">https://mkzz.tistory.com/334</guid>
      <comments>https://mkzz.tistory.com/334#entry334comment</comments>
      <pubDate>Sun, 23 Apr 2023 14:06:10 +0900</pubDate>
    </item>
    <item>
      <title>Testcontainers 로 integration 테스트하기</title>
      <link>https://mkzz.tistory.com/333</link>
      <description>&lt;div class='markdown-body'&gt;오늘 이야기 할 내용은 &lt;code&gt;Testcontainers&lt;/code&gt;라이브러리로 integration 테스트를 해보도록 하자.

Testcontainers는 java 라이브러리로 (다른 언어도 존재는 함) 데이터베이스, 메시지 큐 또는 웹 서버와 같은 의존성이 있는 모듈에서 테스트 할 수 있게 도와주는 도구이다. 기본적으로는 docker 컨테이너 기반으로 동작하기에 docker가 설치는 되어 있어야 한다.

&lt;blockquote&gt;
  만약 docker가 설치 되어 있지 않다면 docker를 설치 해야 된다. 내부적으로는 도커의 이미지를 땡겨와 실행하기 때문이다.
&lt;/blockquote&gt;

&lt;code&gt;Testcontainers&lt;/code&gt; 다양한 테스트 프레임워크를 지원한다. junit4 부터 junit5, Spock등 java 진영에서 주로 사용하는 테스트 프레임워크를 지원하니 만약 다른 프레임워크를 사용한다면 조금은 추가적인 작업이 필요로 할 수도 있다. 하지만 걱정할 필요는 없다. 테스트 프레임워크에 종속성이 없어도 충분히 사용은 가능하다.

사실은 지원한다는 것도 Testcontainers의 라이플사이클만 지원하는 정도이다. 도커의 실행과 종료 정도만 관여하기에 사용해도 되고 원하지 않는다면 사용하지 않아도 좋다.
만약 다른 테스트 프레임워크를 사용한다면 라이플사이클 정도만 추가 하면 된다.

그럼 한번 간단하게 사용해보자. 필자는 junit5를 사용했고 테스트로는 mongodb를 사용할 예정이다.

일단 적절하게 디펜더시를 받도록 하자.

&lt;pre&gt;&lt;code&gt;// 기타 몽고 관련 및 테스트 관련 디펜더시

testImplementation(&quot;org.testcontainers:testcontainers:1.12.0&quot;)

&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
  위의 문법은 gradle kotlin dsl 이다.
&lt;/blockquote&gt;

&lt;h3&gt;기본 문법&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;&lt;br /&gt;import com.mongodb.MongoClient;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;

import java.util.function.Consumer;

import static org.assertj.core.api.Assertions.assertThat;

class JDefaultTestcontatinersTests {

    private GenericContainer mongoDbContainer = new GenericContainer(&quot;mongo:4.0.10&quot;);

    @BeforeEach
    void setup() {

        mongoDbContainer.start();
    }

    @Test
    void default_mongo_db_test() {

        int port = mongoDbContainer.getMappedPort(27017);

        MongoClient mongoClient = new MongoClient(mongoDbContainer.getContainerIpAddress(), port);
        MongoDatabase database = mongoClient.getDatabase(&quot;test&quot;);

        MongoCollection&amp;lt;Document&amp;gt; collection = database.getCollection(&quot;users&quot;);

        Document document = new Document(&quot;name&quot;, &quot;wonwoo&quot;)
                .append(&quot;email&quot;, &quot;test@test.com&quot;);

        collection.insertOne(document);

        FindIterable&amp;lt;Document&amp;gt; documents = collection.find();

        assertThat(documents).hasSize(1);

        documents.forEach((Consumer&amp;lt;? super Document&amp;gt;) it -&amp;gt; {

            assertThat(it.get(&quot;name&quot;)).isEqualTo(&quot;wonwoo&quot;);
            assertThat(it.get(&quot;email&quot;)).isEqualTo(&quot;test@test.com&quot;);

        });

    }

    @BeforeEach
    void close() {

        mongoDbContainer.stop();
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;code&gt;GenericContainer(imageName)&lt;/code&gt; 의 생성자에는 docker image 명을 작성하면 된다. 필자는 몽고디비로 테스트하기 위해 &lt;code&gt;mongo:4.0.10&lt;/code&gt; 라는 이미지를 사용했다.

실제로 외부 포트는 (몽고디비에 경우) 27017 로 열리지 않는다. 외부의 영향을 받지 않기 하기 위해 그런듯 싶다. 외부 포트를 가져오기 위해서는 getMappedPort(originalPort) 메서드를 사용해서 가져올 수 있다.

또한 GenericContainer 에는 docker 와 관련된 많은 메서드들이 있다. 
예를들어 Environment, Command, Label, Network, dependsOn등 docker와 관련된 커멘드들을 사용할 수 있으니 필요하다면 사용해도 된다.

&lt;pre&gt;&lt;code&gt;&lt;br /&gt;private GenericContainer mongoDbContainer = new GenericContainer(&quot;mongo:4.0.10&quot;)
        .withEnv(&quot;FOO&quot;, &quot;BAR&quot;)
        .withCommand(&quot;command test&quot;)
        .withLabel(&quot;TEST&quot;,&quot;LABEL&quot;)
        .withNetwork(Network.newNetwork())
        .dependsOn(new MongoDbContainer());

&lt;/code&gt;&lt;/pre&gt;

그 후에 해당 테스트를 진행 하면된다. 너무 간단하다. 사실 뭐 별거 없다.

&lt;h3&gt;junit5&lt;/h3&gt;

junit5를 사용해서 작성도 해보자.

&lt;pre&gt;&lt;code&gt;&lt;br /&gt;testImplementation(&quot;org.testcontainers:junit-jupiter:1.12.0&quot;)

&lt;/code&gt;&lt;/pre&gt;

testcontainers 에서 지원해주는 junit5를 디펜더시 받아야 한다. 이 또한 너무 간단하다.

&lt;pre&gt;&lt;code&gt;&lt;br /&gt;/// 기타 import

import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
class JJunit5TestContatinersTests {

    @Container
    private GenericContainer mongoDbContainer = new GenericContainer(&quot;mongo:4.0.10&quot;);

    @Test
    void junit5_mongo_db_test() {

        int port = mongoDbContainer.getMappedPort(27017);

        MongoClient mongoClient = new MongoClient(mongoDbContainer.getContainerIpAddress(), port);
        MongoDatabase database = mongoClient.getDatabase(&quot;test&quot;);

       //(대충 테스트한다는 내용)
    }

}
&lt;/code&gt;&lt;/pre&gt;

&lt;code&gt;@Testcontainers&lt;/code&gt; 어노테이션은 junit5의 Extension 클래스로 해당 &lt;code&gt;@Container&lt;/code&gt; 어노테이션이 달린 컨테이너를 실행시키는 어노테이션이다.

@BeforeEach 같이 실행전에 start() 메서드를 실행 할 필요없이 사용하면 되는 그런 어노테이션이다. 
junit4경우에는 &lt;code&gt;@Rule&lt;/code&gt; 어노테이션을 사용하면 된다.

&lt;pre&gt;&lt;code&gt;&lt;br /&gt;@Rule
public GenericContainer mongoDbContainer = new GenericContainer(&quot;mongo:4.0.10&quot;);

&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;docker compose&lt;/h3&gt;

docker compose 파일도 지원한다.

&lt;pre&gt;&lt;code&gt;version: 2
services:
  redis:
    image: redis:3.2
    ports:
      - 6379:6379
    volumes:
      - ~/data/redis:/data

  mongo:
    image: mongo:4.0.10

    ports:
      - 27017:27017

&lt;/code&gt;&lt;/pre&gt;

적절하게 docker compose 파일을 작성후에 테스트도 가능하다. volumes을 사용한다면 특정한 공간에 데이터가 저장되어 데이터가 계속 쌓이게 되므로 유의해야 한다.

&lt;pre&gt;&lt;code&gt;@Testcontainers
class JDockerComposeTests {

    private final int REDIS_PORT = 6379;
    private final int MONGO_PORT = 27017;

    @Container
    private final DockerComposeContainer dockerCompose = new DockerComposeContainer(new File(&quot;src/test/resources/docker-compose.yaml&quot;))
            .withExposedService(&quot;redis_1&quot;, REDIS_PORT)
            .withExposedService(&quot;mongo_1&quot;, MONGO_PORT);


    @Test
    void docker_compose_test() {

        int port = dockerCompose.getServicePort(&quot;mongo_1&quot;, MONGO_PORT);
        String host = dockerCompose.getServiceHost(&quot;mongo_1&quot;, MONGO_PORT);

        //(대충 테스트 한다는 내용)
    }

}
&lt;/code&gt;&lt;/pre&gt;

위와 같이 docker compose 파일을 작성후에 &lt;code&gt;DockerComposeContainer&lt;/code&gt; 클래스를 이용해서 사용하면 된다. 사용법은 기존과 비슷하다. 
참고로 꼭 파일명은 docker-compose 일 필요는 없다. 필자는 docker-compose는 docker-compose 라는 파일명이 익숙해서 그렇게 작성하였다.

&lt;h3&gt;spring boot data mongodb&lt;/h3&gt;

기본적으로 사용법은 배워 봤다. 사실 그리 어렵지 않다. 적당한 디펜더시와 docker의 이미지만 설정한다면 쉽게 사용할 수 있다.

이번에는 우리가 자주 사용하는 spring boot를 사용해서 테스트를 진행 할 것이다. Spring boot 경우에는 일반적으로 자동설정에 있는 몽고 설정을 사용을 한다. 그래서 위와는 조금 다르게 설정을 해야 한다. 한번 살펴보자.

&lt;pre&gt;&lt;code&gt;@DataMongoTest
@ContextConfiguration(initializers = MongoDbContainerInitializer.class)
class JSpringDataTestcontatinersTests {

    private final TodoRepository todoRepository;

    @Autowired
    private JSpringDataTestcontatinersTests(TodoRepository todoRepository) {

        this.todoRepository = todoRepository;
    }


    @Test
    void spring_data_mono_test() {

        todoRepository.save(new Todo(null, &quot;wonwoo&quot;, &quot;test@test.com&quot;));

        List&amp;lt;Todo&amp;gt; todo = todoRepository.findAll();

        assertThat(todo).hasSize(1);

        todo.forEach(it -&amp;gt; {

            assertThat(it.getName()).isEqualTo(&quot;wonwoo&quot;);
            assertThat(it.getEmail()).isEqualTo(&quot;test@test.com&quot;);

        });

    }

}

&lt;/code&gt;&lt;/pre&gt;

필자는 &lt;code&gt;@DataMongoTest&lt;/code&gt; 어노테이션을 이용해서 테스트를 작성했다. 물론 다른 nosql이나 rdb의 경우도 비슷하게 작성하면 된다.

&lt;code&gt;MongoDbContainerInitializer&lt;/code&gt; 라는 클래스가 눈에 띈다. 이건 필자가 작성한 코드이다. 사실 별거 없고 위와 비슷하게 &lt;code&gt;GenericContainer&lt;/code&gt; 실행하는 클래스이다.

&lt;pre&gt;&lt;code&gt;public class MongoDbContainerInitializer implements ApplicationContextInitializer&amp;lt;ConfigurableApplicationContext&amp;gt; {

    private GenericContainer mongoDbContainer = new GenericContainer(&quot;mongo:4.0.10&quot;);

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        mongoDbContainer.start();

        TestPropertyValues.of(

                &quot;spring.data.mongodb.uri=mongodb://&quot; + mongoDbContainer.getContainerIpAddress() + &quot;:&quot; + mongoDbContainer.getMappedPort(27017) + &quot;/test&quot;

        ).applyTo(applicationContext);
    }
}


&lt;/code&gt;&lt;/pre&gt;

spring boot 의 자동설정인 &lt;code&gt;spring.data.mongodb.uri&lt;/code&gt;이라는 프로퍼티에 해당 url을 끼워 넣기 위한 작업을 하는 클래스이다. 위에서 말했다시피 실제 외부 포트는 27017이 아니기 때문이다.

조금 더 간단하게 사용하기 위해 메타 어노테이션을 이용해도 된다.

&lt;pre&gt;&lt;code&gt;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@DataMongoTest
@ContextConfiguration(initializers = MongoDbContainerInitializer.class)
@DirtiesContext //optional
public @interface JDataMongoIntegrationTest {


}

&lt;/code&gt;&lt;/pre&gt;

위와 같이 작성 후에 테스트를 해보자.

&lt;pre&gt;&lt;code&gt;@JDataMongoIntegrationTest
class JSpringDataCustomizedTestcontatinersTests {

    private final TodoRepository todoRepository;

    @Autowired
    private JSpringDataCustomizedTestcontatinersTests(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }

    @Test
    void spring_data_mono_customized_test() {

        // (대충 테스트 한다는 내용)

    }

}

&lt;/code&gt;&lt;/pre&gt;

위와 같이 작성해도 문제 없이 잘 테스트가 진행 될 것이다.

오늘은 이렇게 Testcontainers의 몇가지 기능을 살펴봤다.

Testcontainers 에는 많은 기능이 있지만 기본적인 테스트를 하기 위한 정도로 작성하였다. 추후에 embedded 관련해서도 작성해보려고 한다.

필자의 경우에는 Mongo image를 직접 작성했지만 Testcontainers 에서 지원해주는 모듈들이 많다. 예를들어 MySQLContainer, PostgreSQLContainer, MSSQLServerContainer, Db2Container, CouchbaseContainer, Neo4jContainer, ElasticsearchContainer, KafkaContainer, RabbitMQContainer, MockServerContainer 등등 더 많은 컨테이너들을 기본적으로 지원해주고 있다. 적절한 디펜더시만 받으면 된다. (근데 왜 몽고는 없지..?)

Testcontainers 의 또 다른 장점(?)은 spring boot의 지원을 잘해주고 있다. 뭐 많은 이유가 있겠지만 가장 큰 이유는 해당 프로젝트에 피보탈 개발자분이 한분 계신다. 또한 Spring boot 도 Testcontainers 종종 사용해서 테스트를 진행하고 있다.

사실 원래는 코틀린으로 먼저 작성을 해서 예제의 클래스들이 모두 J~~로 시작한다. 아직 대부분의 개발자분들이 java에 익숙하기 때문에 자바로 추가적으로 작성하였다. 만약 코틀린에도 관심이 있다며 &lt;a href=&quot;https://github.com/wonwoo/testcontainers-test&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;여기(github)&lt;/a&gt;에 자바와 코틀린 소스 모두 있으니 참고하면 되겠다.

아직 필자도 많은 부분을 알고 있지는 않다. 필자도 이제 테스트할 때 도입할 예정이라 기본적으로 부분부터 살펴봤다. 사실 이정도만 알아도 큰 문제는 되지 않아 보인다. 쓰다보면 괜찮아지겠지..&lt;/div&gt;</description>
      <author>머룽</author>
      <guid isPermaLink="true">https://mkzz.tistory.com/333</guid>
      <comments>https://mkzz.tistory.com/333#entry333comment</comments>
      <pubDate>Sun, 23 Apr 2023 14:06:09 +0900</pubDate>
    </item>
  </channel>
</rss>