<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>PPAK</title>
    <link>https://ppaksang.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 15 May 2026 15:56:43 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>PPakSang</managingEditor>
    <item>
      <title>Spring Cloud Data Flow 를 사용하며</title>
      <link>https://ppaksang.tistory.com/48</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ppaksang.tistory.com/38&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;지난 포스팅&lt;/a&gt; 에서 Spring Cloud Data Flow(이하 SCDF) 에 대해 설명했는데 1년이 조금 넘는 시간동안 추가적으로 운영하면서 느낀점을 남기려고 한다.&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;SCDF에 대해서는 생소할 수 있는데, 마이크로 서비스 기반의 스트리밍, 배치 처리 플랫폼이라 하고 쉽게 이야기하면 Spring Cloud Stream, Spring Clodu Batch/Task 파이프라인을 손쉽게 구성할 수 있는 추상화된 기능을 제공한다. 또한, K8S, Cloud Foundry 에서 손쉽게 배포가 가능하다.&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;a href=&quot;https://spring.io/blog/2025/04/21/spring-cloud-data-flow-commercial&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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;The vast majority of usage we see for Spring Cloud Data Flow exists within our Tanzu enterprise customers. Open-source usage represents a very small part of the overall adoption today with an equally small contribution of the maintenance provided by the community.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전반적으로 사용자가 적고 동시에 오픈소스 컨트리뷰터 역시 적기 때문에 오픈소스 프로젝트를 운영하기 위한 제약이 리소스 낭비로 여겨져 상용 제품으로 변경한다는 내용이었다. 나도 근 1년 넘게 꾸준히 사용하는 플랫폼임에도 어떠한 의견을 내지 않고 사용했기에 다소 아쉬운 마음이 들었다.&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 생태계 연동을 지원하고 아래에 설명하겠지만 기능이 강력하기 때문에 메리트가 있는 기술이라고 생각한다. &lt;s&gt;LTS 는 생각보다 더 오랜 기간 사용된다.&lt;/s&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;나도 SCDF 를 접하게 된 것은 굉장한 우연이었는데, 처음 속하게 된 팀에서 해당 기술을 사용하고 있었고 1~2달 정도 뒤에 예상치 못한 조직 변동으로 새로운 팀에 합류하게 되었는데 잠깐 사용했던 SCDF 가 유용해 보이기도 했고 직접 더 써보고 싶은 마음에 팀 내에 직접 도입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;SCDF 는 Spring Cloud Stream, Spring Cloud Task/Batch 로 작성된 애플리케이션을 손쉽게 파이프라이닝 하고 모니터링할 수 있는 기능을 제공한다. 이와 더불어서 K8S 배포를 지원하고, Kafka 와의 손쉬운 연동을 지원한다. (SCDF에서 Batch Job은 Task 로 래핑되어 실행되기 때문에 본 포스팅에서는 Stream과 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;작성된 설정 정보를 토대로 애플리케이션이 배포되는데 파이프라인을 구성하기 위한 대시보드가 제공된다. 파이프라인은 fan-in, fan-out 의 단방향 구조로 구성된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlOSeX/btsNKsIjbbP/vK9qJmbKqTS5eFV7UfosFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlOSeX/btsNKsIjbbP/vK9qJmbKqTS5eFV7UfosFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlOSeX/btsNKsIjbbP/vK9qJmbKqTS5eFV7UfosFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlOSeX%2FbtsNKsIjbbP%2FvK9qJmbKqTS5eFV7UfosFk%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;1280&quot; height=&quot;649&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;649&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 data-ke-size=&quot;size16&quot;&gt;하나의 네모박스(?) 가 빌드된 Stream/Task 애플리케이션이라고 생각하면 된다. Stream 의 경우 위와 같이 파이프라인을 구축하면 자동으로 토픽을 정의하고 데이터 처리 결과(메시지)가 토픽으로 발행된다. Task 의 경우 파이프라인을 구축하면 앞에 위치한 Task 실행이 종료되면 다음 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;Spring Cloud Data Flow 를 구성하는 인스턴스는 크게 dataflow server, skipper server 로 나뉘고 아래와 같이 역할이 나뉜다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #172b4d; text-align: left;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Data Flow 서버 (Dataflow Server):&lt;/b&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;스트림과 태스크의 상태를 추적하고 모니터링 제공.&lt;/li&gt;
&lt;li&gt;스트림 및 태스크의 실행을 시작하고 중단(스트림의 경우 Skipper 서버로 실행 위임).&lt;/li&gt;
&lt;li&gt;위 작업을 위한 대시보드 제공.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Skipper 서버 (Skipper Server):&lt;/b&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;서로 다른 환경 간에 애플리케이션 배포 지원.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dormkd/btsNJQXeH27/QCWYQm5eIf8cYecEfmnMu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dormkd/btsNJQXeH27/QCWYQm5eIf8cYecEfmnMu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dormkd/btsNJQXeH27/QCWYQm5eIf8cYecEfmnMu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdormkd%2FbtsNJQXeH27%2FQCWYQm5eIf8cYecEfmnMu1%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;740&quot; height=&quot;431&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;744&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dataflow server 와 skipper server 는 한번 배포해두면 크게 신경쓸 일은 없다.&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;배포 방법은 아래 공식 문서나&amp;nbsp;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://ppaksang.tistory.com/38&quot;&gt;지난 포스팅&lt;/a&gt; 을 보면 조금 도움이 될 것 같다. SCDF 컴포넌트 자체가 구축하는 입장따라 크다면 크고 작다면 작은데, 개인적으로는 한번 구축해서 1년 넘도록 잘 쓰고 있어서 그리 아까운 시간은 아니었던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dataflow.spring.io/docs/installation/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dataflow.spring.io/docs/installation/&lt;/a&gt; &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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 우리 프로젝트는 버전에 민감한가(참고로 최신 버전 SCDF 는 k8s 1.30.x, Spring Boot 3.x 까지 지원한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Spring Cloud Stream/Batch 둘 다 자주 개발한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Kafka/RabbitMQ 기반 스트리밍 파이프라인이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Stream 파이프라인 버전 관리/롤백/업그레이드가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. Stream 을 관리하기 위한 Web UI 가 필요하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. Container Orchestration 환경에서 Stream 배포가 필요하다&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;대부분의 플랫폼 성격을 띄는 기술이 그렇듯 SCDF 는 여러 기술과의 연동을 통해 특정 작업을 추상화 해둔 것이 많고, 이러한 부분을 사실 직접 구현할 수 있고, 이것이 귀찮은 일이 아니라면 새로운 기술을 도입할 필요도 없다고 생각한다.&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;특히 Task 만 개발하는 조직에서는 SCDF 를 쓰지 않더라도 모니터링, 스케줄링을 더 손쉽게 구성할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&lt;b&gt;버전&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-cloud/spring-cloud-dataflow/releases/tag/v2.10.3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spirng Cloud Data Flow 2.10.3&lt;/a&gt; 을 사용하고 있고 메시지큐로 Kafka 를 사용한다.&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;바인딩&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 stream 을 운영하면 function binding 을 많이 사용할 것인데 아래와 같이 설정하곤 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCuwLq/btsNLzUcCn1/nAQatf63qotllhuuYW4YBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCuwLq/btsNLzUcCn1/nAQatf63qotllhuuYW4YBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCuwLq/btsNLzUcCn1/nAQatf63qotllhuuYW4YBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCuwLq%2FbtsNLzUcCn1%2FnAQatf63qotllhuuYW4YBk%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;313&quot; height=&quot;150&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;spring.cloud.stream&lt;b&gt;.&lt;b&gt;&lt;span style=&quot;color: #c9372c;&quot;&gt;${bean name}&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #c9372c;&quot;&gt;-in-0&lt;/span&gt;:&lt;span&gt; &lt;b&gt;&lt;span style=&quot;color: #cf9f02;&quot;&gt;${&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #cf9f02;&quot;&gt;alias}&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;spring.cloud.stream.bindings.&lt;b&gt;&lt;span style=&quot;color: #cf9f02;&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color: #cf9f02;&quot;&gt;alias&lt;/span&gt;&lt;span style=&quot;color: #cf9f02;&quot;&gt;}&lt;/span&gt;&lt;/b&gt;&lt;span&gt;.destination: &lt;b&gt;&lt;span style=&quot;color: #1d7afc;&quot;&gt;${&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #1d7afc;&quot;&gt;topic}&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(source의 경우&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;b&gt;&lt;b&gt;&lt;span style=&quot;color: #c9372c;&quot;&gt;${bean name}&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #c9372c;&quot;&gt;-out-0&lt;/span&gt;&lt;/b&gt;&lt;b&gt;,&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;processor의 경우&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;in/out&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;둘 다 작성)&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하지만 SCDF 에서는 아래와 같이 설정하고, UI에서 파이프라인을 구성해주기만 하면 자동으로 토픽(destination) 을 &lt;a href=&quot;https://dataflow.spring.io/docs/recipes/functional-apps/scst-function-bindings/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;정의해준다&lt;/a&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5CLYr/btsNKejfFLN/x4HCPYB7nboDOKy8If6S7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5CLYr/btsNKejfFLN/x4HCPYB7nboDOKy8If6S7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5CLYr/btsNKejfFLN/x4HCPYB7nboDOKy8If6S7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5CLYr%2FbtsNKejfFLN%2Fx4HCPYB7nboDOKy8If6S7k%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;318&quot; height=&quot;129&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;390&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 data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단일 모듈 task, stream 관리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 Spring Cloud Task, Stream 을 운영할 때 특정 Bean 을 생성하고, 설정을 주입하기 위한 많은 방법이 존재할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. @ConditionalOnProperty 로 특정 Bean 생성 제어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Task/Stream별로 실행 가능한 모듈을 생성, 빌드 타임에 특정 모듈을 빌드&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 특정 설정 값(task, stream 이름)을 토대로 Enum 을 매핑하고 해당 Enum 을 가진 Bean 이 생성되도록 Condition 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Stream 작성시에 해당 Enum 을 커스텀 어노테이션과 함께 명시해줍니다&lt;/p&gt;
&lt;pre id=&quot;code_1746425675600&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class StreamCondition : Condition {
    override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
        val targetStreamName = Optional.ofNullable(
            context.environment.getProperty(&quot;custom.stream.name&quot;, String::class.java))
            .orElseThrow { RuntimeException(&quot;set custom.stream.name&quot;) }
        val streamName: CustomStreamNames = metadata.annotations
            .get(CustomStream::class.java)
            .getValue(&quot;value&quot;, CustomStreamNames::class.java)
            .orElseThrow { RuntimeException(&quot;set stream name for CustomStream annotation&quot;) }

        val isMatch = StringUtils.equals(targetStreamName.lowercase(Locale.getDefault()), streamName.name.lowercase(Locale.getDefault()))
        if (isMatch) {
            logger.info(&quot;[StreamCondition] Stream: $streamName is created&quot;)
        }
        return isMatch
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746425712552&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@CustomStream(CustomStreamNames.DEMO_SOURCE)
class DemoSource {
    @Bean
    fun sendEvents(): Supplier&amp;lt;Message&amp;lt;DemoMessage&amp;gt;&amp;gt; {
        return Supplier&amp;lt;Message&amp;lt;DemoMessage&amp;gt;&amp;gt; {
            val demoMessage = DemoMessage(&quot;test&quot;, 10)
            MessageBuilder.withPayload(DemoMessage(&quot;test&quot;, 10))
                .setHeader(&quot;partitionKey&quot;, Random.nextInt())
                .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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 스트림마다 설정값이 다를 수 있는데, 특히 consumer, producer 설정이 스트림별로 다르면 특정 시점에 원하는 값만 주입되도록 설정을 해주어야 한다.&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;그리고 스트림을 처음 개발하는 개발자 입장에서 각종 설정에 대해 모두 이해하고 개발하는 것은 다소 비효율적일 수 있다. 나는 이 값을 타겟 Enum이 활성화될 때 모두 주입할 수 있도록 Enum 을 정의하는 시점에 값을 넣도록 세팅했다.&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_1746426144127&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum class StreamNames(
    val streamConfig: StreamConfig,
    val customConfig: Map&amp;lt;String, Any&amp;gt; = emptyMap()
) {

    DEMO_SOURCE(
        CustomSourceConfig(
            &quot;sendEvents&quot;,
            fixedDelay = 100,
            partitionConfig =
            PartitionConfig(
                partitionCount = 30,
                partitionSelectorNameAndKeyExtractorName = Pair(
                    &quot;roundRobinSelectorStrategy&quot;,
                    &quot;randomPartitionKeyExtractorStrategy&quot;
                )
            )
        )
    )
    
    companion object {
        fun getStreamConfigByName(name: String): Map&amp;lt;String, Any&amp;gt; {
            val streamNames = valueOf(name)
            return streamNames.streamConfig.getConfigMap() + streamNames.customConfig
        }

        fun getStreamLocalConfigByName(name: String): Map&amp;lt;String, Any&amp;gt; {
            val streamNames = valueOf(name)
            return streamNames.streamConfig.getLocalConfigMap() + streamNames.customConfig
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1746434224687&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class StreamEnvironmentPostProcessor : EnvironmentPostProcessor {
    private val loader = YamlPropertySourceLoader()
    override fun postProcessEnvironment(environment: ConfigurableEnvironment, application: SpringApplication) {
        val streamName = Optional.ofNullable(environment.getProperty(&quot;custom.stream.name&quot;))
            .orElseThrow { RuntimeException(&quot;set property custom.stream.name&quot;) }
        val activeProfile = Optional.ofNullable(environment.getProperty(&quot;spring.profiles.active&quot;))
            .orElseThrow { RuntimeException(&quot;set property spring.profiles.active&quot;) }

        val streamConfigByName = if (activeProfile.equals(&quot;local-processor&quot;) &amp;amp;&amp;amp; streamName.lowercase().contains(&quot;processor&quot;)) {
            StreamNames.getStreamLocalConfigByName(streamName.uppercase(Locale.getDefault()))
        } else {
            StreamNames.getStreamConfigByName(streamName.uppercase(Locale.getDefault()))
        }
        MapPropertySource(&quot;stream-resource&quot;, streamConfigByName).also { environment.propertySources.addLast(it) }
    }
}&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;b&gt;로컬 테스트 환경 구성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 환경에서 Stream 테스트를 하는 것은 귀찮(?)다. 특히 binder, destination(토픽) 설정을 생각하는게 번거롭다. 따라서 아래와 같은 워크플로우로 개발 가능하게 설정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Stream 컴포넌트(source, processor, sink) 로직 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 타겟 Stream 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Run Stream(테스트 시작)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYFaBX/btsNKWPVHDI/wA4VzHKBRIzczvt9B8YQHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYFaBX/btsNKWPVHDI/wA4VzHKBRIzczvt9B8YQHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYFaBX/btsNKWPVHDI/wA4VzHKBRIzczvt9B8YQHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYFaBX%2FbtsNKWPVHDI%2FwA4VzHKBRIzczvt9B8YQHK%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;317&quot; height=&quot;138&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;664&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;2554&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CeKjr/btsNJSncRgI/bRDqidgxn3jioqoqbIdFS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CeKjr/btsNJSncRgI/bRDqidgxn3jioqoqbIdFS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CeKjr/btsNJSncRgI/bRDqidgxn3jioqoqbIdFS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCeKjr%2FbtsNJSncRgI%2FbRDqidgxn3jioqoqbIdFS1%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;518&quot; height=&quot;135&quot; data-origin-width=&quot;2554&quot; data-origin-height=&quot;664&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 data-ke-size=&quot;size16&quot;&gt;그러기 위해서는 모든 stream 의 function binding 별칭이 input, output 으로 고정되어 있는 SCDF 스펙을 따르면서, 각각의 바인더의 destination 을 위와 같이 잡았다. 단, processor 의 경우 destination 이 2개 존재해야하기 때문에 어쩔 수 없이 local 프로필에 한해서 설정을 분기시켰다.&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;그리고, server.port=0, 인텔리제이 Build and Run &amp;gt; Modify Options &amp;gt; Allow Multiple Instances 를 활성화하면 새로운 Stream 컴포넌트가 추가되어도 위와 같은 워크플로우로 동일하게 테스트할 수 있도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746606767042&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  cloud:
    stream:
      bindings:
        output:
          destination: local.topic
        inputProcessor:
          destination: local.topic
        outputSink:
          destination: local.topic.outputSink
        input:
          destination: local.topic.outputSink&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;b&gt;배포&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;1. Stream 이미지 빌드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;b&gt;Dataflow Server로의 이미지 등록 요청&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 대시보드 내에서 실행&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;Dataflow Server로의 이미지 등록 요청&lt;/b&gt;의 경우 다시 두 가지 방법으로 나뉠 수 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;a href=&quot;https://dataflow.spring.io/docs/feature-guides/streams/java-dsl/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Cloud Data Flow Rest Client&amp;nbsp;&lt;/a&gt; 를 이용한 이미지 등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 대시보드 내에서 이미지 등록(docker://${image 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, SCDF Rest Client 를 지원하는 서버를 통해 CI 단계에서 이미지 빌드가 끝나면 자동으로 대시보드에 등록되도록 했다.&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;운영&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task의 경우 로직만 작성하면 대시보드에서 k8s Cron Job 을 손쉽게 등록할 수 있고 Task 실행시에 리소스도 제어할 수 있다는 장점이 있다. 그리고 무엇보다 Task 간의 단방향 의존 관계를 복잡한 코드가 아니라 UI로 줄을 이어서 실행하기만 하면 되는 것도 편리하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZB8AZ/btsNNE2myXc/5TWnm33Yne78eKKdGfD3k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZB8AZ/btsNNE2myXc/5TWnm33Yne78eKKdGfD3k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZB8AZ/btsNNE2myXc/5TWnm33Yne78eKKdGfD3k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZB8AZ%2FbtsNNE2myXc%2F5TWnm33Yne78eKKdGfD3k1%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;377&quot; height=&quot;257&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;610&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;1056&quot; data-origin-height=&quot;1552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEEtkP/btsNOmfAfxx/Kwpt4j8MCtsZycSP71j1DK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEEtkP/btsNOmfAfxx/Kwpt4j8MCtsZycSP71j1DK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEEtkP/btsNOmfAfxx/Kwpt4j8MCtsZycSP71j1DK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEEtkP%2FbtsNOmfAfxx%2FKwpt4j8MCtsZycSP71j1DK%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;421&quot; height=&quot;619&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;1552&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 data-ke-size=&quot;size16&quot;&gt;Stream 역시 UI 로 스트림 파이프라인을 관리할 수 있고 모니터링이나, 스케일링(replica)을 UI 레벨에서 수행할 수 있어서 매우 직관적이다. 또, 버전 관리를 자동으로 해주기 때문에 배포가 잘못되었을 때 버튼 하나로 롤백처리까지 가능하다는 장점이 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2864&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yOEtq/btsNMK94UFm/M1hwgOSAU6kuUSBSk8GRh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yOEtq/btsNMK94UFm/M1hwgOSAU6kuUSBSk8GRh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yOEtq/btsNMK94UFm/M1hwgOSAU6kuUSBSk8GRh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyOEtq%2FbtsNMK94UFm%2FM1hwgOSAU6kuUSBSk8GRh0%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;2864&quot; height=&quot;282&quot; data-origin-width=&quot;2864&quot; data-origin-height=&quot;282&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 data-ke-size=&quot;size16&quot;&gt;이 외에 스트림 관련 세부 설정은 스트림별 세부 설정 객체를 생성하고 주입하는 방식으로 운영하고 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1746609388560&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class CustomSinkConfig (
    private val inputMethodName: String,
    private val fixedDelay: Long = 10,
    private val autoOffsetReset: String = &quot;latest&quot;,
    private val metadataAge: Int = 30000,
    private val maxPollRecords: Int = 500,
    private val maxPollInterval: Long = 300000,
    private val concurrency: Int = 3,
): StreamConfig {

    companion object {
        val DEFAULT_SINK_CONFIG = { outputMethodName: String -&amp;gt; CustomSinkConfig(outputMethodName)}
    }

    override fun getConfigMap(): Map&amp;lt;String, Any&amp;gt; {
        return mapOf(
            &quot;spring.cloud.stream.function.bindings.${inputMethodName}-in-0: &quot; to inputName,
            &quot;spring.integration.poller.fixed-delay&quot; to &quot;${fixedDelay}ms&quot;,
            &quot;spring.kafka.consumer.auto-offset-reset&quot; to autoOffsetReset,
            &quot;spring.kafka.consumer.properties.metadata.max.age.ms&quot; to metadataAge,
            &quot;spring.kafka.consumer.max-poll-records&quot; to maxPollRecords,
            &quot;spring.kafka.consumer.properties.max.poll.interval.ms&quot; to maxPollInterval,
            &quot;spring.cloud.stream.bindings.${inputName}.consumer.concurrency&quot; to concurrency
        )
    }

    override fun getLocalConfigMap(): Map&amp;lt;String, Any&amp;gt; {
        throw UnsupportedOperationException(&quot;Not implemented&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&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;단일 애플리케이션에서 멀티스레딩만으로 성능을 극대화하는 데에는 한계가 있다. 처리해야 할 데이터의 양이 많은데 개별 처리가 가능한 경우, 데이터를 잘 분리해 여러 개의 배치 프로세스를 병렬로 실행할 수도 있지만, 이미 잘 구축된 스트림 처리 시스템이 있다면 모든 데이터를 토픽으로 발행하고 파티션을 늘린 뒤, 스트림 프로세스를 병렬로 실행하기만 하면 손쉽게 확장 가능한 병렬 데이터 처리 시스템을 구축할 수 있다. (물론 로그 파일이나, 정산 데이터처럼 한번에 집계되어야 하는 데이터는 Hadoop MapReduce, Spark 가 유리할 수 있으니 상황에 맞게 전환을 고려해야 한다)&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;내가 SCDF 를 도입할 때에 Spring Cloud Stream 이라는 프레임워크 자체에 대한 이해도가 낮았는데 SCDF를 도입하면서 Stream 데이터를 처리하는 방식에 대한 이해도 높아졌고 SCDF가 왜 나오게 되었는지 직간접적으로 느낄 수 있었다. 그리고 무엇보다 Stream 자체를 여러 유즈케이스에서 설명하는 것처럼 설정과 배포 과정에서 개발자가 신경써주어야 하는 많은 부분을 SCDF 가 담당하기 때문에 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이러한 관점에서 SCDF 는 (특히) 스트림을 관리하고 개발하는 개발자들에게 정말 용이한 도구라고 생각한다.&lt;/span&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;SCDF를 유용하게 쓰고 있기도 했고 노하우를 쌓아 나가는 입장에서 SCDF가 오픈소스 버전을 더이상 지원하지 않는다는 점은 매우 아쉽다 . 하지만, 여러 제한된 환경으로 인해 아직 SCDF 의 최신 버전조차 사용하지 않고 있기 때문에 당분간(&lt;s&gt;n년간&lt;/s&gt;) 큰 변동은 없을거라 생각한다. 개인적인 생각으론 현재 나와 있는 최신 버전 이후로 Spring Cloud Stream(또는 Kafka Binder) 이나 k8s 인터페이스에 변화가 없지 않는 이상 호환은 계속될 것이라고 생각한다. 그래서 최신 버전을 빠르게 따라가지 않아도 되는 프로젝트라면 조심스럽지만 SCDF 도입을 한번 검토해보길 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>spring</category>
      <author>PPakSang</author>
      <guid isPermaLink="true">https://ppaksang.tistory.com/48</guid>
      <comments>https://ppaksang.tistory.com/48#entry48comment</comments>
      <pubDate>Wed, 7 May 2025 18:53:03 +0900</pubDate>
    </item>
    <item>
      <title>Hazelcast 를 사용하며</title>
      <link>https://ppaksang.tistory.com/47</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;블로그에 Hazelcast 관련 포스팅을 했었는데 실제 서버를 운영하면서 느낀점을 포스팅 해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ppaksang.tistory.com/35&quot;&gt;https://ppaksang.tistory.com/35&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741133780650&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Hazelcast] Distributed Computing (Predicate)&quot; data-og-description=&quot;이번 포스팅에서는 Hazelcast를 프로젝트에서 사용하면서 정리한 내용을 간단하게 적고, 내가 겪은 Hazelcast 관련 문제에 대한 상황과 해결(?)한 방법을 설명하고자 한다. 본 포스팅에서는 Hazelcast 환&quot; data-og-host=&quot;ppaksang.tistory.com&quot; data-og-source-url=&quot;https://ppaksang.tistory.com/35&quot; data-og-url=&quot;https://ppaksang.tistory.com/35&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dNZPCC/hyYmUfQAxw/SwhMOTBZwBDRqO4IhI5WLK/img.png?width=800&amp;amp;height=644&amp;amp;face=0_0_800_644,https://scrap.kakaocdn.net/dn/bVU9Wg/hyYmYCsbbD/SKckNNZsoDIYJ8HhCc2rFk/img.png?width=800&amp;amp;height=644&amp;amp;face=0_0_800_644,https://scrap.kakaocdn.net/dn/GSjmc/hyYm5n1UtA/1l43eV0R1srvSEUmY1FhtK/img.png?width=1130&amp;amp;height=956&amp;amp;face=257_549_951_598&quot;&gt;&lt;a href=&quot;https://ppaksang.tistory.com/35&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ppaksang.tistory.com/35&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dNZPCC/hyYmUfQAxw/SwhMOTBZwBDRqO4IhI5WLK/img.png?width=800&amp;amp;height=644&amp;amp;face=0_0_800_644,https://scrap.kakaocdn.net/dn/bVU9Wg/hyYmYCsbbD/SKckNNZsoDIYJ8HhCc2rFk/img.png?width=800&amp;amp;height=644&amp;amp;face=0_0_800_644,https://scrap.kakaocdn.net/dn/GSjmc/hyYm5n1UtA/1l43eV0R1srvSEUmY1FhtK/img.png?width=1130&amp;amp;height=956&amp;amp;face=257_549_951_598');&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;[Hazelcast] Distributed Computing (Predicate)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 Hazelcast를 프로젝트에서 사용하면서 정리한 내용을 간단하게 적고, 내가 겪은 Hazelcast 관련 문제에 대한 상황과 해결(?)한 방법을 설명하고자 한다. 본 포스팅에서는 Hazelcast 환&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ppaksang.tistory.com&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;h2 data-pm-slice=&quot;1 3 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;Hazelcast란?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Hazelcast는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;여러 대의 컴퓨터 메모리를 하나의 메모리처럼 사용할 수 있는&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;IMDG(In-Memory Data Grid)를 지원하는 캐시 솔루션이다.&lt;/span&gt; 이 IMDG 기술을 사용해 Hazelcast는 클러스터를 구성하여 데이터를 분산 저장하고, 이를 기반으로 빠르고 확장 가능한 아키텍처를 제공한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;Peer-to-Peer 방식의 데이터 분산 처리&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Hazelcast의 가장 큰 특징 중 하나는 &lt;/span&gt;&lt;span&gt;&lt;b&gt;Peer-to-Peer 방식&lt;/b&gt;&lt;/span&gt;&lt;span&gt;으로 데이터를 저장하고 처리한다는 점이다. 즉, 클러스터를 구성하는 모든 노드가 동등한 역할을 하며, 데이터를 서로 공유하고 복제한다. 이러한 구조 덕분에 하나의 노드가 장애를 일으키더라도 다른 노드가 자동으로 데이터를 분배하고 처리할 수 있어 &lt;b&gt;높은 가용성(Availability)&lt;/b&gt;을 보장한다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;데이터 분산 및 복제 방식&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Hazelcast는 데이터를 &lt;/span&gt;&lt;span&gt;&lt;b&gt;파티션(Partition) 단위&lt;/b&gt;&lt;/span&gt;&lt;span&gt;로 나누어 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;키(Key) 기반의 &lt;/span&gt;&lt;span&gt;&lt;b&gt;해시 함수(Hash Function)&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 를 이용해 특정 파티션에 데이터를 저장한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;각 파티션은 &lt;/span&gt;&lt;span&gt;&lt;b&gt;백업본(Replica)&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 을 생성하여 여러 노드에 분산 저장된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;특정 노드가 다운되거나 장애가 발생하면, Hazelcast는 백업본을 활용하여 자동으로 데이터를 재분배한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이러한 데이터 분산 및 복제 방식 덕분에 Hazelcast는 &lt;/span&gt;&lt;span&gt;&lt;b&gt;데이터 유실을 방지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;하면서도 &lt;/span&gt;&lt;span&gt;&lt;b&gt;높은 성능과 확장성&lt;/b&gt;&lt;/span&gt;&lt;span&gt;을 제공할 수 있습니다.&lt;/span&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;2808&quot; data-origin-height=&quot;1030&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eaOOFi/btsMAMuAMeg/LZFaxfuBgyhU4PM6rKtrUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eaOOFi/btsMAMuAMeg/LZFaxfuBgyhU4PM6rKtrUK/img.png&quot; data-alt=&quot;Hazelcast Member-Client 아키텍처 및 파티션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eaOOFi/btsMAMuAMeg/LZFaxfuBgyhU4PM6rKtrUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeaOOFi%2FbtsMAMuAMeg%2FLZFaxfuBgyhU4PM6rKtrUK%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;928&quot; height=&quot;340&quot; data-origin-width=&quot;2808&quot; data-origin-height=&quot;1030&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Hazelcast Member-Client 아키텍처 및 파티션&lt;/figcaption&gt;
&lt;/figure&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;&amp;nbsp;&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;Hazelcast 는 크게 Member 와 Client 로 컴포넌트가 나뉜다고 볼 수 있는데&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;Member 는 실제 데이터를 저장하고 데이터를 분산 처리하는 노드의 역할을 수행한다고 볼 수 있고 Client 는 이 Member 에 접근해 데이터를 읽고 쓰는 주체를 의미한다. (물론 Client 도 내부적으로 Near Cache 를 사용해 데이터를 일부 저장한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;k8s&lt;/b&gt;&lt;/h4&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;Hazelcast 를 운영하면서 느낀 이점&lt;/b&gt;중 하나는 k8s 클러스터에서 배포하고 확장하기 편리하다는 것이다.&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;간단한 namesapce, service-name 설정과 k8s Rolebinding 구성만 하고 Helm Chart를 구성하기만 하면 바로 배포가 가능하다  &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;이것이 가능한 이유가 Hazelcast 에서 지원하는 &lt;b&gt;Auto Discovery&lt;/b&gt; 덕분인데 이것을 사용하면 Member 자체적으로 새 Member 가 추가되는 상황을 감지하고 peer-to-peer 방식으로 &lt;b&gt;&lt;span data-token-index=&quot;3&quot;&gt;파티션 리밸런싱&lt;/span&gt;&lt;/b&gt;을 수행한다. 이로 인해 &lt;span data-token-index=&quot;5&quot;&gt;기존 클러스터 구조를 유지&lt;/span&gt;하면서도 &lt;span data-token-index=&quot;7&quot;&gt;저장할 수 있는 데이터의 총량을 쉽게 확장&lt;/span&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;또, 이 과정에서 데이터를 여러 Member 에 걸쳐서 백업을 하기 때문에 한 노드가 죽더라도 백업본이 저장된 다른 노드에서 데이터를 서빙하기 때문에 전체적인 클러스터 가용성을 높일 수 있다&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;a href=&quot;https://docs.hazelcast.com/hazelcast/5.5/kubernetes/kubernetes-auto-discovery&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.hazelcast.com/hazelcast/5.5/kubernetes/kubernetes-auto-discovery&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741133492434&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;Kubernetes Auto Discovery | Hazelcast Documentation&quot; data-og-description=&quot;By default, Hazelcast distributes partition replicas (backups) randomly and equally among cluster members. However, this is not safe in terms of high availability when a partition and its replicas are stored on the same rack, using the same network, or pow&quot; data-og-host=&quot;docs.hazelcast.com&quot; data-og-source-url=&quot;https://docs.hazelcast.com/hazelcast/5.5/kubernetes/kubernetes-auto-discovery&quot; data-og-url=&quot;https://docs.hazelcast.com/hazelcast/5.5/kubernetes/kubernetes-auto-discovery&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.hazelcast.com/hazelcast/5.5/kubernetes/kubernetes-auto-discovery&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.hazelcast.com/hazelcast/5.5/kubernetes/kubernetes-auto-discovery&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Kubernetes Auto Discovery | Hazelcast Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;By default, Hazelcast distributes partition replicas (backups) randomly and equally among cluster members. However, this is not safe in terms of high availability when a partition and its replicas are stored on the same rack, using the same network, or pow&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.hazelcast.com&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;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Member - Client&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션과의 연동 구조는 데이터 처리 주체인 &lt;b&gt;Hazelcast-Member&lt;/b&gt;와 &lt;b&gt;Hazelcast-Client&lt;/b&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;Near Cache&lt;/b&gt;라 불리는 &lt;b&gt;로컬 캐시&lt;/b&gt;가 존재하는데, 이러한 &lt;b&gt;Near Cache에&lt;/b&gt; 자주 조회되는 데이터를 클라이언트 측에 캐시하여, &lt;b&gt;네트워크 트래픽을 줄이고 성능을 향상&lt;/b&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;또한 Near Cache는 클라이언트의 로컬 메모리에서 작동하기 때문에, 클라이언트의 인스턴스를 늘리기만 해도 &lt;b&gt;클러스터와 독립적으로 확장&lt;/b&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;데이터 일관성 문제&lt;/b&gt;가 발생할 수 있다고 생각할 수 있는데, Hazelcast는 데이터가 변경되었을 경우 &lt;b&gt;무효화(invalidation) 이벤트를 전파해서&lt;/b&gt; 클러스터 내에 있는 노드가 동일한 데이터를 갱신할 수 있도록 유도한다. &lt;b&gt;무효화(invalidation) 이벤트가 유실되는 상황까지&lt;/b&gt; 감지하여, 데이터가 오래된 경우 자동으로 &lt;b&gt;데이터를 갱신&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 읽기 부하가 많은 요청의 경우 네트워크 홉을 줄이는 것도 하나의 성능 개선이 될 수 있는데, Near Cache 존재 자체로 그 고민을 상당히 줄여주었던 것 같다.&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;1412&quot; data-origin-height=&quot;1044&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P4zyb/btsMCl3wwFn/41vdxc2nzzMQ9JwSKjSRs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P4zyb/btsMCl3wwFn/41vdxc2nzzMQ9JwSKjSRs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P4zyb/btsMCl3wwFn/41vdxc2nzzMQ9JwSKjSRs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP4zyb%2FbtsMCl3wwFn%2F41vdxc2nzzMQ9JwSKjSRs1%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;738&quot; height=&quot;546&quot; data-origin-width=&quot;1412&quot; data-origin-height=&quot;1044&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Spring Boot 연동&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 는 Hazelcast 와의 통합을 공식적으로 지원하고 있기 때문에 CacheManager 구성이 용이하고 이를 통해 캐시 관련 어노테이션을 그대로 사용할 수 있다. 이러한 이유로 다른 팀원 입장에서 캐시로 어떤 솔루션을 쓰고 있는지에 대해 크게 신경쓸 필요가 없을 것 같아서 Hazelcast를 선택하기도 했다. (스프링이 공식적으로 지원하는 캐시라는 것만으로도 한번 사용해볼 이유가 되긴 한다)&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;span&gt;&lt;/span&gt;&lt;a href=&quot;https://docs.hazelcast.com/hazelcast/5.3/spring/overview&quot;&gt;https://docs.hazelcast.com/hazelcast/5.3/spring/overview&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741135760731&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;Integrating with Spring | Hazelcast Documentation&quot; data-og-description=&quot;&amp;times; Send us your feedback Thank you for helping us improve Hazelcast documentation.&quot; data-og-host=&quot;docs.hazelcast.com&quot; data-og-source-url=&quot;https://docs.hazelcast.com/hazelcast/5.3/spring/overview&quot; data-og-url=&quot;https://docs.hazelcast.com/hazelcast/5.3/spring/overview&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.hazelcast.com/hazelcast/5.3/spring/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.hazelcast.com/hazelcast/5.3/spring/overview&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Integrating with Spring | Hazelcast Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;times; Send us your feedback Thank you for helping us improve Hazelcast documentation.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.hazelcast.com&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;1596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blpIhd/btsMBlJ6qyX/aJyv63U5F6o9r7IojWUwSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blpIhd/btsMBlJ6qyX/aJyv63U5F6o9r7IojWUwSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blpIhd/btsMBlJ6qyX/aJyv63U5F6o9r7IojWUwSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblpIhd%2FbtsMBlJ6qyX%2FaJyv63U5F6o9r7IojWUwSk%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;523&quot; height=&quot;733&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;1596&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한편, getAsync 요청에서 Serializable 이 구현되지 않은 데이터를 저장하고 읽어올 때 timeout 이 정상적으로 동작하지 않고 client 요청이 블로킹되는 문제가 있었는데 Hazelcast 쪽에 이슈를 남긴지 반년이 지났지만 아직 답변이 오진 않았다  (내가 뭔가 잘못 질문했나 싶어 살펴보니 답변 기간이 그냥 느린 것 같다...)&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;a href=&quot;https://github.com/hazelcast/hazelcast/issues/26454&quot;&gt;https://github.com/hazelcast/hazelcast/issues/26454&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741136220436&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;HazelcastCache.getAsync() timeout issue in Hazelcast Client &amp;middot; Issue #26454 &amp;middot; hazelcast/hazelcast&quot; data-og-description=&quot;Describe the bug We are using Hazelcast 5.3.6 and Hazelcast Client 5.3.6. When I attempt to retrieve cached data asynchronously using HazelcastCache.getAsync() (where the data has a read timeout of...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/hazelcast/hazelcast/issues/26454&quot; data-og-url=&quot;https://github.com/hazelcast/hazelcast/issues/26454&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bzcych/hyYm3KwSd7/FyoCp8cz8DXGs2b6KM3ZO1/img.png?width=1200&amp;amp;height=600&amp;amp;face=1001_106_1079_190,https://scrap.kakaocdn.net/dn/fpun9/hyYm7zplxp/MXen8huEsOGDqtYgAklz1K/img.png?width=1200&amp;amp;height=600&amp;amp;face=1001_106_1079_190&quot;&gt;&lt;a href=&quot;https://github.com/hazelcast/hazelcast/issues/26454&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/hazelcast/hazelcast/issues/26454&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bzcych/hyYm3KwSd7/FyoCp8cz8DXGs2b6KM3ZO1/img.png?width=1200&amp;amp;height=600&amp;amp;face=1001_106_1079_190,https://scrap.kakaocdn.net/dn/fpun9/hyYm7zplxp/MXen8huEsOGDqtYgAklz1K/img.png?width=1200&amp;amp;height=600&amp;amp;face=1001_106_1079_190');&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;HazelcastCache.getAsync() timeout issue in Hazelcast Client &amp;middot; Issue #26454 &amp;middot; hazelcast/hazelcast&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Describe the bug We are using Hazelcast 5.3.6 and Hazelcast Client 5.3.6. When I attempt to retrieve cached data asynchronously using HazelcastCache.getAsync() (where the data has a read timeout of...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&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;
&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;Hazelcast 는 다양한 모니터링 도구를 손쉽게 연동할 수 있도록 설정을 제공하고 이를 통해 매트릭을 추적할 수 있다.&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;나 역시 Management Center, prometheus -&amp;gt; grafana 연동으로 모니터링을 구성해 운영 간 사용하고 있다. Management Center 는 무료로 제공하는 모니터링 도구치고 UI 도 깔끔하고 제공하는 기능(Heap 사용량, CPU 사용량, read/write 부하 모니터링 등등) 도 좋은 것 같다.&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;다만, Management Center 는 라이센스가 없으면 클러스터 멤버를 최대 3개 밖에 모니터링 할 수 없다는 단점이 있긴 하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1732&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkmlzZ/btsMCpdKEbJ/ZFMgvS91DrMvfW9EvmcrF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkmlzZ/btsMCpdKEbJ/ZFMgvS91DrMvfW9EvmcrF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkmlzZ/btsMCpdKEbJ/ZFMgvS91DrMvfW9EvmcrF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkmlzZ%2FbtsMCpdKEbJ%2FZFMgvS91DrMvfW9EvmcrF0%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;1732&quot; height=&quot;702&quot; data-origin-width=&quot;1732&quot; data-origin-height=&quot;702&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 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;각각의 파티션은 50~100MB 수준의 데이터를 저장하는 것을 권장한다. (default partition 271 기준 25~30 GB 데이터를 저장할 때 좋은 효율을 보임)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이미지, GC 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hazelcast 는 최선 버전의 JDK 를 사용하는 것을 권장하고 있고(v5.3 기준 jdk17 까지 지원), JVM heap 16GB 이하, G1GC 환경에서 좋은 퍼포먼스를 보이고 G1GC 의 GCPause 로 인한 지연 시간은 옵션으로 최대 5ms 까지 낮추길 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Gracefulshutdown 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, member 가 다운되었을 때 데이터 손실 없이 다른 노드로 데이터로 마이그레이션 될 수 있도록 gacefulshutdown 옵션을 활성화 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Failover, Fail-Fast 구성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hazelcast 운영에 앞서 발생할 수 있는 장애 시나리오를 찾고 Failover, Fail-Fast 구성도 추가했다.&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;Hazelcast Client 는 기본적으로 아래와 같은 순서로 Member 와 Connection 을 유지하는데&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;주기적으로 heartbeat 를 보낸다&lt;/li&gt;
&lt;li&gt;heartbeat-timeout 이 되면 member 와의 connection 을 끊고 (일정 시간 뒤) 재연결 요청을 한다&lt;/li&gt;
&lt;li&gt;재연결이 실패하면(= 연결할 수 있는 member 가 존재하지 않으면), retry(reconnect) 단계로 들어간다&lt;/li&gt;
&lt;li&gt;cluster-connect-timeout 이 되면 더 이상 connect 요청을 하지 않는다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 아래와 같은 Client의 기본 옵션으로 인해 장애가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- Hazelcast Client 의 default 옵션으로 retry 는 SYNC 로 동작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- HazelcastCache 의 read timeout 이 설정되어 있지 않다&lt;/b&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;문제1.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Member 가 다운되었을 때 Retry 가 SYNC 모드이기 때문에 연결이 재수립될 때까지 캐시를 사용하는 요청이 블로킹된다&lt;/p&gt;
&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;990&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N21Ro/btsMBdyCuYM/jQr6ecxa15UiegTW9yqqQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N21Ro/btsMBdyCuYM/jQr6ecxa15UiegTW9yqqQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N21Ro/btsMBdyCuYM/jQr6ecxa15UiegTW9yqqQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN21Ro%2FbtsMBdyCuYM%2FjQr6ecxa15UiegTW9yqqQ1%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;540&quot; height=&quot;401&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;990&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제2.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 지연이 발생하거나 클러스터 부하가 심해져 데이터를 가져오는 속도가 느릴 때 클라이언트의 응답 시간에 영향(latency) 을 미칠 수 있다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;1130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I4hyT/btsMCn75Ztv/tm1KR1rKtgukUjbAkBR2vK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I4hyT/btsMCn75Ztv/tm1KR1rKtgukUjbAkBR2vK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I4hyT/btsMCn75Ztv/tm1KR1rKtgukUjbAkBR2vK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI4hyT%2FbtsMCn75Ztv%2Ftm1KR1rKtgukUjbAkBR2vK%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;515&quot; height=&quot;380&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;1130&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 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;heartbeat interval 및 timeout을 설정하고 총 i번의 응답 확인을 거쳐 connection 유지 여부 판단을 하도록 설정했다.&lt;/li&gt;
&lt;li&gt;retry 단계로 돌입하기 전 연결 요청에 대한 timeout 은 j초로 설정했다. (= j초간 연결 요청에 대한 응답이 없을 시 retry 단계로 돌입)&lt;/li&gt;
&lt;li&gt;reconnect-mode(retry) 의 경우 ASYNC 로 설정, 연결이 끊어진 상태에서 즉시 예외를 돌려주도록 해 이에 대한 즉각적인 핸들링이 가능하도록 했다.&lt;/li&gt;
&lt;li&gt;retry 요청의 경우 timeout 을 infinite 로 설정해 클러스터가 다시 회복될 때까지 재연결 요청이 가능하도록 했고 클라이언트가 많을 경우 요청이 몰리는 경우가 있기 때문에 exponential backoff + jitter 로 재연결 시도하도록 구성했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 운영 간에 팀 내 배포가 한번 잘못되어 클러스터 연결이 아예 끊어졌을 때가 있었는데 ASYNC 및 read timeout 구성으로 클러스터 장애를 감지하고 DB 에서 데이터가 서빙되도록 하여 네트워크가 복구되기까지 조금의 latency 상승 외에는 큰 영향 없이 서비스가 지속되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;블로그에 Hazelcast 관련 포스팅만 늘어나는 것 같은데 Hazelcast 사용자로써 아직 사용해보지 않은 기능들이 더 많기도 하고 많은 시나리오를 겪어보진 않은 것 같아서 앞으로도 천천히 운영 노하우를 길러보려고 한다&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;1년 정도 Hazelcast 를 사용해본 입장에서는 컨테이너 오케이스트레이션 환경이 디폴트가 된 요즘의 개발 환경에 적합한 솔루션을 찾고 운영해보면서 '아 이거 정말 못 써먹겠네' 하는 생각 없이 사용했다는 것만으로도 다른 사람들에게 추천할 수 있는 기술이라고 생각한다.&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;특히, 팀 내에서 k8s 환경에 캐시 클러스터를 구성할 일이 있다면 Hazelcast 를 고려해보는 것을 추천한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;잘못되거나 궁금한 내용 알려 주시면 감사하겠습니다!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>infra</category>
      <author>PPakSang</author>
      <guid isPermaLink="true">https://ppaksang.tistory.com/47</guid>
      <comments>https://ppaksang.tistory.com/47#entry47comment</comments>
      <pubDate>Thu, 6 Mar 2025 10:28:45 +0900</pubDate>
    </item>
    <item>
      <title>2024년 톺아보기</title>
      <link>https://ppaksang.tistory.com/46</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;어김없이 돌아온 톺아보기 &lt;s&gt;조금씩 톺아보기를 업로드 하는 날짜가 미뤄지는 것은 기분 탓인가...&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ppaksang.tistory.com/40&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ppaksang.tistory.com/40&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1735399994999&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;2023년 톺아보기&quot; data-og-description=&quot;작년에 이어 올해도 이 블로그에 23년 회고를 작성해보고자 한다. 지난 서두에서 그 어느 때보다 짧았던 2022년이라고 표현했는데, 그 말이 무색할 만큼 2023년은 너무 짧았고, 정신없던 한 해였던 &quot; data-og-host=&quot;ppaksang.tistory.com&quot; data-og-source-url=&quot;https://ppaksang.tistory.com/40&quot; data-og-url=&quot;https://ppaksang.tistory.com/40&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/MYzyb/hyXSy5Fhbd/OauCq7f8w6lv8W6WuZPt61/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/X3E6U/hyXSt4kat6/tGHeolum6MDPV93xvun5KK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cD7pRh/hyXSvOCNvF/nkBKVu4wkqVz7MWziD2Y4k/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://ppaksang.tistory.com/40&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ppaksang.tistory.com/40&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/MYzyb/hyXSy5Fhbd/OauCq7f8w6lv8W6WuZPt61/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/X3E6U/hyXSt4kat6/tGHeolum6MDPV93xvun5KK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cD7pRh/hyXSvOCNvF/nkBKVu4wkqVz7MWziD2Y4k/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&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;2023년 톺아보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;작년에 이어 올해도 이 블로그에 23년 회고를 작성해보고자 한다. 지난 서두에서 그 어느 때보다 짧았던 2022년이라고 표현했는데, 그 말이 무색할 만큼 2023년은 너무 짧았고, 정신없던 한 해였던&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ppaksang.tistory.com&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;늘 내 마음속에 담아두고 상기하는 배움, 성장, 도전 이라는 키워드는 올해도 유효했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 거기서 더 나아가 진정한 의미의 휴식을 조금이나마 알게된 해이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 내가 성장할 수 있는 환경에 스스로를 던지는 행동을 잘 하는데 이따금 그것이 나를 지치게 만들기도 했다.&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 그렇게 살아도 괜찮았다. 나는 '아직' 태울 에너지가 많고 앞으로 나아갈 의지가 확고하며 나를 죽이지 못하는 고통은 나를 더욱 강하게 만든다는 말에 고개가 끄덕여지기 때문이다. 그런데, 10년 20년이 지나도 괜찮을까? 결국 잘 달리기 위해서 잘 쉬는 것을 배울 필요가 있다고 생각했다. 그래서 잘 쉬는 방법을 찾는 것이 올 한 해 목표 중 하나였다.&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;잘 쉬기&lt;/b&gt;&lt;/h4&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일의 범위를 잘 정의하기 위해선 내가 그 일에 리소스를 얼마나 투여해야 하는지 즉, 내가 그 일을 얼마만에 끝낼 수 있는지에 대해서 잘 아는 것이 중요하다. 아무리 잘해도 완벽히 예측할 수는 없다. 다만, 그 오차를 점점 좁혀나가는 것이 관건이라고 생각한다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그렇기에 하루 아침에 되는 일은 아니라고 생각한다.&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;슬기로운 개발생활&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&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;- 주어진 요구사항을 일정안에 잘 구현하기(= 1인분 하기)&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;- 한 해 동안 파고들었던 기술을 주제로 대내외 발표하기&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 생각하는 '1인분'은 일정을 준수하면서 최대한 완성도 높은 코드(혹은 인프라)를 작성하는 것을 의미한다. 이를 위해 가장 중요했던 것은 변경에 대응할 수 있는 구조를 만들어가는 것이었다. 이건 비단 개발자에게만 필요한 역량이 아니라, 다양한 업무 영역에서도 요구되는 중요한 능력이라고 생각한다.&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;나는 직접 도입한 기술을 팀원들이 보다 쉽게 사용할 수 있도록 했다. 예를 들어, 복잡하고 반복적인 작업이 필요한 프레임워크스펙의 경우 공통적으로 사용하는 설정을 분리하고 어노테이션 기반으로 필요한 설정값을 넣을 수 있도록 구조를 바꾼다던가, 우리 팀에 적합한 배포 전략을 도입하기도 했다.&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;내 경험을 팀원들에게 효과적으로 공유할 수 있도록 문서화를 꾸준히 하기도 했다. 기술 개념부터, 사용 가이드, IT 컨퍼런스 세미나 참여 내용까지 20개 가까이 되는 글을 작성하고 공유했다. 내 생각을 잘 전달하는 방법을 찾는데 항상 관심이 많다.&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;운이 좋게도 올해 시간을 들여 살펴보고 팀내에 도입한 Hazelcast가 신규 프로젝트 운영환경에 안정적으로 안착되었고 이를 주제로 사내&amp;nbsp; 발표를 하기도 했다. 공개적인 발표를 준비하면서 특히 기술을 주제로 이야기를 하려다 보니 정확한 정보만을 전달하기 위해 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;아주 사소한 것도 여러번 검토했던 기억이 난다. 그러면서 수준 높은 영상을 제작해주신 개발자들이 다시 한번 존경스럽다는 생각이 들었다.&lt;/span&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-filename=&quot;blob&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WVHhB/btsLAVerChX/A2O5RLQT0ELnmIvCPbH1qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WVHhB/btsLAVerChX/A2O5RLQT0ELnmIvCPbH1qk/img.png&quot; data-alt=&quot;신입 개발자의 패기 하나만으로 발표했다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WVHhB/btsLAVerChX/A2O5RLQT0ELnmIvCPbH1qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWVHhB%2FbtsLAVerChX%2FA2O5RLQT0ELnmIvCPbH1qk%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;572&quot; height=&quot;448&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;730&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;신입 개발자의 패기 하나만으로 발표했다&lt;/figcaption&gt;
&lt;/figure&gt;
&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;사이드 프로젝트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 퇴근하고 나서는 대부분의 시간을 운동이나 사이드 프로젝트에 투자했다. 어쩌다보니 1년에 걸쳐 총 3개의 프로젝트를 하게 되었는데 다시 한번 과유불급하지 않는 자세를 가져야겠다고 생각이 들면서도ㅎㅎ 내가 얼마나 리소스를 쓸 수 있는지 그 한계치를 확인할 수 있어서 의미있는 경험이었다고 생각한다.&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;다소 웃긴건 3개의 프로젝트에서 사용하는 기술스택이 정말 제각각인데 스프링 베이스로 kotlin, java 로 나뉘고 java 는 jdk 버전이 상이하며 프로젝트는 gradle 과 maven 으로 나뉘고 메인 db는 mysql, mongo 그리고 클라우드마저 aws, ncp 를 나눠서 쓰다 보니 각각의 프로젝트를 수행하는데 스위칭 비용이 상당했다...&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;그래도 각각의 프로젝트마다 새로운 도전을 해봐서 좋았다. 가령 사용자 데이터를 AI 로 평가하는 파이프라인을 만들어 본다던지 운용 가능한 채팅 시스템 설계를 해보기도 했고 신규 프로젝트를 빠르게 셋업하는 능력은 덤으로 얻을 수 있었다. 모든 프로젝트가 아직 현재 진행형이고 내년에도 기술적 성장을 위한 발판이 될 수 있길 기대한다.&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;261&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSDIiB/btsLBvlL8ik/JkDtgZi8mAdkAHjjdpoY9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSDIiB/btsLBvlL8ik/JkDtgZi8mAdkAHjjdpoY9K/img.png&quot; data-alt=&quot;날씨별 옷차림을 추천해주는 '핏프티'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSDIiB/btsLBvlL8ik/JkDtgZi8mAdkAHjjdpoY9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSDIiB%2FbtsLBvlL8ik%2FJkDtgZi8mAdkAHjjdpoY9K%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;204&quot; height=&quot;209&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;날씨별 옷차림을 추천해주는 '핏프티'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;517&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJKSX5/btsLzYv2EXZ/6UoY1Cqe3kWFVTtgKqecDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJKSX5/btsLzYv2EXZ/6UoY1Cqe3kWFVTtgKqecDk/img.png&quot; data-alt=&quot;앱스토어에서 만날 수 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJKSX5/btsLzYv2EXZ/6UoY1Cqe3kWFVTtgKqecDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJKSX5%2FbtsLzYv2EXZ%2F6UoY1Cqe3kWFVTtgKqecDk%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;625&quot; height=&quot;319&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;517&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;앱스토어에서 만날 수 있다&lt;/figcaption&gt;
&lt;/figure&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;&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;1457&quot; data-origin-height=&quot;879&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brUEJz/btsLAvUCEVl/i2KXctiTBke29jqKAWa3hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brUEJz/btsLAvUCEVl/i2KXctiTBke29jqKAWa3hk/img.png&quot; data-alt=&quot;사용자를 지켜보고 집중 수준에 따라 코멘트를 달아 주는 FocusMonster&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brUEJz/btsLAvUCEVl/i2KXctiTBke29jqKAWa3hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrUEJz%2FbtsLAvUCEVl%2Fi2KXctiTBke29jqKAWa3hk%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;666&quot; height=&quot;402&quot; data-origin-width=&quot;1457&quot; data-origin-height=&quot;879&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사용자를 지켜보고 집중 수준에 따라 코멘트를 달아 주는 FocusMonster&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;학생 때 앞서 나간 많은 개발자들의 경험을 보고 들었던 것이 내게 큰 도움이 됐었고 나도 올해는 누군가에게 도움이 될 수 있는 나의 경험을 공유하고 개발자를 준비하는 많은 분들을 격려하기 위해 다방면으로 노력했다.&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;운 좋게도 그런 내 생각에 공감해주시는 분들이 계셨고, 개발자가 되기 위한 준비 과정과 그 속에서 느낀 점들을 바탕으로 멘토링 3회와 강연 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;올해 기획했던 강연이 생각보다 반응이 좋아서 앞으로도 꾸준히 진행하기로 했고 내년에는 더 많은 채널에서 예비 개발자들을 위해 활동하고 싶은 개인적인 바램이 있다. 그리고 그 과정에서 나도 함께 성장할 수 있다고 믿는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BFYP9/btsLAiuupl2/bGGzPzUp3gQgzGr2KWiUEk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BFYP9/btsLAiuupl2/bGGzPzUp3gQgzGr2KWiUEk/img.jpg&quot; data-alt=&quot;부트캠프 '스위프' 초청 강연&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BFYP9/btsLAiuupl2/bGGzPzUp3gQgzGr2KWiUEk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBFYP9%2FbtsLAiuupl2%2FbGGzPzUp3gQgzGr2KWiUEk%2Fimg.jpg&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;636&quot; height=&quot;477&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;부트캠프 '스위프' 초청 강연&lt;/figcaption&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;1140&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coSOEW/btsLzs5cv9P/kINHMdwanxgkjn0QOLJbn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coSOEW/btsLzs5cv9P/kINHMdwanxgkjn0QOLJbn1/img.png&quot; data-alt=&quot;모교 초청 강연&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coSOEW/btsLzs5cv9P/kINHMdwanxgkjn0QOLJbn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoSOEW%2FbtsLzs5cv9P%2FkINHMdwanxgkjn0QOLJbn1%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;655&quot; height=&quot;361&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;628&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모교 초청 강연&lt;/figcaption&gt;
&lt;/figure&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;s&gt;세상이 정말 좁다&lt;/s&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;826&quot; data-origin-height=&quot;1084&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsAsrh/btsLA8EG19b/KNvKMBBk4JvJvbbpH9svq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsAsrh/btsLA8EG19b/KNvKMBBk4JvJvbbpH9svq0/img.png&quot; data-alt=&quot;합격의 순간을 목격하는건 언제나 뭉클하다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsAsrh/btsLA8EG19b/KNvKMBBk4JvJvbbpH9svq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsAsrh%2FbtsLA8EG19b%2FKNvKMBBk4JvJvbbpH9svq0%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;320&quot; height=&quot;420&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;1084&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;합격의 순간을 목격하는건 언제나 뭉클하다&lt;/figcaption&gt;
&lt;/figure&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;아무리 바빠도 운동은 꾸준히 하려고 한다. 체력이 내 인생의 다음 스텝을 만들어 준다고 생각하고, 취업 준비를 하면서 운동다운 운동을 2년동안 하지 않은게 내 건강에 얼마나 나쁜 영향을 미친지 알았기 때문이다. 그래서 올해는 9월까지 테니스를 정말 미친듯이(?) 했다. 저녁이면 테니스를 쳤고 저녁에 약속이 생기면 새벽 6시에 테니스를 치고 출근했다. 10월부터 회사일이 조금 바빠져서 테니스는 잠시 쉬고 헬스를 하고 있는데 자꾸 테니스가 아른거려서 1월부터는 다시 시작해야겠다 생각하고 있다.&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;아쉬움&lt;/b&gt;&lt;/h4&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 새로운 기술을 많이 배우고 적용한 해였지만, 정작 읽은 기술서는 5권도 채 되지 않았다. 코드를 작성하고 인프라를 구축하는 것도 중요하지만, 다양한 방법론을 알고 이를 비교할 수 있는 능력이 중요하고 그런 능력을 키우는데 독서가 정말 중요한 역할을 한다고 생각한다. 때문에 올해 많은 경험은 쌓았지만, 스스로의 성장에 조금 더 관심을 기울였어야 했다는 아쉬움이 남는다.&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;내년에는&lt;/b&gt;&lt;/h4&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;display: none;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 아름이와 함께하는 즐거운 한 해가 되길 바란다!&lt;/p&gt;</description>
      <category>후기</category>
      <category>2024년</category>
      <category>개발자</category>
      <category>회고</category>
      <author>PPakSang</author>
      <guid isPermaLink="true">https://ppaksang.tistory.com/46</guid>
      <comments>https://ppaksang.tistory.com/46#entry46comment</comments>
      <pubDate>Sun, 29 Dec 2024 00:56:55 +0900</pubDate>
    </item>
    <item>
      <title>mulipart/form-data 다루기(feat. feign-client, restTemplate)</title>
      <link>https://ppaksang.tistory.com/44</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;multipart/form-data&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;multipart/form-data 는 http 를 사용해 데이터를 주고 받는 상황에서 하나의 body 에 여러 데이터를 넣어야 하는 경우를 구현하기 위해 만들어진 Content-Type 이다. (e.g 사진을 전송하는데 이에 대한 설명을 함께 포함해서 전송하고 싶은 경우에 사진은 image/jpeg 타입이지만, 설명은 text/plain 으로 전송해야 하는 경우, 웹브라우저 관점에서는 폼 데이터를 전송할 때 사용하는 Content-Type 이다)&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;670&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M69GF/btsGCWQbyuB/tur8eHhLg1GzgetuXtXET1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M69GF/btsGCWQbyuB/tur8eHhLg1GzgetuXtXET1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M69GF/btsGCWQbyuB/tur8eHhLg1GzgetuXtXET1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM69GF%2FbtsGCWQbyuB%2Ftur8eHhLg1GzgetuXtXET1%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;430&quot; height=&quot;330&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진은 HTTP Request 의 구조이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;multipart/form-data type의 데이터를 전송하겠다는 것의 의미는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ContentType 헤더 값으로 multipart/form-data 를 사용하는 것이고&lt;/li&gt;
&lt;li&gt;body 에 multipart-data 가 담긴다는 것이다.&lt;/li&gt;
&lt;/ul&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;그렇다면, multipart -data 대해서 자세히 알아보자&lt;/u&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각각의 Multipart 데이터(text, file, mp3 &amp;hellip;) 는 &lt;b&gt;boundary 라는 구분문자(delimiter)&lt;/b&gt; 에 의해 나뉘어지며, 데이터의 시작과 끝 부분을 나타내기도 한다&lt;/li&gt;
&lt;li&gt;첫번째 Boundary 가 나오기 전 데이터는 MIME을 지원하지 않는 클라이언트를 위해 사용된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;boundary 는 본문 내용과 충돌이 발생하지 않도록 임의의 문자열을 사용한다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;하나의 multipart 는 크게 &lt;b&gt;Content-Disposition, Content-Type Header, 데이터 본문(보통 binary)&lt;/b&gt; 로 나뉜다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 multipart/form-data request 중 핵심 요소를 추출한 예시다. 빨간 박스(엄청 많지만) 를 자세히 보면&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;boundary&lt;/b&gt; 가 정의되어 있고 사용되는 것을 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;multipart 각각의 ContentDisposition(form-data; name; filename 형식) &lt;b&gt;ContentType&lt;/b&gt; 이 존재한다.&lt;/li&gt;
&lt;li&gt;ContentDisposition 의 &lt;b&gt;name&lt;/b&gt; 은 일종의 multipart 데이터의 key 역할을 담당하고 &lt;b&gt;filename&lt;/b&gt; 은 실제 전송한 파일의 이름을 명시한다.&lt;/li&gt;
&lt;li&gt;마지막으로 실제 데이터 본문이 담긴다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/claHfF/btsGFz0yTbD/RE83O0XuhT2y8HLeAGNj8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/claHfF/btsGFz0yTbD/RE83O0XuhT2y8HLeAGNj8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/claHfF/btsGFz0yTbD/RE83O0XuhT2y8HLeAGNj8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclaHfF%2FbtsGFz0yTbD%2FRE83O0XuhT2y8HLeAGNj8k%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;518&quot; height=&quot;283&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;652&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 data-ke-size=&quot;size16&quot;&gt;다음은 본문의 주제이기도 한 코틀린으로 multipart/form-data 전송하는 방법에 대해서 알아보자&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ISSUE1&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 RestTemplate 을 통해 multipart/form-data 요청을 구성해서 전송하고자 아래와 같은 코드를 작성했다.&lt;/p&gt;
&lt;pre id=&quot;code_1713161838062&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val textHeaders = HttpHeaders().also { it.contentType = MediaType.APPLICATION_JSON }
val textEntity = HttpEntity&amp;lt;String&amp;gt;(objectMapper.writeValueAsString(uploadRequest), textHeaders)

val fileHeaders = HttpHeaders().also { it.set(&quot;Content-Type&quot;, &quot;audio/mp3&quot;) }
val file = ResourceUtils.getFile(&quot;classpath:epidemic/MUSIC_Under_the_sunshine.mp3&quot;)
val fileEntity = HttpEntity&amp;lt;ByteArray&amp;gt;(file.readBytes(), fileHeaders)

val body: MultiValueMap&amp;lt;String, Any&amp;gt; = LinkedMultiValueMap()
body.add(&quot;text&quot;, textEntity)
body.add(&quot;file&quot;, fileEntity)

val headers = HttpHeaders().also { it.contentType = MediaType.MULTIPART_FORM_DATA }
val requestEntity: HttpEntity&amp;lt;MultiValueMap&amp;lt;String, Any&amp;gt;&amp;gt; =
    HttpEntity&amp;lt;MultiValueMap&amp;lt;String, Any&amp;gt;&amp;gt;(body, headers)

val aodUpload: ResponseEntity&amp;lt;UploadResponse&amp;gt; = restTemplate.exchange(
    &quot;$host&quot;,
    HttpMethod.POST,
    requestEntity,
    UploadResponse::class.java)&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;request content-type 이 있는가 &amp;rarr; headers 변수에 담겨 있다. (o)&lt;/li&gt;
&lt;li&gt;text 의 name, filename 이 포함되어 있는가 &amp;rarr; name 은 &amp;ldquo;text&amp;rdquo; 로 잘 설정되어 있고 json 이라서 filename 은 존재하지 않는다. (o)&lt;/li&gt;
&lt;li&gt;file 의 name, filename 이 포함되어 있는가 &amp;rarr; name 은 &amp;ldquo;file&amp;rdquo; 로 설정되어 있는데 filename 이 없다 (문제 발생) (x)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filename 을 포함시키는 두 가지 방법&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;(비추) 직접 file 헤더를 구성한다 &amp;rarr; 번거롭고, 실수가 발생하기 쉬운 방법이다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clrboS/btsGDLnlXW0/dRTMBFqQcc8yIehQbtDmk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clrboS/btsGDLnlXW0/dRTMBFqQcc8yIehQbtDmk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clrboS/btsGDLnlXW0/dRTMBFqQcc8yIehQbtDmk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclrboS%2FbtsGDLnlXW0%2FdRTMBFqQcc8yIehQbtDmk0%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;1194&quot; height=&quot;276&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;spring 에서 제공하는 FileSystemResource 클래스를 사용한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/spring-rest-template-multipart-upload#uploading-a-single-file&quot;&gt;https://www.baeldung.com/spring-rest-template-multipart-upload#uploading-a-single-file&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;위 코드에서 file.readBytes() 부분을 FileSystemResource(file) 로 변경하기만 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 RestTemplate 내부 MessageConverter 에서 FileSystemResource 의 getFileName() 을 통해 filename 을 자동으로 세팅해주기 때문이다.&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 를 open-feign 을 사용해 호출하고 있기 때문에 &lt;b&gt;feign-client&lt;/b&gt; 를 사용하는 방식으로 변경해야 한다.&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;ISSUE2&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestTemplate &amp;rarr; feign-client 로 변경 하는 것은 크게 어렵지 않아서 생략하겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1713161899332&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val  audioUploadFeignClient: AudioUploadFeignClient
... // HttpEntity 객체 구성 코드
audioUploadFeignClient.upload(body) // body: HttpEntity&amp;lt;MultiValueMap&amp;lt;String,Any&amp;gt;&amp;gt;&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;SpringEncoder 에 이러한 LinkedMultiValueMap 을 파싱하는 로직이 존재하지 않아 빈 요청이 만들어지는 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2120&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRkbpC/btsGBY2ionC/nplhokfr4Lz2mn1Oj9MfJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRkbpC/btsGBY2ionC/nplhokfr4Lz2mn1Oj9MfJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRkbpC/btsGBY2ionC/nplhokfr4Lz2mn1Oj9MfJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRkbpC%2FbtsGBY2ionC%2Fnplhokfr4Lz2mn1Oj9MfJ0%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;2120&quot; height=&quot;272&quot; data-origin-width=&quot;2120&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법으로는 LinkedMultiValueMap 을 파싱하는 로직을 포함하는 Encoder(Writer) 를 구현해서 추가하는 방법도 있다. 아래 링크에 좋은 예시가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-cloud/spring-cloud-openfeign/pull/314/files&quot;&gt;https://github.com/spring-cloud/spring-cloud-openfeign/pull/314/files&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;하지만, 기본적으로 주어진 스펙을 사용해서 해결하면 좋을 것 같고 아래에 그 방법이 나와 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/java-feign-file-upload&quot;&gt;File Upload With Open Feign | Baeldung&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;feign-client&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, multipart/form-data 요청을 전송하는 FeignClient 코드를 살펴보자&lt;/p&gt;
&lt;pre id=&quot;code_1713162815093&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FeignClient(
    name = &quot;audio-upload-client&quot;,
    url = &quot;\${url}&quot;
)
interface AudioUploadFeignClient {

    @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
    fun upload(
        @RequestPart(&quot;text&quot;) text: MultipartFile,
        @RequestPart(&quot;file&quot;) file: MultipartFile,
    ) : UploadResponse
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumes 은 일종의 Request Content-Type(multipart/form-data) 로 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각각의 파라미터를 통해 multipart 데이터를 정의할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(text: MultipartFile / file: MultipartFile)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MultipartFile 이 아닌 primitive 타입을 넘기면 text/plain type 의 데이터가 들어간다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MultipartFile 은 쉽게 생각하면 Body의 boundary 사이에 들어갈 데이터를 표현한다고 볼 수 있다.&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;파라미터로 MultipartFile 의 구현체를 넘겨야 하는데, Spring 에서 구현 해놓은 MockMultipartFile 을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/mock/web/MockMultipartFile.html&quot;&gt;Spring 에서는 테스팅에 사용하는 것이 유용하다고 하지만&lt;/a&gt;, 내부 구현을 살펴보면 필요한 함수(getName, getOriginalFilename, getContentType, getBytes&amp;hellip;) 가 의도대로 구현되어 있어 일반적으로 사용해도 된다고 판단했다.&lt;/p&gt;
&lt;figure id=&quot;og_1713162859488&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;MockMultipartFile (Spring Framework 6.1.6 API)&quot; data-og-description=&quot;getContentType Return the content type of the file. Specified by: getContentType&amp;nbsp;in interface&amp;nbsp;MultipartFile Returns: the content type, or null if not defined (or no file has been chosen in the multipart form)&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/mock/web/MockMultipartFile.html&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/mock/web/MockMultipartFile.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/mock/web/MockMultipartFile.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/mock/web/MockMultipartFile.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;MockMultipartFile (Spring Framework 6.1.6 API)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;getContentType Return the content type of the file. Specified by: getContentType&amp;nbsp;in interface&amp;nbsp;MultipartFile Returns: the content type, or null if not defined (or no file has been chosen in the multipart form)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>spring</category>
      <author>PPakSang</author>
      <guid isPermaLink="true">https://ppaksang.tistory.com/44</guid>
      <comments>https://ppaksang.tistory.com/44#entry44comment</comments>
      <pubDate>Mon, 15 Apr 2024 15:26:47 +0900</pubDate>
    </item>
    <item>
      <title>Github Actions 괜찮네</title>
      <link>https://ppaksang.tistory.com/43</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개인 프로젝트를 진행하면서 코드 변경 사항을 테스트, 빌드하고 빠르게 배포하고자 간단한 CI/CD 를 구축할 필요가 있었다.&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;파이프라인을 무엇으로 구축해볼까... 하다가 관성적으로 Jenkins 가 먼저 떠올랐는데, 살짝 지루할 것 같기도 했고 개념적으로만 이해하던 Github Actions 을 이번 기회에 직접 써볼까 해서 사용하게 되었다.&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;389&quot; data-origin-height=&quot;129&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bK1DhH/btsEIPxVWvd/wOxUKiX0YZKDUmfNqXKH2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK1DhH/btsEIPxVWvd/wOxUKiX0YZKDUmfNqXKH2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bK1DhH/btsEIPxVWvd/wOxUKiX0YZKDUmfNqXKH2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK1DhH%2FbtsEIPxVWvd%2FwOxUKiX0YZKDUmfNqXKH2K%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;389&quot; height=&quot;129&quot; data-origin-width=&quot;389&quot; data-origin-height=&quot;129&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 data-ke-size=&quot;size16&quot;&gt;이번에 Github Actions 를 도입하면서 내가 확인해보고 싶었던 것들은 아래 두 가지고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Github Acitons workflow 작성 방법의 간결함 (-&amp;gt; 문법이 익숙하지 않은 동료들에게 설명하기 쉬운가)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Github Actions 을 통한 파이프라인(workflow) 구축 속도 및 확장성 (-&amp;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;이것 외에 성능도 고려하지만 이건 runner 를 직접 배포할 수 있는 것을 확인 했고, 추후 운영 단계에서 필요한 만큼 스케일 아웃 하는데 문제는 없겠다고 생각했다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터 내가 Github Actions 를 쓰면서 느낀점을 이야기 해보겠다&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;생각하기 쉽다&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 CI/CD 도구들은 Github 과 동일한 환경에서 동작하지 않기 때문에 보통 첫 번째 스텝으로 Github 에서 파일들을 가져오는 과정을 거친다. 인증, 디렉토리 구조 등등 한번 세팅해두면 크게 신경 안써도 되긴 하지만, 나처럼 새로운 프로젝트에서 빠르게 빌드업하고 싶은 경우 Github Actions 는 이 과정을 손쉽게 구현 가능하다.&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; 아래에 추가적으로 설명하겠지만 workflow 구성을 도와주는 다양한 플러그인(action)들이 있다. Github Actions 는 기본적으로 Github 에서 생성된 Context 를 사용할 수 있기 때문에 'checkout' 플러그인을 사용하기만 하면 프로젝트 파일들을 작업 경로에 가져올 수 있고 이 모든 작업이 Github 내에서 정의 가능하기 때문에 과정을 생각하기 쉽다.(파일을 관리하기 쉽다)&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;workflow 는 적당히 작성하기 편리하다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;workflow 는 yml 파일로 작성하고 크게 'on' 으로 workflow 트리거를 설정하고 'job'(또 세부 'step' 으로 나뉜다) 으로 실행할 명령을 정의한다. 여기서 job 과 step 을 작성하고 workflow 를 실행하면 Github Actions 페이지에서 UI 로 실행 과정을 보여주는데 이걸 확인해보면 workflow 구조가 얼마나 직관적인지 알 수 있다.&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;workflow 구성에 사용되는 플러그인도 너무 강력하다. docker registry 커넥션을 구축하고 클라우드(VM 혹은 k8s cluster) 에 배포하는 과정을 별도의 스크립트 작성 없이 플러그인(+ 몇 줄의 코드) 만으로 구성이 가능하다.&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;개인적으로 한 가지 아쉬운 점은 yml 로 스크립트를 작성하는 것이 (가끔) 불편하다는 것이다. 대부분의 경우에는 간단한 스크립트를 작성하기에 불편함이 전혀 없지만 스크립트 내부에 분기(조건문, 반복문 등등)를 포함하게 되면 파일이 상당히 지저분해질 수 있다. 이를 위해 custom action 을 정의하고 import 하는 방식으로 어느 정도 해결할 수는 있지만 코드를 작성하다 보면 속도감이 떨어진다는 생각이 들곤 한다.&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;따라서, 지금은 스크립트를 custom action 으로 손쉽게 추출하고, workflow 자동 완성을 도와주는 도구가 있으면 어떨까 하는 생각이 든다(웹에서 제공하는 작성 도구는 살짝 아쉽다...). 사실 이건 helm chart(yml + go) 를 작성하면서도 비슷하게 느낀 것이기도 한데, 지금 상태로도 충분히 쓸만하기도 해서.. 발전시키는데 드는 리소스가 더 비효율적일 것 같다는 생각도 든다.&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;파이프라인 구성이 용이하다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;workflow 는 job 단위로 파이프라인을 구축하는데 job 을 명시하는 것만으로 병렬 처리가 가능하고, 순서가 보장되어야 하는 job 또한 손쉽게 설정(needs) 할 수 있다. 병렬 처리의 경우 jenkins 는 별도로 스크립트 내에서 parallel 설정을 해줘야 한다.&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;개인적으로는 병렬 처리를 기본으로 한 Github Actions 이 유용하다고 생각한다. 왜냐하면 순차 처리가 기본이면 병렬 처리하는 코드가 순차적인 코드 내에 작성되는(A - (B, C, D) - E) 반면, 병렬 처리가 기본이면 (A, B, C, D, E) 작업을 아무 순서(정확히 말하면 관리하기 쉬운 순서)로 정의하고 의존하는 작업을 명시해주기만 하면 되기 때문에 작업을 관리하기 보다 효율적이라고 생각하기 때문이다.&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;마무리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 코드 변경 사항을 테스트하고 EC2 에 컨테이너로 배포하는 과정을 도입하는데 Github Actions 을 사용해봤다. 퇴근 후에 개인적인 시간을 내서 진행하는 프로젝트이니 만큼 상대적으로 많은 시간을 쏟지 못하는 상태라 인프라 측면에서 간결하고 직관적인 세팅을 하고자 하는데 Github Actions 은 그 목적을 달성하는데 굉장히 유용한 도구다.&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;Jenkins 인스턴스를 구성하고 파이프라인을 구축하는데 익숙한 덕도 있지만, workflow 문법을 파악하고 Github Actions 에서 제공하는 public runner(스크립트 실행 주체) 를 이용해 파이프라인을 구축하는데 반나절이 안 걸린 만큼 프로젝트 초기에 도입하기에 좋은 기술이라고 생각한다. 또한, 배포가 아니더라도 Github 에서 생성되는 다양한 이벤트를 손쉽게 핸들링할 수 있다는 점을 기억하고 있으면 여러 방면으로 활용 가능하다고 생각한다.&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;&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;b&gt;&lt;u&gt;혹시라도&lt;span&gt;&amp;nbsp;&lt;/span&gt;내용에 오류가 있거나 더 좋은 방법이 있다면 알려주시면 감사하겠습니다!&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>infra</category>
      <author>PPakSang</author>
      <guid isPermaLink="true">https://ppaksang.tistory.com/43</guid>
      <comments>https://ppaksang.tistory.com/43#entry43comment</comments>
      <pubDate>Sun, 11 Feb 2024 23:53:28 +0900</pubDate>
    </item>
    <item>
      <title>Hazelcast Cluster (feat. hazelcast platform v5.3)</title>
      <link>https://ppaksang.tistory.com/42</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ppaksang.tistory.com/35&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;지난 포스팅&lt;/a&gt; 에서 Hazelcast 에 대해 간략히 알아보고 user-code-deployment 를 사용해 Distributed Computing 과정에서 발생하는 문제점을 해결했다.&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;그때는 팀에서 이미 구축해놓은 클러스터에서 작업을 진행 했었는데, hazelcast 에 대해 조금 더 깊게 알아보고 싶고 버전업도 할 겸 직접 클러스터를 배포해 보았다.&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;v5.0 부터는 IMDG 가 아닌 hazelcast platform 으로 이름이 바뀌면서 이미지 내부 폴더 구조나 설정 파일, 클러스터 시작 방법 등등이 변경되었다.&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;개인적으로는 말 그대로 platform 의 의미를 살려서 hazelcast 중심의 생태계(손쉬운 배포부터 세부 설정 방법까지 제공, 플러그인 강화 등등) 를 구축해나가려고 하는 것 같았다. 캐시라는 주제 하나로 기술을 이렇게 다각화 할 수 있다는게 신기하기도 하고, 새삼 캐시 클러스터를 이렇게 간단히 구축할 수 있게 만든게 대단하기도 하다.&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;서버 배포를 위해 공식 문서를 보다 보면 문득 &quot;그래서 어떻게 하는건데&quot; 라는 생각과 함께 작성자분들이 공들여 인덱싱 해 놓은 문서에서조차 더 빠른 인덱스를 원할 때가 있다... (chart 는 어딨는지, 설정 파일 경로들은 어느 섹션에 있는지, &lt;s&gt;이렇게 하는게 맞는지&lt;/s&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;Hazelcast Cluster 를 배포하시는 개발자 김철수님에게 바칩니다&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;a href=&quot;https://docs.hazelcast.com/hazelcast/5.3/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs v5.3&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/hazelcast/hazelcast&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;hazelcast github&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/hazelcast/charts/tree/master/stable&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;hazelcast chart&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/hazelcast/hazelcast/blob/master/hazelcast/src/main/resources&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;hazelcast config examples&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고  &lt;a href=&quot;https://docs.hazelcast.com/hazelcast/5.3/configuration/understanding-configuration&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; Configure and Manage Clusters&lt;/a&gt;&amp;nbsp;를 참고해서 세부 설정이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외에도 이미지에서 /opt/hazelcast/bin 경로에 파일들이 특히 많이 도움되었다.&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;&amp;nbsp;&lt;/p&gt;</description>
      <author>PPakSang</author>
      <guid isPermaLink="true">https://ppaksang.tistory.com/42</guid>
      <comments>https://ppaksang.tistory.com/42#entry42comment</comments>
      <pubDate>Sat, 13 Jan 2024 20:06:07 +0900</pubDate>
    </item>
    <item>
      <title>2023년 톺아보기</title>
      <link>https://ppaksang.tistory.com/40</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;작년에 이어 올해도 이 블로그에 23년 회고를 작성해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 서두에서 그 어느 때보다 짧았던 2022년이라고 표현했는데, 그 말이 무색할 만큼 2023년은 너무 짧았고, 정신없던 한 해였던 것 같다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;2023년은 지난 날의 노력에 대한 작은 결실을 이룬 기념비적인 해이기도 하지만 그 과정에서 스스로 그 어느 때보다 불안했던 한 해라고 말하고 싶다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1703400183733&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;2022년 톺아보기&quot; data-og-description=&quot;그 어느 때보다 짧았던 2022년 한 해가 끝나간다. 오늘 졸프 최종 성과발표회가 끝나 찐종강을 하기도 했고, 올해 무엇을 했나 정리도 할 겸 어찌보면 고리타분할 수 있는 한 해 마무리를 주제로 &quot; data-og-host=&quot;ppaksang.tistory.com&quot; data-og-source-url=&quot;https://ppaksang.tistory.com/23&quot; data-og-url=&quot;https://ppaksang.tistory.com/23&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bpvDs8/hyUPIw849Z/k9IWX3Q6eDMd7Ye0g9JeBk/img.jpg?width=800&amp;amp;height=1066&amp;amp;face=234_439_587_502,https://scrap.kakaocdn.net/dn/kyLkF/hyUTCoukEF/8ekbgfqHSbQhrb0sU20tZ1/img.jpg?width=800&amp;amp;height=1066&amp;amp;face=234_439_587_502,https://scrap.kakaocdn.net/dn/3acOB/hyUPIRqEGm/0cWhWzDe5IilxkrsLqQRRk/img.jpg?width=3024&amp;amp;height=4032&amp;amp;face=875_1656_2217_1902&quot;&gt;&lt;a href=&quot;https://ppaksang.tistory.com/23&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ppaksang.tistory.com/23&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bpvDs8/hyUPIw849Z/k9IWX3Q6eDMd7Ye0g9JeBk/img.jpg?width=800&amp;amp;height=1066&amp;amp;face=234_439_587_502,https://scrap.kakaocdn.net/dn/kyLkF/hyUTCoukEF/8ekbgfqHSbQhrb0sU20tZ1/img.jpg?width=800&amp;amp;height=1066&amp;amp;face=234_439_587_502,https://scrap.kakaocdn.net/dn/3acOB/hyUPIRqEGm/0cWhWzDe5IilxkrsLqQRRk/img.jpg?width=3024&amp;amp;height=4032&amp;amp;face=875_1656_2217_1902');&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;2022년 톺아보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;그 어느 때보다 짧았던 2022년 한 해가 끝나간다. 오늘 졸프 최종 성과발표회가 끝나 찐종강을 하기도 했고, 올해 무엇을 했나 정리도 할 겸 어찌보면 고리타분할 수 있는 한 해 마무리를 주제로&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ppaksang.tistory.com&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;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;올해 졸업을 했다. 본래 8학기 졸업을 하면 내년 2월이 되겠지만, 한 학기 당겨서 졸업했다. 갑작스러운 결정은 아니지만 그렇다고 무조건 실행할 계획도 아니었다. 되도록 빠르게 커리어를 시작하고 싶었고, 졸업 예정자의 자격(?)은 커리어를 시작하는데 있어 개인적으로 큰 제약으로 다가왔었다. 그래서 2년 전부터 한 학기에 수업을 1~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;그 덕분에 올해 내가 꿈꾸던 국내 기업에서 커리어를 쌓을 기회를 얻었다. 주변에서 많은 축하를 받았고 여기까지 오는데 많은 도움을 준 사람들을 잊지 못한다. 얼마 뒤면 입사 후 6개월이 되지만 아직도 가끔 실감 나지 않는다.&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;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;배움, 성장, 도전&lt;/b&gt;으로 꼽고 싶고 여기에 올해 자주 하던 말인 '할 수 있다'는 말을 추가하고 싶다. 나 스스로에게 하던 말이기도 하지만, 다 같이 달려가는 분위기를 만들고 싶었고 결과적으로 내 주변 친구들과 함께 여러 성취를 이룰 수 있었다. 내년에도 이 마인드셋을 유지하고 싶다.&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;최근 생활&lt;/b&gt;&lt;/h4&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;읽고 싶던 책들도 조금씩 꺼내 읽는데 독서 스터디에서 팀원들과 함께 읽고 있다. 올해 잘 한 일 중에 하나가 독서 스터디를 진행한 것인데, 적잖게 독서를 할 수 있는 동기부여를 주기도 하고, 혼자 읽을 때보다는 함께 읽고 생각을 공유하는게 지루하지도 않다. (수다는 덤)&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;새롭게 합류한 팀에 적응하는데도 최선을 다하고 있다. 이전 팀에 처음 합류하고 얼마 안돼 조직이 강제 이동(?) 돼서 적응하는데 다소 힘들긴 했는데, 또 쉽게 못해볼 경험을 빠르게 한다고 생각하니 마음이 편해졌다. 새로운 팀에서는 상대적으로 경험이 부족한 내가 팀에 어떤 기여를 할 수 있을지 계속해서 고민하는 편이고 신입이지만 신입처럼 보이지 않는 것에 최선을 다하고 있다. 최근에 다시금 깨달은 한 가지가 기술 학습은 끝이 없는 것인데 업무를 하면서 따로 학습해 볼 기술들을 메모해 놓은 것이 벌써 터져 나가려고 한다. 요것도 부지런히 학습해야지&lt;/span&gt;&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;작년과 올해 비교&lt;/b&gt;&lt;/h4&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;앞으로&lt;span&gt; &lt;/span&gt;어떻게&lt;span&gt; &lt;/span&gt;성장할&lt;span&gt; &lt;/span&gt;것인가&lt;/b&gt;&lt;/h4&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;s&gt;유지보수 어렵게 하는 코딩법엔 관심 없다&lt;/s&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회화 학습도 시작하고자 하는데, 아직까지는 우선순위에서 밀렸지만 내년에는 예정된 일들을 모두 쳐내고 시작했으면 하는 바램이 있다. 학습이 마냥 즐겁지 않다는 건 알고 있지만, 어떻게 하면 조금 더 즐겁게 배울 수 있을까 고민하고 있다. 물론 이걸 핑계로 여행도 다니고 할 생각이다. 궁극적으로 20대에 미국에서 일할 기회를 얻고 싶다.&lt;/p&gt;</description>
      <category>후기</category>
      <author>PPakSang</author>
      <guid isPermaLink="true">https://ppaksang.tistory.com/40</guid>
      <comments>https://ppaksang.tistory.com/40#entry40comment</comments>
      <pubDate>Mon, 25 Dec 2023 17:45:40 +0900</pubDate>
    </item>
    <item>
      <title>Spring Rest Docs</title>
      <link>https://ppaksang.tistory.com/39</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;585&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RZ0rZ/btsBGpb2p1G/MEXrJ112VrFOSQgR0Z9Knk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RZ0rZ/btsBGpb2p1G/MEXrJ112VrFOSQgR0Z9Knk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RZ0rZ/btsBGpb2p1G/MEXrJ112VrFOSQgR0Z9Knk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRZ0rZ%2FbtsBGpb2p1G%2FMEXrJ112VrFOSQgR0Z9Knk%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;532&quot; height=&quot;389&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;585&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Rest Docs 는 API 인터페이스를 손쉽게 만들기 위해서 사용하는데, Spring MVC Test 혹은 WebTestClient를 통해 생성된 파일(adoc 파일, snippets)을 조합하는 방식으로 수행한다. 비슷한 목적으로 Swagger를 사용하곤 한다.&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;Swagger의 경우 프로덕션 코드에 Swagger 코드가 섞여 들어가는게 다소 부담됐고 변화하는 API 스펙에 맞춰 Swagger 코드 또한 수정해야 했었는데, Rest Docs는 테스트 코드를 바탕으로 문서가 생성되기 때문에 이러한 문제가 해결 된다. 즉, 테스트를 통과한 코드에 대해서 문서를 생성하기 때문에 잘못된 문서를 제공할 일이 줄어든다.&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;물론, Swagger를 사용하면 웹에서 API 호출 테스트를 손쉽게 수행할 수 있기 때문에 기호에 맞게 섞어 사용해도 무방하다고 생각한다. 또한, Rest Docc의 경우 adoc 파일을 개발자가 직접 조합해서 완성된 문서를 생성해야 하는 등 조금은 더 공수가 들어간다고 볼 수 있다.&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 Rest Docs 를 프로젝트에 도입하는 것과 관련해 많은 글들이 있지만, 이해가 되지 않는 부분에 대해서는 공식 문서를 참고하는 것도 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1702209953378&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;Spring REST Docs&quot; data-og-description=&quot;Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test or WebTestClient.&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/&quot; data-og-url=&quot;https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Spring REST Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test or WebTestClient.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.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;Rest Docs 문서(html) 생성은 크게 세 가지 단계로 나누어 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;adoc 문서는 Spring Rest Docs 에서 생성하고 관리하는 문서 확장자 중 하나이다.&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;1. 테스트 코드 작성/실행 -&amp;gt; adoc 문서 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. adoc 문서 조합 -&amp;gt; 완성된 문서를 위한 adoc 문서 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. adoc 문서를 html 문서로 변환&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;위와 같은 작업을 위해서  관련 의존성 설정 및 Task Pipeline 을 작성해야 한다. (본문은 kotlin 8.5 기준)&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;requirements&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Rest Docs를 사용하기 위해 최소 &lt;u&gt;&lt;b&gt;Java17, Spring Framework 6&lt;/b&gt;&lt;/u&gt; 이상의 환경이 &lt;a href=&quot;https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#getting-started-requirements&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;세팅되어 있어야 한다&lt;/a&gt;.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;build.gradle.kts&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;adoc 문서를 html로 변환하기 위해 사용되는 플러그인을 추가한다 (gradle 7.x 이상)&lt;/p&gt;
&lt;pre id=&quot;code_1702212204744&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;id(&quot;org.asciidoctor.jvm.convert&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asciidoctorExt configuration에 asciidoctorExtensions 의존성을 주입해준다. 각종 자동 설정이 추가 되고, 대표적으로 adoc 파일이 build/generated-snippets 경로에 저장되도록 설정해준다.&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 MVC Test를 할 때 Rest Docs에서 제공하는 메소드를 사용해야 하기 때문에 이를 위한 의존성도 같이 추가해 준다.&lt;/p&gt;
&lt;pre id=&quot;code_1702216001283&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val asciidoctorExt by configurations.creating

dependencies {
    asciidoctorExt(&quot;org.springframework.restdocs:spring-restdocs-asciidoctor&quot;)
    testImplementation(&quot;org.springframework.restdocs:spring-restdocs-mockmvc&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적인 adoc 문서 생성 -&amp;gt; html 변환 -&amp;gt; 최종 빌드 결과물 산출의 파이프라인을 kotlin dsl을 활용해 설정했다.&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;1. test와 asciidoctor 에서의 output, input dir 설정은 &lt;a href=&quot;https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#getting-started-build-configuration&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;incremental builds를 위해 설정&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 위에서 주입 받은 asciidoctorExt configurations를 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. copyDocs는 프로젝트 빌드 결과물에 html 파일을 static 디렉토리 아래에 포함시키기 위해 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;별도로 추가한 task 이다 (팀의 정책에 따라 경로를 수정하면 된다. finalizedBy 를 통해 html 변환이 완료된 후 수행한다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;4. baseDirFollowsSourceFile 셋팅 시 baseDir 이 모듈 루트에서, null로 셋팅해 루트 외부에 존재하는 adoc 파일에도 접근 가능하게 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;5. 기존 빌드 작업에  추가(asciidoctor 수행 후 bootJar 수행)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1702216544306&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tasks {
    val snippetsDir = file(&quot;build/generated-snippets&quot;)
    test {
        outputs.dir(snippetsDir)
        useJUnitPlatform()
    }

    val copyDocs by registering(Copy::class) {
        from(&quot;build/docs/asciidoc&quot;)
        into(&quot;build/resources/main/static/docs&quot;)
    }

    asciidoctor {
        dependsOn(test)
        configurations(&quot;asciidoctorExt&quot;)
        inputs.dir(snippetsDir)

        baseDirFollowsSourceFile()

        finalizedBy(copyDocs)
    }

    bootJar {
    	dependsOn(asciidoctor)
    }
}&lt;/code&gt;&lt;/pre&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;Test 를 통한 Snippets 생성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 언급했듯 Rest Docs는 Test 코드 실행 결과를 바탕으로 생성된 Snippets(.adoc) 을 바탕으로 완성된 Html 문서를 만든다고 했다.&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;간단한 MockMvc 테스트 코드를 작성해 보자 (본문에서 MockMvc 사용 및 테스트 코드 작성은 간단히 한다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;304&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5nYcV/btsBJcivNgv/Aq29kKrgFFDNcy9AgY7ruK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5nYcV/btsBJcivNgv/Aq29kKrgFFDNcy9AgY7ruK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5nYcV/btsBJcivNgv/Aq29kKrgFFDNcy9AgY7ruK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5nYcV%2FbtsBJcivNgv%2FAq29kKrgFFDNcy9AgY7ruK%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;304&quot; height=&quot;405&quot; data-origin-width=&quot;304&quot; data-origin-height=&quot;405&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBootTest 말고 WebMvcTest로 웹레이어만 테스트가 가능하기도 하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestDocs에서 제공하는 @AutoConfigureRestDocs 를 추가해 생성된 snippets이 사전 정의된 경로에 저장될 수 있도록 한다.&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;일반적인 mockMvcTest 와는 조금 다르게 RestDocs에서 제공하는 래핑된 &lt;b&gt;RestDocumentationRequestBuilder&lt;/b&gt; 를 사용하고, 필요에 따라 &lt;b&gt;RestDocumentationResultHandler&amp;nbsp;&lt;/b&gt;를 추가해 snippets 을 조작할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wEXRh/btsBMUaN3FN/CZPdvXnzstPUlAIUYw2SkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wEXRh/btsBMUaN3FN/CZPdvXnzstPUlAIUYw2SkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wEXRh/btsBMUaN3FN/CZPdvXnzstPUlAIUYw2SkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwEXRh%2FbtsBMUaN3FN%2FCZPdvXnzstPUlAIUYw2SkK%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;702&quot; height=&quot;594&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;594&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 data-ke-size=&quot;size16&quot;&gt;테스트 실행 결과 build 하위에 아래와 같은 snippets 이 생성된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;275&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI9RYV/btsBG6C1rUN/jN14NPsBgWaQ8N1TUYDWj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI9RYV/btsBG6C1rUN/jN14NPsBgWaQ8N1TUYDWj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI9RYV/btsBG6C1rUN/jN14NPsBgWaQ8N1TUYDWj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI9RYV%2FbtsBG6C1rUN%2FjN14NPsBgWaQ8N1TUYDWj0%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;275&quot; height=&quot;255&quot; data-origin-width=&quot;275&quot; data-origin-height=&quot;255&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;html 파일은 어떤 문서를 바탕으로 생성될까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 3번째 과정에서 build/docs/asciidoc 경로에 html 파일이 생성된다고 했다.&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;html 파일은 Rest Docs 에서 사전에 정의한 경로에 내가 작성한 adoc 문서를 바탕으로 생성해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Gradle 기준 src/docs/asciidoc/*.adoc 경로에 있는 파일 전체를 대상으로 한다)&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;간단한 adoc 문서를 하나 작성해 보자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;195&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/scIVR/btsBGlHufVD/8Q9kqlTDfPop0m9SH2n3u1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/scIVR/btsBGlHufVD/8Q9kqlTDfPop0m9SH2n3u1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/scIVR/btsBGlHufVD/8Q9kqlTDfPop0m9SH2n3u1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FscIVR%2FbtsBGlHufVD%2F8Q9kqlTDfPop0m9SH2n3u1%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;195&quot; height=&quot;142&quot; data-origin-width=&quot;195&quot; data-origin-height=&quot;142&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 data-ke-size=&quot;size16&quot;&gt;adoc 은 크게 include, operation 을 통해 외부에 있는 adoc 문서(e.g 위에서 생성된 snippets)를 불러올 수 있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;opertation 의 경우 include와 다르게 baseDir 이 snippets 경로로 설정돼 있고, 자동으로 제목이 붙는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;593&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DfyLt/btsBJfM6Bfh/H3UlrDMa9sBN1wqGWgsn1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DfyLt/btsBJfM6Bfh/H3UlrDMa9sBN1wqGWgsn1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DfyLt/btsBJfM6Bfh/H3UlrDMa9sBN1wqGWgsn1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDfyLt%2FbtsBJfM6Bfh%2FH3UlrDMa9sBN1wqGWgsn1K%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;1206&quot; height=&quot;593&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;593&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 data-ke-size=&quot;size16&quot;&gt;이제 asciidoctor를 통해 html 문서를 생성할 수 있다.&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;공식 문서에서는 Gradle 기준 Generated files 경로가 아래와 같이 나와 있는데, (아마 converter 버전 차이 때문인지) 실행해보면 build/docs/asciidoc 하위에 생성되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4Acd1/btsBGxVfbDw/v7zwrGmkN4KwkfEKjV3rc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4Acd1/btsBGxVfbDw/v7zwrGmkN4KwkfEKjV3rc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4Acd1/btsBGxVfbDw/v7zwrGmkN4KwkfEKjV3rc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4Acd1%2FbtsBGxVfbDw%2Fv7zwrGmkN4KwkfEKjV3rc1%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;1470&quot; height=&quot;288&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;288&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 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-filename=&quot;blob&quot; data-origin-width=&quot;462&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kyvux/btsBF3NEKBj/rXQiIgqj9kiJtRSYPH9yHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kyvux/btsBF3NEKBj/rXQiIgqj9kiJtRSYPH9yHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kyvux/btsBF3NEKBj/rXQiIgqj9kiJtRSYPH9yHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkyvux%2FbtsBF3NEKBj%2FrXQiIgqj9kiJtRSYPH9yHk%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;267&quot; height=&quot;199&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;462&quot; data-origin-height=&quot;344&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;2030&quot; data-origin-height=&quot;1322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1oRC1/btsBJe1JSCr/w4MOEhJSEAMvXzQzVS1hUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1oRC1/btsBJe1JSCr/w4MOEhJSEAMvXzQzVS1hUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1oRC1/btsBJe1JSCr/w4MOEhJSEAMvXzQzVS1hUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1oRC1%2FbtsBJe1JSCr%2Fw4MOEhJSEAMvXzQzVS1hUk%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;2030&quot; height=&quot;1322&quot; data-origin-width=&quot;2030&quot; data-origin-height=&quot;1322&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;</description>
      <category>spring</category>
      <author>PPakSang</author>
      <guid isPermaLink="true">https://ppaksang.tistory.com/39</guid>
      <comments>https://ppaksang.tistory.com/39#entry39comment</comments>
      <pubDate>Mon, 11 Dec 2023 09:47:46 +0900</pubDate>
    </item>
    <item>
      <title>Spring Cloud Data Flow</title>
      <link>https://ppaksang.tistory.com/38</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;입사하고 시간이 얼마 안 지났지만 굉장히 다사다난(?)한 일들을 겪었는데, 그 중 하나가 조직 이동이다. 연말 회고에서 그 때의 심경을 남기겠지만 이번 포스팅은  그 과정에서 운이 좋게도(?) 새로운 프로젝트를 시작하면서 Spring Cloud Data Flow(SCDF) 구축한 내용에 대해서 남기고자 한다.&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;이전에 스트림, 배치 기반의 마이크로서비스 개발 경험이 없어서(이번에 처음 Stream, Task 개념에 대해 학습했다) SCDF가 기존의 비슷한 역할을 수행하는 솔루션들과 비교했을 때 얼마나 큰 효용이 있는지 체감 못했지만, 현재 사용하는 입장에서 느낀 편리한 점은 스트림/배치 파이프라이닝이 굉장히 편하고, 모니터링 구축이 용이하다는 것이다.&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;기본적으로 Spring Cloud Stream과 Binder(Kafka, RabbitMQ 등)를 통해 파이프라인 구축을 위해서 각 컴포넌트 간의 메시징 토픽을 결정하는 불편함이나, 하나의 파이프라인이지만 한 눈에 확인하기 어려운 문제가 생길 수 있는데, &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SCDF에서 제공하는 대시보드를 사용하면 아래 보이는 것처럼 편리하게 UI로 스트림 컴포넌트(Source, Processor, Sink) 파이프라이닝이 가능하다.&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;물론 그냥 뚝닥 되진 않고, input, output 별칭으로 설정된 functional bindings을 사용해 배포할 때 컴포넌트를 매핑해 준다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;a style=&quot;color: #0593d3;&quot; href=&quot;https://dataflow.spring.io/docs/recipes/functional-apps/scst-function-bindings/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dataflow.spring.io/docs/recipes/functional-apps/scst-function-bindings/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2543&quot; data-origin-height=&quot;1291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZ29Je/btsBirH1yCF/vn0uDjG4nhUv5HiZKwCQ6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZ29Je/btsBirH1yCF/vn0uDjG4nhUv5HiZKwCQ6K/img.png&quot; data-alt=&quot;스트림 파이프라인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZ29Je/btsBirH1yCF/vn0uDjG4nhUv5HiZKwCQ6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZ29Je%2FbtsBirH1yCF%2Fvn0uDjG4nhUv5HiZKwCQ6K%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;2543&quot; height=&quot;1291&quot; data-origin-width=&quot;2543&quot; data-origin-height=&quot;1291&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스트림 파이프라인&lt;/figcaption&gt;
&lt;/figure&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;span style=&quot;color: #000000;&quot;&gt;Task(&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;독립적으로 실행되며, 간단한 데이터 처리, 배치 작업을 처리하는 단위)의 순차 실행을 구축하기도 쉽고, 스케줄링(crontab 사용)도 제공한다.&lt;/span&gt;&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;SCDF는 로컬에서 세팅하고 로컬에 존재하는 jar 파일을 등록해 배포가 가능하다는 것도 편리하지만, SCDF 자체적으로 Spring Cloud Deployer를 사용하기 때문에 쿠버네티스 클러스터 환경에도 손쉽게 배포 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://dataflow.spring.io/docs/installation/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dataflow.spring.io/docs/installation/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Spring Cloud Data Flow&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본적인 SCDF 구축에 필요한 서버 컴포넌트가 다양한 만큼 레퍼런스의 양도 상당하다.&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;개요, 버전 정보&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://dataflow.spring.io/docs/installation/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dataflow.spring.io/docs/installation/&lt;/a&gt;&lt;/b&gt;&lt;/span&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;간단한(?) 셋업 가이드(local, k8s, cloud foundry...)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://spring.io/projects/spring-cloud-dataflow&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://spring.io/projects/spring-cloud-dataflow&lt;/a&gt;&lt;/b&gt;&lt;/span&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;세부 설정 가이드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-dataflow/docs/current/reference/htmlsingle/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-cloud-dataflow/docs/current/reference/htmlsingle/&lt;/a&gt;&lt;/b&gt;&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;SCDF를 구축하기 위해서 띄워야 하는 서버 컴포넌트는 크게 4가지다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. Dataflow Server&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. Skipper Server&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. DB(mysql, mariadb, oracle... 등)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. Message Queue(RabbitMQ, Kafka)&lt;/span&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;&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;2434&quot; data-origin-height=&quot;1380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8WgJA/btsBiVhTnJn/aRJlA807p8BHkwVhmQKouK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8WgJA/btsBiVhTnJn/aRJlA807p8BHkwVhmQKouK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8WgJA/btsBiVhTnJn/aRJlA807p8BHkwVhmQKouK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8WgJA%2FbtsBiVhTnJn%2FaRJlA807p8BHkwVhmQKouK%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;566&quot; height=&quot;321&quot; data-origin-width=&quot;2434&quot; data-origin-height=&quot;1380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;dataflow server:&lt;/b&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;스트림과 태스크의 상태를 추적하고 모니터링&lt;/li&gt;
&lt;li&gt;스트림 및 태스크의 실행을 시작하고 중단&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;skipper:&lt;/b&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;서로 다른 환경 간의 애플리케이션 배포를 단순화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포된 태스크는 단일 작업을 수행하고 종료되며, 스트림의 경우 컴포넌트(source, processor, sink) 간 MessageQueue(위에서 kafka)통해 비동기 메시징 처리를 수행한다.&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;skipper, dataflow server 를 구축하면서 세팅한 설정 정보는 크게 아래와 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Skipper&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;db connection&lt;/li&gt;
&lt;li&gt;imagePullSecrets 설정(private image hub 사용시)&lt;/li&gt;
&lt;li&gt;Binder(kafka, zookeeper) 설정&lt;/li&gt;
&lt;li&gt;prometheus export&lt;/li&gt;
&lt;/ol&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;Dataflow&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;db connection&lt;/li&gt;
&lt;li&gt;imagePullSecrets 설정(private image hub 사용시)&lt;/li&gt;
&lt;li&gt;docker registry 설정&lt;/li&gt;
&lt;li&gt;skipper server connection&lt;/li&gt;
&lt;li&gt;prometheus export&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;DB&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dataflow server 와 skipper server 에서 사용하는 테이블이 사전에 세팅돼 있어야 한다. (RDB만 지원)&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;기본적으로 내부에서 flyway를 통해 DB 테이블을 세팅해주지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://docs.spring.io/spring-cloud-dataflow/docs/current/reference/htmlsingle/#configuration-local-rdbms&quot;&gt;직접 세팅&lt;/a&gt;할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-cloud/spring-cloud-dataflow/tree/main/spring-cloud-dataflow-server-core/src/main/resources/schemas&quot;&gt;https://github.com/spring-cloud/spring-cloud-dataflow/tree/main/spring-cloud-dataflow-server-core/src/main/resources/schemas&lt;/a&gt;&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;Monitoring&lt;/b&gt;&lt;/h4&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;그래서 SCDF에서는 prometheus rsocket을 사용해 매트릭을 수집한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dataflow.spring.io/docs/feature-guides/general/server-monitoring/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dataflow.spring.io/docs/feature-guides/general/server-monitoring/&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;rsocket 설정 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dataflow.spring.io/docs/installation/kubernetes/kubectl/#enable-monitoring&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dataflow.spring.io/docs/installation/kubernetes/kubectl/#enable-monitoring&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;크게 복잡한 것은 아닌데, 태스크/스트림에서 prometheus-rsocket(proxy)서버로 매트릭을 전송하고, 우리가 운영 중인 prometheus 서버에서 주기적으로 이 proxy 서버에서 저장하고 있는 내용을 스크랩 해 가는 방식인데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/micrometer-metrics/prometheus-rsocket-proxy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;에서, proxy 서버의 사전 정의된 스크랩 엔드포인트(/metrics/connected, /metrics/proxy)를 참조한다고 한다.&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;직접 프로메테우스를 구축하면 해당 엔드포인트로 스크랩 설정을 해줘야 하지만, 프로메테우스를 추상화한 솔루션을 사용하면 prometheus.io/scrape, prometheus.io/path 어노테이션으로 손쉽게 기존 인프라와 연동이 가능하다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #0d1117; color: #e6edf3; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Prometheus is configured to scrape the&lt;span&gt;&amp;nbsp;&lt;/span&gt;/metrics/connected&lt;span&gt;&amp;nbsp;&lt;/span&gt;and&lt;span&gt;&amp;nbsp;&lt;/span&gt;/metrics/proxy&lt;span&gt;&amp;nbsp;&lt;/span&gt;endpoints of the proxy(ies) and not the application instances.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 proxy 서버에 매트릭을 적재하고 이를 기존 인프라(prometheus)에서 수집해 가는 방식이 새삼 유용하다 생각했는데, 이런 구조를 채택하면 스트림과 태스크의 지표를 별도로 적재하면서도 기존에 운영하던 인프라와 손쉽게 붙였다 떼었다 할 수 있기 때문이다.&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;TIP(?)&lt;/b&gt;&lt;/h4&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 때, 학습에 큰 도움이 됐던 방법은 (내가 종종 새로운 프레임워크를 학습할 때 쓰는 방법이기도 한데)프로젝트를 하나 판 뒤에 maven 에서 실제 의존성을 하나씩 추가하면서 yml 설정을 샘플과 똑같이 해보고, 코드에서 실제로 어떻게 동작하는지 command+b 등으로 탐색하면서 이해하면 조금 더 쉬웠던 것 같다&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;SCDF 공식 레포 이름을 바탕으로 maven Repository에 서칭 가능하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-cloud/spring-cloud-dataflow&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/spring-cloud/spring-cloud-dataflow&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>PPakSang</author>
      <guid isPermaLink="true">https://ppaksang.tistory.com/38</guid>
      <comments>https://ppaksang.tistory.com/38#entry38comment</comments>
      <pubDate>Sat, 2 Dec 2023 21:54:57 +0900</pubDate>
    </item>
    <item>
      <title>[책 리뷰] 클린 코드</title>
      <link>https://ppaksang.tistory.com/37</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅에서도 언급했듯 2023년도에는 현실적으로 운영 가능한 시스템을 구축하는 능력을 키우는데 초점을 맞췄다. 그 중 하반기는 탄탄한 시스템을 구축하기 위한 협업 방식을 습득하는데 집중했고, 이를 위해 프로그래밍 스킬 외에도 다양한 소프트 스킬을 키우기 위해서 노력했다.&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 챕터(냄새와 휴리스틱)에서는 저자(로버트 C. 마틴)가 사례(코드 냄새)를 소개하는데, 이펙티브 자바에서 아이템을 설명하는 방식과 유사하게 느껴졌고 책의 전체적인 내용을 복습하는 느낌이 들어서 읽기 편했던 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&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;&lt;b&gt;의미 전달&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서 이야기하는 것에서 중요한 키워드가 뭘까 생각했을 때 바로 떠오른 것이 &lt;b&gt;의미 전달&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;null 은 주지도 받지도 말자&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;1. 인스턴스 없이 null이 넘어온다면? -&amp;gt; 검사 해야 하나? -&amp;gt; null이면 어떤 예외를?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 유효하지 않은 요청이면 null을 넘기나? -&amp;gt; null이 아니면 예외를 던지나? default 객체를 넘기나?&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;두 가지 모두 null을 다루기 때문에 생기는 고민인데, 책에서는 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;null을 반환하지 않는 것은 쉽다. 기존 null을 반환하는 케이스를 모두 예외 혹은 default 객체를 만들어 넘기면 된다.&lt;/p&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;null을 반환하기 보다는 적절한 예외를 던져주는 것이 장애의 전파를 막을 수 있고, 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;&lt;b&gt;경계 분리, 그리고 단일 책임 원칙&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 테스팅, 동시성 처리에 대한 내용을 다루는데 여러 가지 패턴을 학습하는데 도움이 된 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 &lt;a href=&quot;https://ppaksang.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[책 리뷰] 객체지향의 사실과 오해, 오브젝트&lt;/a&gt; 와 내용적인 측면에서 비슷하게 서술된 내용이 많기도 해서 이번 글에서는 따로 정리하진 않았다.&lt;/p&gt;
&lt;figure id=&quot;og_1698662591373&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[책 리뷰] 객체지향의 사실과 오해, 오브젝트&quot; data-og-description=&quot;객체지향의 사실과 오해는 약 2달, 오브젝트는 부록을 포함해 약 600페이지 가량의 오브젝트 책을 5달에 걸쳐 다 읽었다. 중간에 다른 일들이 바빠져 2달 정도는 쉬었지만 아무튼 정말 많은 내용&quot; data-og-host=&quot;ppaksang.tistory.com&quot; data-og-source-url=&quot;https://ppaksang.tistory.com/34&quot; data-og-url=&quot;https://ppaksang.tistory.com/34&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/tCot3/hyUkkDLBXl/NlQK9grDr4YKS7Y4Mwn5dk/img.png?width=800&amp;amp;height=1025&amp;amp;face=0_0_800_1025,https://scrap.kakaocdn.net/dn/cbPMXy/hyUnMyxvMi/u3BWsEULdEcAZmpNuElNaK/img.png?width=800&amp;amp;height=1025&amp;amp;face=0_0_800_1025,https://scrap.kakaocdn.net/dn/UV5Ag/hyUnVWxlLC/MKg0Mz8Kv18yMXumSYpQ5k/img.png?width=1470&amp;amp;height=1884&amp;amp;face=0_0_1470_1884&quot;&gt;&lt;a href=&quot;https://ppaksang.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ppaksang.tistory.com/34&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/tCot3/hyUkkDLBXl/NlQK9grDr4YKS7Y4Mwn5dk/img.png?width=800&amp;amp;height=1025&amp;amp;face=0_0_800_1025,https://scrap.kakaocdn.net/dn/cbPMXy/hyUnMyxvMi/u3BWsEULdEcAZmpNuElNaK/img.png?width=800&amp;amp;height=1025&amp;amp;face=0_0_800_1025,https://scrap.kakaocdn.net/dn/UV5Ag/hyUnVWxlLC/MKg0Mz8Kv18yMXumSYpQ5k/img.png?width=1470&amp;amp;height=1884&amp;amp;face=0_0_1470_1884');&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;[책 리뷰] 객체지향의 사실과 오해, 오브젝트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;객체지향의 사실과 오해는 약 2달, 오브젝트는 부록을 포함해 약 600페이지 가량의 오브젝트 책을 5달에 걸쳐 다 읽었다. 중간에 다른 일들이 바빠져 2달 정도는 쉬었지만 아무튼 정말 많은 내용&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ppaksang.tistory.com&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;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;이 책을 읽으면서 클린한 코드(유지 보수 가능한 코드)를 작성하는 법을 담은 책들이 어느정도 공통된 이야기를 하고 있다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중 인상깊었던 내용 중 하나가 '이 책을 읽어서 기분 좋은 책으로 남기지 말라' 라는 문구였는데, 패턴 몇가지를 읽고만 넘어가지 말고 패턴을 적용한 사례를 분석하고 실제로 적용해 효용이 있는지 느껴보는 것이 중요하다는 것을 강조하는 것을 의미한다. (= 경험이 중요하다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음으로 아키텍처 구현과 관련된 내용이나 예제 시스템을 구현하는 내용을 담은 책을 읽으면 어떨까? 하는 생각이다.(+ 토이 프로젝트도 진행해보면 좋을 것 같다)&lt;/p&gt;</description>
      <category>후기</category>
      <author>PPakSang</author>
      <guid isPermaLink="true">https://ppaksang.tistory.com/37</guid>
      <comments>https://ppaksang.tistory.com/37#entry37comment</comments>
      <pubDate>Mon, 30 Oct 2023 19:46:01 +0900</pubDate>
    </item>
  </channel>
</rss>