<?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>Tue, 21 Jul 2026 14:41:01 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>LichKing</managingEditor>
    <item>
      <title>에러가 없는데 써킷브레이커가 열린다면(resilience4j)</title>
      <link>https://multifrontgarden.tistory.com/331</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;내가 처음 써킷브레이커 개념에 대해 접했던건 Netflix OSS 의 &lt;a href=&quot;https://github.com/netflix/hystrix&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;hystrix&lt;/a&gt; 를 통해서였다. 그때만 해도 생소했었는데 이제 써킷브레이커는 굉장히 친숙한 기술이 됐다. 그 사이 hystrix 는 저물고 &lt;a href=&quot;https://github.com/resilience4j/resilience4j&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;써킷브레이커의 개념은 신입 개발자를 채용할때도 어렵지 않게 대답을 들을 수 있다. 특정 루틴이 일정량 실패하면 해당 루틴을 실행하지 않고 즉시 실패했다가(OPEN), 이후 루틴을 다시 실행해보고(HALF-OPEN) 성공한다면 계속 실행하고(CLOSE) 실패한다면 계속 즉시실패(OPEN)하는 형태로 의존 루틴의 장애가 전파되지 않도록 차단하는 기술이다.&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_1784259159539&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;의존시스템의 에러응답 로그
의존시스템의 에러응답 로그
의존시스템의 에러응답 로그
... 설정량만큼 에러응답 발생
써킷브레이커가 열렸다는 로그
써킷브레이커가 열렸다는 로그
... 이후 복구&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 최근 에러로그 없이 써킷브레이커가 열리는 이슈가 있었고, 이를 정리해보려한다.&lt;/p&gt;
&lt;pre id=&quot;code_1784259261043&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;... 사전 에러로그 없음
써킷브레이커가 열렸다는 로그
써킷브레이커가 열렸다는 로그
... 이후 복구&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;pre id=&quot;code_1784260520935&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 써킷브레이커 설정
public CircuitBreakerConfig createCircuitBreakerConfig() {                         
    return CircuitBreakerConfig.custom()                                           
            // 써킷브레이커 상태변이를 계산하기 위한 최소 실행 횟수                                       
            .minimumNumberOfCalls(5)                                               
            .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) 
            // 써킷브레이커 조건을 계산하기 위해 결과를 저장하는 윈도우 크기                                  
            .slidingWindowSize(10)                                                 
            // 써킷브레이커가 열리기 위한 실패율                                                  
            .failureRateThreshold(50)                                              
            // 슬로우콜로 인지할 시간                                                        
            .slowCallDurationThreshold(Duration.ofMillis(200))                     
            // 써킷브레이커가 열리기 위한 슬로우콜 비율                                              
            .slowCallRateThreshold(50)                                             
            .build();                                                              
}                                                                                  

// 써킷브레이커 실행, 모든 호출은 실패하며 10번 반복
public void simulate(CircuitBreaker circuitBreaker) {            
    Runnable runnable = circuitBreaker.decorateRunnable(() -&amp;gt; {  
        // 보통 외부 API 호출
        if (true) {
          throw new RuntimeException(&quot;Failed!&quot;);
        }
        System.out.println(&quot;Succeed&quot;);
    });                                                          
                                                                 
    for(int i = 0; i &amp;lt; 10; i++) {                                
        try {                                                    
            runnable.run();                                      
        } catch (Exception e) {                                  
            System.out.println(&quot;Call failed: &quot; + e.getMessage());
        }                                                        
    }                                                            
}                          

// 실행코드
CircuitBreakerSimulator simulator = new CircuitBreakerSimulator();          
CircuitBreakerConfig config = simulator.createCircuitBreakerConfig();       
CircuitBreaker circuitBreaker = CircuitBreaker.of(&quot;circuitBreaker&quot;, config);
simulator.simulate(circuitBreaker);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 최소 5번 이상의 호출에서 실패율이 50% 이상이라면 써킷브레이커가 열리는 코드이다. 그리고 현재는 10번 호출해서 10번 모두 실패하게끔 되어있다. 실행결과는 이렇다.&lt;/p&gt;
&lt;pre id=&quot;code_1784260709663&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Call failed: Failed!
Call failed: Failed!
Call failed: Failed!
Call failed: Failed!
Call failed: Failed!
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10번 모두 실패하지만 최소 5번까지는 써킷브레이커가 열리지 않기 때문에 5번까지는 내부 RuntimeException 메시지가 출력된다. 그리고 6번째부터는 써킷브레이커가 열리게 된다. 내부 RuntimeException 으로 표현한 로직이 외부 API 호출이라면 해당 API 의 에러가 발생하게 될 것이다. 보통은 써킷브레이커가 열리는 시점엔 이런 유형의 로그가 남게되고, 써킷브레이크가 열리기 이전 에러로그를 보면서 써킷브레이커가 열리게된 원인을 파악하게 된다.&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;pre id=&quot;code_1784261132936&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Succeed
Succeed
Succeed
Succeed
Succeed
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이런 로그가 발생한다면? 분명히 다섯번째까지 잘 성공해놓고, 여섯번째부터 써킷브레이커가 열리고있다. 처음엔 어디선가 에러로그는 삼키고있는게 아닌가 하는 생각에 삽질을 좀 했었는데 이를 재현하기 위한 코드는 이렇다.&lt;/p&gt;
&lt;pre id=&quot;code_1784261332553&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Runnable runnable = circuitBreaker.decorateRunnable(() -&amp;gt; {
    // 보통 외부 API 호출                                        
    try {                                                  
        Thread.sleep(500);                                 
    } catch (InterruptedException e) {                     
        throw new RuntimeException(e);                     
    }                                                      
    System.out.println(&quot;Succeed&quot;);                         
});&lt;/code&gt;&lt;/pre&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;p data-ke-size=&quot;size16&quot;&gt;- 해당 루틴이 슬로우콜 설정 이상의 시간을 소요함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- resilience4j 는 슬로우콜을 중단시키거나 에러로그를 남기지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 슬로우콜은 계속 실행하면서 슬로우콜 카운트만 증가시킴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 슬로우콜 카운트가 설정에 도달하면 조용히 써킷브레이커 상태를 OPEN 으로 변경&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;지금과 같은 예제코드에서는 코드 변경이 확 눈에 띄어 문제를 찾기가 쉽지만 현실의 코드에서는 외부 API 를 호출하는 로직이 들어있는 경우가 많다. 예를들어 API 호출시 read timeout 은 10초를 걸어놓고, resilience4j 의 slowCallDurationThreshold 는 5초를 잡아놓은 상태에서 API 가 7초가 걸려 응답한다면 API 는 모두 성공하지만 어느순간 써킷브레이커가 열리게되고 원인을 찾아내기가 쉽지 않다. 참고로 slowCallDurationThreshold 속성의 기본값은 60000ms(1분)이다.&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;써킷브레이커가 열리는 조건을 설정할때 주로 보는 프로퍼티는 slidingWindowSize, minimumNumberOfCalls 가 있다. slidingWindowSize 는 써킷브레이커의 실행 결과를 저장하는 갯수의 크기이다. 100으로 지정하면 최근 100개의 결과를 저장하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;minimumNumberOfCalls 는 써킷브레이커의 상태변이를 위한 최소 실행 갯수이다. 해당 설정이 100으로 되어있으면 99번째 호출까지는 100% 실패하더라도 써킷브레이커가 열리지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의해야할 부분은 두 설정의 상관관계이다. minimumNumberOfCalls 가 slidingWindowSize 보다 클 경우 minimumNumberOfCalls 는 slidingWindowSize 와 동일하게 동작한다.&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;- minimumNumberOfCalls 100 slidingWindowSize 10&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설정일 경우 100번 호출되기 전까진 써킷브레이커가 열리지 않겠지 라고 생각하면 큰 오산이라는 의미다. 10번째 호출부터 써킷브레이커가 열릴 수 있다. 이런 설명이 문서에는 상당히 빈약하기 때문에 slidingWindowSize 는 작게 가져가고 minimumNumberOfCalls 를 크게 가져간다면 그 설정은 아무런 의미가 없게된다. 참고로 slidingWindowSize 는 기본값이 100 이다.&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;위 내용은 claude opus 4.8 모델도 잘못대답하고 있었고 정정 프롬프트를 넣기 전까지는 틀린 내용을 답해주고 있었기 때문에 AI 만 믿고 사용하다가도 큰코다칠 수 있으니 유의하자.&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;참고로 위 두 설정의 상관관계는 slidingWindowType 이 COUNT_BASED 일때 동작한다. TIME_BASED 일때는 상관없으므로 TIME_BASED 일땐 무시해도 된다.&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;&lt;a href=&quot;https://resilience4j.readme.io/docs/circuitbreaker&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://resilience4j.readme.io/docs/circuitbreaker&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1784263015496&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;CircuitBreaker&quot; data-og-description=&quot;Getting started with resilience4j-circuitbreaker&quot; data-og-host=&quot;resilience4j.readme.io&quot; data-og-source-url=&quot;https://resilience4j.readme.io/docs/circuitbreaker&quot; data-og-url=&quot;https://resilience4j.readme.io/docs/circuitbreaker&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cxUSug/dJMb9g5ntS0/nZ2YlRB8cgwK6KtKpqNXS1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/ba81Do/dJMb9fZHPjz/Rqp6OdazFGkBEXLKOG9YH0/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/biebam/dJMb9jgJUbN/V0ANUS3VObJ2dsxn3YsaBK/img.png?width=607&amp;amp;height=874&amp;amp;face=0_0_607_874&quot;&gt;&lt;a href=&quot;https://resilience4j.readme.io/docs/circuitbreaker&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://resilience4j.readme.io/docs/circuitbreaker&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cxUSug/dJMb9g5ntS0/nZ2YlRB8cgwK6KtKpqNXS1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/ba81Do/dJMb9fZHPjz/Rqp6OdazFGkBEXLKOG9YH0/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/biebam/dJMb9jgJUbN/V0ANUS3VObJ2dsxn3YsaBK/img.png?width=607&amp;amp;height=874&amp;amp;face=0_0_607_874');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;CircuitBreaker&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Getting started with resilience4j-circuitbreaker&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;resilience4j.readme.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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/331</guid>
      <comments>https://multifrontgarden.tistory.com/331#entry331comment</comments>
      <pubDate>Fri, 17 Jul 2026 13:27:14 +0900</pubDate>
    </item>
    <item>
      <title>HTTP QUERY 메서드</title>
      <link>https://multifrontgarden.tistory.com/330</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;조회 API 를 만들어야한다면 어떤 메서드를 써야할까? 다들 몇초 걸리지 않아 GET 이라고 답할 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1782539478026&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /members/928&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 리소스 id 로 접근하는 경우 GET 으로 만드는데 아무런 어려움이 없다. 리소스 id 가 아니라 다른 조건으로 조회를 해야한다면 어떻게 할 수 있을까?&lt;/p&gt;
&lt;pre id=&quot;code_1782539928465&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /members?name={name}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 query string 을 사용해 표현할 수 있다. 보통은 이정도 수준으로 조회 API 를 설계한다. 하지만 조회 조건이 복잡해지는 경우 query string 을 써야할지 고민이 되는 경우가 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1782541045937&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /members?param1={param1}&amp;amp;param2={param2}&amp;amp;param3={param3}&amp;amp;param4={param4}&amp;amp;param5={param5}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 url 이 지저분해지다보니 뭔가 직관적으로 마음에 들지 않고, 다른 방안을 고민해보게 된다.&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;# POST + body&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇가지 방안이 있겠지만 HTTP 메서드를 POST 로 변경해서 request body 를 사용하는 방법이다. 이렇게하면 깔끔한 url 을 유지할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1782541341383&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /members
Content-Type: application/json

{
  &quot;param1&quot;: {param1},
  &quot;param2&quot;: {param2},
  &quot;param3&quot;: {param3},
  &quot;param4&quot;: {param4},
  &quot;param5&quot;: {param5}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 깔끔한 url 을 유지할 수 있지만 몇가지 단점이 있다.&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;내 조회결과를 상대방한테 전달할때 조회조건을 같이 전달할 수 없다. 네이버나 구글에서 검색한뒤 해당 결과를 공유하고 싶을때 url 을 전달해봤자 상대방은 네이버나 구글의 메인페이지를 보게된다.&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 cache 불가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 스펙엔 cache 스펙이 있다. cache 스펙에 따르면 cache key 에&amp;nbsp; request body 는 필수포함이 아니기 때문에 cache 를 적극적으로 활용하기 어렵다. request body 가 달라져도 동일한 요청으로 간주하기 때문이다.&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;- API 의도 불명확&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST 는 내부 상태를 변경할때 사용한다. 단순 조회인 경우 내부 상태를 변경하지 않고, 그렇기에 멱등성을 지키지만 POST 로 구현되어 있다면 이런 내용을 스펙으로 드러낼 수 없다.&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;# GET + body&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1782542380287&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /members
Content-Type: application/json

{
  &quot;param1&quot;: {param1},
  &quot;param2&quot;: {param2},
  &quot;param3&quot;: {param3},
  &quot;param4&quot;: {param4},
  &quot;param5&quot;: {param5}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 request body 사용하면 되지 메서드를 변경할 필요있나? GET 에다가 request body 를 사용하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적인 생각에 이 방식은 상당한 테토방식인데, 이 방식은 POST + body 에서 설명했던 단점 중 API 의도 불명확을 해소할 수 있다. GET 메서드이기 때문에 조회전용임을 드러낼 수 있기 때문이다. 다만 나머지 2가지 단점은 여전히 해결할 수 없다.&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;이 방식은 추가적으로 다른 문제가 한가지 있는데 RFC 스펙에서 GET + body 를 권장하지 않는다는 것이다( &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9110.html#name-get&quot;&gt;https://www.rfc-editor.org/rfc/rfc9110.html#name-get&lt;/a&gt; ). 해당 스펙에서 클라이언트는 GET 요청시 body 를 추가하지 않는걸 권장한다고 한다. 예외적으로 서버가 GET body 를 허용한다고 정의했을때만 추가할 수 있고, 이 경우에도 클라이언트와 서버가 직접 통신하는 경우에만 사용할 수 있다고 정의한다. 왜냐하면 직접 통신이 아니라 중간에 다른 홉들이 있게되면 해당 홉에서 뒷단으로 body 전달을 보장하지 않기 때문이다. 일반적인 api 를 만들땐 GET 에 body 를 고려하지 말라는 의미다.&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;왜 스펙을 이리 애매하게 정의했을까를 고민해보면 이미 GET 요청에서 body 를 처리하는 서버들이 있기 때문에 하위호환을 위해 어쩔 수 없이 허용한게 아닐까 싶다.&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;# GET + query string&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굳이 이렇게 단점들을 감수하고 body 를 쓰려고 할 필요있나? 그냥 기존대로 GET 을 쓰는 방안이다. url 이 길어진다고? 좀 길어질수도 있지 그게 쓰지 못할 이유가 되지 않는다.&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;간혹 파라미터가 너무 길면 길이제한에 걸린다고 주장하는 경우가 있으나 실제로 이로인해 문제가 생기는 경우는 거의 없다. 톰캣이나 nginx 를 비롯한 많은 서버는 기본적으로 8KB 를 지원하고 있기 때문에 조회 조건 좀 추가한다고 쉽게 넘기기 어렵기 때문이다. 다만 8KB 제한은 url 에 한해서만 제한하는게 아니라 헤더를 포함한다. 그래서 간혹 쿠키 관리를 잘못할 경우 이 제한에 걸리는 경우를 본적이 있다. 그럴땐 쿠키 관리 로직을 확인하거나, 8KB 제한을 늘려서 해결할 수 있다. 이 경우에도 일반적인 조회 조건 갯수가 이슈가 되는 경우는 본적 없다. 이 방안으로 갈 경우 위에서 언급한 모든 단점들이 싹 사라진다.&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;이 방안의 문제는 두가지 정도가 있는데, 첫번째는 조회 조건에 민감한 정보들이 포함되는 경우다. 민감 정보가 문제라면 TLS 를 사용하여 데이터를 보호할 일이지 민감한 정보들을 query string 이 아니라 body 에 넣는다고 안전해지는게 있냐고 반박할 수 있다. 맞는 말이다. 하지만 보통 기업에서 많은 사용자들을 처리할때 요청 데이터는 다양한 서버를 통해 실제 처리 서버로 도달한다. 그리고 일정 수준 이후부터는 평문화된 요청 메시지를 전달한다. 그러면 웹서버들은 액세스 로그를 남기게 되는데, 이때 query string 은 url 로 보기 때문에 모든 정보가 로그로 남게된다. 이 경우 민감 정보가 로그로 남는 문제가 발생하는 것이다. 참고로 body 는 로그를 남기지 않기 때문에 문제가 없다. 하지만 또 의문을 갖는 분이 있을 수 있다. body 에 넘어오더라도 그걸 로그로 남기는건 어렵지 않기 때문이다. 실제로 현업에선 로그 추적을 위해 request, response body 를 로그로 남기는건 흔한 일이다. 이 이슈는 &lt;i&gt;&lt;u&gt;기본 정책이 무엇이냐&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;두번째는 파라미터들의 그룹화 문제다. 조회 조건에는 이런 조건이 있을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1784255925985&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[{
  &quot;code&quot;: &quot;superA&quot;,
  &quot;ids&quot;: [1, 2, 3]
},{
  &quot;code&quot;: &quot;superB&quot;,
  &quot;ids&quot;: [1, 2, 3]
}]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 1, 2, 3 이라는 id 이지만 그보다 상위 개념인 code 프로퍼티를 통해 각각 다르게 식별할 수 있는 경우다. json 으로 표현하면 어려울게 하나도 없지만 이를 query string 으로 표현하기는 쉽지 않다. query string 은 기본적으로 계층구조를 지원하지 않기 때문이다. 이런 경우가 발생하면 조회 조건을 request body 로 넘기는 것에 대한 고려를 시작하게 된다.&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;# QUERY&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 모두 해소하기 위해 신규 HTTP 메서드가 추가됐다. 바로 QUERY 메서드이다. 2026년 6월에 발표된 따끈따끈한 스펙이다. QUERY 메서드는 공식적으로 조회 조건을 body 에 첨부할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1782624185490&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;QUERY /members
Content-Type: application/json

{
  &quot;param1&quot;: {param1},
  &quot;param2&quot;: {param2},
  &quot;param3&quot;: {param3},
  &quot;param4&quot;: {param4},
  &quot;param5&quot;: {param5}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 얘기했던 조회 조건을 body 에 넣는 경우 단점에 대해 알아보자.&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 단점은 QUERY 를 사용해도 여전하다. 이 때문에 조회 조건 공유가 꼭 필요하다거나 민감한 정보가 아니라면 가급적 GET 을 쓰는게 좋다.&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;- HTTP cache 불가&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;QUERY 메서드의 요청은 request body 까지 포함해서 cache key 로 활용하는게 스펙이다. 이 때문에 body 가 달라지는 경우에 대한 cache 가 가능하다. 물론 이를 구현하기 위해 cache 구현시 request body 까지 읽어야하는 비용이 추가됐다.&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;- API 의도 불명확&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;QUERY 는 그 자체로 조회를 나타내며, 이 때문에 안전하고 멱등성 제공을 드러낼 수 있다.&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;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;민감 정보를 body 에 담을 경우 액세스 로그 등에 남지 않으므로 좀 더 편안하게 사용할 수 있다.&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;QUERY 를 사용하더라도 조회 조건 공유는 할 수 없다. 이 때문에 조회 조건 공유가 필요하다면 GET 을 사용하는게 좋다. 다만 QUERY 메서드가 신규 스펙이기 때문에 현실에서 사용하기엔 제약이 있을 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;조회 api 설계시 일단은 GET 으로 구현하는걸 1순위로 생각하고, 민감 정보 포함과 같은 문제가 발생시 POST + request body 를 고려하면 좋다. 이후 QUERY 메서드를 지원하는 환경이라면 QUERY 를 도입하는게 좋을 것이다. GET+ request body 의 경우 스펙도 명확하지 않고, 서버에서 지원하지 않을 수 있기 때문에 이 방식은 권장하지 않는다.&lt;/p&gt;</description>
      <category>Network</category>
      <author>LichKing</author>
      <guid isPermaLink="true">https://multifrontgarden.tistory.com/330</guid>
      <comments>https://multifrontgarden.tistory.com/330#entry330comment</comments>
      <pubDate>Sun, 28 Jun 2026 14:33:07 +0900</pubDate>
    </item>
    <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>
  </channel>
</rss>