<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>장똥구리의 돈 굴리기</title>
    <link>https://jangddonguri.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 14 Apr 2026 14:42:19 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>장똥구리</managingEditor>
    <image>
      <title>장똥구리의 돈 굴리기</title>
      <url>https://tistory1.daumcdn.net/tistory/6872196/attach/fd7ee2dc3b434324801fc4fb775a260f</url>
      <link>https://jangddonguri.tistory.com</link>
    </image>
    <item>
      <title>Spring WebClient 적용기</title>
      <link>https://jangddonguri.tistory.com/9</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Situation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 피파온라인 전적검색 프로젝트에서 사용자의 전적을 갱신해야하는 기능이 있다. 이 기능은 사용자의 정보를 갱신하고, 사용자의 Match 리스트를 불러온 뒤, 리스트에 포함된 각각의 Match 상세 정보를 Nexon API에 요청해야하는 작업이 필요했다. 이 과정에서 평균적으로 약 200번의 API 요청이 필요했고, 이를 처리하기 위해 @Async 와 ThreadPoolTaskExectuor를 사용하여 RestTempalte을 통해 비동기적으로 요청을 하는 로직을 구현했다. 기존에 동기적인 방식에 싱글스레드는 약 100s가 걸렸지만, 해당 비동기 로직으로 변경한 후 15s 로 줄일 수 있었다. 하지만, 이것도 매우 느렸다. 왜 이렇게 처리 속도가 늦을까 생각을하다가, 2가지 고민을 하게되었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;200개의 API 요청 시 각각 Tcp/Ip 연결을 매번 생성하는게 아닐까? keep - alive 설정이 안되어있는가?&lt;/li&gt;
&lt;li&gt;RestTemplate은 동기적 I/O 작업이기에 느린걸까?&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번의 경우는 아니었다. 설정이 되어있음에도 불구하고 느린것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번의 경우, RestTemplate을 사용하여 API 요청을 처리할 때, 각 요청은 이전 요청이 완료될 때까지 대기해아 하며, 이는 다수의 요청을 동시에 처리하는 경우 성능 저하의 주요 원인이었다. 요청 처리 과정에서 I/O 작업이 블로킹 되어, CPU 리소스를 효율적으로 활용하지 못하고, 네트워크 지연 시간 동안에도 스레드가 대기 상태에 머무르게 된다. 따라서 이 문제를 해결하기 위해서는 Non-Blocking 방식을 선택해야 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RestTemplate 구조&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://assu10.github.io/assets/img/dev/2023/0923/rest-template.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HttpMessageConverter&lt;/code&gt; 는 HTTP 바디 메시지를 자바 객체로 변환하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST-API 서버와 커넥션을 맺고 요청 메시지를 전달하며 응답 메시지를 받아오는 모든 네트워킹 과정을 처리하는 것은 &lt;code&gt;ClientHttpRquestFactory&lt;/code&gt; 가 담당한다. 해당 인터페이스의 구현체는 아래와 같고, 스프링부트는 기본으로 &lt;code&gt;SimpleClientHttpRequestFactory&lt;/code&gt; 를 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;OkHttp3ClientHttpRequestFactory&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드에서 많이 사용하는 OkHttp 로 작성한 구현체&lt;/li&gt;
&lt;li&gt;동기식/비동기식 모두 가능한 장점이 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;Netty4ClientHttpRequestFactory&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 논블로킹 프레임워크인 Netty 를 사용하여 작성된 구현체&lt;/li&gt;
&lt;li&gt;비동기 프로그래밍을 계획한다면 가장 먼저 고려하는 것이 좋음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;SimpleClientHttpRequestFactory&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동기식으로 동작하며, JDK 에서 제공하는 라이브러리를 사용하여 작성된 구현체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;HttpComponentsClientHttpRequestFactory&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동기식으로 동작하며, JDK 에서 제공하는 라이브러리를 사용하여 작성된 구현체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8UepW/btsGWb7zIem/YYlfTDOv05TVeiSwNgIfS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8UepW/btsGWb7zIem/YYlfTDOv05TVeiSwNgIfS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8UepW/btsGWb7zIem/YYlfTDOv05TVeiSwNgIfS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8UepW%2FbtsGWb7zIem%2FYYlfTDOv05TVeiSwNgIfS0%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;1316&quot; height=&quot;460&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Task&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Blocking I/O 작업을 하는 로직을 Non-Blocking I/O 작업으로 변경해야한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Action&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 RestTemplate 대신 비동기적이고 non-blocking I/O 작업을 지원하는 WebClient를 사용하는 것이 바람직하다. WebClient는Spring 5에서 도입되었으며, 비동기적으로 작동하고, Reactive Programming을 지원하여, 더 나은 성능과 리소스 활용 효율성을 제공한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebClient&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ec9eow/btr01U5wfIQ/1M9goKtw9a0kLG0BDKhGCk/img.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Single Thread + Non-Blocking 방식이다.&lt;/li&gt;
&lt;li&gt;Core 당 1개의 Thread 이용&lt;/li&gt;
&lt;li&gt;각 요청은 Event Loop 내에 Job으로 등록된다.&lt;/li&gt;
&lt;li&gt;Event Loop는 각 Job을 제공자에게 요청한 후, 결과를 기다리지 않고 다른 Job을 처리한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 외부 API 요청이 많은 서비스에 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Code&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YYw1T/btsGWL1QCf5/1ENaP4zfQN19rqbUhdIFQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YYw1T/btsGWL1QCf5/1ENaP4zfQN19rqbUhdIFQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YYw1T/btsGWL1QCf5/1ENaP4zfQN19rqbUhdIFQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYYw1T%2FbtsGWL1QCf5%2F1ENaP4zfQN19rqbUhdIFQ0%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;1670&quot; height=&quot;668&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;668&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;1054&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgpXHF/btsGXQVvAcm/MlrXOMzYYDoNR9VHyFYp2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgpXHF/btsGXQVvAcm/MlrXOMzYYDoNR9VHyFYp2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgpXHF/btsGXQVvAcm/MlrXOMzYYDoNR9VHyFYp2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgpXHF%2FbtsGXQVvAcm%2FMlrXOMzYYDoNR9VHyFYp2k%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;1528&quot; height=&quot;1054&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;1054&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;1046&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBVoac/btsGWat4KKy/erC5e25GGacZxDanYCm5W1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBVoac/btsGWat4KKy/erC5e25GGacZxDanYCm5W1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBVoac/btsGWat4KKy/erC5e25GGacZxDanYCm5W1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBVoac%2FbtsGWat4KKy%2FerC5e25GGacZxDanYCm5W1%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;1352&quot; height=&quot;1046&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;1046&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebClientConfig 클래스에서 웹 클라이언트의 설정을 분리하여 관리함으로써, 나중에 설정을 변경하거나 추가할 때 코드의 다른 부분을 변경하지 않고 관리할 수 있도록 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 refresh가 수행되는 서비스에서는&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자의 ouid(사용자 고유 식별자) 를 동기적으로 가져오고 (block() 메소드를 통해)&lt;/li&gt;
&lt;li&gt;ouid 를 이용하여 사용자의 정보(Level 등)을 비동기로 가져온다.&lt;/li&gt;
&lt;li&gt;ouid 를 이용하여 사용자의 Match List를 가져온다.&lt;/li&gt;
&lt;li&gt;Match List를 이용하여 모든 Match 세부 정보를 비동기적으로 가져온다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Match List 는 String[] 이며 Match 에 대한 고유ID값이 들어가있다.&lt;/li&gt;
&lt;li&gt;Match 고유 ID 값을 통해 Match의 세부 정보를 비동기적으로 요청한다. Non-Blocking&lt;/li&gt;
&lt;li&gt;Match 고유 ID 값을 통해 해당 Match 에 참여하는 사용자들의 티어를 비동기적으로 요청한다. Non-Blocking.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;모든 요청에 대한 응답을 받게되면 DB에 저장을한다.&lt;/li&gt;
&lt;li&gt;2번에서 요청한 응답값을 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Result&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비교&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;RestTemplate - 동기&lt;/li&gt;
&lt;li&gt;RestTmplate + @Aync , ThreadPool - 비동기 --- 현재&lt;/li&gt;
&lt;li&gt;WebClient - Non-Blokcing &amp;amp; 비동기&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RestTemplate - 동기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brUPgP/btsGXqW3tjR/Mvj9KVWjCI8aTa4xkvmRQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brUPgP/btsGXqW3tjR/Mvj9KVWjCI8aTa4xkvmRQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brUPgP/btsGXqW3tjR/Mvj9KVWjCI8aTa4xkvmRQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrUPgP%2FbtsGXqW3tjR%2FMvj9KVWjCI8aTa4xkvmRQ1%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;2100&quot; height=&quot;632&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RestTemplate + @Async , ThreadPool&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2066&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddbQvM/btsGXUcxaTQ/D0aHGJbmkwWLyIsNYULUGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddbQvM/btsGXUcxaTQ/D0aHGJbmkwWLyIsNYULUGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddbQvM/btsGXUcxaTQ/D0aHGJbmkwWLyIsNYULUGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddbQvM%2FbtsGXUcxaTQ%2FD0aHGJbmkwWLyIsNYULUGk%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;2066&quot; height=&quot;850&quot; data-origin-width=&quot;2066&quot; data-origin-height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebClient - Non-Blocking &amp;amp; 비동기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1974&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlegWa/btsGWr3sJ1c/0eBR0dHlXAQap6PTPyFnIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlegWa/btsGWr3sJ1c/0eBR0dHlXAQap6PTPyFnIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlegWa/btsGWr3sJ1c/0eBR0dHlXAQap6PTPyFnIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlegWa%2FbtsGWr3sJ1c%2F0eBR0dHlXAQap6PTPyFnIk%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;1974&quot; height=&quot;642&quot; data-origin-width=&quot;1974&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadPoolTaskExecutor와 @Async의 사용으로 데이터 처리 시간을 기존 100초에서 15초로 단축했고, 이후 WebClient 도입으로 15초에서 2.8초로 줄여, 총 처리 시간을 약 97.2% 단축할 수 있게되었다.&lt;/p&gt;</description>
      <category>개발 이야기/Spring</category>
      <category>@Async</category>
      <category>Non-Blocking</category>
      <category>RestTemplate</category>
      <category>Spring WebClient</category>
      <category>ThreadPoolTaskExecutor</category>
      <author>장똥구리</author>
      <guid isPermaLink="true">https://jangddonguri.tistory.com/9</guid>
      <comments>https://jangddonguri.tistory.com/9#entry9comment</comments>
      <pubDate>Fri, 26 Apr 2024 01:33:41 +0900</pubDate>
    </item>
    <item>
      <title>CloudPlatform Integrity Check</title>
      <link>https://jangddonguri.tistory.com/8</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️배경&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;861&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8GNqQ/btsGe9BVrlu/sCiZzSg3l7khuMNYrhhuQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8GNqQ/btsGe9BVrlu/sCiZzSg3l7khuMNYrhhuQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8GNqQ/btsGe9BVrlu/sCiZzSg3l7khuMNYrhhuQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8GNqQ%2FbtsGe9BVrlu%2FsCiZzSg3l7khuMNYrhhuQ1%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;794&quot; height=&quot;861&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;861&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 CloudPlatform Service를 만들고있는데, 주요 서버는 두개가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;User Data Control Server (Spring Boot)&lt;/li&gt;
&lt;li&gt;System Operation Management Server (Go Lang)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client -&amp;gt; UDCS (Proxy Server) -&amp;gt; SOMS 형식으로 통신하게된다.&lt;br /&gt;UDCS 서버에서는 로그인 및 회원가입과 같은 인증 및 인가, 그리고 로깅을 담당한다.&lt;br /&gt;즉 Client가 아래 처럼 request와 accessToekn이 포함된 헤더가 있다면 이 정보를 이용하여 SOMS 서버에 똑같이 요청을한다(Proxy 역할)&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{ 
&quot;dest&quot;: &quot;/vm/create&quot;,
&quot;method&quot;: &quot;POST&quot;,
&quot;data&quot;: &quot;{ blah blah }&quot; 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 형식으로 SOMS에 요청하게 되고, 아래와 같은 로그를 남긴다. 요청하기전에 ENROLL ,응답이오면 END 로그를 생성하게된다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;    private void logEnroll(RequestDto requestDto, String transactionId,String remoteAddr) {
        log.info(&quot;Type = {}, Dest = {}, Method = {}, Data = {}, Time = {}, IP = {}, Transaction_Id = {}&quot;,
                TransactionType.ENROLL,
                requestDto.getDest(),
                requestDto.getMethod(),
                requestDto.getData(),
                LocalDateTime.now(),
                remoteAddr,
                transactionId);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;    private void logEnd(String targetTransactionId,String transactionId) {
        log.info(&quot;Type = {}, Target_Transaction_Id = {}, Time = {}, Transaction_Id = {}&quot;,
                TransactionType.END,
                targetTransactionId,
                LocalDateTime.now(),
                transactionId);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 SOMS와 통신중 비정상적으로 스프링 서버(UDCS)가 종료되었을 때는 어떻게해야할까?&lt;br /&gt;예를들어, 클라이언트가 vm create 를 요청하였으며 SOMS에 해당 요청까지 전달되었는데 스프링 서버가 비정상적으로 종료되었다면 응답과 상관없이 END 로그가 찍히지 않을 것이다.&lt;br /&gt;정상 FLOW : ENROLL &amp;rarr; SOMS Forwarding &amp;rarr; 응답 &amp;rarr; END&lt;br /&gt;Failed Case&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ENROLL &amp;rarr; SOMS Forwarding &amp;rarr; 작업 정상 수행 &amp;rarr; 스프링 서버 비정상적인 종료 &amp;rarr; (NOT END LOG)&lt;/li&gt;
&lt;li&gt;ENROLL &amp;rarr; SOMS Forwarding &amp;rarr; 작업 비정상 수행 &amp;rarr; 스프링 서버 비정상적인 종료 &amp;rarr; (NOT END LOG)&lt;/li&gt;
&lt;li&gt;ENROLL &amp;rarr; SOMS Forwarding &amp;rarr; 스프링 서버 비정상적인 종료 &amp;rarr; (NOT END LOG)&lt;/li&gt;
&lt;li&gt;ENROLL &amp;rarr; 스프링 서버 비정상적인 종료 &amp;rarr; (NOT END LOG, NOT forwarding SOMS)&lt;br /&gt;따라서 이 경우의 Integrity Check가 필요하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️해결방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현재 환경&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVKrh3/btsGdRh6a2b/EseaYhHJBOhxi0TunfNhK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVKrh3/btsGdRh6a2b/EseaYhHJBOhxi0TunfNhK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVKrh3/btsGdRh6a2b/EseaYhHJBOhxi0TunfNhK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVKrh3%2FbtsGdRh6a2b%2FEseaYhHJBOhxi0TunfNhK0%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;632&quot; height=&quot;358&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;155&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cY28Zs/btsGgATeKGj/wnKYuRHyUBCT2qGw3aB1iK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cY28Zs/btsGgATeKGj/wnKYuRHyUBCT2qGw3aB1iK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cY28Zs/btsGgATeKGj/wnKYuRHyUBCT2qGw3aB1iK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcY28Zs%2FbtsGgATeKGj%2FwnKYuRHyUBCT2qGw3aB1iK%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;748&quot; height=&quot;155&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;155&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ELK 를 통해 로그정보를 확인하고있으며 서버 내에 file로도 저장하고있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링이 재시작될 때 Integrity Check&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt; @PostConstruct
    public void integrityCheck(){

    1. 최신 로그를 찾는다.
    2. 뒤에서부터 END가 나오지않았는데 ENROLL 만 있는 경우를 탐색한다.
    3. 해당 ENROLL LOG를 보고 작업을 재수행한다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 시작 시 최근 로그를 조회하고, ENROLL만 있는 로그를 찾는다. 해당 ENROLL 로그를 기반으로 작업을 다시 수행하고, 작업의 상태에 따라 로그를 기록한다. 이를 통해 스프링 서버가 비정상적으로 종료되었을 때의 Integrity Check를 수행할 수 있다. 시스템의 안정성과 일관성을 유지할 수 있게된다.&lt;/p&gt;</description>
      <category>프로젝트/CloudPlatform</category>
      <category>Integrity Check</category>
      <author>장똥구리</author>
      <guid isPermaLink="true">https://jangddonguri.tistory.com/8</guid>
      <comments>https://jangddonguri.tistory.com/8#entry8comment</comments>
      <pubDate>Sun, 31 Mar 2024 19:58:12 +0900</pubDate>
    </item>
    <item>
      <title>Spring paging count query 조건</title>
      <link>https://jangddonguri.tistory.com/7</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Code&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 코드가 아닌 예시 코드입니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Table(name = &quot;community_post&quot;)
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {
​
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String subject;
    private String content;
}
postRepository.findAll(PageRequest.of(0, 10));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Post(게시물) 을 Pagenation 사용하여 데이터를 조회하는 중에 데이터가 9개 이하일 때는 하나의 쿼리가 나가지만 10개 이상일 경우에는 쿼리가 두개나가는 현상이 발생하였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비교&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터가 9개 이하인 경우&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FRBj3/btsFQVRfAXB/8ut5wlvqkHJdoMx1Dn8i81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FRBj3/btsFQVRfAXB/8ut5wlvqkHJdoMx1Dn8i81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FRBj3/btsFQVRfAXB/8ut5wlvqkHJdoMx1Dn8i81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFRBj3%2FbtsFQVRfAXB%2F8ut5wlvqkHJdoMx1Dn8i81%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;470&quot; height=&quot;404&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터가 10개 이상인 경우&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/scPuH/btsFQhmToEf/nPJjQkrmC8mLGg4n9vsFl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/scPuH/btsFQhmToEf/nPJjQkrmC8mLGg4n9vsFl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/scPuH/btsFQhmToEf/nPJjQkrmC8mLGg4n9vsFl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FscPuH%2FbtsFQhmToEf%2FnPJjQkrmC8mLGg4n9vsFl1%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;500&quot; height=&quot;620&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 크기가 전체 데이터 개수보다 큰 경우, 페이지네이션에 대한 정보를 얻기 위해 count query를 실행하는 것은 불필요하다. 먼저 데이터를 가져온 후 영속성 컨텍스트에서 페이지 size가 전체 데이터 개수보다 적은 경우에는 count query를 실행한다. 페이징 처리를 할 때 count query가 있어야 전체 페이지를 구할 수 있어서 필요하다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;❗️마지막 페이지인 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 페이지의 경우에도 count query를 실행하지 않는다. 동일한 이유이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 Paging 코드&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-03-29 오전 12.11.55.png&quot; data-origin-width=&quot;2778&quot; data-origin-height=&quot;1610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cg5f5b/btsGbY1nbbr/Ju4zVuk6KDIwj2Wyjw39j1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cg5f5b/btsGbY1nbbr/Ju4zVuk6KDIwj2Wyjw39j1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cg5f5b/btsGbY1nbbr/Ju4zVuk6KDIwj2Wyjw39j1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcg5f5b%2FbtsGbY1nbbr%2FJu4zVuk6KDIwj2Wyjw39j1%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;2778&quot; height=&quot;1610&quot; data-filename=&quot;스크린샷 2024-03-29 오전 12.11.55.png&quot; data-origin-width=&quot;2778&quot; data-origin-height=&quot;1610&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Count Query가 발생하는 지에 대한 여부는 중요하다. 왜냐하면 Count Query는 full table scan 이기 때문이다. 그런데 Spring JPA 에서 알아서 최적화를 해준다... 테스트 중에 쿼리 수가 예상과 달라 알아보았는데, paging 원리에 대해서 알게되어서 유익했던 것같다. 다만 join 되어있을 시 고려해야할 부분이 많이 있다. 이는 나중에 포스팅할 것이다.&lt;/p&gt;</description>
      <category>개발 이야기/Spring</category>
      <category>Count Query</category>
      <category>Spring paging</category>
      <author>장똥구리</author>
      <guid isPermaLink="true">https://jangddonguri.tistory.com/7</guid>
      <comments>https://jangddonguri.tistory.com/7#entry7comment</comments>
      <pubDate>Fri, 29 Mar 2024 00:33:15 +0900</pubDate>
    </item>
    <item>
      <title>FOSS 프로젝트 DB 저장 비용 vs API 요청</title>
      <link>https://jangddonguri.tistory.com/6</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 피파 전적검색 앱 프로젝트를 진행중이다. 사용자가 전적갱신을 하게되면 다음과 같은 로직으로 진행하게된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;닉네임을 통한 고유Id 가져오기.&lt;/li&gt;
&lt;li&gt;고유Id를 통해 해당 유저의 매치 List 가져오기.&lt;/li&gt;
&lt;li&gt;매치 List를 돌면서 하나씩 요청하여 상세 정보 가져오기. 1번과 2번 로직은 문제가 없다. 하지만 3번에서 문제가 있는데, Nexon 에서 제공해주는 피파 API 에서 가끔 쓰레기 데이터까지 가져온다. 예를들어 상대방이 없다거나 매치결과가 승무패가 아닌 ERROR 인 경우가 있다. 이 때문에 유효성 검사를한 후 2가지 방법으로 나뉜다.&lt;/li&gt;
&lt;li&gt;유효하지 않은 매치정보일 경우에 validation 값을 false로 설정하고 db에 저장한다.&lt;/li&gt;
&lt;li&gt;유효하지 않은 매치정보일 경우에 db에 저장하지 않는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2가지 방법을 각각 비교해보자.&lt;br /&gt;일단 용어 정의를 하고 넘어가겠다. 매치정보 리스트를 조회한다 == 매치정보 리스트 N개의 고유ID 들을 가져온다(API 요청 1번) , N개의 고유ID를 통해 상세 정보 조회 (API 요청 N번)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️Validation = false , DB 저장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 유저의 매치정보 리스트를 조회하면 많게는 100개중 50개가 더미 데이터이다. DB에 저장했기 때문에 한번 더 매치정보 리스트를 조회하게 되면 API 요청을 하는 것이 , DB에서 가져온다. 따라서 API 요청에 의한 비용은 발생하지 않는다. 하지만, DB에 저장하기 때문에 DB용량이 늘어나게 되고 사용자가 많다면 결국 Nexon API DB와 다를게 없을 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️DB에 저장 X&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 데이터를 DB에 저장하지 않는다. 따라서 시스템 자원과 용량을 절약할 수 있다. 하지만 사용자가 처음에 매치정보 리스트를 조회 했는데 100개중에 50개가 더미 데이터일 경우, 다음 매치정보 리스트에는 API요청을 50번이나 할 것이다. 즉, 재요청 하는 횟수가 많아진다. 이는 추가적인 네트워크 부하를 초래할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 1번째 방법을 선택하고 있다. DB에 저장하는 것이 API 재요청하는 것보다 훨씬 빠르므로 사용자가 더 나은 서비스를 경험할 수 있기 때문이다. 또한,API 요청이 현재 ThreadPoolTaskExecutor 에 의해 관리되고 있는데 maxPoolSize 를 넘어가게 되면 thread 는 queue에 들어가게 되고 더 많아 진다면 queue의 크기를 넘어가게 되고 예외를 발생시킬 것이다.&lt;/p&gt;</description>
      <category>프로젝트</category>
      <author>장똥구리</author>
      <guid isPermaLink="true">https://jangddonguri.tistory.com/6</guid>
      <comments>https://jangddonguri.tistory.com/6#entry6comment</comments>
      <pubDate>Fri, 15 Mar 2024 17:53:35 +0900</pubDate>
    </item>
    <item>
      <title>ResponseEntityExceptionHandler 사용하는 이유</title>
      <link>https://jangddonguri.tistory.com/5</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 @ControllerAdvice를 통해 custom 한 ErrorResponse 를 응답하고 있다. 그런데 Spring MVC Exception 은 내가 정의한 custom 한 ErrorResponse 를 응답하지 않는다. API 일관성을 지키기 위해서는 Spring MVC Exception 또한 내 custom한 ErrorResponse 를 응답해야한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️ResponseEntityExceptionHandler가 없는 경우&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1830&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R3fPl/btsFM85eYaj/nXCElbq7fWJhRgnson2nE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R3fPl/btsFM85eYaj/nXCElbq7fWJhRgnson2nE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R3fPl/btsFM85eYaj/nXCElbq7fWJhRgnson2nE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR3fPl%2FbtsFM85eYaj%2FnXCElbq7fWJhRgnson2nE0%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;784&quot; height=&quot;207&quot; data-origin-width=&quot;1830&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 ResponseEntityExceptionHandler가 처리하는 예외들이다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1762&quot; data-origin-height=&quot;1064&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be4x9B/btsFM85ffrF/u0uxarSBYadjQgtwZlOSE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be4x9B/btsFM85ffrF/u0uxarSBYadjQgtwZlOSE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be4x9B/btsFM85ffrF/u0uxarSBYadjQgtwZlOSE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe4x9B%2FbtsFM85ffrF%2Fu0uxarSBYadjQgtwZlOSE1%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;756&quot; height=&quot;457&quot; data-origin-width=&quot;1762&quot; data-origin-height=&quot;1064&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1684&quot; data-origin-height=&quot;1358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6Pmrm/btsFNuAfQic/ba6cxspeWLj6YBPG5KdbGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6Pmrm/btsFNuAfQic/ba6cxspeWLj6YBPG5KdbGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6Pmrm/btsFNuAfQic/ba6cxspeWLj6YBPG5KdbGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6Pmrm%2FbtsFNuAfQic%2Fba6cxspeWLj6YBPG5KdbGk%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;694&quot; height=&quot;560&quot; data-origin-width=&quot;1684&quot; data-origin-height=&quot;1358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;기본적인 응답이 나오게 된다. 이러면 다른 Exception 들은 내가 커스터마이징한 응답 메세지가 나오지만, Spring MVC Exception은 그러지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️ResponseEntityExceptionHandler 사용하는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매개 변수가 누락된 경우 예외 처리를 하려면 그에 해당하는 함수를 오버라이딩 해서 재정의 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 Exception에 대해 예외처리를 하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1922&quot; data-origin-height=&quot;690&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G6SDP/btsFMDq8ZTD/yalXrrxg829qjK6o05nTfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G6SDP/btsFMDq8ZTD/yalXrrxg829qjK6o05nTfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G6SDP/btsFMDq8ZTD/yalXrrxg829qjK6o05nTfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG6SDP%2FbtsFMDq8ZTD%2FyalXrrxg829qjK6o05nTfK%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;1922&quot; height=&quot;690&quot; data-origin-width=&quot;1922&quot; data-origin-height=&quot;690&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;똑같이 매개변수가 필요한 HTTP 요청 시에 요청 매개 변수를 누락시키고 보내보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;1326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBVDqn/btsFNu748En/81rTtlbPMFYOzeD3LLrzY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBVDqn/btsFNu748En/81rTtlbPMFYOzeD3LLrzY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBVDqn/btsFNu748En/81rTtlbPMFYOzeD3LLrzY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBVDqn%2FbtsFNu748En%2F81rTtlbPMFYOzeD3LLrzY1%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;1746&quot; height=&quot;1326&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;1326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;커스터마이징한 응답 메세지가 나오게 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️장점&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;일관성 있는 응답 : 클라이언트가 예외에 대해 어떤 응답을 받을 지 미리 알 수 있다.&lt;/li&gt;
&lt;li&gt;코드 중복 최소화 : 모든 예외 처리 코드를 한 곳에서 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;커스터마이징 : 예외 처리 로직을 모두 커스터마이징할 수 있다.&lt;br /&gt;이러한 장점들은 개발자가 일관된 API를 구축하고 유지할 수 있도록 도와준다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발 이야기/Spring</category>
      <category>ResponseEntityExceptionHandler</category>
      <category>RestControllerAdvice</category>
      <category>Spring MVC Exception</category>
      <category>예외 처리</category>
      <category>전역 예외</category>
      <author>장똥구리</author>
      <guid isPermaLink="true">https://jangddonguri.tistory.com/5</guid>
      <comments>https://jangddonguri.tistory.com/5#entry5comment</comments>
      <pubDate>Fri, 15 Mar 2024 06:37:12 +0900</pubDate>
    </item>
    <item>
      <title>Spring ThreadPoolTaskExecutor @Async 비동기</title>
      <link>https://jangddonguri.tistory.com/4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 피파 전적검색 앱 프로젝트를 진행 중이다. 사용자가 전적갱신을 하게 되면 다음과 같은 로직으로 진행하게 된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자의 닉네임을 입력받아서 고유한 사용자 식별자(UID)를 가져온다.&lt;/li&gt;
&lt;li&gt;1번의 UID를 통해 해당 사용자의 매치 리스트를 조회한다.&lt;/li&gt;
&lt;li&gt;매치 리스트를 가져와서 각 매치에 대한 상세 정보를 요청한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 만약 매치가 100개라고 가정했을 때, 3번의 요청이 동기적으로 수행된다면 다음과 같은 일이 발생한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각각의 요청이 하나씩 순차적으로 처리하게 된다. 즉, 첫 번째 요청이 완료되어야만 두 번째 요청이 시작되고, 두 번째 요청이 완료되어야 세 번째 요청이 시작된다.&lt;/li&gt;
&lt;li&gt;따라서, 3번째 요청은 1번째 요청이 완료되고 2번째 요청이 진행 중일 때까지 대기하게 된다.&lt;/li&gt;
&lt;li&gt;이 경우, 전체적으로 매치 리스트를 가져오는 데 시간이 오래 걸리고, 사용자는 요청을 보낸 후 오랜 시간 동안 대기해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 동기적인 방식으로 요청을 처리하면 전체적인 성능이 저하될 수 있다. 따라서 비동기적인 방식으로 요청을 처리하는 것이 좋다. 이를 통해 요청을 병렬적으로 처리하여 전체적인 응답 시간을 단축할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️@Async&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Async는 Spring AOP에 의해 작동된다. 프록시 패턴 기반이다.&lt;br /&gt;@Async 가 붙은 메소드를 호출하게 되면 스프링이 가로채서 프록시 객체를 생성하게 되고 그 스레드에서 작업을 수행하게 된다.&lt;br /&gt;이렇게 수행하니 &lt;b&gt;private method는 적용할 수 없다.&lt;/b&gt; 왜냐하면 스프링이 다른 클래스에서 호출해야 하는데, private 면 호출할 수 없다.&lt;br /&gt;또한 &lt;b&gt;self-invocation 자가 호출의 경우에도 동작하지 않는다.&lt;/b&gt; 왜냐하면 프록시 객체를 거치지 않고 직접 method를 호출하기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SimpleAsyncTaskExecutor&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Executor를 Bean에서 등록하지 않으면 Spring에서는 AsyncTaskExecutor을 사용해서 알아서 Executor를 등록한다.&lt;br /&gt;많은 블로그에서는 기본으로 SimpleAsyncTaskExecutor 에 의해서 스레드가 관리된다고 한다. 하지만 직접 확인해보니, SimpleAsyncTaskExecutor 에 의해 스레드 관리되는 게 아닌, ThreadPoolTaskExecutor 에 의해 관리된다... 즉, 기본값은 ThreadPoolTaskExecutor 이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2724&quot; data-origin-height=&quot;1192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VLw54/btsE6TmoJuQ/dn6GNBmdkQAecpq2UNWdKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VLw54/btsE6TmoJuQ/dn6GNBmdkQAecpq2UNWdKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VLw54/btsE6TmoJuQ/dn6GNBmdkQAecpq2UNWdKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVLw54%2FbtsE6TmoJuQ%2Fdn6GNBmdkQAecpq2UNWdKk%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;2724&quot; height=&quot;1192&quot; data-origin-width=&quot;2724&quot; data-origin-height=&quot;1192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;왜 그런가 봤더니 &lt;b&gt;Spring Boot 에서는 AutoConfiguration 을 통해 ThreadPoolTaskExcutor을 자동으로 등록한다.&lt;/b&gt; 많은 블로그에서는 Spring Boot 환경이 아닌 Spring 이어서 그런 것같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2022&quot; data-origin-height=&quot;1256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S6iNr/btsE5GgL0rn/qXaQdLQfzJ36Ky7mKNp0Q0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S6iNr/btsE5GgL0rn/qXaQdLQfzJ36Ky7mKNp0Q0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S6iNr/btsE5GgL0rn/qXaQdLQfzJ36Ky7mKNp0Q0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS6iNr%2FbtsE5GgL0rn%2FqXaQdLQfzJ36Ky7mKNp0Q0%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;2022&quot; height=&quot;1256&quot; data-origin-width=&quot;2022&quot; data-origin-height=&quot;1256&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 SimpleAsyncTaskExecutor에 대해 알아보자면 SimpleAsyncTaskExecutor는 스레드풀 방식이 아니다. 스레드가 필요할 때마다 생성하게 된다. 즉, 재사용을 하지 않기에 자원이 많이 낭비된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️ThreadPoolTaskExecutor&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Spring은 ThreadPoolTaskExecutor 을 사용하지만, 내부 설정 값을 변경할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2724&quot; data-origin-height=&quot;1192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbHs8K/btsE0HAxRbc/sNWBVA1jgD2WV1BJ9GS2i1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbHs8K/btsE0HAxRbc/sNWBVA1jgD2WV1BJ9GS2i1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbHs8K/btsE0HAxRbc/sNWBVA1jgD2WV1BJ9GS2i1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbHs8K%2FbtsE0HAxRbc%2FsNWBVA1jgD2WV1BJ9GS2i1%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;2724&quot; height=&quot;1192&quot; data-origin-width=&quot;2724&quot; data-origin-height=&quot;1192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CorePoolSize&lt;/b&gt; : 스레드 풀의 기본 크기이다. 최소 이 만큼 스레드가 유지된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MaxPoolSize&lt;/b&gt; : 스레드 풀에서 허용하는 최대 스레드 수이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;QueueCapacity&lt;/b&gt; : 작업 대기 큐의 용량이다. CorePoolSize 를 초과해서 스레드 생성 요청 시 해당 요청을 Queue에 저장한다. 이때 최대 수용 가능한 Queue의 수이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ThreadNamePrefix&lt;/b&gt; : 생성되는 Thread 접두사 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️ThreadPoolTaskExecutor 동작 방식&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스레드풀에 작업을 등록하면, 스레드풀에 CorePoolSize 만큼의 스레드가 존재하는지 확인한다.&lt;/li&gt;
&lt;li&gt;스레드에 작업을 할당한다.&lt;/li&gt;
&lt;li&gt;스레드풀의 스레드 개수가 CorePoolSize보다 작으면, 스레드풀에 새로운 스레드를 생성하고 작업을 할당하지만, 초과하면 Queue에 추가한다.&lt;/li&gt;
&lt;li&gt;Queue 가 가득 찬 경우, 현재 스레드풀의 스레드 수가 MaxPoolSize 보다 적으면 새로운 스레드를 생성하여 작업을 추가한다.&lt;br /&gt;스레드 풀의 스레드 수가 MaxPoolSize 에 도달한 경우, 새로운 작업 요청이 들어오면 TaskRejectedException 이 발생하게 된다.&lt;/li&gt;
&lt;li&gt;작업 중인 스레드가 할 일을 다하면 Queue에 대기 중인 작업이 있는지 확인하고, 있으면 작업을 가져와서 수행한다. 없으면 해당 스레드는 대기 상태로 돌아간다.&lt;/li&gt;
&lt;li&gt;스레드풀의 스레드 개수가 CorePoolSize보다 크면 keepAliveTime이 지나고 해당 스레드는 스레드풀에서 제거된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 @Async 만 붙여주면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mhiKF/btsE0jmmr1H/lpz197iV5ewGOh9iRp10M0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mhiKF/btsE0jmmr1H/lpz197iV5ewGOh9iRp10M0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mhiKF/btsE0jmmr1H/lpz197iV5ewGOh9iRp10M0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmhiKF%2FbtsE0jmmr1H%2Flpz197iV5ewGOh9iRp10M0%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;1824&quot; height=&quot;682&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;682&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;결과&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KGbzm/btsE6PqNqe6/YuBYhO89OfQv3sEjdNm4g0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KGbzm/btsE6PqNqe6/YuBYhO89OfQv3sEjdNm4g0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KGbzm/btsE6PqNqe6/YuBYhO89OfQv3sEjdNm4g0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKGbzm%2FbtsE6PqNqe6%2FYuBYhO89OfQv3sEjdNm4g0%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;2004&quot; height=&quot;1036&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶주의 사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Async 와 @Transactional을 같이 사용할 때는 주의가 필요하다. &lt;b&gt;@Async 어노테이션이 붙은 메서드는 호출한 메서드와 독립적인 스레드에서 동작하기 때문에, 비동기 메서드 내에서 생성된 트랜잭션은 호출한 메서드의 트랜잭션과는 독립적으로 동작한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 비동기 메서드 내에서 예외가 발생하여 트랜잭션이 롤백되어야 하지만, 다른 스레드에서 독립적으로 처리하므로 원래의 호출 메서드의 트랜잭션에 영향을 미치지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Async , ThreadPoolTaskExecutor 에 대해서 알게 되었다. 참고로 ThreadPoolExecutor 랑 ThreadPoolTaskExecutor의 차이는 ThredPoolExecutor는 Java 패키지에 있고 ThreadPoolTaskExecutor는 Spring 패키지에 있다. 결국에 Spring이 ThreadPoolExecutor를 쉽게 사용하게 만들어 준 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2196&quot; data-origin-height=&quot;1196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOfKiD/btsE1DZd3OD/T5k7yF9lHW8loy2bbaKLO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOfKiD/btsE1DZd3OD/T5k7yF9lHW8loy2bbaKLO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOfKiD/btsE1DZd3OD/T5k7yF9lHW8loy2bbaKLO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOfKiD%2FbtsE1DZd3OD%2FT5k7yF9lHW8loy2bbaKLO0%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;2196&quot; height=&quot;1196&quot; data-origin-width=&quot;2196&quot; data-origin-height=&quot;1196&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 많은 고민을 하는 것은 CorePoolSize 와 MaxPoolSize , QueueCapacity 를 어떻게 설정해야 할지 모르겠다.&lt;br /&gt;&lt;b&gt;적정 스레드 풀 개수 = CPU 수 * (CPU 목표 사용량) * (1+대기 시간/서비스 시간)&lt;/b&gt; 이것으로 CorePollSize 를 잡고... QueueCapacity 는 최댓값으로 설정할지 아니면 큐 사이즈를 낮게하고 MaxPoolSize 를 크게 할 지 고민이다.&lt;br /&gt;전자로 하면 트래픽이 몰릴 때 너무 느리고, 후자로 하면 리소스 낭비로 이어질 가능성이 크다. 모니터링을 통해 적절한 값을 찾아봐야겠다.&lt;/p&gt;</description>
      <category>개발 이야기/Spring</category>
      <category>@Async</category>
      <category>SimpleAsyncTaskExecutor</category>
      <category>Spring boot</category>
      <category>ThreadPoolExecutor</category>
      <category>ThreadPoolTaskExecutor</category>
      <category>비동기</category>
      <author>장똥구리</author>
      <guid isPermaLink="true">https://jangddonguri.tistory.com/4</guid>
      <comments>https://jangddonguri.tistory.com/4#entry4comment</comments>
      <pubDate>Tue, 20 Feb 2024 03:14:15 +0900</pubDate>
    </item>
    <item>
      <title>Cursor Based Pagination(커서 기반 페이지네이션)</title>
      <link>https://jangddonguri.tistory.com/3</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶️상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Art:Care 라는 프로젝트를 진행중인데, 앱에 무한스크롤 기능을 구현해야 했다. 스크롤을 내릴 때 이전 데이터가 중복되서 갱신되지 않고, 마지막 데이터 기준으로 데이터를 계속 불러오게 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;페이지네이션이란?&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량의 데이터에서 필요한 데이터만 전달하는 방법이다.&lt;br /&gt;크게 &lt;b&gt;오프셋 기반 페이지네이션(Offset Based Pagination)&lt;/b&gt; 과 &lt;b&gt;커서 기반 페이지네이션(Cursor Based Pagination)&lt;/b&gt; 이 있다.&lt;br /&gt;페이지네이션을 사용하면 한 번에 로드해야 하는 데이터 양이 줄어들어 로딩 시간을 줄일 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶Offset Based Pagination&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오프셋 기반 페이지네이션에 대해 간단하게 알아보겠다.&lt;br /&gt;일정 Offset부터 일정 개수의 아이템을 가져오는 방식이다. 예를 들어 게시판에 100개의 게시글이 있고, 1페이지에 10개의 게시글이 들어간다고 가정해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.사용자 A가 1페이지에서 10개의 게시글을 조회했다.&lt;br /&gt;2.사용자 A가 아닌 다른 사용자들이 10개의 개시글을 생성했다.&lt;br /&gt;3.사용자 A가 2페이지를 조회했다.&lt;br /&gt;4.사용자 A는 1단계에서 봤던 10개의 게시글을 똑같이 보게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 오프셋 기반 페이지네이션은 데이터 중복 문제를 야기한다. 만약 해당 게시판에 생성 또는 삭제가 빈번하게 일어나는 경우, 데이터들이 뒤죽박죽 될 것이고 사용자들에게 혼란을 야기시킬 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;성능 문제&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오프셋 기반 페이지네이션은 페이지 번호가 증가할수록 데이터베이스나 서버에서 불필요한 작업을 수행해햐한다. 페이지 100에 있는 게시글을 보려면 처음부터 100번째 페이지의 모든 데이터를 읽어야 하기 때문이다. 즉 Offset 값이 커지면 커질수록 성능 저하 문제가 발생한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶Cursor Based Pagination&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor 라는 것이 있는데, 이 Cursor는 게시글에서는 게시글을 불러오는 '기준'인 postId 가 되는 것이고, 댓글에서는 댓글을 불러오는 기준인 commentId가 되는 것이다. 즉, 사용자에게 응답해준 마지막 데이터의 식별자 값을 의미한다.&lt;br /&gt;&lt;b&gt;마지막 데이터를 기준으로 다음 n개의 데이터를 응답해주는 방식이다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;574&quot; data-origin-height=&quot;574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nHGZI/btsEWNutwtm/jWh0nbtca8eLrw8O64Eqbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nHGZI/btsEWNutwtm/jWh0nbtca8eLrw8O64Eqbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nHGZI/btsEWNutwtm/jWh0nbtca8eLrw8O64Eqbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnHGZI%2FbtsEWNutwtm%2FjWh0nbtca8eLrw8O64Eqbk%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;574&quot; height=&quot;574&quot; data-origin-width=&quot;574&quot; data-origin-height=&quot;574&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶구현 QueryDSL&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;968&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQL3E9/btsEZctkbLJ/BYDx4v1K5l8uKZ7WQrGbm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQL3E9/btsEZctkbLJ/BYDx4v1K5l8uKZ7WQrGbm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQL3E9/btsEZctkbLJ/BYDx4v1K5l8uKZ7WQrGbm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQL3E9%2FbtsEZctkbLJ%2FBYDx4v1K5l8uKZ7WQrGbm0%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;542&quot; height=&quot;432&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;968&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;where에 있는 postIdLessThan(cursorId) 만 보면된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/th8u3/btsEWh3DBxd/VvadpMXKllJ0cIbxjfzgMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/th8u3/btsEWh3DBxd/VvadpMXKllJ0cIbxjfzgMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/th8u3/btsEWh3DBxd/VvadpMXKllJ0cIbxjfzgMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fth8u3%2FbtsEWh3DBxd%2FVvadpMXKllJ0cIbxjfzgMK%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;551&quot; height=&quot;114&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;처음 페이지인 경우&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 페이지인 경우 null 또는 0인 값을 받게 하였다. 즉, 클라이언트에서는 cursorId에 대한 값을 보내지 않는다. null 인경우 where 절은 실행되지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;일반 페이지인 경우&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postId 보다 작은 값을 가져오는 where 절이 실행된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;▶마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 id 값을 cursorId로 사용하고 있어서 쉬웠지만, 만약 다른 조건으로 정렬해야한다면 여러 문제가 생길 수 있을 것같다. 예를 들어 정렬해야하는 조건이 중복이 되는 경우에 데이터들이 생략되는 경우가 있을 것이다. 예를 들어 cursorId 를 day로 했다고 가정해 보자. 2월 16일 게시글이 8개 2월 17일 게시글이 12개 일 때 10개씩 불러온다고 해보자. 맨 마지막은 17일 일것이고 cursorId는 17일이 될 것이다. 그렇다면 2월 17일의 데이터 10개는 불러오지 않게된다. 이러한 조건이 있다면 다른 unique 값과 연결 지어 해결하면 좋을 것같다...&lt;/p&gt;</description>
      <category>개발 이야기</category>
      <category>Cursor Based Pagination</category>
      <category>pagenation</category>
      <category>querydsl</category>
      <category>Spring</category>
      <author>장똥구리</author>
      <guid isPermaLink="true">https://jangddonguri.tistory.com/3</guid>
      <comments>https://jangddonguri.tistory.com/3#entry3comment</comments>
      <pubDate>Sat, 17 Feb 2024 15:47:51 +0900</pubDate>
    </item>
    <item>
      <title>Spring Profile 개발 서버와 운영 서버 분리, Environment Configurations</title>
      <link>https://jangddonguri.tistory.com/2</link>
      <description>&lt;h2&gt;▶️상황&lt;/h2&gt;
&lt;p&gt;현재 프로젝트에서 금융결제원 오픈 API를 사용하고있는데, 테스트 API와 실제 운용 API가 나누어져있다.&lt;br&gt;개발 서버에서 실제 운용 API를 사용하여 테스트하면 안되기 때문에 이를 각각 개발 서버와 운영 서버에 맞게 분리 해야하고, 환경 설정 값들을 그에 맞게 적용해야한다.&lt;/p&gt;
&lt;h2&gt;▶️Spring 에서 설정 값을 어떻게 적용시키는가?&lt;/h2&gt;
&lt;p&gt;참고문헌 &lt;a href=&quot;https://docs.spring.io/spring-boot/docs/1.2.0.M1/reference/html/boot-features-external-config.html%5D&quot;&gt;https://docs.spring.io/spring-boot/docs/1.2.0.M1/reference/html/boot-features-external-config.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1223&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmbWQr/btsEkvA6cNH/L7JxjqFlOotZOKabg669HK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmbWQr/btsEkvA6cNH/L7JxjqFlOotZOKabg669HK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmbWQr/btsEkvA6cNH/L7JxjqFlOotZOKabg669HK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmbWQr%2FbtsEkvA6cNH%2FL7JxjqFlOotZOKabg669HK%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;1223&quot; height=&quot;452&quot; data-origin-width=&quot;1223&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nkWyK/btsEmDFxQjJ/cdsQPPj0sYSPMzpI48V1t1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nkWyK/btsEmDFxQjJ/cdsQPPj0sYSPMzpI48V1t1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nkWyK/btsEmDFxQjJ/cdsQPPj0sYSPMzpI48V1t1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnkWyK%2FbtsEmDFxQjJ%2FcdsQPPj0sYSPMzpI48V1t1%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;448&quot; height=&quot;763&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;763&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Spring Boot 에서는 &lt;strong&gt;Environment&lt;/strong&gt; 추상화를 통해 환경 설정 값들에 접근할 수 있다고 한다. 우리는 &lt;strong&gt;PropertySource Interface&lt;/strong&gt; 가 &lt;strong&gt;Environment&lt;/strong&gt;에 로드되어 있으므로 이를 편리하게 사용하면 된다.&lt;/p&gt;
&lt;h2&gt;▶️환경 설정값을 적용하는 방법&lt;/h2&gt;
&lt;h3&gt;내부,외부 설정 파일을 이용하는 경우&lt;/h3&gt;
&lt;p&gt;ex) application.properties , application.yml&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmBUub/btsElItAUFg/dnyX1trez67zpupcqCgzC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmBUub/btsElItAUFg/dnyX1trez67zpupcqCgzC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmBUub/btsElItAUFg/dnyX1trez67zpupcqCgzC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmBUub%2FbtsElItAUFg%2FdnyX1trez67zpupcqCgzC0%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;734&quot; height=&quot;314&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;OS 환경 변수&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;359&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RVIq5/btsEkCNJ6lH/Gal9TkigkxkjeCERhTkIIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RVIq5/btsEkCNJ6lH/Gal9TkigkxkjeCERhTkIIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RVIq5/btsEkCNJ6lH/Gal9TkigkxkjeCERhTkIIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRVIq5%2FbtsEkCNJ6lH%2FGal9TkigkxkjeCERhTkIIk%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;359&quot; height=&quot;180&quot; data-origin-width=&quot;359&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;커맨드 라인 옵션 인수&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$ java -jar artCare-0.0.1-SNAPSHOT.jar --server.port=8086&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;자바 시스템 속성&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$ java -jar -Dserver.port=8086 artCare-0.0.1-SNAPSHOT.jar&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;▶️코드 내에서 사용하기&lt;/h2&gt;
&lt;p&gt;application.properties 또는 application.yml 에 해당 값이 있다고 가정하자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;337&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boNZoR/btsEmo9ipYB/nIBI0hVNAMq98ui5zhLmk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boNZoR/btsEmo9ipYB/nIBI0hVNAMq98ui5zhLmk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boNZoR/btsEmo9ipYB/nIBI0hVNAMq98ui5zhLmk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboNZoR%2FbtsEmo9ipYB%2FnIBI0hVNAMq98ui5zhLmk0%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;337&quot; height=&quot;114&quot; data-origin-width=&quot;337&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;@Value&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;@Value&lt;/strong&gt;로 외부 설정 정보의 키 값을 입력받고, 해당하는 value 값을 가져올 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Value(&amp;quot;${jwt.client-secret}&amp;quot;)
    private String jwtClientSecret;
@Value(&amp;quot;${jwt.issuer}&amp;quot;)
    private String jwtIssuer;
@Value(&amp;quot;${jwt.refresh-token-expire}&amp;quot;)
    private Integer jwtRefreshTokenExpire;
@Value(&amp;quot;${jwt.token-expire}&amp;quot;)
    private Integer jwtTokenExpire;&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;@ConfigurationProperties&lt;/h3&gt;
&lt;p&gt;외부 설정 값을 객체를 통해서 활용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;374&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ts3yP/btsElKEYwiV/qk7yqAgAMtysdkkC9pCMG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ts3yP/btsElKEYwiV/qk7yqAgAMtysdkkC9pCMG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ts3yP/btsElKEYwiV/qk7yqAgAMtysdkkC9pCMG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fts3yP%2FbtsElKEYwiV%2Fqk7yqAgAMtysdkkC9pCMG1%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;374&quot; height=&quot;244&quot; data-origin-width=&quot;374&quot; data-origin-height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;▶️개발 서버와 운영 서버의 설정 값 분리하기.&lt;/h2&gt;
&lt;p&gt;개발 서버와 운영 서버 jar 마다 설정 값을 변경하기는 어려우니 profile을 사용하여 편리하게 사용한다. profile 을 사용하면 내 입맛대로 내부에 있는 설정 파일을 선택할 수 있다. 즉, 개발 서버에서는 &lt;strong&gt;spring.profiles.active=local&lt;/strong&gt; 을 이용하여 application-local.properties 파일을 적용하고, 운용 서버에서는 &lt;strong&gt;spring.profiles.active=prod&lt;/strong&gt;을 이용하여 application-prod.properties 파일을 적용할 수 있다.&lt;/p&gt;
&lt;p&gt;application-{profile}.properties처럼 파일을 나누어서 사용하거나 하나의 파일에서 사용할 수 있다.&lt;br&gt;아래는 하나의 파일을 사용하여 설정하는 예시이다. &lt;strong&gt;#--&lt;/strong&gt; 를 기준으로 profile을 나눌 수 있는데, 아무 값이 없으면 &lt;strong&gt;default&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;489&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2kWzW/btsEkyq4Lu8/w0fWYtNrCdflvLigcIUVvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2kWzW/btsEkyq4Lu8/w0fWYtNrCdflvLigcIUVvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2kWzW/btsEkyq4Lu8/w0fWYtNrCdflvLigcIUVvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2kWzW%2FbtsEkyq4Lu8%2Fw0fWYtNrCdflvLigcIUVvK%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;786&quot; height=&quot;489&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;489&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;보기 쉽게 실제 프로젝트에서 사용하는 다른 값들은 삭제하였다. &lt;strong&gt;spring.config.activate.on-profile&lt;/strong&gt;을 이용하여 profile 값을 설정할 수 있다.&lt;br&gt;개발 서버에서는 그냥 실행하면 되고, 운용 서버에서는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -jar *.jar --spring.profiles.active=prod&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;를 사용하여 실행하면된다.&lt;/p&gt;
&lt;p&gt;현재 프로젝트에서는 .properties를 사용하였는데, 설정 값들이 많다보니 가독성이 떨어진다. 이후에 yml로 바꾸는 작업을 해야한다. properties 에서는 Profile 구분을 #--- 로 하였지만, yml 에서는 ---로 해야한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1233&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/begu5a/btsEli9mQZl/nKPFcq9LfkriDFOuisXm40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/begu5a/btsEli9mQZl/nKPFcq9LfkriDFOuisXm40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/begu5a/btsEli9mQZl/nKPFcq9LfkriDFOuisXm40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbegu5a%2FbtsEli9mQZl%2FnKPFcq9LfkriDFOuisXm40%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;1233&quot; height=&quot;428&quot; data-origin-width=&quot;1233&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;▶️개발 서버와 운영 서버에 등록되는 Bean 분리하기.&lt;/h2&gt;
&lt;p&gt;개발 서버에서는 금융결제원의 Test Api 를 사용하는 LocalFinanceService를 적용하고 운용 서버에서는 실제 Api를 사용하는 ProdFinanceService를 적용하고 싶다.&lt;/p&gt;
&lt;h3&gt;@Profile 사용하기&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;FinanceService&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface FinanceService {
    void send(int money);
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;LocalFinanceService&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Profile(&amp;quot;local&amp;quot;)
public class LocalFinanceService implements FinanceService {
    @Override
    public void send(int money) {

    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;ProdFinanceService&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Profile(&amp;quot;prod&amp;quot;)
public class ProdFinanceService implements FinanceService{
    @Override
    public void send(int money) {

    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Profile에 맞게 Bean을 등록해주어야 한다.&lt;br&gt;&lt;strong&gt;FinanceServiceConfig&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class FinanceServiceConfig {

    @Bean
    @Profile(&amp;quot;prod&amp;quot;)
    public FinanceService prodFinanceService() {
        return new ProdFinanceService();
    }

    @Bean
    @Profile(&amp;quot;local&amp;quot;)
    public FinanceService localFinanceService() {
        return new LocalFinanceService();
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;profile 을 local 또는 prod 값을 적용할 경우 각각 다른 Bean 이 등록되어 FinanceController 에서 FinanceService 만 사용하면 Local 환경에서는 LocalFinanceService 를 적용하고, Prod 환경에서는 ProdFinanceService를 적용하게 된다. 결국 추상화가 핵심이다.&lt;/p&gt;</description>
      <category>개발 이야기/Spring</category>
      <category>@Profile</category>
      <category>@Value</category>
      <category>ConfigurationProperties</category>
      <category>Spring</category>
      <category>Spring boot</category>
      <category>Spring Profile</category>
      <category>개발서버 운영서버 분리</category>
      <author>장똥구리</author>
      <guid isPermaLink="true">https://jangddonguri.tistory.com/2</guid>
      <comments>https://jangddonguri.tistory.com/2#entry2comment</comments>
      <pubDate>Sat, 3 Feb 2024 18:31:52 +0900</pubDate>
    </item>
  </channel>
</rss>