<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>우리집앞마당</title>
    <link>https://multifrontgarden.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 8 May 2026 06:57:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>LichKing</managingEditor>
    <item>
      <title>resilience4j-spring-boot, spring-cloud-circuitbreaker 어떤걸 사용해야할까</title>
      <link>https://multifrontgarden.tistory.com/329</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;MSA 환경이 보편화되면서 Circuit Breaker 는 필수가 되어가고있다. java + spring 환경에서는 Netflix 가 만든 &lt;a href=&quot;https://github.com/Netflix/Hystrix&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Hystrix&lt;/a&gt; 를 시작으로 Circuit Breaker 가 대중화됐고, 요즘은 &lt;a href=&quot;https://resilience4j.readme.io&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Resilience4j&lt;/a&gt; 가 사실상 표준역할을 하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resilience4j 를 spring 에 통합해서 사용하는 방법은 대표적으로 두가지 방법이 있는데, spring cloud 에서 제공하는 &lt;a href=&quot;https://docs.spring.io/spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-resilience4j.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;spring cloud circuitbreaker resilience4j&lt;/a&gt; 를 사용하는 방법과 Resilience4j 가 제공하는 &lt;a href=&quot;https://resilience4j.readme.io/docs/getting-started-3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;resilience4j spring boot&lt;/a&gt; 를 사용하는 방법이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1771664859500&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// resilience4j spring boot
dependencies {
    implementation(&quot;io.github.resilience4j:resilience4j-spring-boot3:2.3.0&quot;)
}

// spring cloud
dependencies {
    implementation(&quot;org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j:2025.1.0&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두가지 모두 Circuit Breaker 를 구현하고, 구현체로 Resilience4j 를 이용하기 때문에 어떤 구현을 사용하든 상관없다고 생각했었다. 그리고 그렇다면 spring cloud 를 사용하는게 더 나을거라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring cloud 에 의존해서 Circuit Breaker 를 사용하면 CircuitBreakerFactory, CircuitBreaker 와 같은 인터페이스들도 spring cloud 가 제공해주는걸 사용하기 때문에 코드 모양이 살짝 달라진다.&lt;/p&gt;
&lt;pre id=&quot;code_1771665595387&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// resilience4j spring boot
@Test                                                                        
void http_요청을_실행한다() {                                                       
    HttpClient httpClient = new HttpClient();                                
    CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker(&quot;test&quot;);       
                                                                             
    assertThatNoException().isThrownBy(() -&amp;gt; {                               
        cb.executeRunnable(() -&amp;gt; httpClient.call());                         
    });                                                                      
}

// spring cloud
@Test                                                         
void http_요청을_실행한다() {                                        
    HttpClient httpClient = new HttpClient();                 
    CircuitBreaker cb = circuitBreakerFactory.create(&quot;test&quot;); 
                                                              
    assertThatNoException().isThrownBy(() -&amp;gt; {                
        cb.run(() -&amp;gt; httpClient.call());                      
    });                                                       
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CircuitBreaker 를 각자의 방식으로 생성하고, 그 안에서 http 호출을 일으킨다. 참고로 HttpClient 코드는 임의의 구현이다.&lt;/p&gt;
&lt;pre id=&quot;code_1771665727969&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class HttpClient {       
    public String call() {      
        try {                  
            Thread.sleep(2000);
        } catch(Exception e) { 
                               
        }                      
                               
        return &quot;success&quot;;      
    }                          
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# TimeoutException&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 테스트 코드는 모두 예외가 발생하지 않으면 통과하는 테스트다. 현재 Circuit Breaker 관련한 별도의 설정은 아무것도 진행하지 않았다. 이 상태로 테스트를 실행하면 Resilience4j 를 사용한 테스트는 성공하지만 spring cloud 를 사용하는 테스트는 실패한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Circuit Breaker 관점에서만 보면 HttpClient 는 2초간 스레드를 블락한 뒤 리턴을 수행하기 때문에 문제될 여지가 없다. 지금은 임의로 스레드를 블락하게 했지만 실제로 외부 http 호출이 일어난다고 가정하면 http client 에 설정하는 timeout 이 동작할거고, timeout 이 발생하면 예외가 발생하면서 Circuit Breaker 가 동작하게 될 것이다. 때문에 성공한 Resilience4j 가 정상으로 보인다. spring cloud 는 왜 실패할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;u&gt;Caused&amp;nbsp;by:&amp;nbsp;java.util.concurrent.TimeoutException:&amp;nbsp;TimeLimiter&amp;nbsp;'test'&amp;nbsp;recorded&amp;nbsp;a&amp;nbsp;timeout&amp;nbsp;exception.&lt;/u&gt;&lt;/i&gt;&lt;br /&gt;&lt;i&gt;&lt;u&gt;at&amp;nbsp;java.base/java.util.concurrent.FutureTask.get(FutureTask.java:206)&lt;/u&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택트레이스를 확인해보면 TimeoutException 이라는게 발생했다. 엥? 그 어떤 타임아웃도 설정한게 없는데?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-cloud/spring-cloud-circuitbreaker/blob/2378f7f52260dbc6fcbca076a51b0a9fb49f5df0/spring-cloud-circuitbreaker-resilience4j/src/main/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JAutoConfiguration.java#L73&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;spring cloud 는 자동 구성에서 TimeLimiter 도 구성&lt;/a&gt;하게 되는데, 이 TimeLimiter 의 기본설정이 1초이다. 이 때문에 스레드 블락 2초를 대기하다가 TimeLimiter 가 먼저 끊게된다. 지금이야 샘플 코드를 기반으로 설명하니 문제없지만 이런 구성을 모른채로 Circuit Breaker 를 사용한다면 http client timeout 을 1초 넘게 잡아놓은 상태에서도 TimeLimiter 가 요청을 끊어버리는 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TimeLimiter 까지 사용하게되면 timeout 설정이 복잡해지니 해당 설정을 끄고싶을 수 있다(내가 그랬다). 이때는 설정파일에서 프로퍼티로 제어할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1771666935744&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  cloud:
    circuitbreaker:
      resilience4j:
        disable-time-limiter: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프로퍼티를 설정해주고 테스트를 실행하면 테스트는 성공한다. 참고로 해당 프로퍼티의 기본값은 false 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# NoFallbackAvailableException&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 상황을 재현하기위해 코드를 변경해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1771667337862&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class HttpClient {                                 
    public String call() {                                
        throw new CustomException();                     
    }                                                    
}                                                        
                                                         
class CustomException extends RuntimeException {} 

// resilience4j spring boot
@Test                                                                         
void http_요청을_실행한다() {                                                        
    HttpClient httpClient = new HttpClient();                                 
    CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker(&quot;test&quot;);        
                                                                              
    assertThatThrownBy(() -&amp;gt; {                                                
        cb.executeRunnable(() -&amp;gt; httpClient.call());                          
    }).isInstanceOf(CustomException.class);                                   
}                                                                             

// spring cloud
@Test                                                         
void http_요청을_실행한다() {                                        
    HttpClient httpClient = new HttpClient();                 
    CircuitBreaker cb = circuitBreakerFactory.create(&quot;test&quot;); 
                                                              
    assertThatThrownBy(() -&amp;gt; {                                
        cb.run(() -&amp;gt; httpClient.call());                      
    }).isInstanceOf(CustomException.class);                   
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드를 블락하면 HttpClient 는 이제 호출하면 예외를 일으키도록 변경되었다. 테스트 코드는 예외가 발생했을때 의도한 예외가 발생했는지를 검증한다. 테스트를 실행하면 또 Resilience4j 만 성공하고, spring cloud 는 실패한다. spring cloud 는 CustomException 이 아니라 NoFallbackAvailableExceptipn 을 일으킨다. spring cloud 의 CircuitBreaker 인터페이스는 fallback 을 요구하기 때문이다. 마땅히 넣을 fallback 정책이 없는데? 그래도 일단은 fallback 을 구현해서 전달해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# ExecutionException&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1771736245650&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test                                                                                 
void http_요청을_실행한다() {                                                                
    HttpClient httpClient = new HttpClient();                                         
    CircuitBreaker cb = circuitBreakerFactory.create(&quot;test&quot;);                         
                                                                                      
    assertThatThrownBy(() -&amp;gt; {                                                        
        cb.run(() -&amp;gt; httpClient.call(), e -&amp;gt; { throw (RuntimeException) e; });        
    }).isInstanceOf(CustomException.class);                                           
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;run 메서드는 두번째 파라미터로 fallback 을 요구한다. 람다를 이용해서 전달받은 예외를 그대로 다시 던지도록 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그럼에도 테스트는 통과하지 않는다. 스택트레이스를 보면 fallback 구현했기 때문에 이전에 발생한 NoFallbackAvailiableException 은 보이지 않는다. 대신에 이번엔 ExecutionException 이 발생했음을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도대체 이건 왜 발생한걸까? 그리고 이 테스트는 어떻게 통과시킬 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 아까 설정했던 disable-time-limiter 프로퍼티를 기본값인 false 로 변경한 다음에 테스트를 다시 실행해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 비로소 테스트가 통과한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# Resilience4j, Spring Cloud&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resilience4j 를 Circuit Breaker 정도로만 알고있는 경우가 많은데 Resilience4j 는 그 외에도 Bulkhead, RateLimiter, Retry, TimeLimiter, Cache 까지도 제공하는 꽤나 방대한 라이브러리이다. CircuitBreaker 를 직접 생성해서 사용한다면 resilience4j spring boot 를 사용할때는 CircuitBreakerRegistry 라는 인터페이스를, spring cloud circuitbreaker resilience4j 를 사용하면 CircuitBreakerFactory 라는 인터페이스를 사용해서 CircuitBreaker 를 생성하게 된다.(참고로 두 가지 생성으로 인해 생성하는 CircuitBreaker 객체는 클래스명만 같을뿐 패키지가 아예 다른 별도의 클래스를 기반으로 생성된다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 CircuitBreakerRegistry 를 사용해서 생성하는 CircuitBreaker 는 Circuit Breaker 에만 충실한 객체를 주지만 CircuitBreakerFactory 가 생성하는 CircuitBreaker 는 spring cloud 가 한번 더 추상화를 해서 TimeLimiter 랑 통합한 CircuitBreaker 객체를 주게된다. 이 차이 때문에 첫번째 이슈였던 TimeoutException 이 발생하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 또 의문이 하나 생긴다. 위에서 ExecutionException 이 발생했을때 disable-time-limiter 를 false 로 바꿨더니 정상동작하는 이유는 뭘까? &lt;a href=&quot;https://github.com/resilience4j/resilience4j/blob/78983c5d80a6b7ac925b9ca6c2f39386fd7c013c/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/internal/TimeLimiterImpl.java#L42&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TimeLimiter 는 내부에서 예외가 발생했을때 ExecutionException 이 발생하면 예외를 한꺼풀 까서 전파하도록 구현&lt;/a&gt;되어있다. disable-time-limiter 속성을 true 로 해서 TimeLimiter 사용을 거부했을땐 TimeLimiter 에 구현되어 있는 예외를 까는 로직이 동작하지 않게 되기 때문에 ExecutionException 이 그대로 전달된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 본 예외가 전파되길 바란다면 직접 ExecuteException 을 한번 까서 전파하도록 로직을 작성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# ExecutorService&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 위 설정들만 넣어주면 resilience4j spring boot 를 사용하는 것과 spring cloud circuitbreaker resilience4j 를 사용하는게 똑같아진걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않다. spring cloud 는 TimeLimiter 동작을 위해 스레드풀을 사용한다. 이 스레드풀에 대한 힌트는 CircuitBreakerFactory 의 구현체인 &lt;a href=&quot;https://github.com/spring-cloud/spring-cloud-circuitbreaker/blob/2378f7f52260dbc6fcbca076a51b0a9fb49f5df0/spring-cloud-circuitbreaker-resilience4j/src/main/java/org/springframework/cloud/circuitbreaker/resilience4j/Resilience4JCircuitBreakerFactory.java#L56&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Resilience4JCircuitBreakerFactory&lt;/a&gt; 에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값이 Executors.newCachedThreadPool() 로 되어있는데 이 설정은 엔터프라이즈 환경에서는 지양하는 설정이다. 무한대로 스레드를 만들 위험이 있기 때문이다. 때문에 아래처럼 직접 만드는 스레드 풀을 넣어주는게 좋다.&lt;/p&gt;
&lt;div style=&quot;background-color: #131314; color: #ebebeb;&quot;&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;@Bean
public Customizer&amp;lt;Resilience4JCircuitBreakerFactory&amp;gt; customizer() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    return factory -&amp;gt; factory.configureExecutorService(executor.getThreadPoolExecutor());
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니면 설정파일에서 스레드풀을 사용하지 않도록 설정할 수 있다.&lt;/p&gt;
&lt;div style=&quot;background-color: #131314; color: #ebebeb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;spring:
  cloud:
    circuitbreaker:
      resilience4j:
        disable-thread-pool: true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 스레드풀을 사용하는 이유는 TimeLimiter 때문인데, TimeLimiter 를 사용하지 않는다면 몰라도 사용한다면 스레드풀을 사용하지 않는것보단 별도의 스레드풀을 만들어서 주입해주는걸 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로는 위 스레드풀에 대한 내용이 spring cloud circuitbreaker 공식 가이드에 자세히 나와있지 않은 부분이 상당히 불만이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Circuit Breaker 를 적용해야하는데 spring boot 에 사용할 수 있는 방법이 Resilience4j 가 제공하는 방법과 spring cloud 가 제공하는 방법 두가지가 있어 처음엔 별 생각없이 사용했었다. 그러다가 spring cloud 를 사용할때 TimeLimiter 때문에 TimeoutException 을 맞게됐고, 이 예외가 왜 발생하는지를 파악해보다가 두 방식의 다른점들을 알게되어 정리하게 된 글이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resilience4j 가 제공하는 방법은 사실 큰 문제 없이 직관적인데 반해 spring cloud 가 제공하는 방법이 한번 더 추상화가 들어가고, 그리고 spring cloud 팀의 사상이 더 접목된 방식(fallback 필수, 암묵적인 TimeLimiter, thread pool 설정)이라 잘 모르고 사용하는 경우 어려움이 더 많았다. 개인적으로는 TimeLimiter 를 사용한 타임아웃 설정보다는 내부에서 사용하는 http client 의 타임아웃 설정만 바라보는게 운영상 더 편한 부분이 많아 spring cloud 보단 resilience4j spring boot 를 사용하고 있다.&lt;/p&gt;</description>
      <category>Java/spring</category>
      <author>LichKing</author>
      <guid isPermaLink="true">https://multifrontgarden.tistory.com/329</guid>
      <comments>https://multifrontgarden.tistory.com/329#entry329comment</comments>
      <pubDate>Fri, 27 Feb 2026 15:08:36 +0900</pubDate>
    </item>
    <item>
      <title>실전에서 OOP하기(트래픽 제어 구현)</title>
      <link>https://multifrontgarden.tistory.com/328</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향, 디자인패턴 등을 공부하고나면 다형성의 이로움과 함께 실무 코드에 적용하고 싶다는 욕구가 생긴다.&lt;br&gt;하지만 막상 해보려고하면 어떻게 해야하는지 감이 잘 잡히지 않고, 꾸역꾸역 해내도 결과물이 썩 만족스럽지 않은 경우가 많다.&lt;br&gt;이 차이는 우리가 공부하는 객체지향 관련 자료들은 java 개발의 사실상 표준 환경이 되어버린 spring 을 염두하고있지 않기 때문이다.&lt;br&gt;간단한 예제와 함께 spring 환경에서 객체지향을 어떻게 구현할 수 있는지 살펴보자.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# 요구사항&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;- 트래픽 제어 객체인 TrafficGate 를 구현한다&lt;br&gt;- 정수로 이루어진 유저 id와 modulo 연산을 사용해 10% 단위로 제어하도록 구현한다&lt;br&gt;- 허용 비율은 외부 API를 호출하여 획득한다&lt;br&gt;- 외부 API 가 구현되기 전까지는 모든 유저를 통과시킨다&lt;br&gt;- 화이트리스트에 등록된 id는 항상 통과시킨다&lt;br&gt;- 사용사례는 아래와 같다&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RestController
class SampleController {
&amp;nbsp;&amp;nbsp;private final TrafficGate trafficGate;

&amp;nbsp;&amp;nbsp;// 생성자 생략

&amp;nbsp;&amp;nbsp;@GetMapping
&amp;nbsp;&amp;nbsp;public void request(UserId userId) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if(!trafficGate.isOpenable(userId)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;throw new TrafficGateClosedException(userId);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# 구현&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;거창하게 요구사항을 정리했지만 막상 구현하면 크게 어렵지 않은 내용이다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
class TrafficGate {
&amp;nbsp;&amp;nbsp;private RestClient permitRateRestClient;
&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;public boolean isOpenable(UserId userId) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int permitRate = permitRateRestClient.get();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return userId.value() % 10 &amp;lt; permitRate;
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API를 호출하여 허용비율을 구하고 이를 사용해서 통과 여부를 결정한다. 완성했으니 배포하고 싶지만 요구사항에 나와있듯 허용비율 API가 아직 정상동작을 하지 않으니 모든 유저를 통과시켜야한다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
class TrafficGate {
&amp;nbsp;&amp;nbsp;// private RestClient permitRateRestClient;
&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;public boolean isOpenable(UserId userId) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// int permitRate = permitRateRestClient.get();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// return userId.value() % 10 &amp;lt; permitRate;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return true;
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드를 주석 처리하여 배포하고, 리턴값을 하드코딩하면 문제없다. 이런식으로 구현해서 배포하거나, 아니면 허용비율 API 가 발목을 잡아 해당 API 가 제공되기 전까지 구현을 시작하지 않는 상황도 흔히 볼 수 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
class TrafficGate {
&amp;nbsp;&amp;nbsp;private RestClient permitRateRestClient;
&amp;nbsp;&amp;nbsp;private TrafficGateWhiteUsers trafficGateWhiteUsers;
&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;public boolean isOpenable(UserId userId) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if(trafficGateWhiteUsers.isIncluded(userId)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return true;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int permitRate = permitRateRestClient.get();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return userId.value() % 10 &amp;lt; permitRate;
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항에 나와있는 화이트리스트 유저를 제어하는 로직도 추가됐다. TrafficGate 클래스는 요구사항에 따라 지속적으로 변화하고있다. 이 구현을 다형성을 이용해 구현한다면 어떻게 할 수 있을까?&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# 다형성을 이용한 구현&lt;/b&gt;&lt;/h2&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;interface TrafficGate {
&amp;nbsp;&amp;nbsp;boolean isOpenable(UserId userId);
}

@Component
class AlwaysOpenTrafficGate implements TrafficGate {
&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;public boolean isOpenable(UserId userId) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return true;
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;TrafficGate 인터페이스를 정의하고, 외부 API가 완성되기까지 사용할 기본 구현체를 정의한다. 기본 구현체는 항상 허용하는 구현이므로 적절한 네이밍을 해주어 구현했다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
class PermitRateTrafficGate implements TrafficGate {
&amp;nbsp;&amp;nbsp;private RestClient permitRateRestClient;

&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;public boolean isOpenable(UserId userId) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int permitRate = permitRateRestClient.get();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return userId.value() % 10 &amp;lt; permitRate;
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API가 완성됐다면 새로운 구현을 추가해주면 된다. 기존 코드는 변경이 발생하지 않는다. 하지만 이렇게 구현해서 애플리케이션을 실행시키면 에러가 발생한다. TrafficGate 타입의 spring bean 이 2개이기 때문이다. 해결하는 방법은 몇가지 있겠지만 빈생성을 외부로 추출한다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
class TrafficGateConfiguration {
&amp;nbsp;&amp;nbsp;@Bean
&amp;nbsp;&amp;nbsp;public TrafficGate trafficGate(RestClient permitRateRestClient) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return new PermitRateTrafficGate(permitRateRestClient);
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 외부로 분리하고, TrafficGate 구현체들에는 @Component 를 제거한다. 이제는 에러없이 구동된다.&lt;br&gt;이제 마지막 남은 화이트리스트 제어를 구현하자.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class TrafficGateWhiteUsersProxy implements TrafficGate {
&amp;nbsp;&amp;nbsp;private TrafficGateWhiteUsers trafficGateWhiteUsers;
&amp;nbsp;&amp;nbsp;private TrafficGate trafficGate;
&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;public boolean isOpenable(UserId userId) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if(trafficGateWhiteUsers.isIncluded(userId)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return true;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return trafficGate.isOpenable(userId);
&amp;nbsp;&amp;nbsp;}
}

@Configuration
class TrafficGateConfiguration {
&amp;nbsp;&amp;nbsp;@Bean
&amp;nbsp;&amp;nbsp;public TrafficGate trafficGate(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;TrafficGateWhiteUsers trafficGateWhiteUsers, RestClient permitRateRestClient
&amp;nbsp;&amp;nbsp;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return new TrafficGateWhiteUsersProxy(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;trafficGateWhiteUsers, new PermitRateTrafficGate(permitRateRestClient)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;);
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;화이트리스트 제어 로직도 기존 구현을 전혀 변경하지 않고 프록시 객체로 구현했다. 위에서 빈 중복 등록 문제가 있었을때 해결하는 방법 중 하나는 @Primary 애노테이션을 이용하는 방법이 있었다. 만약 위에서 이를 이용해서 문제를 해결했다면 프록시를 구현했을때 또 새로운 문제를 맞이했을 것이다. 이런식으로 객체 구성을 다양하게 조립할때는 정적으로 의존관계를 맺어주는 컴포넌트 스캔보다는 런타임에 직접 조립해주는게 훨씬 낫다. 관련된 내용은 이전에 작성했던 글이 있다( &lt;a href=&quot;https://multifrontgarden.tistory.com/311&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://multifrontgarden.tistory.com/311&lt;/span&gt;&lt;/a&gt; ).&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;561&quot; data-origin-height=&quot;171&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cx1XoG/btsQ31ApJqu/r9sRkoQ09c5LqAmn3YhGT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cx1XoG/btsQ31ApJqu/r9sRkoQ09c5LqAmn3YhGT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cx1XoG/btsQ31ApJqu/r9sRkoQ09c5LqAmn3YhGT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcx1XoG%2FbtsQ31ApJqu%2Fr9sRkoQ09c5LqAmn3YhGT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;561&quot; height=&quot;171&quot; data-origin-width=&quot;561&quot; data-origin-height=&quot;171&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스와 구현체의 클래스 다이어그램을 그려보면 이런 모양이 된다. 요구사항별로 새로운 구현체를 추가하여 기존 코드에 변경없이 확장을 이어가는 OCP를 충족하는 구현이다. 이 외에도 추가적인 요구사항이 들어온다면 추가 구현체를 통해 문제를 해결할 수 있을 것이다.&lt;br&gt;그리고 객체 구성은 설정파일에서만 변경해주면 다른 코드는 변경없이 새로운 임무를 수행할 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;어떠한 구현이 더 낫다를 얘기하고 싶다기보다는 객체지향에 관심이 많고, 실무에서도 도입해보고 싶은데 익숙치 않아 좌절하는 모습들을 많이 봐왔다. 그러다보면 객체지향이나 디자인패턴은 이론으로나 가능하고 면접용 지식일뿐 실무에서는 적용하기 어렵다는 결론을 내는 사례들 심심찮게 보게된다. 거창하게 생각하지 않고 작은 부분에서부터 적용할 수 있다는 것을 예제로 설명해보고 싶었다.&lt;/p&gt;</description>
      <category>Java/spring</category>
      <author>LichKing</author>
      <guid isPermaLink="true">https://multifrontgarden.tistory.com/328</guid>
      <comments>https://multifrontgarden.tistory.com/328#entry328comment</comments>
      <pubDate>Wed, 8 Oct 2025 13:21:19 +0900</pubDate>
    </item>
    <item>
      <title>apache httpcomponents client 의 timeout 동작 테스트</title>
      <link>https://multifrontgarden.tistory.com/327</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://multifrontgarden.tistory.com/326&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;이전글&lt;/span&gt;&lt;/a&gt;에서 timeout 에 대한 설명을 적어놨었다. 이후 실제로 문서에 나와있는대로 동작하는지 확인해보기로 했다. 해당글에서 정리한 내용이 정확하다면 response timeout 과 socket timeout 을 훨씬 초과하여 응답이 오더라도 바이트들만 제 시간 내에 도착한다면 예외없이 요청이 성공해야한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;테스트에 사용한 apache httpcomponents client 의 버전은 5.5이다.&lt;br&gt;&lt;u&gt;&lt;i&gt;org.apache.httpcomponents.client5:httpclient5:5.5&lt;/i&gt;&lt;/u&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# 코드준비&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 stream 으로 응답을 하는 간단한 API 하나를 구성했다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@GetMapping(value = &quot;/stream&quot;, produces = MediaType.TEXT_PLAIN_VALUE)&amp;nbsp;&amp;nbsp; 
public StreamingResponseBody stream() {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return outputStream -&amp;gt; {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (int i = 0; i &amp;lt; 50; i++) {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;String data = &quot;data &quot; + i + &quot;\n&quot;;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;outputStream.write(data.getBytes(StandardCharsets.UTF_8));&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;outputStream.flush();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Thread.sleep(100); // 100ms 간격&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (InterruptedException e) {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;e.printStackTrace();&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;해당 API 는 &quot;data 0, data 1, data 2 ...&quot; 형태로 응답을 스트리밍으로 하게된다. 그리고 스트리밍 간격은 100ms 이다. 로컬에서 서버를 구동시켜서 curl 로 실행해보면 응답을 확인해볼 수 있다.&lt;br&gt;&lt;u&gt;&lt;i&gt;curl&amp;nbsp;http://localhost:8080/stream&lt;/i&gt;&lt;/u&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;해당 API 를 호출할 클라이언트 코드를 작성한다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static void main(String[] args) throws Exception {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ConnectionConfig connectionConfig = ConnectionConfig.custom()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.setConnectTimeout(50, TimeUnit.MILLISECONDS)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.setSocketTimeout(100, TimeUnit.MILLISECONDS) // socket timeout 100ms&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.build();&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;poolingHttpClientConnectionManager.setDefaultConnectionConfig(connectionConfig);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;RequestConfig requestConfig = RequestConfig.custom()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.setConnectionRequestTimeout(200, TimeUnit.MILLISECONDS)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.setResponseTimeout(100, TimeUnit.MILLISECONDS) // response timeout 100ms&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.build();&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CloseableHttpClient httpClients = HttpClients.custom()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.setConnectionManager(poolingHttpClientConnectionManager)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.setDefaultRequestConfig(requestConfig)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.build();&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HttpGet httpGet = new HttpGet(&quot;http://localhost:8080/stream&quot;);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;httpClients.execute(httpGet, (classicHttpResponse) -&amp;gt; {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HttpEntity entity = classicHttpResponse.getEntity();&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try (InputStream in = entity.getContent();&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; BufferedReader reader = new BufferedReader(&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; new InputStreamReader(in, StandardCharsets.UTF_8))) {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;String line;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while ((line = reader.readLine()) != null) {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;System.out.println(line);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return null;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;timeout 4개를 모두 설정하고 있지만 주석으로 표시한 socketTimeout, responseTimeout 이 핵심이니 이 부분만 변경해주며 테스트하면 된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# 테스트&lt;/b&gt;&lt;/h2&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;## socketTimeout 150ms responseTimeout 150ms&lt;/b&gt;&lt;/h4&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;180&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bamkKm/btsP3bLj5LL/MFhHDPzq04aCUO1ysqUDT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bamkKm/btsP3bLj5LL/MFhHDPzq04aCUO1ysqUDT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bamkKm/btsP3bLj5LL/MFhHDPzq04aCUO1ysqUDT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbamkKm%2FbtsP3bLj5LL%2FMFhHDPzq04aCUO1ysqUDT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;180&quot; height=&quot;256&quot; data-origin-width=&quot;180&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 바이트를 대기하는 responseTimeout 도 안정권이고, 바이트 간격을 대기하는 socketTimeout 도 안정권이다. 성공적으로 응답을 받고, 차례대로 출력하는걸 볼 수 있다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;## socketTimeout 150ms responseTimeout 80ms&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 바이트를 대기하는 시간이 80ms 로 줄었다. 하지만 위 예제 API 는 첫번째 바이트는 sleep 없이 바로 응답하고, 두번째 바이트부터 sleep 을 걸고있기 때문에 timeout 이 문서대로 동작한다면 요청은 성공해야한다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blTpQS/btsP255qw7T/6rgWYarda5eimBbqTPis5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blTpQS/btsP255qw7T/6rgWYarda5eimBbqTPis5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blTpQS/btsP255qw7T/6rgWYarda5eimBbqTPis5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblTpQS%2FbtsP255qw7T%2F6rgWYarda5eimBbqTPis5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1432&quot; height=&quot;146&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 요청이 실패했다. 그런데 자세히보면 첫번째 바이트인 &quot;data 0&quot; 은 정상출력한걸 볼 수 있다. 즉 두번째 바이트를 대기하다가 예외가 발생한 것이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;## socketTimeout 80ms responseTimeout 150ms&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 두 타임아웃 값을 반대로 변경했다. 예상대로 동작한다면 위에서 발생한 예외가 위가 아니라 여기서 발생해야한다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;162&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br2Wlu/btsP3bq1FMI/wBHoKRARbJyRd88U8DgYp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br2Wlu/btsP3bq1FMI/wBHoKRARbJyRd88U8DgYp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br2Wlu/btsP3bq1FMI/wBHoKRARbJyRd88U8DgYp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr2Wlu%2FbtsP3bq1FMI%2FwBHoKRARbJyRd88U8DgYp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;162&quot; height=&quot;176&quot; data-origin-width=&quot;162&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오잉 성공했다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;## socketTimeout 80ms responseTimeout 80ms&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;예상대로 동작한다면 첫번째 바이트만 출력되고, 예외가 발생해아한다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vsMHo/btsP4xfICro/O4ZbeQhnHcJgrVyqV9b8nK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vsMHo/btsP4xfICro/O4ZbeQhnHcJgrVyqV9b8nK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vsMHo/btsP4xfICro/O4ZbeQhnHcJgrVyqV9b8nK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvsMHo%2FbtsP4xfICro%2FO4ZbeQhnHcJgrVyqV9b8nK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;136&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상대로 동작한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# 문제&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;socketTimeout 과 responseTimeout 이 모두 여유롭거나 모두 부족할땐 예상대로 동작했는데 두 값이 한쪽만 부족할땐 예상과 다르게 동작하고 있다. 원인이 무엇일까?&lt;br&gt;apache httpcomponents client 는 제대로된 문서가 없고, javadoc 이 문서를 대체하고있어 일단 공식적인 문서자료를 확인하기가 매우 어려웠다. 결국은 코드를 들여다볼 수 밖에 없었는데 코드에서 원인을 찾을 수 있었다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YsABY/btsP5H9KECE/PvJpS1SOWfFmsjWWJII161/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YsABY/btsP5H9KECE/PvJpS1SOWfFmsjWWJII161/img.png&quot; data-alt=&quot;PoolingHttpClientConnectionManager.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YsABY/btsP5H9KECE/PvJpS1SOWfFmsjWWJII161/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYsABY%2FbtsP5H9KECE%2FPvJpS1SOWfFmsjWWJII161%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1334&quot; height=&quot;186&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PoolingHttpClientConnectionManager.java&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 직접적인 HTTP Connection 을 생성하는 PoolingHttpClientConnectionManager 클래스를 확인해보면 이렇게 connection 생성시 socketTimeout 을 설정하고 있다. 그런데 코드를 더 들어가보면&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1392&quot; data-origin-height=&quot;238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUGUv5/btsP3xUTkmR/Y4y82zZ4Bs4tYLx3gpeP80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUGUv5/btsP3xUTkmR/Y4y82zZ4Bs4tYLx3gpeP80/img.png&quot; data-alt=&quot;InternalExecRuntime.java&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUGUv5/btsP3xUTkmR/Y4y82zZ4Bs4tYLx3gpeP80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUGUv5%2FbtsP3xUTkmR%2FY4y82zZ4Bs4tYLx3gpeP80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1392&quot; height=&quot;238&quot; data-origin-width=&quot;1392&quot; data-origin-height=&quot;238&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;InternalExecRuntime.java&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 요청을 실행하는 시점에 responseTimeout 을 꺼내고, socketTimeout 에 설정하고 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이걸 확인하는 순간 responseTimeout 이 발생하는 시점에도 ResponseTimeoutException 이 아니라 SocketTimeoutException 이 발생하는 이유까지 밝혀졌다. responseTimeout 이 socketTimeout 을 덮어쓰고 있는 것이다.&lt;br&gt;즉 문서에서 명시하고 있는대로 첫번째 응답 바이트까지 responseTimeout 이 관여하고, 두번째 바이트부터 socketTimeout 이 관여하는게 아니다. 사실 이전에 timeout 정리를 할때 개인적으로 느꼈던 부분은 responseTimeout 과 socketTimeout 을 이 정도로 분리해서 관리하면 코드 작성하기가 번거롭겠다 라고 생각했었는데, 그저 그냥 덮어쓰고 있던 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고 socketTimeout 은 ConnectionConfig 에서, responseTimeout 은 RequestConfig 라는 서로 다른 Config 객체를 통해 설정하고 있던것도 문맥을 이해하게 되었다. socketTimeout 은 connection 생성시 전달하고, responseTimeout 은 실제 요청(request)시 전달하기 때문이었던 것이다. 이 때문에 connection 수준에서는 socketTimeout 이 적용되고, 요청 수준에서는 responseTimeout 이 적용된다. responseTimeout 을 설정했다면 socketTimeout 은 적용되지 않는다!!!&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# 마무리&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이런 내용은 공식문서는 물론이고, 웹 검색에서도 찾기가 쉽지 않았다. 하지만 apache httpcomponents 를 사용하는 입장에서는 매우 중요한 요소이다. 생각보다 문서화가 상당히 부실하여 실망스럽기도 했지만 문서 변경에 대한 PR( &lt;a href=&quot;https://github.com/apache/httpcomponents-client/pull/712&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://github.com/apache/httpcomponents-client/pull/712&lt;/span&gt;&lt;/a&gt; ) 을 제출했다. 받아들여질지는 모르겠지만.&lt;/p&gt;</description>
      <category>Java</category>
      <author>LichKing</author>
      <guid isPermaLink="true">https://multifrontgarden.tistory.com/327</guid>
      <comments>https://multifrontgarden.tistory.com/327#entry327comment</comments>
      <pubDate>Sat, 23 Aug 2025 21:08:29 +0900</pubDate>
    </item>
    <item>
      <title>apache httpcomponents client 사용시 상황별 timeout과 connection pool 동작</title>
      <link>https://multifrontgarden.tistory.com/326</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;spring 에서 제공해주는 RestTemplate 이나 RestClient 를 사용한다면 코어 http 클라이언트로 apache httpcomponents client 를 많이 사용한다. 다양한 설정을 제공해고, 특히 http connection pool 을 제공해주는게 매력적인데 RestClient 와 함께 설정하는 방법은 &lt;a href=&quot;https://multifrontgarden.tistory.com/318&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전에 적은 글&lt;/a&gt;이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에선 각 timeout 과 해당 timeout 이 발생했을때 connection pool 의 동작을 알아본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# Timeout&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RequestConnectionTimeout&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connection pool 에서 connection 을 가져오기까지 대기하는 시간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connection pool 이 작아 모든 connection 이 사용중인 경우 해당 timeout 설정으로 인해 예외 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ResponseTimeout&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답의 첫번째 바이트를 받기까지 대기하는 시간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ConnectTimeout&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connection 을 맺기까지 대기하는 시간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 핸드쉐이크가 오래 걸리면 예외 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SocketTimeout&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답을 받기 시작한 이후 바이트 사이사이를 대기하는 시간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지속적으로 바이트가 timeout 설정 이내로 도착한다면 전체 응답시간은 response timeout 이나 socket timeout 설정값을 초과할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# timeout 이 발생했을때 connection 관리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RequestConnectionTimeout&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;connection pool 에서 connection 을 가져오기 위해 대기하다가 발생하는 예외이기 때문에 해당 예외가 발생한다고해서 pool 안에 있던 connection 이 영향받지 않는다. connection 을 대기하던 스레드만 ConnectionRequestTimeoutException 예외를 일으키게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ResponseTimeout&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;connection 을 생성하고, 응답의 첫번째 바이트를 대기하다가 시간초과가 발생했을때 SocketTimeoutException 을 일으킨다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 해당 connection 은 폐기되어 재사용되지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ConnectTimeout&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;connection 생성을 시도하다가 시간초과시 ConnectTimeoutException 을 일으킨다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;connection pool 이란 생성된 connection 을 관리하는 곳이기 때문에 지금처럼 애초에 생성되지 못했을땐 기존 connection pool 에 영향을 주지는 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SocketTimeout&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;응답 바이트를 받다가 설정한 시간을 초과하면 SocketTimeoutException 을 일으킨다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 사용했던 connection 은 폐기되어 재사용되지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# 발생할 수 있는 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ResponseTimeout 과 SocketTimeout 의 관계&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ResponseTimeout 은 첫번째 응답 바이트가 오기까지 시간을 설정하는 것이고, SocketTimeout 은 첫번째 바이트를 받은 후의 시간을 설정하는 것이다. ResponseTimeout 200ms, SocketTimeout 100ms 로 설정되어 있다면 첫번째 바이트는 150ms 에 오고 두번째 바이트부터 50ms 마다 오게되면 해당 http 호출은 예외없이 진행된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ResponseTimeout 과 SocketTimeout 은 모두 전체 응답시간을 보장하지 않는다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;계속 얘기하지만 &lt;u&gt;&lt;i&gt;ResponseTimeout 은 첫번째 바이트의 도착시간&lt;/i&gt;&lt;/u&gt;, &lt;i&gt;&lt;u&gt;SocketTimeout 은 두번째 이후 바이트의 도착시간&lt;/u&gt;&lt;/i&gt;이다. 그 어떤 것도 전체 바이트의 도착을 관장하지 않는다. 위와 동일하게 ResponseTimeout 200ms, SocketTimeout 100ms 로 설정되어 있다고 가정하고 첫번째 바이트가 150ms, 두번째 이후 바이트가 50ms 마다 오게되는 상황에서 전체 바이트가 모두 도착하는데 5000ms 가 걸린다면 해당 요청은 아무런 예외없이 5000ms 가 정상적으로 동작하게 된다. 때문에 네트워크 상황에 따라 timeout 설정에 비해 전체 응답이 오기까지 더 긴 시간이 소요될 수 있다.&lt;/p&gt;</description>
      <category>Java</category>
      <author>LichKing</author>
      <guid isPermaLink="true">https://multifrontgarden.tistory.com/326</guid>
      <comments>https://multifrontgarden.tistory.com/326#entry326comment</comments>
      <pubDate>Fri, 15 Aug 2025 20:13:28 +0900</pubDate>
    </item>
    <item>
      <title>synchronized 구문에서 virtual thread 동작과 JEP 491</title>
      <link>https://multifrontgarden.tistory.com/325</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최신 프로젝트는 현재 기준 최신 LTS 버전인 JDK 21 에서 개발하고 있다. JDK 21 에 추가된 기능 중 하나는 &lt;a href=&quot;https://openjdk.org/jeps/444&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JEP(&lt;span style=&quot;background-color: #ffffff; color: #001d35; text-align: start;&quot;&gt;Java Enhancement Proposal&lt;/span&gt;) 444&lt;/a&gt; 에 포함된 virtual thread 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;virtual thread 에 대해 간략하게 설명하면 OS 스레드와 별개로 JVM 수준에서 한번 더 추상화한 스레드를 만들고, 이를 OS 스레드와 연결(mount 라고 표현한다)해서 멀티 스레드 프로그래밍을 하도록 하는 개념이다. virtual thread 는 OS 스레드와는 독립적인 라이프 사이클을 갖게되며 virtual thread 를 생성한다고해서 꼭 OS 스레드를 추가로 생성하진 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 JVM 에서는 JVM 수준의 스레드인 플랫폼 스레드라는 개념이 있었지만 이 플랫폼 스레드는 항상 OS 스레드와 1:1 로 매핑이 되는 관계였다. 때문에 플랫폼 스레드를 추가 생성한다는 것은 곧 OS 스레드를 추가 생성하는 것이고, 이 때문에 스레드를 많이 생성하는 것 자체가 리소스에 부담이 되는 경우가 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;virtual thread 는 OS 스레드와 1:1 매핑이 아니기 때문에 스레드 추가 생성에 대한 부담이 매우 낮아졌고, 이 때문에 virtual thread 는 굳이 스레드 풀을 만들어서 사용하는걸 권하지도 않는다. IO 작업이 빈번한 경우 virtual thread 를 생성해서 실행하면 JVM 이 알아서 적절한 플랫폼 스레드를 통해 task 를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 virtual thread 에 많은 기대를 하고 있는 편이다. 다만 virtual thread 를 사용할때 동기화 방식에서 한가지 이슈가 있는데 전통적인 자바의 동기화 키워드인 synchronized 를 사용하면 효과를 제대로 내기 어렵다는 것이다. 조금 더 정확히 얘기하면 synchronized 내에서 CPU 를 사용하는 CPU 바운드 작업을 할때는 상관없지만 synchronized 내에서 IO 가 발생하면 이때 이슈가 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1754824075964&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Thread.ofVirtual().start(() -&amp;gt; { httpCall() });

synchronized void httpCall() { 
  // http IO                               
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 virtual thread 는 httpCall() 메서드를 실행한다. httpCall() 메서드는 synchronized 키워드로 동기화되어 있으며 내부에서 http IO 를 발생시킨다. 만약 http 요청이 1초가 걸린다면 virtual thread 는 1초간 블록킹되며 이때 실제 OS 스레드인 플랫폼 스레드는 다른 virtual thread 에게 할당되어 코어 활용을 더욱 효율적으로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 virtual thread 의 의도지만 위 코드는 그렇게 동작하지 않는다. synchronized 키워드 내에서는 플랫폼 스레드가 pinning 되어 다른 virtual thread 에 할당되지 못하고, 이 때문에 virtual thread 의 이점을 살릴 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하는 방법은 두가지인데 하나는 동기화 방법을 바꾸는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1754824388807&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private ReentrantLock lock = new ReentrantLock();

public void pinning() {                                 
    Thread.ofVirtual().start(() -&amp;gt; { httpCall(); });   
}                                                             
                                                       
void httpCall() {                                       
    lock.lock();                                       
    // http IO                                                   
    lock.unlock();                                     
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;synchronized 키워드 대신에 ReentrantLock 객체를 사용하는 것이다. ReentrantLock 은 java 1.5 에서 concurrent 패키지에 추가된 API 이다. 둘다 락을 잡는건 동일하지만 synchronized 를 이용하는 것과 ReentrantLock 을 이용할때 내부 메커니즘이 달라 ReentrantLock 을 이용하는 경우엔 플랫폼 스레드가 다른 virtual thread 에게 할당되지만 synchronized 를 이용하는 경우엔 위에서 언급한대로 다른쪽으로 할당되지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째는 IO 작업을 락 내에서 하지 않는 것이다. 웬만한 경우엔 이 방식이 정답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우리도 모르는 사이에 아주 핵심적인 곳에서 synchronized 동기화 내에서 IO 를 발생시키는 대표적인 부분이 있다. 바로 JDBC Driver 구현체들이다. 이때문에 virtual thread 가 세상에 나온 이후로 JDBC Driver 구현체들은 동기화 방식을 ReetrantLcok 으로 변경하는 작업을 진행했다. MySQL Connector/J 는 9.0.0 release notes 에 해당 변경이 포함됐다. virtual thread 를 이용한다면 버전을 확인해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;i&gt;Synchronized&amp;nbsp;blocks&amp;nbsp;in&amp;nbsp;the&amp;nbsp;Connector/J&amp;nbsp;code&amp;nbsp;were&amp;nbsp;replaced&amp;nbsp;with&amp;nbsp;ReentrantLocks.&amp;nbsp;This&amp;nbsp;allows&amp;nbsp;carrier&amp;nbsp;threads&amp;nbsp;to&amp;nbsp;unmount&amp;nbsp;virtual&amp;nbsp;threads&amp;nbsp;when&amp;nbsp;they&amp;nbsp;are&amp;nbsp;waiting&amp;nbsp;on&amp;nbsp;IO&amp;nbsp;operations,&amp;nbsp;making&amp;nbsp;Connector/J&amp;nbsp;virtual-thread&amp;nbsp;friendly.&amp;nbsp;Thanks&amp;nbsp;to&amp;nbsp;Bart&amp;nbsp;De&amp;nbsp;Neuter&amp;nbsp;and&amp;nbsp;Janick&amp;nbsp;Reynders&amp;nbsp;for&amp;nbsp;contributing&amp;nbsp;to&amp;nbsp;this&amp;nbsp;patch.&amp;nbsp;(Bug&amp;nbsp;#110512,&amp;nbsp;Bug&amp;nbsp;#35223851)&lt;/i&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이런 변경에 무색하게 그 이후 새로운 제안이 발표되었다. synchronized 키워드에서도 virtual thread 가 정상적으로 unmount 되도록 하는 내용의 &lt;a href=&quot;https://openjdk.org/jeps/491&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JEP 491&lt;/a&gt;이다. 해당 내용은 JDK 24 부터 포함됐고, LTS 기준으로는 JDK 25 부터 포함되게 된다. JDK 24 이상을 사용하게 되면 미처 ReentrantLock 으로 변경하지 못한 라이브러리를 사용하더라도 virtual thread 의 이점을 살릴 수 있을 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만... 할 수 있었으면 처음부터 같이 해주지...&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;# 참고자료&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://openjdk.org/jeps/444&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://openjdk.org/jeps/444&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://openjdk.org/jeps/491&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://openjdk.org/jeps/491&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/relnotes/connector-j/en/news-9-0-0.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/relnotes/connector-j/en/news-9-0-0.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <author>LichKing</author>
      <guid isPermaLink="true">https://multifrontgarden.tistory.com/325</guid>
      <comments>https://multifrontgarden.tistory.com/325#entry325comment</comments>
      <pubDate>Sun, 10 Aug 2025 20:33:06 +0900</pubDate>
    </item>
    <item>
      <title>private 프로퍼티를 사용하는 함수를 inline 으로 만드려면 어떻게 해야할까</title>
      <link>https://multifrontgarden.tistory.com/324</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 제네릭을 이용할때 가장 불편한 부분은 타입 소거로 인해 타입 토큰을 전달해야하는 점이다.&lt;/p&gt;
&lt;pre id=&quot;code_1752902991665&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 런타임에 T 정보를 알 수 없으므로 이렇게 작성하면 컴파일 에러가 발생한다
public &amp;lt;T&amp;gt; T create() throws Exception {           
    return T.class.getConstructor().newInstance();
}                                                 

// 타입 파라미터와 별개로 타입 토큰을 전달해야한다
public &amp;lt;T&amp;gt; T create(Class&amp;lt;T&amp;gt; clazz) throws Exception { 
    return clazz.getConstructor().newInstance();       
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린은 inline + reified 키워드를 이용해 이런 부분을 개선했다.&lt;/p&gt;
&lt;pre id=&quot;code_1752903236070&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 두 코드 모두 정상적으로 컴파일되고, 실행된다
inline fun &amp;lt;reified T&amp;gt; create(): T {
    return T::class.java.getConstructor().newInstance()
}

inline fun &amp;lt;reified T&amp;gt; create(): T {
    return T::class.constructors.first { it.parameters.isEmpty() }.call()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 코틀린으로 작성된 코드들은 제네릭을 사용하더라도 타입 토큰을 직접 전달하는 경우가 거의 없다. 다만 코드를 작성하다보면 이런 문제가 생길때가 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1752903353512&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class SomeClass(
    private val someProperty: String,
) {
    inline fun &amp;lt;reified T&amp;gt; create(): T {
        println(someProperty)
        return T::class.constructors.first { it.parameters.isEmpty() }.call()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 형태로 inline 으로 공개하고 싶은 메서드가 내부에서 private 프로퍼티 혹은 private 메서드를 호출하는 경우다. inline 키워드는 해당 메서드를 호출하는 클라이언트에 직접 코드가 복사되게 되는데, 이렇게 되면 컴파일 된 결과물에선 외부에서 private 프로퍼티에 접근하게 되기 때문에 컴파일이 되지 않는다. 이럴땐 어쩔 수 없이 inline을 포기하고 자바처럼 타입 토큰을 전달하거나 inline을 유지하고 싶다면 프로퍼티의 접근 제어자를 변경해주는 방법이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1752903656832&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class SomeClass(
    val someProperty: String,
) {
    inline fun &amp;lt;reified T&amp;gt; create(): T {
        println(someProperty)
        return T::class.constructors.first { it.parameters.isEmpty() }.call()
    }
}

class SomeClass(
    private val someProperty: String,
) {
    fun &amp;lt;T : Any&amp;gt; create(clazz: KClass&amp;lt;T&amp;gt;): T {
        println(someProperty)
        return clazz.constructors.first { it.parameters.isEmpty() }.call()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 둘 다 썩 마음에 드는 방법은 아니다. 이럴때 권장하는 방법은 확장함수를 제공하는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1752904200090&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class SomeClass(
    private val someProperty: String,
) {
    fun &amp;lt;T : Any&amp;gt; create(clazz: KClass&amp;lt;T&amp;gt;): T {
        println(someProperty)
        return clazz.constructors.first { it.parameters.isEmpty() }.call()
    }
}

inline fun &amp;lt;reified T : Any&amp;gt; SomeClass.create(): T {
    return this.create(T::class)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 실제로 많은 코틀린 라이브러리에서 사용하는 방식이고, 기존 자바 라이브러리들이 코틀린용 모듈을 제공할때 사용하는 방식이다. 두번째 방식은 @PublishedApi 애노테이션을 사용하는 방식이다.&lt;/p&gt;
&lt;pre id=&quot;code_1752904604503&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class SomeClass(
    @PublishedApi
    internal val someProperty: String,
) {
    inline fun &amp;lt;reified T&amp;gt; create(): T {
        println(someProperty)
        return T::class.constructors.first { it.parameters.isEmpty() }.call()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;private 이었던 프로퍼티의 접근제어자를 internal 로 변경하고, @PublishedApi 를 붙여주면 된다. 이렇게하면 외부 모듈에서 create() 메서드를 호출할 수 있다. 다만 이 방식은 공개 라이브러리를 만드는 경우 바이너리 호환성 문제가 발생할 수 있다. 또한 처음 설계의도가 internal 이면 모르겠지만 여전히 타입때문에 접근제어자를 변경해야하는 점, 변경하고나면 모듈 내에선 public 과 마찬가지가 되는 부분 때문에 이 방식보다는 확장함수를 이용하는 방식이 더 낫다고 생각한다.&lt;/p&gt;</description>
      <category>kotlin</category>
      <author>LichKing</author>
      <guid isPermaLink="true">https://multifrontgarden.tistory.com/324</guid>
      <comments>https://multifrontgarden.tistory.com/324#entry324comment</comments>
      <pubDate>Sat, 19 Jul 2025 15:02:00 +0900</pubDate>
    </item>
    <item>
      <title>RestClient 내부 ObjectMapper 설정</title>
      <link>https://multifrontgarden.tistory.com/323</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;RestClient를 애플리케이션의 http client로 활용할 수 있다. 이때 MediaType이 application/json 인 경우 RestClient는 ObjectMapper를 이용해 body의 직렬화/역직렬화를 수행한다. Http MediaType은 종류가 여러가지인만큼 RestClient는 ObjectMapper를 직접 이용하는게 아니라 각 MediaType을 MessageConverter라는 인터페이스로 추상화하여 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 최근에는 json이 표준처럼 사용되고 있는만큼 다른 타입에 비해 json을 핸들링해야하는 경우가 많은데, 이때 내부 ObjectMapper의 설정을 변경해줘야할때가 있다. ObjectMapper의 default naming 전략은 camel case인데 snake case로 변경한다거나 직접 구현한 Serializer/Deserializer를 등록한다거나 하는 경우가 그렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. &lt;b&gt;MessageConverter&lt;/b&gt; 변경&lt;/h2&gt;
&lt;pre id=&quot;code_1752378964410&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 원하는 설정의 ObjectMapper 생성
public ObjectMapper create() {                                        
    SimpleModule module = new SimpleModule();                                   
    module.addSerializer(); // custom serializer                                
    module.addDeserializer(); // custom deserializer                            
                                                                                
    JsonMapper jsonMapper = JsonMapper.builder()                                
            // property naming strategy                                         
            .propertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE)  
            .addModule(module)                                                  
            .build();
            
    return jsonMapper;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1752379119795&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(create()); 

// MessageConverter list를 교체하는 방식
RestClient.builder()                                                         
        .messageConverters(List.of(converter))                                                       
        .build();

// MessageConverter list를 변경하는 방식
RestClient.builder()                       
        .messageConverters(converters -&amp;gt; { 
            converters.clear();            
            converters.add(converter);     
        })                                 
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestClient는 내부에 미리 정의된 MessageConverter list를 갖게 된다. 이를 통해 각 MediaType에 알맞은 MessageConverter 구현체를 사용하게 되는 것이다. 위에 소개한 방법은 RestClient의 builder를 이용해서 MessageConverter list를 변경하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 위 방식은 json 이외 다른 MessageConverter를 모두 지우게 된다. 뭔가 바람직해 보이는 방식은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 클래스에 메타 정보 선언&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjectMapper를 이용할때 설정을 변경하는 방법은 두가지다. ObjectMapper 자체를 변경하거나 그게 아니라면 각각 클래스에 선언해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1752381974725&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class)
class Person {
    @JsonSerialize(using = NameSerializer.class)
    @JsonDeserialize(using = NameDeserializer.class)
    private Name name;
    @JsonSerialize(using = AgeSerializer.class)
    @JsonDeserialize(using = AgeDeserializer.class)
    private Age age;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 설정을 클래스쪽으로 옮기는 대신 ObjectMapper는 건드릴 필요 없다는 장점이 있지만 저 설정들을 매번 복붙해야한다는 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 자동구성된 RestClient.Builder 사용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring에서는 RestClient.Builder를 기본적으로 빈에 등록해주고 있다. 그리고 이 빈은 ObjectMapper를 주입받게 되는데 이때 spring 빈으로 등록된 ObjectMapper를 주입하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1752382950477&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// spring 빈 등록
@Bean
public ObjectMapper objectMapper() {
  return create();
}

@Component
@AllArgsConstructor
class SomeClass {
  private RestClient.Builder builder; // 이미 등록돼있던 Builder가 주입됨
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjectMapper도 미리 자동 구성되어 빈으로 등록되기 때문에 ObjectMapper를 빈으로 등록하는 것도 필요하지 않을 수 있다. 자동 구성되는 ObjectMapper의 설정을 변경해야한다면 이전 포스팅( &lt;a href=&quot;https://multifrontgarden.tistory.com/300&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://multifrontgarden.tistory.com/300&lt;/a&gt; )을 참고하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 공식문서에서도 빈으로 등록된 Builder 사용을 권장하고 있다( &lt;a href=&quot;https://docs.spring.io/spring-boot/reference/io/rest-client.html#io.rest-client.restclient&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-boot/reference/io/rest-client.html#io.rest-client.restclient&lt;/a&gt; ). 자동구성되는 내용은 여기서( &lt;a href=&quot;https://github.com/spring-projects/spring-boot/blob/main/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfiguration.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/spring-projects/spring-boot/blob/main/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/RestClientAutoConfiguration.java&lt;/a&gt; ) 확인할 수 있다.&lt;/p&gt;</description>
      <category>Java/spring</category>
      <author>LichKing</author>
      <guid isPermaLink="true">https://multifrontgarden.tistory.com/323</guid>
      <comments>https://multifrontgarden.tistory.com/323#entry323comment</comments>
      <pubDate>Sun, 13 Jul 2025 14:16:22 +0900</pubDate>
    </item>
    <item>
      <title>JPA 엔티티는 POJO인가</title>
      <link>https://multifrontgarden.tistory.com/322</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;POJO 란 Plain Old Java Object 의 줄임말로 특별한 제약이 없는 객체를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특별한 제약이란 무엇일까? 객체 설계 관점에서 필요에 의한게 아니라 다른 외부 기술의 사용 때문에 객체에 제약이 생기는 경우이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 서블릿을 생각해보자. 서블릿을 이용해서 http 요청에 매핑하는 것과 스프링의 컨트롤러를 이용하는 코드를 비교해자.&lt;/p&gt;
&lt;pre id=&quot;code_1747136632334&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// HttpServletRequest 를 상속받는다
public class HttpRequestMapping extends HttpServletRequest { }

// 애노테이션으로 요청을 받음을 표현한다
@Controller
public class HttpRequestMapping { }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 같은 경우 HttpServletRequest 를 필수로 상속 받아야하고, 스프링 컨트롤러는 애노테이션만 붙이게 된다. 이 둘은 어떤 차이가 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 자바에서 상속은 단일 상속만 지원된다. HTTP 요청을 처리하는 객체가 무언가의 이유로 상속을 활용해야하는 상황이 됐을때 서블릿 객체는 이미 기술적 이유로 상속을 받고있어 추가 상속을 받을 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 기술에 대한 의존이다. 두 클래스의 인스턴스를 생성자를 이용해 직접 생성해보자. 서블릿 객체의 인스턴스를 손쉽게 만들 수 있는가? 또한 상속을 통해 문제해결을 하고 있기 때문에 불필요한 메서드들의 공개를 막을길이 없다. 손쉽게 객체를 만들어 단위테스트를 작성한다고 가정했을때 스프링 컨트롤러가 압도적으로 쉽다. 물론 애노테이션이 붙었으니 스프링 컨트롤러도 완전한 POJO는 아니지 않냐고 의문을 제기하는 사례도 있다. 이는 좀 더 뒤에서 다뤄보도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 관점에서 JPA 엔티티는 POJO 인지 아닌지 항상 얘기가 많다. 이번 포스팅에서는 POJO 인지 아닌지를 알아보고, 실용적 관점에서 어떻게 바라볼 수 있는지 정리해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# JPA 애노테이션&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 쉽게 이의를 제기할 수 있는 부분으로는 JPA 애노테이션이 객체에 침투한다는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1747452152464&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Person {
  @Id
  private Long id;
  private String name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 간단한 객체인데 이를 JPA 엔티티로 정의하면 최대한 관례를 이용한다고 해도 위 2개의 애노테이션은 붙여줘야 한다. 명시적인 설정을 좋아하면 @Table, @Column 등의 애노테이션이 추가로 붙는다. 더욱이 실제 JPA 엔티티는 저정도로 간단하지 않으므로 더 많은 JPA 의존이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 JPA 엔티티는 POJO 가 아니라고 주장하는 의견 중 가장 쉽게 볼 수 있는 의견이며, JPA 가 classpath에서 사라지면 Person 클래스는 컴파일이 불가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 의견은 분명히 맞는말이나 POJO 에 대한 논의가 이루어질때 어느정도 수준의 순수함을 보장해야하느냐의 결정에 따라 달라질 수 있는 부분이다. 애노테이션은 상속처럼 외부 기술에 대한 종속을 강하게 결합하지 않으며, 컴파일만 가능하다면 원할때 외부 기술 없이 객체를 사용할 수 있기 때문이다. 위 Person 클래스의 경우 생성자나 메서드만 정상적으로 제공되고 있다면 JPA 애노테이션이 붙어있어도 인스턴스를 만들고, 사용하는데 아무런 제약이 없다. 다만 엄격한 관점에서는 애노테이션 참조도 침투로 보기 때문에 POJO 가 아니라는 주장도 합당하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;## xml 매핑&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1747452965538&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// persistence.xml
&amp;lt;entity class=&quot;com.yong.jpa.jpa.person.Person&quot; access=&quot;FIELD&quot;&amp;gt; 
    &amp;lt;table name=&quot;person&quot;/&amp;gt;                                     
    &amp;lt;attributes&amp;gt;                                               
        &amp;lt;id name=&quot;id&quot;&amp;gt;                                         
            &amp;lt;column name=&quot;person_id&quot;/&amp;gt;                         
            &amp;lt;generated-value strategy=&quot;IDENTITY&quot;/&amp;gt;             
        &amp;lt;/id&amp;gt;                                                  
        &amp;lt;basic name=&quot;name&quot;&amp;gt;                                    
            &amp;lt;column name=&quot;username&quot;/&amp;gt;                          
        &amp;lt;/basic&amp;gt;                                               
    &amp;lt;/attributes&amp;gt;                                              
&amp;lt;/entity&amp;gt;

// 설정 yml
spring:
  jpa:
    mapping-resources: META-INF/persistence.xml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 JPA 가 애노테이션 설정이 아닌 외부에서 설정할 수 있다면 어떨까? 실제로 JPA 는 해당 기능을 제공하고 있으며 xml 로 매핑을 설정하면 JPA 관련 모든 애노테이션을 제거할 수 있다. JPA 는 &lt;a href=&quot;https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#a16944&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;xml 설정 방식&lt;/a&gt;과 &lt;a href=&quot;https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#annotations-for-objectrelational-mapping&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;애노테이션 방식&lt;/a&gt;을 모두 제공하고 있는데, 보통은 더 편리한 애노테이션 방식을 사용하는 것 뿐이다. 애노테이션 침투가 불편하다면 xml 방식을 사용하면 된다. 편의에 의해서 선택에 의해 애노테이션 방식을 사용하고 있으면서 JPA 엔티티는 POJO 가 아니라고 얘기할 수 있을까? 그럼 POJO 로 사용하기 위해 xml 매핑 방식을 사용할 것인가? 고민해볼만한 내용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# id 할당&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만일 애노테이션 기반이 아닌 xml 방식을 사용한다고 하면 그때는 JPA 엔티티를 완전하게 POJO 라고 부를 수 있을까? xml 방식을 이용한다고 가정하면 classpath에 JPA 가 사라져도 컴파일이 되는건 확실한 것이다. 그러면 이제 POJO 일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티의 고유성은 내부 상태들을 기반으로 판단하기보다는 고유한 id로 판단한다. 두 객체의 상태가 달라도 id가 같으면 같은 엔티티로 판단한다는 것이다. 때문에 id를 할당하는 로직은 상당히 중요한데, 편리한 방식으로는 데이터베이스의 id 할당을 그대로 사용하는 것이다. 이 방식을 많이 사용하고 있기 때문에 이를 기반으로 설명하겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1747454609675&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Person {
  private Long id;
  private String name;
  
  public Person() {}
  public Person(Long id, String name) {}
}

@Test
void 엔티티를_저장하면_id가_할당된다() {
  Person person = new Person(null, &quot;name&quot;);
  Person savedPerson = personRepository.save(person);

  assertThat(savedPerson.getId()).isNotNull();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자를 이용해 person 을 초기화하고 저장했다. 저장한 다음엔 id가 데이터베이스를 이용해 할당되기 때문에 해당 테스트는 통과한다. id를 직접 할당할 일은 없으니 null을 매번 전달하는 것보단 해당 생성자를 없애는게 나을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1747454938309&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Person {
  private Long id;
  private String name;
  
  public Person() {}
  public Person(String name) {}
}

@Test
void 엔티티를_저장하면_id가_할당된다() {
  Person person = new Person(&quot;name&quot;);
  Person savedPerson = personRepository.save(person);

  assertThat(savedPerson.getId()).isNotNull();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 null 전달이 사라지고 더 깔끔한 코드가 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티를 이용하는 다른 객체 IdChecker 가 있다고 가정해보자. 쉬운 예제를 작성하다보니 다소 억지스러울 수 있는 예제지만 현실에서 id를 꺼내서 사용하는 코드는 어렵지 않게 찾아볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1747455332876&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class IdChecker {
  public void check(Person person) {
    if(person.getId() == null) {
      throw new EntityIdCheckException();
    }
  }
}

@Test
void 정상적으로_초기화된_엔티티는_예외가_발생하지_않는다() {
  IdChecker checker = new IdChecker();
  Person person = new Person(&quot;name&quot;); // id를 넣을 수 없다!
  checker.check(person); // id가 없으니 예외가 발생하고, 테스트는 성공할 수 없다
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Person 엔티티가 xml 을 이용해 매핑하도록 했다면, JPA 가 없는 상태에서도 정상적으로 컴파일이 된다. 하지만 JPA 가 id를 매핑한다는 가정하에 객체가 디자인됐다면 JPA 가 없는 상태에서는 (리플렉션을 쓰지 않는 한)완전한 상태로 초기화할 수 없다. 이는 바이너리 수준에서 기술 의존은 없지만 논리적으로 기술에 의존하게 되는 셈이고, 이런 디자인은 POJO 로 보기 어렵다. 객체 디자인에서도 JPA 가 초기화한다는 가정과 JPA 가 초기화하는 방식에 의존해선 안된다.&lt;/p&gt;
&lt;pre id=&quot;code_1747455589631&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Person {
  private Long id;
  private String name;
  
  public Person() {}
  public Person(String name) {}
  public Person(Long id, String name) {}
}

@Test
void 정상적으로_초기화된_엔티티는_예외가_발생하지_않는다() {
  IdChecker checker = new IdChecker();
  Person person = new Person(1L, &quot;name&quot;);
  checker.check(person); // id가 할당됐으니 테스트는 성공한다
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 완전한 인스턴스를 생성할 수 있는 방식을 제공해야 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;JPA 가 없는 상태에서도&lt;span&gt;&amp;nbsp;단위 테스트를 통과할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;# 완전한 POJO 로 가는 길&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;매핑 정보를 xml 로 설정하고, JPA 없이도 id 를 할당할 수 있다면 POJO 일까? JPA 스펙에는 &lt;a href=&quot;https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#a18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;엔티티 클래스가 가져야하는 규약&lt;/a&gt;이 있다. 이 규약들을 모두 숙지하고 엔티티를 만드는 경우도 있겠지만, 보통은 규약을 잘 모르는 상태에서 일반적인 클래스를 정의해도 문제없이 동작한다. 다만 한가지 규약은 많은 이들이 알고 있을텐데 proteced 혹은 public 의 기본생성자가 존재해야한다는 것이다. 이 규약 때문에 롬복을 이용하는 경우 이 애노테이션이 계속 붙게 될 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1747541264056&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Person {
  private Long id;
  private String name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 디자인 관점에서 추가된 생성자가 아니라 기술에 의해 필요한 생성자이기 때문에 public 으로 공개하기는 싫고, 그보다 아래 공개 수준인 protected 로 선언하는 AccessLevel.PROTECTED 를 붙이는게 최선이다. 이는 현재의 JPA 스펙에선 필수 규약이기 때문에 객체 디자인에 기술이 침투하는 모습이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# 마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서 언급한 주제들과 예로 사용한 JPA 엔티티는 최소한의 설정을 갖고있는 엔티티이다. 보통의 실무에서는 이보다 더 복잡한 엔티티를 사용하게 되는데, 그런상황에서는 더 많은 주제로 얘기해볼 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론으로 얘기해서 JPA 엔티티는 완전한 수준에서 POJO 라고 보기 어렵다. 하지만 JPA 가 세상에 나오기 전엔 이보다 훨씬 기술의 침투가 만연했고, 그걸 최대한 극복해서 거의 POJO 처럼 사용할 수 있게한 공로는 인정해야한다. 설계는 항상 트레이드오프이고, 엄밀한 수준의 POJO 로 활용하려면 큰 노력을 요구하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기회가 된다면 JPA 를 사용하면서 정말 POJO 를 사용하려면 어떻게 하면 좋을지 고민해보고 시도해보는 것도 좋을 것이다. 그러면 POJO 로 가기 위해 어느정도의 비용을 치뤄야하는지, 또한 타협을 한다면 실용적 관점에서 어디에서 타협을 할지도 고민해볼 기회가 될 것이라 생각한다.&lt;/p&gt;</description>
      <category>Java/jpa</category>
      <author>LichKing</author>
      <guid isPermaLink="true">https://multifrontgarden.tistory.com/322</guid>
      <comments>https://multifrontgarden.tistory.com/322#entry322comment</comments>
      <pubDate>Wed, 21 May 2025 12:04:51 +0900</pubDate>
    </item>
    <item>
      <title>JSR354 JavaMoney</title>
      <link>https://multifrontgarden.tistory.com/321</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;객체에 분류와 구현에 대해 공부하다보면 VO(Value Object)라는 용어를 알게되고, VO를 공부하면 대표적으로 등장하는 예시가 Money 클래스이다. long이나 BigDecimal 같은 숫자를 다루는 타입으로 통화를 표현하게 되는데 이런 타입을 사용하지 말고 직접 통화를 의미하는 타입을 만들어서 표현력을 올리고, 통화의 책임을 다루라는 의미다. &lt;a href=&quot;https://cr3.shopping.naver.com/v2/bridge/book/searchGate?cat_id=50010702&amp;amp;frm=PBOKPRO&amp;amp;h=12b858c93a38c18f75a07555cc1ea414dbc17a27&amp;amp;nv_mid=32455539962&amp;amp;query=%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%A3%BC%EB%8F%84%EA%B0%9C%EB%B0%9C&amp;amp;t=M9WMJ953&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;테스트주도개발&lt;/span&gt;&lt;/a&gt;이라는 책에서도 Money 클래스를 TDD로 만들어가는걸로 책을 시작한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;java에서는 JDK 안에 time 패키지를 제공함으로써 날짜에 대한 표준 구현 클래스들을 제공한다. 언어표준이기 때문에 대부분의 라이브러리에서도 지원하고 있어 날짜나 시간을 직접 구현하거나 String같은 타입으로 표현하는 경우는 드물다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;미처 몰랐는데 통화에 대해서도 java 표준 스펙이 있다는걸 알게되어 소개해보려한다. jackson 2.19의 &lt;a href=&quot;https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.19&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;release note&lt;/span&gt;&lt;/a&gt;를 보다가 javax.money를 지원한다는 내용을 보고, 이게 뭐지? 하면서 알아본 내용인데 더 알아보니 이미 추가된지 10년은 된 스펙이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTG36b/btsNzLaJWN3/FbfP9mXFDINoG4e6SGPvnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTG36b/btsNzLaJWN3/FbfP9mXFDINoG4e6SGPvnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTG36b/btsNzLaJWN3/FbfP9mXFDINoG4e6SGPvnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTG36b%2FbtsNzLaJWN3%2FFbfP9mXFDINoG4e6SGPvnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1848&quot; height=&quot;434&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# JavaMoney&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSR354에 추가된 스펙인데 이 스펙은 JDK 내장으로 채택되지 않았기에 사용하려면 별도 의존성 추가가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 의존성 추가
implementation(&quot;javax.money:money-api:1.1&quot;)

// javax.money 패키지 사용
MonetaryAmountFactory&amp;lt;?&amp;gt; factory = Monetary.getDefaultAmountFactory();
MonetaryAmount usd = factory.setCurrency(&quot;KRW&quot;)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.setNumber(500)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.create();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;의존성을 추가하면 javax.money 패키지를 이용할 수 있게되고 위와 같은 팩토리를 사용할 수 있게된다. 위 코드를 그대로 실행하면 예외가 발생하는데 money-api는 인터페이스만 제공하는 모듈이기 때문이다. 레퍼런스 구현인 moneta 의존성도 추가해야한다. JSR310에 추가된 스펙인 time 패키지가 JDK 내장으로 들어와있기에 별도 의존성 추가 없이 구현체까지 사용할 수 있는걸 생각해보면 둘의 차이나 사용법을 비교해보는 것도 재미있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 moneta는 공식 구현이 아니라 레퍼런스 구현이라는 표현을 사용하고 있는데, javax.money가 인터페이스들이니만큼 누구나 구현할 수 있고, 이때 기준이 되는 구현일 뿐 time 패키지의 LocalDate와 같은 공식 구현은 아니라는 의미다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation(&quot;org.javamoney:moneta:1.4.5&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 의존성을 추가해주면 예외는 발생하지 않는다. 그리고 아래와 같은 테스트 코드도 성공한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;CurrencyUnit currency = Monetary.getCurrency(&quot;KRW&quot;);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
MonetaryAmountFactory&amp;lt;?&amp;gt; factory = Monetary.getDefaultAmountFactory(); 
MonetaryAmount krw = factory.setCurrency(currency)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.setNumber(500)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.create();&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
MonetaryAmount addedKrw = krw.add(Money.of(300, currency));&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
assertThat(addedKrw).isEqualTo(Money.of(800, currency));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금액이 달라지거나 통화가 달라지면 테스트는 실패한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# Jackson&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 JavaMoney에 대해 알게해준 jackson을 이용해서 json화 시켜보자&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;MonetaryAmount krw = Money.of(300, Monetary.getCurrency(&quot;KRW&quot;));&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
ObjectMapper objectMapper = new ObjectMapper();&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
String json = objectMapper.writeValueAsString(krw);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
System.out.println(json);

// 출력결과
{&quot;currency&quot;:{&quot;context&quot;:{&quot;providerName&quot;:&quot;java.util.Currency&quot;,&quot;empty&quot;:false},&quot;currencyCode&quot;:&quot;KRW&quot;,&quot;defaultFractionDigits&quot;:0,&quot;numericCode&quot;:410},&quot;number&quot;:300,&quot;factory&quot;:{&quot;defaultMonetaryContext&quot;:{&quot;fixedScale&quot;:false,&quot;amountType&quot;:&quot;org.javamoney.moneta.Money&quot;,&quot;maxScale&quot;:63,&quot;precision&quot;:0,&quot;providerName&quot;:null,&quot;empty&quot;:false},&quot;maxNumber&quot;:null,&quot;minNumber&quot;:null,&quot;amountType&quot;:&quot;org.javamoney.moneta.Money&quot;,&quot;maximalMonetaryContext&quot;:{&quot;fixedScale&quot;:false,&quot;amountType&quot;:&quot;org.javamoney.moneta.Money&quot;,&quot;maxScale&quot;:-1,&quot;precision&quot;:0,&quot;providerName&quot;:null,&quot;empty&quot;:false}},&quot;context&quot;:{&quot;fixedScale&quot;:false,&quot;amountType&quot;:&quot;org.javamoney.moneta.Money&quot;,&quot;maxScale&quot;:-1,&quot;precision&quot;:256,&quot;providerName&quot;:null,&quot;empty&quot;:false},&quot;zero&quot;:false,&quot;negative&quot;:false,&quot;positive&quot;:true,&quot;numberStripped&quot;:3E+2,&quot;negativeOrZero&quot;:false,&quot;positiveOrZero&quot;:true,&quot;scale&quot;:0,&quot;precision&quot;:3}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽을 순 있지만 일반적으로 원하는 모양은 아니다. 계속 비교군으로 언급되는 time 패키지의 LocalDate와 같은 타입도 원하는 모양이 아닌 형태로 json화 되는걸 경험해본 경험이 있을 것이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation(&quot;com.fasterxml.jackson.core:jackson-databind:2.19.0&quot;)
// javax.money 지원
implementation(&quot;com.fasterxml.jackson.datatype:jackson-datatype-javax-money:2.19.0&quot;)
// 구현체인 moneta 지원
implementation(&quot;com.fasterxml.jackson.datatype:jackson-datatype-moneta:2.19.0&quot;)

// 둘중 하나를 추가
objectMapper.registerModule(new JavaxMoneyModule());
objectMapper.registerModule(new MonetaMoneyModule());

// 출력결과
{&quot;amount&quot;:300,&quot;currency&quot;:&quot;KRW&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jackson은 두 모듈을 지원하고 있는데, 예제차원으로 MonetaryAmount 타입을 json화하는 정도에선 자바 표준을 지원하고 있는 javax-money 모듈로도 충분하고, moneta에 대한 더 구체적인 지원이 필요할때 moneta 모듈을 추가하는게 좋아보인다. 둘중 하나를 추가하고 다시 코드를 실행하면 원하는 형태의 json이 출력됨을 알 수 있다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# 마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개요 수준으로 간략하게 살펴봤다. moneta는 구현클래스인 Money외에도 FastMoney와 같은 구현체도 제공하고 있으니 도입을 고려한다면 더 심도깊게 알아보고 사용하자. 새로운 스펙인줄알고 신나게 찾아봤는데 이미 10년이나 된 스펙이라 김이 좀 샜지만 돈을 다루는 프로젝트에서는 도입을 검토해보면 좋겠다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# 참고자료&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://javamoney.github.io&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://javamoney.github.io&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://javamoney.github.io/api.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://javamoney.github.io/api.html&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://github.com/JavaMoney/jsr354-ri/blob/master/moneta-core/src/main/asciidoc/userguide.adoc&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://github.com/JavaMoney/jsr354-ri/blob/master/moneta-core/src/main/asciidoc/userguide.adoc&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <category>java</category>
      <category>money</category>
      <author>LichKing</author>
      <guid isPermaLink="true">https://multifrontgarden.tistory.com/321</guid>
      <comments>https://multifrontgarden.tistory.com/321#entry321comment</comments>
      <pubDate>Fri, 25 Apr 2025 19:55:24 +0900</pubDate>
    </item>
    <item>
      <title>Hibernate 6.6.x merge 동작방식 변경</title>
      <link>https://multifrontgarden.tistory.com/319</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;spring boot 버전을 올리면서 3.4.x 버전을 사용하게 됐고, 동작을 확인하던 도중 기존 코드에서 에러가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;u&gt;Row&amp;nbsp;was&amp;nbsp;updated&amp;nbsp;or&amp;nbsp;deleted&amp;nbsp;by&amp;nbsp;another&amp;nbsp;transaction&amp;nbsp;(or&amp;nbsp;unsaved-value&amp;nbsp;mapping&amp;nbsp;was&amp;nbsp;incorrect)&lt;/u&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 로우가 다른 트랜잭션에서 업데이트되거나 삭제되었다는 메시지인데, 버전을 올리지 않으면 해당 에러는 발생하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제가 발생할 수 있는 상황은 이렇다.&lt;/p&gt;
&lt;pre id=&quot;code_1739590424852&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 엔티티 정의
@Entity
@Table(name = &quot;person&quot;)
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Person {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;person_id&quot;)
    private Long id;

    @Column(name = &quot;person_name&quot;)
    private String name;
}

// 문제가 발생하는 테스트 코드
@Test                                                
void id를_할당한채로_저장시도() {                              
    Person person = new Person(100L, &quot;LichKing&quot;);
    personRepository.save(person); // hibernate 6.6.0 부터 예외 발생                   
}                                                    

@Test                                                      
void 엔티티_제거_후_재저장() {                                      
    Person person = new Person(null, &quot;LichKing&quot;);          
    Person saved = personRepository.save(person);          
    personRepository.delete(saved);                        
    personRepository.save(person); // hibernate 6.6.0 부터 예외 발생                   
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장하려는 엔티티에 id 가 할당되어 있으면 spring data jpa 는 EntityManager 의 persist 가 아닌 merge 를 호출하게 된다. merge 가 호출되면 그때부턴 hibernate 의 영역인데, 할당된 id 에 해당하는 로우가 있으면 update 를 실행하고 없으면 insert 를 실행한다. 이때 id 생성을 DB 에 위임하고 있다면(쉽게 얘기해서 @GeneratedValue 애노테이션이 달려있다면) 실제 전달된 id 를 사용하지 않고, DB 에서 정해주는 id 로 엔티티를 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 id 를 100으로 할당해서 저장을 했을때 auto increment 로 id 가 60을 가져야하는 상황이라면 100은 버리고 60으로 저장된다는 의미이다. 이게 옳으냐마냐의 고민과는 별개로 hibernate 6.5.x 까지는 이렇게 동작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보건대 특별한 이유가 있지 않은 이상 존재하지 않는 id에 대해서 저장을 하는 경우는 없을 것이기에 웬만하면 이 변경을 눈치채지 못하고 그냥 넘어갈 것 같다. 하지만 혹시라도 버전을 올리면서 위 에러를 만난다면 알게모르게 어디선가 id를 갖고있는 상태로 저장을 하는건 아닌지 찾아서 수정하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 변경은 hibnernate 6.6.0 부터 반영되었으며, spring boot 기준 3.4.0 부터 hibernate 6.6.x 을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.jboss.org/hibernate/orm/6.6/migration-guide/migration-guide.html#merge-versioned-deleted&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.jboss.org/hibernate/orm/6.6/migration-guide/migration-guide.html#merge-versioned-deleted&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/jpa</category>
      <author>LichKing</author>
      <guid isPermaLink="true">https://multifrontgarden.tistory.com/319</guid>
      <comments>https://multifrontgarden.tistory.com/319#entry319comment</comments>
      <pubDate>Sat, 15 Feb 2025 12:50:57 +0900</pubDate>
    </item>
  </channel>
</rss>