<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>@deveely</title>
    <link>https://do-study.tistory.com/</link>
    <description>자기실력이 좋다고 느껴지는건 공부를 안하고 있다는 신호</description>
    <language>ko</language>
    <pubDate>Sun, 31 May 2026 19:14:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>@deveely</managingEditor>
    <image>
      <title>@deveely</title>
      <url>https://tistory1.daumcdn.net/tistory/2949529/attach/6652d4a69b174b6a8c3024ff0ec1148a</url>
      <link>https://do-study.tistory.com</link>
    </image>
    <item>
      <title>Vector DB - Qdrant 를 활용하여 벡터 데이터 다뤄보기</title>
      <link>https://do-study.tistory.com/139</link>
      <description>&lt;h1&gt;1. Vector DB란?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Vector DB&lt;/b&gt;는 데이터를 벡터(숫자 배열) 형태로 저장하고 벡터 간 &lt;b&gt;유사도(Similarity)&lt;/b&gt; 를 계산해주는 &lt;b&gt;AI 특화 데이터베이스&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트, 이미지, 오디오처럼 비정형 데이터를 &lt;b&gt;임베딩(Embedding)&lt;/b&gt; 모델을 통해 벡터로 변환하고 이 벡터들의 &amp;ldquo;의미적 거리&amp;rdquo;를 계산함으로써 &lt;b&gt;의미 기반 검색(Semantic Search)&lt;/b&gt;, &lt;b&gt;추천 시스템&lt;/b&gt;, &lt;b&gt;RAG(Retrieval Augmented Generation)&lt;/b&gt; 등에 활용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 많이 사용되는 Vector DB입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 특징 주요 사용 사례&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Pinecone&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;완전 관리형 SaaS. 인프라 관리 불필요&lt;/td&gt;
&lt;td&gt;빠른 RAG 구축, 대규모 상용 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Qdrant&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;오픈소스 기반, HTTP&amp;middot;gRPC 모두 지원&lt;/td&gt;
&lt;td&gt;로컬/클라우드 환경, Spring AI 연동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Weaviate&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;GraphQL API, 다양한 백엔드 스토리지 연동&lt;/td&gt;
&lt;td&gt;멀티모달(텍스트+이미지) 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 Qdrant를 활용하여 Vector DB와 가까워져보고자 합니다.&lt;/p&gt;
&lt;h1&gt;2. 설치방법&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Qdrant는 Docker를 활용하여 매우 간단하게 설치할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 이미지를 Pull 받고 실행해주면 끝입니다!&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;gt; docker pull qdrant/qdrant
&amp;gt; docker run -d \\
    --name qdrant \\
    -p 6333:6333 \\ // for HTTP
    -p 6334:6334 \\ // for gRPC 
    -v ~/qdrant_storage:/qdrant/storage \\ // 데이터 볼륨 설정
    qdrant/qdrant
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 실행 명령어를 조금 들여다보면 6333 6334 2개 포트를 호스트와 연결시켜주는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Qdrant는 HTTP, gRPC 2가지 통신 방식을 지원하기 때문에 서비스 특성에 맞춰 사용하시면 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 gRPC에 대해 처음 접하시는 분들은 &lt;a href=&quot;https://do-study.tistory.com/94&quot;&gt;gRPC Reference - 개요 및 특징&lt;/a&gt; 살펴보시는것도 추천드립니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 기동 후 아래 명령으로 컬렉션 목록이 보인다면 성공입니다.&lt;/p&gt;
&lt;h1&gt;3. 간단한 실습&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 실습하기 전에 몇가지 사전지식을 알아보고 가겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 Collection, Point, Vector, Payload 가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Vector : 데이터를 임베딩하여 얻은 숫자 배열 데이터&lt;/li&gt;
&lt;li&gt;Payload : 벡터에 대한 부연설명을 할 수 있는 메타 데이터&lt;/li&gt;
&lt;li&gt;Point : Vector + Payload&lt;/li&gt;
&lt;li&gt;Collection : 여러 Point 로 이뤄진 집합&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Collection은 Point (Payload를 같는 Vector)들의 집합이라고 정의할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Qdrant에서는 같은 Collection에 속한 모든 Vector가 &lt;b&gt;동일한 차원(Dimension)&lt;/b&gt; 과 &lt;b&gt;같은 Metric(유사도 계산 방식)&lt;/b&gt; 을 가져야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 Metric 종류는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Metric 설명 특징 / 사용 예시&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Cosine&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;코사인 유사도&lt;/td&gt;
&lt;td&gt;문장, 단어 임베딩 등 &lt;b&gt;방향(semantic)&lt;/b&gt; 이 중요한 NLP에 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Dot&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;벡터 내적&lt;/td&gt;
&lt;td&gt;크기와 방향을 모두 반영, &lt;b&gt;이미지&amp;middot;멀티모달&lt;/b&gt; 임베딩에 자주 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Euclid&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;유클리드 거리&lt;/td&gt;
&lt;td&gt;물리적 거리나 좌표형 데이터 등 &lt;b&gt;절대 거리&lt;/b&gt; 기반 비교에 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Collection 은 아래와 같이 만들어 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름을 지정하여 생성하며 size는 차원, distance는 유사도 계산 방식을 의미합니다.&lt;/p&gt;
&lt;pre class=&quot;scilab&quot;&gt;&lt;code&gt;&amp;gt; curl -X PUT &quot;&amp;lt;http://localhost:6333/collections/{컬렉션명}&amp;gt;&quot; -d '{
  &quot;vectors&quot;: {
    &quot;size&quot;: 3,
    &quot;distance&quot;: &quot;Cosine&quot;
  }
}'

{&quot;result&quot;:true,&quot;status&quot;:&quot;ok&quot;,&quot;time&quot;:0.059407167}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 방법으로 현재 만들어져있는 컬렉션 목록을 확인해볼수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;gt; curl -X GET &quot;&amp;lt;http://localhost:6333/collections&amp;gt;&quot;

{&quot;result&quot;:{&quot;collections&quot;:[{&quot;name&quot;:&quot;test_collection&quot;}]},&quot;status&quot;:&quot;ok&quot;,&quot;time&quot;:7.667e-6}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 만들어진 컬렉션에 벡터를 저장해보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;wait 파라미터가 true이면 동기, false면 비동기로 처리합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;scilab&quot;&gt;&lt;code&gt;&amp;gt; curl -X PUT &quot;&amp;lt;http://localhost:6333/collections/{컬렉션명}/points?wait=true&amp;gt;&quot; -d '{
  &quot;points&quot;: [
      {
          &quot;id&quot;: 1,
          &quot;payload&quot;: {&quot;color&quot;: &quot;red&quot;},
          &quot;vector&quot;: [0.9, 0.1, 0.1]
      },
      {
          &quot;id&quot;: 2,
          &quot;payload&quot;: {&quot;color&quot;: &quot;green&quot;},
          &quot;vector&quot;: [0.1, 0.9, 0.1]
      },
      {
          &quot;id&quot;: 3,
          &quot;payload&quot;: {&quot;color&quot;: &quot;blue&quot;},
          &quot;vector&quot;: [0.1, 0.1, 0.9]
      }
  ]
}'

{&quot;result&quot;:{&quot;operation_id&quot;:0,&quot;status&quot;:&quot;acknowledged&quot;},&quot;status&quot;:&quot;ok&quot;,&quot;time&quot;:0.000972208}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scroll API를 활용해 조회해보면 3개 포인트가 정상적으로 조회되는 부분이 확인됩니다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;&amp;gt; curl -s -X POST &amp;lt;http://localhost:6333/collections/test_collection/points/scroll&amp;gt; \\
  -H &quot;Content-Type: application/json&quot; \\
  -d '{&quot;limit&quot;: 10, &quot;with_vector&quot;: false, &quot;with_payload&quot;: true}'

{&quot;result&quot;:{&quot;points&quot;:[{&quot;id&quot;:1,&quot;payload&quot;:{&quot;color&quot;:&quot;red&quot;}},{&quot;id&quot;:2,&quot;payload&quot;:{&quot;color&quot;:&quot;green&quot;}},{&quot;id&quot;:3,&quot;payload&quot;:{&quot;color&quot;:&quot;blue&quot;}}],&quot;next_page_offset&quot;:null},&quot;status&quot;:&quot;ok&quot;,&quot;time&quot;:0.000514917} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 가볍게 컬렉션을 생성하고 벡터 저장 및 조회를 해봤습니다.&lt;/p&gt;
&lt;h1&gt;4. 마무리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Qdrant는 이 외에도 필터링, 조건 검색, Named Vector 등 다양한 고급 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 내용은 &lt;a href=&quot;https://qdrant.tech/documentation/overview/&quot;&gt;공식 문서&lt;/a&gt;를 참고하시면 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 Spring AI + Vector DB 를 활용한 RAG 아키텍쳐를 구현해보도록 하겠습니다.&lt;/p&gt;</description>
      <category>AI &amp;amp; LLM</category>
      <category>AI</category>
      <category>LLM</category>
      <category>qdrant</category>
      <category>RAG</category>
      <category>vectordb</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/139</guid>
      <comments>https://do-study.tistory.com/139#entry139comment</comments>
      <pubDate>Sun, 19 Oct 2025 23:55:30 +0900</pubDate>
    </item>
    <item>
      <title>[Spring AI 공식문서 읽기] 3. Advisors API</title>
      <link>https://do-study.tistory.com/138</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script&gt;
const _0x5eef=['classList','92935nhtnYq','setAttribute','push','innerHTML','getElementById','toLowerCase','tt_adsense_top','another_category','style','//p[contains(text(),\x27[목차여기]\x27)]','1954669aacfHB','div','appendChild','toc-ym','title','forEach','DOMContentLoaded','call','addEventListener','length','insertBefore','firstElementChild','log','27309qNoTHN','62SuwPRc','parentNode','querySelector','revenue_unit_wrap','tagName','23736mMyuUa','singleNodeValue','trim','17723tUfPMr','textContent','1STKGDu','getAttribute','contains','nextSibling','791846eKKEom','createElement','outerText','FIRST_ORDERED_NODE_TYPE','querySelectorAll','72wJWnLP','hasAttribute','669103LLOFBD','toc'];function _0x330c(_0x5d40d0,_0x4afdad){_0x5d40d0=_0x5d40d0-0xec;let _0x5eef71=_0x5eef[_0x5d40d0];return _0x5eef71;}const _0x2078d2=_0x330c;(function(_0xbea334,_0x392453){const _0x2c3076=_0x330c;while(!![]){try{const _0x5a087d=-parseInt(_0x2c3076(0x117))+parseInt(_0x2c3076(0xf7))+parseInt(_0x2c3076(0xfa))+-parseInt(_0x2c3076(0x11a))*-parseInt(_0x2c3076(0xf5))+parseInt(_0x2c3076(0x112))*parseInt(_0x2c3076(0x111))+parseInt(_0x2c3076(0xec))*-parseInt(_0x2c3076(0xf0))+-parseInt(_0x2c3076(0x104));if(_0x5a087d===_0x392453)break;else _0xbea334['push'](_0xbea334['shift']());}catch(_0x47ff63){_0xbea334['push'](_0xbea334['shift']());}}}(_0x5eef,0xea9e9),document[_0x2078d2(0x10c)](_0x2078d2(0x10a),function(){const _0x7eb51e=_0x2078d2;try{const _0x591681=document[_0x7eb51e(0x114)]('.contents_style'),_0x1762f9=document[_0x7eb51e(0xfe)](_0x7eb51e(0x107));if(_0x591681&amp;&amp;!_0x1762f9)htmlTableOfContents();else return![];}catch(_0x250abc){console[_0x7eb51e(0x110)]('');}}));function htmlTableOfContents(_0x4f1c99){const _0x388803=_0x2078d2;var _0x4f1c99=_0x4f1c99||document;const _0x44fb35=document[_0x388803(0xf1)]('div');_0x44fb35[_0x388803(0xfb)]('id',_0x388803(0x107));const _0x2117e2=document['querySelector']('.contents_style');var _0x35e549=_0x388803(0x103),_0x552a33=document['evaluate'](_0x35e549,document,null,XPathResult[_0x388803(0xf3)],null)[_0x388803(0x118)];let _0x407aa0;_0x552a33?(_0x407aa0=_0x552a33,_0x407aa0[_0x388803(0x11b)]='',_0x407aa0[_0x388803(0x106)](_0x44fb35)):(_0x407aa0=_0x2117e2[_0x388803(0x10f)],_0x407aa0['classList'][_0x388803(0xee)](_0x388803(0x100))||_0x407aa0[_0x388803(0xf9)]['contains'](_0x388803(0x115))?_0x2117e2['insertBefore'](_0x44fb35,_0x407aa0[_0x388803(0xef)]):_0x407aa0[_0x388803(0x113)][_0x388803(0x10e)](_0x44fb35,_0x407aa0));const _0x3e06b5=document['getElementById'](_0x388803(0x107)),_0x5ee2f2=[]['slice'][_0x388803(0x10b)](_0x2117e2[_0x388803(0xf4)]('h1,\x20h2,\x20h3,\x20h4,\x20h5,\x20h6')),_0x454032=[];for(i=0x0;i&lt;_0x5ee2f2[_0x388803(0x10d)];i++){if(_0x5ee2f2[i][_0x388803(0xf2)][_0x388803(0x119)]()==='')continue;else{if(_0x5ee2f2[i][_0x388803(0xf9)][_0x388803(0xee)](_0x388803(0x108)))continue;else{if(_0x5ee2f2[i][_0x388803(0x113)]['classList'][_0x388803(0xee)](_0x388803(0x101)))continue;else _0x454032[_0x388803(0xfc)](_0x5ee2f2[i]);}}}_0x454032[_0x388803(0x109)](function(_0x5d97e0,_0x2112a5){const _0x4b3465=_0x388803;var _0x94aa2e=_0x4b3465(0xf8)+_0x2112a5;if(_0x5d97e0[_0x4b3465(0xf6)]('id'))_0x94aa2e=_0x5d97e0[_0x4b3465(0xed)]('id');else _0x5d97e0[_0x4b3465(0xfb)]('id',_0x94aa2e);var _0x34278b=_0x4f1c99[_0x4b3465(0xf1)]('a');_0x34278b[_0x4b3465(0xfb)]('href','#'+_0x94aa2e),_0x34278b['textContent']='•\x20'+_0x5d97e0[_0x4b3465(0x11b)];var _0x118edf=_0x4f1c99[_0x4b3465(0xf1)](_0x4b3465(0x105));_0x118edf[_0x4b3465(0xfb)]('class',_0x5d97e0[_0x4b3465(0x116)][_0x4b3465(0xff)]()),_0x118edf[_0x4b3465(0x106)](_0x34278b),_0x3e06b5[_0x4b3465(0x106)](_0x118edf);});const _0xd72dc='\x0a\x20\x20\x20\x20#toc-ym\x20div.h1\x20{\x20margin-left:\x200em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h2\x20{\x20margin-left:\x200.5em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h3\x20{\x20margin-left:\x201em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h4\x20{\x20margin-left:\x201.5em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h5\x20{\x20margin-left:\x202em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h6\x20{\x20margin-left:\x202.5em\x20}\x0a\x20\x20\x20\x20\x0a\x20\x20\x20\x20#toc-ym\x20{\x0a\x20\x20\x20\x20\x20\x20margin:\x2030px\x200px\x2030px\x200px;\x0a\x20\x20\x20\x20\x20\x20padding:\x2020px\x2020px\x2010px\x2015px;\x0a\x20\x20\x20\x20\x20\x20border:\x201px\x20solid\x20#dadada;\x0a\x20\x20\x20\x20\x20\x20background-color:\x20#ffffff;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym::before\x20{\x0a\x20\x20\x20\x20\x20\x20content:\x20\x22목\x20\x20차\x22;\x0a\x20\x20\x20\x20\x20\x20display:\x20block;\x0a\x20\x20\x20\x20\x20\x20width:\x20120px;\x0a\x20\x20\x20\x20\x20\x20background-color:\x20rgb(255,\x20255,\x20255);\x0a\x20\x20\x20\x20\x20\x20text-align:\x20center;\x0a\x20\x20\x20\x20\x20\x20font-size:\x2018px;\x0a\x20\x20\x20\x20\x20\x20font-weight:\x20bold;\x0a\x20\x20\x20\x20\x20\x20margin:\x20-40px\x20auto\x200px;\x0a\x20\x20\x20\x20\x20\x20padding:\x205px\x200px;\x0a\x20\x20\x20\x20\x20\x20border-width:\x201px;\x0a\x20\x20\x20\x20\x20\x20border-style:\x20solid;\x0a\x20\x20\x20\x20\x20\x20border-color:\x20rgb(218,\x20218,\x20218);\x0a\x20\x20\x20\x20\x20\x20border-image:\x20initial;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div{\x0a\x20\x20\x20\x20\x20\x20margin:\x205px\x200px;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div:first-child{\x0a\x20\x20\x20\x20\x20\x20margin-top:\x2015px;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div:last-child{\x0a\x20\x20\x20\x20\x20\x20margin-bottom:\x2015px;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div\x20a\x20{\x0a\x20\x20\x20\x20\x20\x20text-decoration:\x20none;\x0a\x20\x20\x20\x20\x20\x20color:\x20#337ab7;\x0a\x20\x20\x20\x20\x20\x20transition:\x20all\x20ease\x200.2s;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div\x20a:hover\x20{\x0a\x20\x20\x20\x20\x20\x20\x0a\x20\x20\x20\x20\x20\x20color:\x20#333333;\x0a\x20\x20\x20\x20\x20\x20background-color:\x20#ecc7ff;\x0a\x20\x20\x20\x20\x20\x20\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20/*\x0a\x20\x20\x20\x20.contents_style\x20h3{\x0a\x20\x20\x20\x20\x20\x20margin-bottom:7px;\x0a\x20\x20\x20\x20\x20\x20padding:\x2010px\x2015px;\x0a\x20\x20\x20\x20\x20\x20border-left:\x205px\x20solid\x20#757575;\x0a\x20\x20\x20\x20\x20\x20background-color:\x20#e5e5e5;\x0a\x20\x20\x20\x20\x20\x20font-weight:\x20500;\x0a\x20\x20\x20\x20\x20\x20color:\x20#000000\x20!important;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20*/\x0a\x20\x20\x20\x20',_0x3ed036=document[_0x388803(0xf1)](_0x388803(0x102));_0x3ed036[_0x388803(0xfd)]=_0xd72dc,_0x2117e2[_0x388803(0x10e)](_0x3ed036,_0x407aa0);}
&lt;/script&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;Spring AI의 &lt;b&gt;Advisors API&lt;/b&gt;는 Spring 애플리케이션에서 AI 기반 상호작용을 가로채고(intercept), 수정하고(modify), 향상(enhance)할 수 있는 유연하고 강력한 방법을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Advisors API를 활용하면 개발자는 더 정교하고, 재사용 가능하며, 유지 관리가 용이한 AI 컴포넌트를 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 이점에는 반복되는 생성형 AI 패턴을 캡슐화하고, 대규모 언어 모델(LLM)에 보내고 받는 데이터를 변환하며, 다양한 모델과 사용 사례에서 이식성을 제공하는 것이 포함됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예제와 같이 ChatClient API를 사용해 기존 Advisor를 구성할 수 있습니다&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;ChatMemory chatMemory = ... // Initialize your chat memory store
VectorStore vectorStore = ... // Initialize your vector store

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        MessageChatMemoryAdvisor.builder(chatMemory).build(), // chat-memory advisor
        QuestionAnswerAdvisor.builder(vectorStore).build()    // RAG advisor
    )
    .build();

var conversationId = &quot;678&quot;;

String response = this.chatClient.prompt()
    // Set advisor parameters at runtime
    .advisors(advisor -&amp;gt; advisor.param(ChatMemory.CONVERSATION_ID, conversationId))
    .user(userText)
    .call()
	.content();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 시점에 &lt;b&gt;builder의 defaultAdvisors() 메서드&lt;/b&gt;를 사용하여 Advisor를 등록하는 것이 권장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Advisor는 &lt;b&gt;Observability 스택&lt;/b&gt;에도 참여하므로, 실행과 관련된 메트릭과 트레이스를 확인할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html#_questionansweradvisor&quot;&gt;QuestionAnswerAdvisor 상세 가이드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-ai/reference/api/chat-memory.html#_memory_in_chat_client&quot;&gt;ChatMemoryAdvisor 상세 가이드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;1. 핵심 컴포넌트&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Advisor API는 &lt;b&gt;비스트리밍(non-streaming)&lt;/b&gt; 시나리오를 위한 CallAdvisor 와 CallAdvisorChain,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스트리밍(streaming)&lt;/b&gt; 시나리오를 위한 StreamAdvisor 와 StreamAdvisorChain으로 구성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;프롬프트 요청&lt;/b&gt;을 표현하는 ChatClientRequest 와 &lt;b&gt;채팅 완료(Chat Completion) 응답&lt;/b&gt;을 표현하는 ChatClientResponse를 포함합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 객체 모두 &lt;b&gt;advisor 체인 전체에서 상태를 공유하기 위한 advise-context&lt;/b&gt;를 보유하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2240&quot; data-origin-height=&quot;1786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ce3rWy/btsQ6gkIZdM/nsZHaJXcswMuK9c5jdtkdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ce3rWy/btsQ6gkIZdM/nsZHaJXcswMuK9c5jdtkdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ce3rWy/btsQ6gkIZdM/nsZHaJXcswMuK9c5jdtkdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fce3rWy%2FbtsQ6gkIZdM%2FnsZHaJXcswMuK9c5jdtkdK%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;2240&quot; height=&quot;1786&quot; data-origin-width=&quot;2240&quot; data-origin-height=&quot;1786&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;adviseCall() 과 adviseStream() 메서드는 Advisor의 핵심 메서드로, 일반적으로 다음과 같은 작업을 수행합니다.&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;unsealed Prompt 데이터 검사&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Prompt 데이터 커스터마이징 및 보강(augmenting)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;advisor 체인의 다음 엔티티 호출&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;요청을 차단(옵션)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Chat Completion 응답 검사&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;처리 오류를 나타내기 위해 예외 발생&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 getOrder() 메서드는 체인 내에서 Advisor의 실행 순서를 결정하며 getName() 메서드는 고유한 Advisor 이름을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AI 프레임워크가 생성하는 &lt;b&gt;Advisor Chain&lt;/b&gt;은 getOrder() 값에 따라 정렬된 여러 Advisor를 순차적으로 호출할 수 있도록 합니다. 값이 &lt;b&gt;낮을수록 먼저 실행&lt;/b&gt;되며, &lt;b&gt;마지막 Advisor&lt;/b&gt;(자동으로 추가됨)가 실제로 요청을 LLM에 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 흐름도는 &lt;b&gt;Advisor Chain&lt;/b&gt;과 &lt;b&gt;Chat Model&lt;/b&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;1964&quot; data-origin-height=&quot;2037&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnQQLz/btsQ5RyJXDV/9SgD7hqSMMzQL5W8PMqRY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnQQLz/btsQ5RyJXDV/9SgD7hqSMMzQL5W8PMqRY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnQQLz/btsQ5RyJXDV/9SgD7hqSMMzQL5W8PMqRY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnQQLz%2FbtsQ5RyJXDV%2F9SgD7hqSMMzQL5W8PMqRY0%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;1964&quot; height=&quot;2037&quot; data-origin-width=&quot;1964&quot; data-origin-height=&quot;2037&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Spring AI 프레임워크는 사용자의 &lt;b&gt;Prompt&lt;/b&gt; 로부터 ChatClientRequest 를 생성하고, 빈 &lt;b&gt;advisor context 객체&lt;/b&gt;를 함께 초기화합니다.&lt;/li&gt;
&lt;li&gt;체인에 포함된 각 &lt;b&gt;Advisor&lt;/b&gt;는 요청을 처리하며 필요하면 이를 &lt;b&gt;수정&lt;/b&gt;할 수 있습니다. 또는 요청을 다음 엔티티에 전달하지 않고 &lt;b&gt;차단(block)&lt;/b&gt; 할 수도 있으며, 이 경우 해당 Advisor가 직접 &lt;b&gt;응답을 작성&lt;/b&gt;해야 합니다.&lt;/li&gt;
&lt;li&gt;프레임워크에서 제공하는 &lt;b&gt;마지막 Advisor&lt;/b&gt;가 최종적으로 요청을 &lt;b&gt;Chat Model&lt;/b&gt;에 전달합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Chat Model&lt;/b&gt;이 반환한 응답은 다시 &lt;b&gt;Advisor Chain&lt;/b&gt;을 거치며 전달되고, 이 과정에서 응답은 &lt;b&gt;ChatClientResponse&lt;/b&gt; 로 변환됩니다. 이 객체에는 &lt;b&gt;공유된 advisor context 인스턴스&lt;/b&gt;가 포함됩니다.&lt;/li&gt;
&lt;li&gt;각 &lt;b&gt;Advisor&lt;/b&gt;는 이 단계에서 응답을 &lt;b&gt;검사하거나 수정&lt;/b&gt;할 수 있습니다.&lt;/li&gt;
&lt;li&gt;최종적으로 완성된 &lt;b&gt;ChatClientResponse&lt;/b&gt; 는 &lt;b&gt;ChatCompletion&lt;/b&gt; 을 추출하여 클라이언트에 반환됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1.1. Advisor Order&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Advisor 체인에서 실행 순서는 &lt;b&gt;getOrder() 메서드&lt;/b&gt;에 의해 결정됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;값이 낮은 Advisor&lt;/b&gt;가 먼저 실행됩니다.&lt;/li&gt;
&lt;li&gt;Advisor 체인은 **스택(stack)**처럼 동작합니다:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;체인의 첫 번째 Advisor가 &lt;b&gt;요청(request)을 가장 먼저 처리&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;동시에 이 Advisor가 &lt;b&gt;응답(response)을 가장 마지막에 처리&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;실행 순서를 제어하려면:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Ordered.HIGHEST_PRECEDENCE&lt;/b&gt;(가장 작은 값)에 가깝게 설정하면 Advisor가 &lt;b&gt;요청을 가장 먼저 처리&lt;/b&gt;하고 &lt;b&gt;응답을 가장 마지막에 처리&lt;/b&gt;하게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ordered.LOWEST_PRECEDENCE&lt;/b&gt;(가장 큰 값)에 가깝게 설정하면 Advisor가 &lt;b&gt;요청을 가장 마지막에 처리&lt;/b&gt;하고 &lt;b&gt;응답을 가장 먼저 처리&lt;/b&gt;하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;값이 클수록 우선순위가 낮게 해석됩니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;여러 Advisor가 동일한 order 값을 가진 경우, &lt;b&gt;실행 순서는 보장되지 않습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, 다음은 Spring &lt;b&gt;Ordered&lt;/b&gt; 인터페이스의 의미(동작 방식)입니다:&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public interface Ordered {

    /**
     * Constant for the highest precedence value.
     * @see java.lang.Integer#MIN_VALUE
     */
    int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;

    /**
     * Constant for the lowest precedence value.
     * @see java.lang.Integer#MAX_VALUE
     */
    int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

    /**
     * Get the order value of this object.
     * &amp;lt;p&amp;gt;Higher values are interpreted as lower priority. As a consequence,
     * the object with the lowest value has the highest priority (somewhat
     * analogous to Servlet {@code load-on-startup} values).
     * &amp;lt;p&amp;gt;Same order values will result in arbitrary sort positions for the
     * affected objects.
     * @return the order value
     * @see #HIGHEST_PRECEDENCE
     * @see #LOWEST_PRECEDENCE
     */
    int getOrder();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;aside&amp;gt;  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력과 출력 &lt;b&gt;양쪽에서 모두 체인의 첫 번째&lt;/b&gt;가 되어야 하는 사용 사례의 경우:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;입력용 Advisor&lt;/b&gt;와 &lt;b&gt;출력용 Advisor&lt;/b&gt;를 &lt;b&gt;별도로 구현&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;각각을 &lt;b&gt;다른 order 값&lt;/b&gt;으로 설정합니다.&lt;/li&gt;
&lt;li&gt;두 Advisor 간에 상태를 공유하기 위해 &lt;b&gt;advisor context&lt;/b&gt;를 사용합니다. &amp;lt;/aside&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;2. API 개요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 Advisor 인터페이스들은 org.springframework.ai.chat.client.advisor.api 패키지에 위치해 있습니다. 커스텀 Advisor를 만들 때 접하게 될 핵심 인터페이스들은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface Advisor extends Ordered {
	
	String getName();
	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기식 및 반응형 방식을 위한 두가지 서브 인터페이스가 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public interface CallAdvisor extends Advisor { // sync

	ChatClientResponse adviseCall(
		ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public interface StreamAdvisor extends Advisor { // reactive

	Flux&amp;lt;ChatClientResponse&amp;gt; adviseStream(
		ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 방식에 해당하는 &lt;b&gt;Advisor&lt;/b&gt;가 실행되기 위한 체인도 동작방식별로 존재합니다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;public interface CallAdvisorChain extends AdvisorChain {

	/**
	 * Invokes the next {@link CallAdvisor} in the {@link CallAdvisorChain} with the given
	 * request.
	 */
	ChatClientResponse nextCall(ChatClientRequest chatClientRequest);

	/**
	 * Returns the list of all the {@link CallAdvisor} instances included in this chain at
	 * the time of its creation.
	 */
	List&amp;lt;CallAdvisor&amp;gt; getCallAdvisors();

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public interface StreamAdvisorChain extends AdvisorChain {

	/**
	 * Invokes the next {@link StreamAdvisor} in the {@link StreamAdvisorChain} with the
	 * given request.
	 */
	Flux&amp;lt;ChatClientResponse&amp;gt; nextStream(ChatClientRequest chatClientRequest);

	/**
	 * Returns the list of all the {@link StreamAdvisor} instances included in this chain
	 * at the time of its creation.
	 */
	List&amp;lt;StreamAdvisor&amp;gt; getStreamAdvisors();

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3. Advisor 구현&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Advisor를 생성하려면 CallAdvisor 또는 StreamAdvisor(또는 둘 다)를 구현하면 됩니다. 핵심 메서드는 논스트리밍의 경우 nextCall()이고, 스트리밍 advisor의 경우 nextStream()입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3.1 Examples&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관찰(observing) 및 보강(augmenting) 사용 사례를 구현하는 방법을 보여주기 위해 몇 가지 실습 예제를 제공하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.1.1 Logging Advisor&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체인에서 다음 Advisor를 호출하기 전에는 ChatClientRequest, 호출한 후에는 ChatClientResponse를 로깅하는 간단한 로깅 Advisor를 구현할 수 있습니다. 이 Advisor는 요청과 응답을 관찰만 하며 수정하지는 않습니다. 본 구현은 비스트리밍과 스트리밍 시나리오를 모두 지원합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class SimpleLoggerAdvisor implements CallAdvisor, StreamAdvisor {

	private static final Logger logger = LoggerFactory.getLogger(SimpleLoggerAdvisor.class);

	@Override
	public String getName() {
	  // Advisor 에 유니크한 이름 제공
		return this.getClass().getSimpleName();
	}

	@Override
	public int getOrder() {
	  // 실행 순서 제어
		return 0;
	}

	@Override
	public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
		logRequest(chatClientRequest);

		ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);

		logResponse(chatClientResponse);

		return chatClientResponse;
	}

	@Override
	public Flux&amp;lt;ChatClientResponse&amp;gt; adviseStream(ChatClientRequest chatClientRequest,
			StreamAdvisorChain streamAdvisorChain) {
		logRequest(chatClientRequest);

		Flux&amp;lt;ChatClientResponse&amp;gt; chatClientResponses = streamAdvisorChain.nextStream(chatClientRequest);

		return new ChatClientMessageAggregator().aggregateChatClientResponse(chatClientResponses, this::logResponse);
	}

	private void logRequest(ChatClientRequest request) {
		logger.debug(&quot;request: {}&quot;, request);
	}

	private void logResponse(ChatClientResponse chatClientResponse) {
		logger.debug(&quot;response: {}&quot;, chatClientResponse);
	}

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트리밍 방식 예제에서 사용된 MessageAggregator는 Flux 형태의 응답들을 단일 ChatClientResponse로 집계하는 유틸리티 클래스입니다. 이는 스트림의 개별 항목이 아니라 전체 응답을 관찰해야 하는 로깅 또는 기타 처리에 유용합니다. 또한 읽기 전용으로 동작하므로, MessageAggregator 내부에서는 응답을 변경할 수 없습니다.&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;3.3.2 Re-Reading (Re2) Advisor&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://arxiv.org/pdf/2309.06275&quot;&gt;Re-Reading Improves Reasoning in Large Language Models&lt;/a&gt;라는 논문에서는 Re-Reading(Re2) 이라는 기법을 소개합니다. 이 기법은 대규모 언어 모델(LLM) 의 추론(reasoning) 능력을 향상시키는 방법으로, 입력 프롬프트를 아래와 같이 보강(augmenting) 하는 과정을 필요로 합니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;{Input_Query}
Read the question again: {Input_Query}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoggingAdvisor 예시와 달리 before, after 로 나누어 접근하는 방법도 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Re2 적용은 요청 전 단계에서만 작업하면 되므로 before 메서드를 활용하여 구현할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;public class ReReadingAdvisor implements BaseAdvisor {

	private static final String DEFAULT_RE2_ADVISE_TEMPLATE = &quot;&quot;&quot;
			{re2_input_query}
			Read the question again: {re2_input_query}
			&quot;&quot;&quot;;

	private final String re2AdviseTemplate;

	private int order = 0;

	public ReReadingAdvisor() {
		this(DEFAULT_RE2_ADVISE_TEMPLATE);
	}

	public ReReadingAdvisor(String re2AdviseTemplate) {
		this.re2AdviseTemplate = re2AdviseTemplate;
	}

	@Override
	public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) { 
	  // 요청에서 사용자 입력 프롬프트를 Re2 적용된 프롬프트로 바꾼다
		String augmentedUserText = PromptTemplate.builder()
			.template(this.re2AdviseTemplate)
			.variables(Map.of(&quot;re2_input_query&quot;, chatClientRequest.prompt().getUserMessage().getText()))
			.build()
			.render();

		return chatClientRequest.mutate()
			.prompt(chatClientRequest.prompt().augmentUserMessage(augmentedUserText))
			.build();
	}

	@Override
	public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
		return chatClientResponse;
	}

	@Override
	public int getOrder() { 
		return this.order;
	}

	public ReReadingAdvisor withOrder(int order) {
		this.order = order;
		return this;
	}

}
&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;3.3.3. Spring Built-in Advisors&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AI Framework 에서는 AI와 여러 상호작용을 하기 위한 여러 built-in advisor를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유형 Advisor 이름 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Chat Memory Advisors&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;MessageChatMemoryAdvisor&lt;/td&gt;
&lt;td&gt;메모리를 검색하여 메시지 컬렉션으로 프롬프트에 추가. 대화 히스토리 구조 유지. (모든 AI 모델이 지원하는 것은 아님)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;PromptChatMemoryAdvisor&lt;/td&gt;
&lt;td&gt;메모리를 검색하여 프롬프트의 시스템 텍스트에 통합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;VectorStoreChatMemoryAdvisor&lt;/td&gt;
&lt;td&gt;VectorStore에서 메모리를 검색하여 프롬프트의 시스템 텍스트에 추가. 대규모 데이터셋에서 효율적인 검색 및 정보 추출에 유용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Question Answering Advisor&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;QuestionAnswerAdvisor&lt;/td&gt;
&lt;td&gt;Vector Store를 사용하여 질의응답 기능 제공. 일반적인 RAG 패턴 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;RetrievalAugmentationAdvisor&lt;/td&gt;
&lt;td&gt;org.springframework.ai.rag 패키지의 빌딩 블록을 사용하여 일반적인 RAG 플로우 구현. Modular RAG Architecture 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Reasoning Advisor&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ReReadingAdvisor&lt;/td&gt;
&lt;td&gt;LLM 추론을 위한 RE2(Re-Reading) 전략 구현. 입력 단계에서 이해력 향상. &lt;a href=&quot;https://www.notion.so/deveely/arxiv.org/pdf/2309.06275&quot;&gt;논문&lt;/a&gt; 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Content Safety Advisor&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;SafeGuardAdvisor&lt;/td&gt;
&lt;td&gt;모델이 유해하거나 부적절한 콘텐츠를 생성하지 않도록 방지하는 간단한 Advisor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3.4. Streaming VS Non-streaming&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2807&quot; data-origin-height=&quot;1425&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dICI2d/btsQ9ShY32t/mmCvZIOsvHRNY7QzesySKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dICI2d/btsQ9ShY32t/mmCvZIOsvHRNY7QzesySKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dICI2d/btsQ9ShY32t/mmCvZIOsvHRNY7QzesySKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdICI2d%2FbtsQ9ShY32t%2FmmCvZIOsvHRNY7QzesySKk%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;2807&quot; height=&quot;1425&quot; data-origin-width=&quot;2807&quot; data-origin-height=&quot;1425&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Non-streaming Advisor 는 요청, 응답을 단일객체로 한번에 처리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;요청 &amp;rarr; [Advisor 1] &amp;rarr; [Advisor 2] &amp;rarr; [Chat Model - 10초 대기] 
                                        &amp;darr;
                                    &quot;AI는 인공지능입니다...&quot;
                                        &amp;darr;
응답 &amp;larr; [Advisor 1] &amp;larr; [Advisor 2] &amp;larr; 완성된 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Streaming Advisor는 요청은 한번 처리하되 응답은 여러 번 호출될 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답 Chunk 만큼 수행될 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;요청 &amp;rarr; [Advisor 1] &amp;rarr; [Advisor 2] &amp;rarr; [Chat Model]
                                        &amp;darr;
                                    &quot;AI는&quot; (0.1초)
                                        &amp;darr;
응답 조각1 &amp;larr; [Advisor 1 AFTER] &amp;larr; [Advisor 2 AFTER] &amp;larr; &quot;AI는&quot;
                                        &amp;darr;
                                    &quot;인공지능을&quot; (0.2초)
                                        &amp;darr;
응답 조각2 &amp;larr; [Advisor 1 AFTER] &amp;larr; [Advisor 2 AFTER] &amp;larr; &quot;인공지능을&quot;
                                        &amp;darr;
                                    &quot;의미합니다&quot; (0.3초)
                                        &amp;darr;
응답 조각3 &amp;larr; [Advisor 1 AFTER] &amp;larr; [Advisor 2 AFTER] &amp;larr; &quot;의미합니다&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Logging 을 예시로 보면 아래와 같이 동작할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;// Non-streaming: 전체 응답 한 번에 로깅
.map(response -&amp;gt; {
    log.info(&quot;완성된 응답: {}&quot;, response.getContent());
    // 출력: &quot;완성된 응답: AI는 인공지능을 의미합니다...&quot;
    return response;
});

// Streaming: 각 조각마다 로깅
.map(chunk -&amp;gt; {
    log.info(&quot;응답 조각: {}&quot;, chunk.getContent());
    // 출력1: &quot;응답 조각: AI는&quot;
    // 출력2: &quot;응답 조각: 인공지능을&quot;
    // 출력3: &quot;응답 조각: 의미합니다&quot;
    return chunk;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3.5 Best Practices&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;특정 Advisor가 특정 작업에 집중하도록 구현한다 (모듈화)&lt;/li&gt;
&lt;li&gt;Advisor 간 상태 공유가 필요한 경우 adviseContext 를 활용한다.&lt;/li&gt;
&lt;li&gt;유연성을 극대화하기 위해 Advisor 구현 시 Streaming, Non-streaming 방식 모두 구현하는 것이 좋다.&lt;/li&gt;
&lt;li&gt;적절한 데이터 흐름을 보장하기 위해 Advisor 체인 순서를 잘 구성하는 것이 중요하다&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>AI &amp;amp; LLM</category>
      <category>AI</category>
      <category>ChatGPT</category>
      <category>Claude</category>
      <category>LLM</category>
      <category>spring ai</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/138</guid>
      <comments>https://do-study.tistory.com/138#entry138comment</comments>
      <pubDate>Tue, 14 Oct 2025 09:40:18 +0900</pubDate>
    </item>
    <item>
      <title>[Spring AI 공식문서 읽기] 2. ChatClient API</title>
      <link>https://do-study.tistory.com/137</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script&gt;
const _0x5eef=['classList','92935nhtnYq','setAttribute','push','innerHTML','getElementById','toLowerCase','tt_adsense_top','another_category','style','//p[contains(text(),\x27[목차여기]\x27)]','1954669aacfHB','div','appendChild','toc-ym','title','forEach','DOMContentLoaded','call','addEventListener','length','insertBefore','firstElementChild','log','27309qNoTHN','62SuwPRc','parentNode','querySelector','revenue_unit_wrap','tagName','23736mMyuUa','singleNodeValue','trim','17723tUfPMr','textContent','1STKGDu','getAttribute','contains','nextSibling','791846eKKEom','createElement','outerText','FIRST_ORDERED_NODE_TYPE','querySelectorAll','72wJWnLP','hasAttribute','669103LLOFBD','toc'];function _0x330c(_0x5d40d0,_0x4afdad){_0x5d40d0=_0x5d40d0-0xec;let _0x5eef71=_0x5eef[_0x5d40d0];return _0x5eef71;}const _0x2078d2=_0x330c;(function(_0xbea334,_0x392453){const _0x2c3076=_0x330c;while(!![]){try{const _0x5a087d=-parseInt(_0x2c3076(0x117))+parseInt(_0x2c3076(0xf7))+parseInt(_0x2c3076(0xfa))+-parseInt(_0x2c3076(0x11a))*-parseInt(_0x2c3076(0xf5))+parseInt(_0x2c3076(0x112))*parseInt(_0x2c3076(0x111))+parseInt(_0x2c3076(0xec))*-parseInt(_0x2c3076(0xf0))+-parseInt(_0x2c3076(0x104));if(_0x5a087d===_0x392453)break;else _0xbea334['push'](_0xbea334['shift']());}catch(_0x47ff63){_0xbea334['push'](_0xbea334['shift']());}}}(_0x5eef,0xea9e9),document[_0x2078d2(0x10c)](_0x2078d2(0x10a),function(){const _0x7eb51e=_0x2078d2;try{const _0x591681=document[_0x7eb51e(0x114)]('.contents_style'),_0x1762f9=document[_0x7eb51e(0xfe)](_0x7eb51e(0x107));if(_0x591681&amp;&amp;!_0x1762f9)htmlTableOfContents();else return![];}catch(_0x250abc){console[_0x7eb51e(0x110)]('');}}));function htmlTableOfContents(_0x4f1c99){const _0x388803=_0x2078d2;var _0x4f1c99=_0x4f1c99||document;const _0x44fb35=document[_0x388803(0xf1)]('div');_0x44fb35[_0x388803(0xfb)]('id',_0x388803(0x107));const _0x2117e2=document['querySelector']('.contents_style');var _0x35e549=_0x388803(0x103),_0x552a33=document['evaluate'](_0x35e549,document,null,XPathResult[_0x388803(0xf3)],null)[_0x388803(0x118)];let _0x407aa0;_0x552a33?(_0x407aa0=_0x552a33,_0x407aa0[_0x388803(0x11b)]='',_0x407aa0[_0x388803(0x106)](_0x44fb35)):(_0x407aa0=_0x2117e2[_0x388803(0x10f)],_0x407aa0['classList'][_0x388803(0xee)](_0x388803(0x100))||_0x407aa0[_0x388803(0xf9)]['contains'](_0x388803(0x115))?_0x2117e2['insertBefore'](_0x44fb35,_0x407aa0[_0x388803(0xef)]):_0x407aa0[_0x388803(0x113)][_0x388803(0x10e)](_0x44fb35,_0x407aa0));const _0x3e06b5=document['getElementById'](_0x388803(0x107)),_0x5ee2f2=[]['slice'][_0x388803(0x10b)](_0x2117e2[_0x388803(0xf4)]('h1,\x20h2,\x20h3,\x20h4,\x20h5,\x20h6')),_0x454032=[];for(i=0x0;i&lt;_0x5ee2f2[_0x388803(0x10d)];i++){if(_0x5ee2f2[i][_0x388803(0xf2)][_0x388803(0x119)]()==='')continue;else{if(_0x5ee2f2[i][_0x388803(0xf9)][_0x388803(0xee)](_0x388803(0x108)))continue;else{if(_0x5ee2f2[i][_0x388803(0x113)]['classList'][_0x388803(0xee)](_0x388803(0x101)))continue;else _0x454032[_0x388803(0xfc)](_0x5ee2f2[i]);}}}_0x454032[_0x388803(0x109)](function(_0x5d97e0,_0x2112a5){const _0x4b3465=_0x388803;var _0x94aa2e=_0x4b3465(0xf8)+_0x2112a5;if(_0x5d97e0[_0x4b3465(0xf6)]('id'))_0x94aa2e=_0x5d97e0[_0x4b3465(0xed)]('id');else _0x5d97e0[_0x4b3465(0xfb)]('id',_0x94aa2e);var _0x34278b=_0x4f1c99[_0x4b3465(0xf1)]('a');_0x34278b[_0x4b3465(0xfb)]('href','#'+_0x94aa2e),_0x34278b['textContent']='•\x20'+_0x5d97e0[_0x4b3465(0x11b)];var _0x118edf=_0x4f1c99[_0x4b3465(0xf1)](_0x4b3465(0x105));_0x118edf[_0x4b3465(0xfb)]('class',_0x5d97e0[_0x4b3465(0x116)][_0x4b3465(0xff)]()),_0x118edf[_0x4b3465(0x106)](_0x34278b),_0x3e06b5[_0x4b3465(0x106)](_0x118edf);});const _0xd72dc='\x0a\x20\x20\x20\x20#toc-ym\x20div.h1\x20{\x20margin-left:\x200em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h2\x20{\x20margin-left:\x200.5em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h3\x20{\x20margin-left:\x201em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h4\x20{\x20margin-left:\x201.5em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h5\x20{\x20margin-left:\x202em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h6\x20{\x20margin-left:\x202.5em\x20}\x0a\x20\x20\x20\x20\x0a\x20\x20\x20\x20#toc-ym\x20{\x0a\x20\x20\x20\x20\x20\x20margin:\x2030px\x200px\x2030px\x200px;\x0a\x20\x20\x20\x20\x20\x20padding:\x2020px\x2020px\x2010px\x2015px;\x0a\x20\x20\x20\x20\x20\x20border:\x201px\x20solid\x20#dadada;\x0a\x20\x20\x20\x20\x20\x20background-color:\x20#ffffff;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym::before\x20{\x0a\x20\x20\x20\x20\x20\x20content:\x20\x22목\x20\x20차\x22;\x0a\x20\x20\x20\x20\x20\x20display:\x20block;\x0a\x20\x20\x20\x20\x20\x20width:\x20120px;\x0a\x20\x20\x20\x20\x20\x20background-color:\x20rgb(255,\x20255,\x20255);\x0a\x20\x20\x20\x20\x20\x20text-align:\x20center;\x0a\x20\x20\x20\x20\x20\x20font-size:\x2018px;\x0a\x20\x20\x20\x20\x20\x20font-weight:\x20bold;\x0a\x20\x20\x20\x20\x20\x20margin:\x20-40px\x20auto\x200px;\x0a\x20\x20\x20\x20\x20\x20padding:\x205px\x200px;\x0a\x20\x20\x20\x20\x20\x20border-width:\x201px;\x0a\x20\x20\x20\x20\x20\x20border-style:\x20solid;\x0a\x20\x20\x20\x20\x20\x20border-color:\x20rgb(218,\x20218,\x20218);\x0a\x20\x20\x20\x20\x20\x20border-image:\x20initial;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div{\x0a\x20\x20\x20\x20\x20\x20margin:\x205px\x200px;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div:first-child{\x0a\x20\x20\x20\x20\x20\x20margin-top:\x2015px;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div:last-child{\x0a\x20\x20\x20\x20\x20\x20margin-bottom:\x2015px;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div\x20a\x20{\x0a\x20\x20\x20\x20\x20\x20text-decoration:\x20none;\x0a\x20\x20\x20\x20\x20\x20color:\x20#337ab7;\x0a\x20\x20\x20\x20\x20\x20transition:\x20all\x20ease\x200.2s;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div\x20a:hover\x20{\x0a\x20\x20\x20\x20\x20\x20\x0a\x20\x20\x20\x20\x20\x20color:\x20#333333;\x0a\x20\x20\x20\x20\x20\x20background-color:\x20#ecc7ff;\x0a\x20\x20\x20\x20\x20\x20\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20/*\x0a\x20\x20\x20\x20.contents_style\x20h3{\x0a\x20\x20\x20\x20\x20\x20margin-bottom:7px;\x0a\x20\x20\x20\x20\x20\x20padding:\x2010px\x2015px;\x0a\x20\x20\x20\x20\x20\x20border-left:\x205px\x20solid\x20#757575;\x0a\x20\x20\x20\x20\x20\x20background-color:\x20#e5e5e5;\x0a\x20\x20\x20\x20\x20\x20font-weight:\x20500;\x0a\x20\x20\x20\x20\x20\x20color:\x20#000000\x20!important;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20*/\x0a\x20\x20\x20\x20',_0x3ed036=document[_0x388803(0xf1)](_0x388803(0x102));_0x3ed036[_0x388803(0xfd)]=_0xd72dc,_0x2117e2[_0x388803(0x10e)](_0x3ed036,_0x407aa0);}
&lt;/script&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1&gt;0. Overview&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AI의 &lt;b&gt;ChatClient&lt;/b&gt;는 AI 모델과 대화하기 위한 도구입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동기식과 스트리밍 방식을 모두 지원하고,&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fluent API&lt;/b&gt;로 프롬프트를 쉽게 구성할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트는 사용자 메시지와 시스템 메시지로 이루어지며, 실행 시점에 변수를 치환할 수 있는 &lt;b&gt;플레이스홀더&lt;/b&gt;도 활용할 수 있습니다. 또한 사용할 &lt;b&gt;AI 모델 종류&lt;/b&gt;와 &lt;b&gt;temperature 값&lt;/b&gt; 같은 옵션을 통해 답변의 톤과 창의성을 조절할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&lt;b&gt;1. ChatClient 생성&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatClient 는 ChatClient.Builder 를 통해 생성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 환경에서는 자동 구성된 ChatClient.Builder 를 주입받아 생성할 수 있고, 프로그래밍적인 방법으로 직접 생성할 수 도 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 자동구성된 ChatClient.Builder 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 간단한 사용사례로 ChatClient.Builder 를 주입받아 ChatClient 를 생성하고 사용자 입력에 대한 String 응답을 가져오는 예시입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;call() 메서드는 AI모델로 요청을 보내고, content() 메서드는 AI응답을 문자열로 반환합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
class MyController {

    private final ChatClient chatClient;

    public MyController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping(&quot;/ai&quot;)
    String generation(String userInput) {
        return this.chatClient.prompt()
            .user(userInput)
            .call()
            .content();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2. 다중 Chat Models 활용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 애플리케이션에서 여러 개의 &lt;b&gt;Chat Model&lt;/b&gt;을 사용해야 하는 상황이 있습니다. 예를 들어:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;작업별 다른 모델 사용 :&lt;/b&gt; 복잡한 추론에는 강력한 모델, 단순 작업에는 빠르고 저렴한 모델 활용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장애 대비(Fallback) :&lt;/b&gt; 특정 모델 서비스가 불가능할 때 다른 모델로 대체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;A/B 테스트 :&lt;/b&gt; 서로 다른 모델이나 설정을 비교 실험&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 선택 제공 :&lt;/b&gt; 사용자 취향에 따라 모델을 고를 수 있도록 옵션 제공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전문화된 모델 조합 :&lt;/b&gt; 코드 생성용 모델, 창의적 글쓰기용 모델 등 역할별 모델을 함께 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동구성 이용 시 단일 모델에 대한 ChatClient.Builder 만 사용이 가능하기 때문에 애플리케이션 내에서 여러 모델을 이용하려면 자동구성을 비활성화하고 ChatClient 인스턴스를 직접 생성해야합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  ai:
    chat:
      client:
        enabled: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatModel 에 따라 각각 클라이언트를 구성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Configuration
public class ChatClientConfig {

    @Bean
    public ChatClient openAiChatClient(OpenAiChatModel chatModel) {
        return ChatClient.create(chatModel);
    }

    @Bean
    public ChatClient anthropicChatClient(AnthropicChatModel chatModel) {
        return ChatClient.create(chatModel);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 환경에 맞게 모델을 주입받고 적절히 분기하여 사용되게 코드를 구성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@Configuration
public class ChatClientExample {

    @Bean
    CommandLineRunner cli(
            @Qualifier(&quot;openAiChatClient&quot;) ChatClient openAiChatClient,
            @Qualifier(&quot;anthropicChatClient&quot;) ChatClient anthropicChatClient) {

        return args -&amp;gt; {
            var scanner = new Scanner(System.in);
            ChatClient chat;

            // Model selection
            System.out.println(&quot;\\nSelect your AI model:&quot;);
            System.out.println(&quot;1. OpenAI&quot;);
            System.out.println(&quot;2. Anthropic&quot;);
            System.out.print(&quot;Enter your choice (1 or 2): &quot;);

            String choice = scanner.nextLine().trim();

            if (choice.equals(&quot;1&quot;)) {
                chat = openAiChatClient;
                System.out.println(&quot;Using OpenAI model&quot;);
            } else {
                chat = anthropicChatClient;
                System.out.println(&quot;Using Anthropic model&quot;);
            }

            // Use the selected chat client
            System.out.print(&quot;\\nEnter your question: &quot;);
            String input = scanner.nextLine();
            String response = chat.prompt(input).call().content();
            System.out.println(&quot;ASSISTANT: &quot; + response);

            scanner.close();
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은 단일 ChatModel을 이용하더라도 시스템 프롬프트 등 구성을 달리하는 ChatClient 를 여러 개 구성할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;ChatClient.Builder builder = ChatClient.builder(myChatModel);

// 기본 클라이언트
ChatClient defaultChatClient = builder.build();

// 시스템프롬프트 커스텀한 클라이언트
ChatClient customChatClient = builder
    .defaultSystemPrompt(&quot;You are a helpful assistant.&quot;)
    .build();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3. 다중 OpenAI API 호환 엔드포인트 연동 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI 모델과 OpenAI API 호환 모델 (e.g. Groq) 을 동시에 사용하는 경우 기준 모델을 mutate() 메서드로 간단히 속성만 변경하여 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Configuration
public class ChatClientConfig {

    @Autowired
    private OpenAiChatModel baseChatModel;

    @Autowired
    private OpenAiApi baseOpenAiApi;

    @Bean
    public ChatClient groqChatClient(OpenAiChatModel groqModel) {
        return ChatClient.builder(groqModel).build();
    }

    @Bean
    public ChatClient gpt4ChatClient(OpenAiChatModel gpt4Model) {
        return ChatClient.builder(gpt4Model).build();
    }

    @Bean
    public OpenAiChatModel groqModel(OpenAiApi groqApi) {
        return baseChatModel.mutate()
            .openAiApi(groqApi)
            .defaultOptions(OpenAiChatOptions.builder()
                .model(&quot;llama3-70b-8192&quot;)
                .temperature(0.5)
                .build())
            .build();
    }

    @Bean
    public OpenAiChatModel gpt4Model(OpenAiApi gpt4Api) {
        return baseChatModel.mutate()
            .openAiApi(gpt4Api)
            .defaultOptions(OpenAiChatOptions.builder()
                .model(&quot;gpt-4&quot;)
                .temperature(0.7)
                .build())
            .build();
    }
    
    @Bean
    public OpenAiApi groqApi() {
        return baseOpenAiApi.mutate()
            .baseUrl(&quot;&amp;lt;https://api.groq.com/openai&amp;gt;&quot;)
            .apiKey(System.getenv(&quot;GROQ_API_KEY&quot;))
            .build();
    }

    @Bean
    public OpenAiApi gpt4Api() {
        return baseOpenAiApi.mutate()
            .baseUrl(&quot;&amp;lt;https://api.openai.com&amp;gt;&quot;)
            .apiKey(System.getenv(&quot;OPENAI_API_KEY&quot;))
            .build();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;&lt;b&gt;2. ChatClient Fluent API&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatClient의 prompt 메서드를 사용해 세 가지 방식으로 프롬프트를 생성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;prompt()&lt;/td&gt;
&lt;td&gt;인자가 없는 이 메서드는 플루언트 API를 시작할 때 사용하며, 사용자(user), 시스템(system) 등의 메시지를 단계적으로 추가해 프롬프트를 구성할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prompt(Prompt prompt)&lt;/td&gt;
&lt;td&gt;Prompt 인스턴스를 인자로 받아, 플루언트 API가 아닌 방식으로 미리 생성한 Prompt 객체를 전달할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prompt(String content)&lt;/td&gt;
&lt;td&gt;사용자의 텍스트 내용을 바로 전달할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;&lt;b&gt;3. ChatClient Responses&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatClient를 사용하여 AI 모델의 응답을 다양한 방식으로 포맷할 수 있는 방법을 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. ChatResponse 반환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 모델의 응답은 ChatResponse ****타입으로 정의된 풍부한 구조를 가지고 있습니다.&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;/b&gt;가 포함되며, 이는 호스팅되는 AI 모델이 요청당 사용된 토큰 수에 따라 과금하기 때문에 중요한 정보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시는 call() 메서드 호출 후 chatResponse()를 사용하여 메타데이터가 포함된 ChatResponse 객체를 반환하는 방법을 보여줍니다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;ChatResponse chatResponse = chatClient.prompt()
    .user(&quot;Tell me a joke&quot;)
    .call()
    .chatResponse();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. Entity 반환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 모델의 응답을 단순 문자열이 아니라 모델 객체로 매핑해 반환하고 싶을 때가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatClient의 entity() 메서드를 통해 응답을 특정 클래스로 매핑할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;record ActorFilms(String actor, List&amp;lt;String&amp;gt; movies) {}

// 무작위 배우 대상 배우, 영화목록 형태 응답을 기대
ActorFilms actorFilms = chatClient.prompt()
    .user(&quot;Generate the filmography for a random actor.&quot;)
    .call()
    .entity(ActorFilms.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은 아래와 같이 List, Map 과 같은 복잡한 타입을 알려줄 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// Tom Hanks와 Bill Murray 배우의 영화목록 형태 응답을 기대
List&amp;lt;ActorFilms&amp;gt; actorFilms = chatClient.prompt()
    .user(&quot;Generate the filmography of 5 movies for Tom Hanks and Bill Murray.&quot;)
    .call()
    .entity(new ParameterizedTypeReference&amp;lt;List&amp;lt;ActorFilms&amp;gt;&amp;gt;() {});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3. 응답 Streaming&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stream() 메서드를 사용하면 아래와 같이 비동기(reactive) 방식으로 응답을 스트리밍할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;Flux&amp;lt;String&amp;gt; output = chatClient.prompt()
    .user(&quot;Tell me a joke&quot;)
    .stream()
    .content();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 stream() 메서드를 사용할 때 Java 엔티티를 바로 반환할 수 있는 편의 메서드가 제공될 예정입니다만 현재 버전에서는 아래 예시처럼 Structured Output Converter를 사용해 스트리밍된 응답을 직접 변환해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시에서는 param() 메서드를 활용한 파라미터를 사용하는 방법을 함께 보여줍니다. (자세한 내용은 문서의 후반부에서 설명됩니다)&lt;/p&gt;
&lt;pre class=&quot;pony&quot;&gt;&lt;code&gt;var converter = new BeanOutputConverter&amp;lt;&amp;gt;(new ParameterizedTypeReference&amp;lt;List&amp;lt;ActorsFilms&amp;gt;&amp;gt;() {});

Flux&amp;lt;String&amp;gt; flux = this.chatClient.prompt()
    .user(u -&amp;gt; u.text(&quot;&quot;&quot;
                        Generate the filmography for a random actor.
                        {format}
                      &quot;&quot;&quot;)
            .param(&quot;format&quot;, this.converter.getFormat()))
    .stream()
    .content();

String content = this.flux.collectList().block().stream().collect(Collectors.joining());

List&amp;lt;ActorsFilms&amp;gt; actorFilms = this.converter.convert(this.content);
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;4. 프롬프트 템플릿&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatClient는 런타임에 변수가 치환되는 템플릿 형태로 사용자(user)와 시스템(system) 텍스트를 제공할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;String answer = ChatClient.create(chatModel).prompt()
    .user(u -&amp;gt; u
            .text(&quot;Tell me the names of 5 movies whose soundtrack was composed by {composer}&quot;)
            .param(&quot;composer&quot;, &quot;John Williams&quot;))
    .call()
    .content();

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 ChatClient는 PromptTemplate ****클래스를 사용해 사용자 및 시스템 텍스트를 처리하며, 런타임에 제공된 값으로 변수를 치환합니다. 이때 TemplateRenderer 구현체를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Spring AI는 StTemplateRenderer 구현체를 사용하며, 이는 Terence Parr가 개발한 오픈 소스 StringTemplate 엔진을 기반으로 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Spring AI는 템플릿 처리가 필요하지 않은 경우를 위해 NoOpTemplateRenderer 도 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;aside&amp;gt;  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatClient에 직접 설정한 TemplateRenderer(예: .templateRenderer() 사용)는 빌더 체인에서 직접 정의한 프롬프트 내용(예: .user(), .system())에만 적용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 QuestionAnswerAdvisor와 같이 어드바이저 내부에서 사용되는 템플릿에는 영향을 주지 않으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 어드바이저들은 자체적인 템플릿 커스터마이징 메커니즘을 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(자세한 내용은 Custom Advisor Templates 섹션을 참고하십시오.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;/aside&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 다른 템플릿 엔진을 사용하고 싶다면, TemplateRenderer 인터페이스를 직접 구현하여 ChatClient에 제공할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 기본적으로 제공되는 StTemplateRenderer 를 계속 사용하되, 사용자 정의 설정으로 커스터마이징할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 기본적으로 템플릿 변수는 {} 구문으로 식별됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트에 JSON을 포함할 계획이라면 JSON의 중괄호({})와 충돌을 피하기 위해 다른 구분자를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시는 템플릿 변수 식별자를 &amp;lt; 와 &amp;gt; 로 변경하는 예제입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;TemplateRenderer customTemplateRenderer = StTemplateRenderer.builder()
    .startDelimiterToken('&amp;lt;')
    .endDelimiterToken('&amp;gt;')
    .build();

String answer = ChatClient.create(chatModel).prompt()
    .user(u -&amp;gt; u
            .text(&quot;Tell me the names of 5 movies whose soundtrack was composed by &amp;lt;composer&amp;gt;&quot;)
            .param(&quot;composer&quot;, &quot;John Williams&quot;))
    .templateRenderer(customTemplateRenderer)
    .call()
    .content();

&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;5. call() return values&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatClient에서 call() 메서드를 지정한 후에는 응답을 다양한 형태로 받을 수 있는 여러 옵션이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String content()&lt;/td&gt;
&lt;td&gt;응답의 문자열 내용만 반환합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatResponse chatResponse()&lt;/td&gt;
&lt;td&gt;여러 Generations(응답 후보)을 포함하고, 응답 생성에 사용된 토큰 수 등 메타데이터를 담은 ChatResponse 객체를 반환합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatClientResponse chatClientResponse()&lt;/td&gt;
&lt;td&gt;ChatResponse 와 함께 ChatClient 실행 컨텍스트를 포함하는 객체를 반환합니다. 이를 통해 어드바이저 실행 중 사용된 추가 데이터(예: RAG 플로우에서 검색된 문서 등)에 접근할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ResponseEntity&amp;lt;?&amp;gt; responseEntity()&lt;/td&gt;
&lt;td&gt;상태 코드, 헤더, 바디를 포함한 전체 HTTP 응답을 ResponseEntity 로 반환합니다. 저수준 HTTP 세부 정보가 필요할 때 유용합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;entity() 메서드로 특정 모델로 바로 변환할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;entity()&lt;/td&gt;
&lt;td&gt;Java 타입으로 변환된 엔티티를 반환합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;entity(ParameterizedTypeReference type)&lt;/td&gt;
&lt;td&gt;컬렉션 형태의 엔티티 타입을 반환할 때 사용합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;entity(Class type)&lt;/td&gt;
&lt;td&gt;특정 단일 엔티티 타입으로 변환합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;entity(StructuredOutputConverter structuredOutputConverter)&lt;/td&gt;
&lt;td&gt;StructuredOutputConverter 인스턴스를 지정해 문자열을 원하는 엔티티 타입으로 변환합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;aside&amp;gt;  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;call() 메서드를 호출하는 것만으로는 실제 AI 모델 실행이 이루어지지 않습니다. 이 메서드는 Spring AI에게 동기 호출을 할지 스트리밍 호출을 할지를 지정하는 역할만 합니다. 실제 AI 모델 호출은 content(), chatResponse(), responseEntity() 와 같은 메서드를 호출할 때 수행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;/aside&amp;gt;&lt;/p&gt;
&lt;h1&gt;6. stream() return values&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatClient에서 stream() 메서드를 지정한 후에는 응답을 받을 수 있는 몇 가지 옵션이 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flux content()&lt;/td&gt;
&lt;td&gt;AI 모델이 생성하는 문자열을 Flux 형태로 반환합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flux chatResponse()&lt;/td&gt;
&lt;td&gt;메타데이터를 포함한 ChatResponse 객체를 Flux 형태로 반환합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flux chatClientResponse()&lt;/td&gt;
&lt;td&gt;ChatResponse 와 ChatClient 실행 컨텍스트를 포함하는 ChatClientResponse 객체를 Flux 형태로 반환합니다. 이를 통해 어드바이저 실행 중 사용된 추가 데이터(예: RAG 플로우에서 검색된 문서 등)에 접근할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;7. Using Defaults&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration 클래스에서 기본 시스템 텍스트를 설정하여 ChatClient를 생성하면 런타임 코드가 간결해집니다. 기본값을 설정해두면 ChatClient를 호출할 때 사용자 텍스트만 지정하면 되며, 매번 요청마다 시스템 텍스트를 설정할 필요가 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1. Default System Text 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예제와 같이 LLM 시스템 대화의 기본 행동태도를 ChatClient 설정으로 변경해줄수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 내용은 해당 ChatClient의 모든 system 메시지에 적용됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem(&quot;You are a friendly chat bot that answers question in the voice of a Pirate&quot;)
            .build();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트하기위한 간단한 RestController 를 작성하고 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
class AIController {

	private final ChatClient chatClient;

	AIController(ChatClient chatClient) {
		this.chatClient = chatClient;
	}

	@GetMapping(&quot;/ai/simple&quot;)
	public Map&amp;lt;String, String&amp;gt; completion(@RequestParam(value = &quot;message&quot;, defaultValue = &quot;Tell me a joke&quot;) String message) {
		return Map.of(&quot;completion&quot;, this.chatClient.prompt().user(message).call().content());
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 응답하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;❯ curl localhost:8080/ai/simple
{&quot;completion&quot;:&quot;Why did the pirate go to the comedy club? To hear some arrr-rated jokes! Arrr, matey!&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2. Default System Text 에 Parameter 적용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아례 예시에서는 설정 시점에 파라미터를 갖는 defaultSystem을 설정해두고 런타임에 값을 지정하는 예시입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem(&quot;You are a friendly chat bot that answers question in the voice of a {voice}&quot;)
            .build();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatClient 의 system 메서드를 통해 파라미터를 지정합니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;@RestController
class AIController {
	private final ChatClient chatClient;

	AIController(ChatClient chatClient) {
		this.chatClient = chatClient;
	}

	@GetMapping(&quot;/ai&quot;)
	Map&amp;lt;String, String&amp;gt; completion(@RequestParam(value = &quot;message&quot;, defaultValue = &quot;Tell me a joke&quot;) String message, String voice) {
		return Map.of(&quot;completion&quot;,
				this.chatClient.prompt()
						.system(sp -&amp;gt; sp.param(&quot;voice&quot;, voice))
						.user(message)
						.call()
						.content());
	}

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3. 다른 설정가능한 default 값&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatClient.Builder 정의 시점에 system 외 다른 기본값도 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;defaultOptions(ChatOptions chatOptions)&lt;/td&gt;
&lt;td&gt;ChatOptions 클래스에서 정의된 범용 옵션이나, OpenAiChatOptions 와 같은 모델별 옵션을 전달할 수 있습니다. 모델별 ChatOptions 구현체에 대한 자세한 내용은 JavaDocs를 참고하십시오.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;defaultFunction(String name, String description, java.util.function.Function&amp;lt;I, O&amp;gt; function)&lt;/td&gt;
&lt;td&gt;name 은 사용자 텍스트에서 함수를 참조할 때 사용되며, description 은 함수의 목적을 설명하고 AI 모델이 올바른 함수를 선택해 정확한 응답을 생성하도록 돕습니다. function 인자는 모델이 필요 시 실행할 Java 함수 인스턴스입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;defaultFunctions(String&amp;hellip; functionNames)&lt;/td&gt;
&lt;td&gt;애플리케이션 컨텍스트에 정의된 java.util.Function 빈(bean)의 이름을 지정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;defaultUser(String text)defaultUser(Resource text)defaultUser(Consumer&amp;lt;UserSpec&amp;gt; userSpecConsumer)&lt;/td&gt;
&lt;td&gt;기본 사용자 텍스트를 정의합니다. Consumer&amp;lt;UserSpec&amp;gt; 을 사용하면 람다식으로 사용자 텍스트와 기본 파라미터를 지정할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;defaultAdvisors(Advisor&amp;hellip; advisor)&lt;/td&gt;
&lt;td&gt;Advisor 를 통해 프롬프트를 구성하는 데이터를 수정할 수 있습니다. QuestionAnswerAdvisor 구현체를 사용하면 사용자 텍스트와 관련된 컨텍스트 정보를 프롬프트에 추가해 Retrieval Augmented Generation 패턴을 구현할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;defaultAdvisors(Consumer&amp;lt;AdvisorSpec&amp;gt; advisorSpecConsumer)&lt;/td&gt;
&lt;td&gt;Consumer&amp;lt;AdvisorSpec&amp;gt; 을 사용해 여러 Advisor 를 설정할 수 있습니다. 예를 들어, QuestionAnswerAdvisor 를 추가하여 사용자 텍스트 기반으로 관련 컨텍스트를 프롬프트에 덧붙이는 Retrieval Augmented Generation 패턴을 구성할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 기본 설정은 런타임에 동일한 이름에서 default 접두어가 없는 메서드를 사용하여 재정의할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;options(ChatOptions chatOptions)&lt;/li&gt;
&lt;li&gt;function(String name, String description, java.util.function.Function&amp;lt;I, O&amp;gt; function)&lt;/li&gt;
&lt;li&gt;functions(String&amp;hellip; functionNames)&lt;/li&gt;
&lt;li&gt;user(String text), user(Resource text), user(Consumer&amp;lt;UserSpec&amp;gt; userSpecConsumer)&lt;/li&gt;
&lt;li&gt;advisors(Advisor&amp;hellip; advisor)&lt;/li&gt;
&lt;li&gt;advisors(Consumer&amp;lt;AdvisorSpec&amp;gt; advisorSpecConsumer)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;8. Advisors&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Advisors API는 Spring 애플리케이션에서 AI 기반 상호작용을 가로채고(intercept), 수정하고(modify), 향상시키는(enhance) 유연하고 강력한 방법을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 모델을 사용자 입력과 함께 호출할 때 자주 사용하는 패턴 중 하나는 프롬프트에 컨텍스트 데이터(contextual data)를 추가하거나 보강하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 컨텍스트 데이터는 여러 유형일 수 있으며, 일반적으로 다음과 같은 유형이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Your own data : AI 모델이 학습하지 않은 데이터입니다. 모델이 유사한 데이터를 학습했더라도, 추가된 컨텍스트 데이터가 응답 생성 시 우선적으로 반영됩니다.&lt;/li&gt;
&lt;li&gt;Conversational history : 채팅 모델의 API는 상태를 저장하지 않습니다. 예를 들어, 모델에 본인의 이름을 알려도 이후 대화에서 기억하지 못합니다. 이전 상호작용을 고려하도록 하려면 &lt;b&gt;매 요청마다 대화 이력&lt;/b&gt;을 함께 전달해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1. ChatClient에 Advisor 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatClient의 유창한(Fluent) API는 AdvisorSpec 인터페이스를 제공하여 어드바이저를 구성할 수 있게 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인터페이스는 파라미터를 추가하거나, 한 번에 여러 파라미터를 설정하거나, 하나 이상의 어드바이저를 체인에 추가하는 메서드를 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;interface AdvisorSpec {
    AdvisorSpec param(String k, Object v);
    AdvisorSpec params(Map&amp;lt;String, Object&amp;gt; p);
    AdvisorSpec advisors(Advisor... advisors);
    AdvisorSpec advisors(List&amp;lt;Advisor&amp;gt; advisors);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;aside&amp;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;lt;/aside&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 구성에서 MessageChatMemoryAdvisor 는 먼저 실행되어 대화 이력을 프롬프트에 추가합니다. 그 다음 QuestionAnswerAdvisor 가 사용자의 질문과 추가된 대화 이력을 기반으로 검색을 수행하여, 더 관련성 높은 결과를 제공할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;ChatClient.builder(chatModel)
    .build()
    .prompt()
    .advisors(
        MessageChatMemoryAdvisor.builder(chatMemory).build(),
        QuestionAnswerAdvisor.builder(vectorStore).build()
    )
    .user(userText)
    .call()
    .content();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2. Retrieval Augmented Generation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Advisor를 활용한 Retrieval Augmented Generation (RAG) 내용은 &lt;a href=&quot;https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html&quot;&gt;별도 가이드 문서&lt;/a&gt;에서 상세하게 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.3. Logging&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimpleLoggerAdvisor 는 ChatClient의 요청과 응답 데이터를 로깅하는 어드바이저입니다. 이 어드바이저는 AI 상호작용을 디버깅하거나 모니터링할 때 유용하게 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Logging을 활성화하려면 ChatClient를 생성할 때 어드바이저 체인에 SimpleLoggerAdvisor를 추가하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 체인의 끝부분에 추가하는 것을 권장합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ChatResponse response = ChatClient.create(chatModel).prompt()
        .advisors(new SimpleLoggerAdvisor())
        .user(&quot;Tell me a joke?&quot;)
        .call()
        .chatResponse();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 확인하려면 application.properties 또는 application.yaml 파일에 다음과 같이 설정해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;advisor 패키지의 로깅 레벨을 DEBUG로 지정합니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;logging.level.org.springframework.ai.chat.client.advisor=DEBUG
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 아래와 같이 생성자를 사용하면 AdvisedRequest와 ChatResponse에서 어떤 데이터를 로깅할지 커스터마이징할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;SimpleLoggerAdvisor(
    Function&amp;lt;ChatClientRequest, String&amp;gt; requestToString,
    Function&amp;lt;ChatResponse, String&amp;gt; responseToString,
    int order
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 예시:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;SimpleLoggerAdvisor customLogger = new SimpleLoggerAdvisor(
    request -&amp;gt; &quot;Custom request: &quot; + request.prompt().getUserMessage(),
    response -&amp;gt; &quot;Custom response: &quot; + response.getResult(),
    0
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 로깅되는 정보를 애플리케이션의 요구 사항에 맞게 조정할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;9. Chat Memory&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatMemory 인터페이스는 채팅 대화 메모리 저장소를 나타냅니다. 이 인터페이스는 대화에 메시지를 추가하고, 대화에서 메시지를 조회하며, 대화 기록을 초기화하는 메서드를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 기본으로 제공되는 구현체는 MessageWindowChatMemory 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MessageWindowChatMemory 는 지정된 최대 크기(기본값: 20개 메시지)까지 메시지 윈도우를 유지하는 채팅 메모리 구현체입니다.&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;MessageWindowChatMemory 는 ChatMemoryRepository 추상화를 기반으로 하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 채팅 대화 메모리를 저장하기 위한 다양한 구현체를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 가능한 구현체에는 InMemoryChatMemoryRepository, JdbcChatMemoryRepository,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CassandraChatMemoryRepository, Neo4jChatMemoryRepository 등이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 내용과 사용 예시는 &lt;a href=&quot;https://docs.spring.io/spring-ai/reference/api/chat-memory.html&quot;&gt;Chat Memory&lt;/a&gt; 문서를 참고하십시오.&lt;/p&gt;
&lt;h1&gt;10. Implementation Notes&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ChatClient 는 &lt;b&gt;명령형(imperative)과 반응형(reactive) 프로그래밍 모델을 함께 사용하는&lt;/b&gt; 독특한 API입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대부분의 애플리케이션은 보통 둘 중 하나만 사용하지만, ChatClient 는 두 모델을 모두 지원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모델 구현의 &lt;b&gt;HTTP 클라이언트 상호작용을 커스터마이징&lt;/b&gt;할 때는 RestClient 와 WebClient 모두를 설정해야 합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot 3.4의 버그 때문에 spring.http.client.factory=jdk 속성을 반드시 설정해야 합니다. 그렇지 않으면 기본값이 reactor 로 설정되며, ImageModel 과 같은 일부 AI 워크플로가 정상적으로 동작하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스트리밍(streaming)은 반응형 스택에서만 지원&lt;/b&gt;됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명령형 애플리케이션에서도 스트리밍을 사용하려면 &lt;b&gt;반응형 스택&lt;/b&gt;(예: spring-boot-starter-webflux)을 반드시 포함해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비스트리밍(non-streaming)은 서블릿 스택에서만 지원&lt;/b&gt;됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반응형 애플리케이션에서 비스트리밍 호출을 사용하려면 &lt;b&gt;서블릿 스택&lt;/b&gt;(예: spring-boot-starter-web)을 포함해야 하며, 일부 호출은 블로킹될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;도구 호출(tool calling)&lt;/b&gt; 은 명령형 방식으로 동작하며, 이는 워크플로를 블로킹하게 만듭니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이로 인해 Micrometer 관측이 부분적 또는 중단될 수 있습니다.&lt;/li&gt;
&lt;li&gt;(예: ChatClient span 과 tool calling span 이 연결되지 않아 첫 번째 span 이 미완성 상태로 남을 수 있습니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내장 어드바이저&lt;/b&gt;는 일반 호출에서는 블로킹 방식으로, 스트리밍 호출에서는 논블로킹 방식으로 동작합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어드바이저의 스트리밍 호출에서 사용되는 &lt;b&gt;Reactor Scheduler&lt;/b&gt; 는 각 어드바이저 클래스의 Builder를 통해 설정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;참고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-ai/reference/api/chatclient.html#_creating_a_chatclient&quot;&gt;Spring AI Reference - Chat Client API&lt;/a&gt;&lt;/p&gt;</description>
      <category>AI &amp;amp; LLM</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/137</guid>
      <comments>https://do-study.tistory.com/137#entry137comment</comments>
      <pubDate>Sun, 5 Oct 2025 01:18:45 +0900</pubDate>
    </item>
    <item>
      <title>Spring AI 를 활용한 MCP 서버 구현 및 Claude AI에 적용하기</title>
      <link>https://do-study.tistory.com/136</link>
      <description>&lt;h1&gt;1. MCP란?&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Model Context Protocol&lt;/b&gt;의 약자로, AI 모델과 외부 데이터 소스를 연결하기 위한 표준 프로토콜&lt;/li&gt;
&lt;li&gt;LLM이 외부 시스템의 데이터나 기능에 접근할 수 있도록 하는 개방형 표준입니다.&lt;/li&gt;
&lt;li&gt;Tool과 Resource를 통해 AI가 실시간 데이터를 조회하거나 특정 작업을 수행할 수 있게 합니다.&lt;/li&gt;
&lt;li&gt;클로드 개발사인 Anthropic 이 주도하여 개발했으며, 다양한 언어의 SDK를 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP는 다양한 통신 방식을 지원하여 유연한 구현이 가능합니다.&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;HTTP 기반 JSON-RPC&lt;/b&gt; 방식으로 통신하며 STDIO(표준입출력), Streamable HTTP 방식을 지원합니다.&lt;/li&gt;
&lt;li&gt;초기에는 SSE(Server-Sent Events) 방식이 기본 스펙이었으나, &lt;b&gt;Streamable HTTP로 변경&lt;/b&gt;되었고 SSE는 선택사항이 되었습니다.&lt;/li&gt;
&lt;li&gt;MCP 서버가 원격지에 구성되어 있을 경우 표준입출력 통신이 불가능하기 때문에 &lt;b&gt;HTTP 기반 통신방식을 사용&lt;/b&gt;할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단일 endpoint만 제공&lt;/b&gt;해야 하며, 서버로 요청할 때는 &lt;b&gt;POST 메서드&lt;/b&gt;를 사용해야 합니다.&lt;/li&gt;
&lt;li&gt;MCP 서버는 클라이언트에세 서버가 제공하는 리소스, 프롬프트, 툴 목록과 설명을 제공합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세한 스펙은 &lt;a href=&quot;https://modelcontextprotocol.io/specification/2025-06-18&quot;&gt;공식 문서&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;h1&gt;2. 간단한 MCP 서버 구현&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 MCP 서버를 구현하고 테스트해보고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침 현재 여러 회사의 채용공고를 수집하여 제공하는 사이드 프로젝트를 운영하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 DB에 수집되고 있는 채용정보를 LLM이 검색할 수 있도록 기능을 만들어서 제공하고, ChatGPT나 Claude 같은 서비스에서 사용할 것이기 때문에 &lt;b&gt;Streamable HTTP 통신방식&lt;/b&gt;을 통해 구현해보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 Resource vs Tool: 무엇을 선택할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP는 데이터 제공 방식에 따라 Resource와 Tool 두 가지 타입을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resource Tool&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;용도&lt;/td&gt;
&lt;td&gt;정적이거나 반정형화된 데이터 제공&lt;/td&gt;
&lt;td&gt;동적 데이터 조회 및 작업 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;갱신주기&lt;/td&gt;
&lt;td&gt;갱신되지 않거나 시간 / 일 단위로 느린 갱신&lt;/td&gt;
&lt;td&gt;실시간 혹은 빠른 갱신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;예시&lt;/td&gt;
&lt;td&gt;Readme 파일, 정책 문서, 코드베이스&lt;/td&gt;
&lt;td&gt;데이터베이스 검색, 주가 등 외부 데이터 검색, 데이터 변경 작업 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 구현하고자 하는 예시에서는 데이터베이스 조회를 통하기 때문에 &lt;b&gt;Tool&lt;/b&gt;이 적합하다고 판단하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. Spring AI MCP 활용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP는 다양한 언어의 SDK를 제공하며, Java SDK도 그 중 하나입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이미 Spring Boot 기반으로 서비스를 운영 중이었고, 기존에 구현해둔 채용정보 검색 기능을 재사용하고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring AI&lt;/b&gt; 에서는 &lt;b&gt;MCP&lt;/b&gt; 서버, 클라이언트를 쉽게 구현할 수 있도록 구현체를 제공합니다. MCP Java SDK를 래핑하여 Spring 환경에서 쉽게 사용할 수 있도록 해주며, 필요시 저수준 API(MCP Java SDK)도 그대로 활용할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;3. 코드 작성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 구현하고자 하는 방식에 맞는 적절한 starter 를 의존성을 추가해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 경우는 원격지에 서버를 구동시켜두고자 하여 웹 의존성이 있는 스타터를 사용하였습니다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;dependencies {
    implementation(platform(&quot;org.springframework.ai:spring-ai-bom:1.0.2&quot;))
    implementation(&quot;org.springframework.ai:org.springframework.ai:spring-ai-starter-mcp-server-webmvc&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 아래와 같이 통신방식과 insturctions 를 설정해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능한 모든 설정 목록은 &lt;a href=&quot;https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html#_server_capabilities&quot;&gt;공식문서&lt;/a&gt;를 확인하세요.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;spring:
  application:
    name: mcp
  ai:
    mcp:
      server:
        protocol: stateless
        instructions: &quot;XXX 서비스에서 제공하는 채용공고에 대한 검색 기능을 제공하는 MCP 서버&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Tool 어노테이션만으로 간단히 MCP Tool을 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;description 에 툴에 대해 설명을 명시하고, ToolParam 으로 해당 툴의 파라미터를 명시하여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 로직은 자유롭게 구현하면 Spring 이 MCP 서버 제공 스펙에 맞춰 변환해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(아래 예시는 빠른 구현을 위해 DTO 없이 Map을 반환했습니다. 실제 프로덕션 환경에서는 명확한 응답 모델을 정의하는 것을 권장합니다.)&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Tool(description = &quot;&quot;&quot;
    # Request
    - searchKeyword 로 채용공고를 검색합니다. 제목 기반 like 검색 결과를 반환합니다.
    - companyGroup 이 지정되면 해당 회사 그룹 내에서만 검색합니다.
    
    # Response
    - infoNo : 공고번호. fetch 툴에서 상세 정보를 조회할 때 사용합니다.
    - title : 공고 제목
    - companyGroup : 회사 그룹 (예: 네이버, 카카오)
    - startAt : 공고 시작일 (yyyy-MM-dd), null 일 경우 확인 불가 케이스
    - endAt : 공고 마감일 (yyyy-MM-dd), null 일 경우 확인 불가 케이스
    - jobCategory : 직군 (예: 개발, 디자인)
    - recruitmentLink : 공고 URL
&quot;&quot;&quot;)
fun search(
    @ToolParam(
        description = &quot;회사 enum 값을 참고하여 네이버 -&amp;gt; NAVER, 카카오 -&amp;gt; KAKAO 와 같이 Enum 값으로 요청해야합니다.&quot;, 
        required = false
    ) companyGroup: CompanyGroup?,
    @ToolParam(description = &quot;검색 키워드&quot;) searchKeyword: String
): List&amp;lt;Map&amp;lt;String, Any?&amp;gt;&amp;gt; {
    val searchResult = recruitmentInfoRepository.findAllBySearchParams(
        RecruitmentInfoSearchParams(
            title = searchKeyword,
            companyGroup = companyGroup
        )
    )
    
    return searchResult.map {
        mapOf(
            &quot;infoNo&quot; to it.infoNo,
            &quot;title&quot; to it.title,
            &quot;companyGroup&quot; to it.companyGroup.nameKr,
            &quot;startAt&quot; to it.startAt?.let { date -&amp;gt; TimeUtils.format(date, &quot;yyyy-MM-dd&quot;) },
            &quot;endAt&quot; to it.endAt?.let { date -&amp;gt; TimeUtils.format(date, &quot;yyyy-MM-dd&quot;) },
            &quot;jobCategory&quot; to it.jobCategory,
            &quot;recruitmentLink&quot; to it.recruitmentLink
        )
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 여기서 Tool은 Spring AI 에서 제공하는 Tool로 꼭 MCP로만 제공되지 않아도 됩니다. 아직 마일스톤 버전이지만 1.1.0 에서는 McpTool 과 같이 명시적으로 MCP 용 애노테이션들이 제공되는것으로 확인하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현은 위가 전부입니다.&lt;/p&gt;
&lt;h1&gt;4. Claude AI에 연결하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 서버를 배포한 후 서비스에 추가해봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 커넥터에 MCP 서버 주소와 이름을 입력해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTemCI/btsQ3pAOxGo/NdfglVNkD7uB7EGlFIO9G0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTemCI/btsQ3pAOxGo/NdfglVNkD7uB7EGlFIO9G0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTemCI/btsQ3pAOxGo/NdfglVNkD7uB7EGlFIO9G0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTemCI%2FbtsQ3pAOxGo%2FNdfglVNkD7uB7EGlFIO9G0%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;1072&quot; height=&quot;628&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;628&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o0rOL/btsQ37sTP0H/4VbqGhaNCnyoVjbKRo8X11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o0rOL/btsQ37sTP0H/4VbqGhaNCnyoVjbKRo8X11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o0rOL/btsQ37sTP0H/4VbqGhaNCnyoVjbKRo8X11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo0rOL%2FbtsQ37sTP0H%2F4VbqGhaNCnyoVjbKRo8X11%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;1510&quot; height=&quot;684&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;684&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;MCP 연결을 하고나면 LLM 이 연결된 MCP를 올바르게 인식하고 있는지 확인해볼수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vrAyX/btsQ2qG1I0v/MaNqVcvtm1srFtCQ96bFi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vrAyX/btsQ2qG1I0v/MaNqVcvtm1srFtCQ96bFi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vrAyX/btsQ2qG1I0v/MaNqVcvtm1srFtCQ96bFi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvrAyX%2FbtsQ2qG1I0v%2FMaNqVcvtm1srFtCQ96bFi0%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;1318&quot; height=&quot;736&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;736&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;1620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cObDzP/btsQ2qAlgMH/gYhdeU97uFB82bF7csyp1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cObDzP/btsQ2qAlgMH/gYhdeU97uFB82bF7csyp1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cObDzP/btsQ2qAlgMH/gYhdeU97uFB82bF7csyp1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcObDzP%2FbtsQ2qAlgMH%2FgYhdeU97uFB82bF7csyp1k%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;1540&quot; height=&quot;1620&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;1620&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;의도한대로 MCP 적절히 활용하여 대답하는것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문을 잘 해석하고 파라미터도 적절하게 사용하여 요청하는게 아주 놀랍습니다.&lt;/p&gt;
&lt;h1&gt;마치며&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP를 활용하여 LLM이 우리의 데이터에 매우 간단하게 접근하도록 할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Spring AI MCP를 사용하여 기존 Spring Boot 애플리케이션에 쉽고 자연스럽게 통합할 수 있어 사용경험도 괜찮았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점이라면 MCP의 단점은 아니지만 Spring AI 는 이제 막 1.0.0 릴리즈 수준으로 극 초기 단계이다보니 완성도 면에서 성숙하지는 못한 단계라고 느꼈습니다. 이는 시간이 지나면서 차차 개선되길 기대해봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 글 봐주셔서 감사합니다.&lt;/p&gt;</description>
      <category>AI &amp;amp; LLM</category>
      <category>AI</category>
      <category>ChatGPT</category>
      <category>Claude</category>
      <category>LLM</category>
      <category>MCP</category>
      <category>spring ai</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/136</guid>
      <comments>https://do-study.tistory.com/136#entry136comment</comments>
      <pubDate>Fri, 3 Oct 2025 03:37:14 +0900</pubDate>
    </item>
    <item>
      <title>[Spring AI 공식문서 읽기] 1. Spring AI 핵심 개념과 컨셉</title>
      <link>https://do-study.tistory.com/135</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring AI Document를 읽으며 공부한 내용을 정리한 글입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Spring AI 소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AI는 스프링 생태계에서 인공지능 기능을 쉽게 통합할 수 있도록 돕는 프레임워크입니다.&lt;br /&gt;Python 진영의 LangChain, LlamaIndex에서 영감을 받았지만 단순 이식이 아니라, Java/Spring 환경에 맞춘 독자적 접근을 취합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OpenAI, Anthropic, Microsoft, Amazon, Google 등 다양한 AI 모델 지원&lt;br /&gt;(Chat, Embedding, Text&amp;rarr;Image, Audio, Moderation 등)&lt;/li&gt;
&lt;li&gt;구조화된 출력(POJO 매핑)과 RAG 구현을 위한 벡터 데이터베이스 연동&lt;br /&gt;(PostgreSQL PGVector, Pinecone, Redis, Milvus 등 다수)&lt;/li&gt;
&lt;li&gt;Function Calling(도구/함수 호출)로 모델이 외부 API나 클라이언트 기능 실행 가능&lt;/li&gt;
&lt;li&gt;Observability(관측성) 제공, ETL 기반 문서 처리, 생성형 AI 모델 평가 유틸리티 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Spring AI는 기업 데이터&amp;middot;API와 AI 모델을 쉽게 연결하고,&lt;br /&gt;실무 친화적인 기능들을 제공하는 &lt;b&gt;스프링 스타일 AI 통합 플랫폼&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;h2 data-ke-size=&quot;size26&quot;&gt;2. 핵심 개념&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Models (모델)&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;Embeddings(임베딩)으로 의미 기반 표현 활용&lt;/li&gt;
&lt;li&gt;Pre-trained 모델을 쉽게 활용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Prompts &amp;amp; Prompt Templates (프롬프트와 템플릿)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순 문자열이 아닌 역할(Role) 기반 메시지 포함&lt;/li&gt;
&lt;li&gt;Prompt Engineering으로 효과적 지시 가능&lt;/li&gt;
&lt;li&gt;Spring AI는 &lt;code&gt;StringTemplate&lt;/code&gt;을 이용한 동적 프롬프트 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Embeddings (임베딩)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;텍스트/이미지/영상 &amp;rarr; 벡터 변환&lt;/li&gt;
&lt;li&gt;의미 기반 유사도 검색 및 RAG의 핵심&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Tokens (토큰)&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;GPT-3 (4K 토큰), Claude (100K 토큰), 최신 모델은 최대 1M 토큰&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Structured Output (구조화된 출력)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순 문자열 응답을 POJO 등 구조화 객체로 매핑 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Bringing Your Data &amp;amp; APIs (데이터와 API 연결)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fine-tuning: 모델 수정 (비용&amp;uarr;)&lt;/li&gt;
&lt;li&gt;Prompt stuffing: 데이터 포함 (RAG)&lt;/li&gt;
&lt;li&gt;Tool calling: 외부 API/도구 호출&lt;/li&gt;
&lt;li&gt;Spring AI는 RAG + Tool Calling 직접 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Evaluating AI Responses (응답 평가)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모델 응답 품질 검증 API 제공&lt;/li&gt;
&lt;li&gt;관련성, 일관성, 사실성 등을 평가&lt;/li&gt;
&lt;li&gt;요청/응답을 다시 모델에 검증시킬 수도 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 Spring AI는 모델, 프롬프트 작성, 임베딩, 토큰 기반 처리, 구조화된 응답 처리, 데이터 ETL, 그리고 응답 평가까지&lt;br /&gt;LLM 연동하는 개발자가 쉽게 연동할 수 있도록 하는 기능 전반을 제공합니다.&lt;br /&gt;스프링에 익숙한 Java / Spring 기반 개발자라면 랭체인 등 타 언어 프레임워크 보다 훨씬 익숙하다는 부분도 있겠습니다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;3. Spring AI를 이용하려면?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 BOM 을 프로젝트에 추가해줍니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;dependencyManagement {
    imports {
        mavenBom &quot;org.springframework.ai:spring-ai-bom:1.0.0&quot; // Replace with desired version
    }
}&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;p data-ke-size=&quot;size16&quot;&gt;e.g. OpenAI 모델 이용 시&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.ai:spring-ai-starter-model-openai'
}&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;사용가능한 Starters 목록은 &lt;a href=&quot;https://github.com/spring-projects/spring-ai/tree/main/spring-ai-spring-boot-starters&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;에서 확인 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 문서의 Reference를 보면서 실습해보는 시간을 가져보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI &amp;amp; LLM</category>
      <category>AI</category>
      <category>ChatGPT</category>
      <category>LLM</category>
      <category>openai</category>
      <category>spring ai</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/135</guid>
      <comments>https://do-study.tistory.com/135#entry135comment</comments>
      <pubDate>Sun, 31 Aug 2025 22:34:50 +0900</pubDate>
    </item>
    <item>
      <title>랭체인(Langchain) 기본 개념과 간단한 활용 예제</title>
      <link>https://do-study.tistory.com/134</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;일반적인 백엔드 개발자 관점에서 학습한 내용을 정리한 글로 부정확한 내용이 있을수있습니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;LLM 공부와 LLM을 활용한 서비스 구현을 계획하면서 공부하면서 자연스럽게 파이썬 생태계에서 시작된 LangChain을 접하게 되었습니다. 예전 같으면 모델 API 연동, RAG용 외부 데이터 연결, 프롬프트 설계 등을 각각 구현했을 것입니다.&lt;br /&gt;LangChain은 프롬프트&amp;middot;체인&amp;middot;RAG&amp;middot;에이전트&amp;middot;툴까지 LLM 기반 서비스 전 과정을 아우르는 프레임워크/라이브러리 모음입니다.&lt;br /&gt;이 글에서는 제가 이해한 LangChain의 핵심과 간단한 활용 예제를 간단히 정리하겠습니다.&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;665&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MZz3M/btsP5hDBe3s/uZLdlZHYTL1NJmJI85H8n1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MZz3M/btsP5hDBe3s/uZLdlZHYTL1NJmJI85H8n1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MZz3M/btsP5hDBe3s/uZLdlZHYTL1NJmJI85H8n1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMZz3M%2FbtsP5hDBe3s%2FuZLdlZHYTL1NJmJI85H8n1%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;665&quot; height=&quot;334&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;334&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;h3 data-ke-size=&quot;size23&quot;&gt;1. 모델 I/O&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LLM 과 상호작용 (Input, Output) 하기 위한 컴포넌트 영역&lt;/li&gt;
&lt;li&gt;LLM에 전달할 프롬프트 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력 임베딩 &amp;amp; RAG 검색 결과&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LLM API 호출&lt;/li&gt;
&lt;li&gt;LLM 응답 출력
&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;JSON, CSV 등 LLM에 출력 형식을 알려주고 응답을 파싱&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;예시코드&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;from langchain import PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.output_parsers import CommaSeparatedListOutputParser

openai = ChatOpenAI(temperature = 0, model_name='gpt-4')

output_parser = CommaSeparatedListOutputParser()
format_instructions = output_parser.get_format_instructions()

# 프롬프트 생성 (문자열 템플레이팅, 출력 형식 지정)
prompt = PromptTemplate(
    template = &quot;7개의 팀을 보여줘 {subject}.\\n{format_instructions}&quot;,
    input_variables = [&quot;subject&quot;],    
    partial_variables={&quot;format_instructions&quot;: format_instructions} 
)

# LLM API 호출 &amp;amp; 응답 수신
res = openai.predict(prompt.format(subject=&quot;한국의 야구팀은?&quot;))

# 결과 파싱 (이 케이스에선 콤마로 연결된 값들을 List로 변환)
print(output_parser.parse(res))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 데이터 연결&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 ETL에 해당&lt;/li&gt;
&lt;li&gt;문서 가져오기 (document loaders) : 다양한 소스에서 문서를 가져오는 것&lt;/li&gt;
&lt;li&gt;문서 변환 (document transformers) : 문서를 청크로 분할하고 결합 등&lt;/li&gt;
&lt;li&gt;문서 임베딩 (embedding model): 문서를 벡터 임베딩
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FAISS (인메모리 벡터 DB)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;벡터 저장소 (vector stores) : 임베딩 결과물 적재&lt;/li&gt;
&lt;li&gt;검색기 (retrievers) : 언어 모델과 결합할 문서 가져오기 위한 정보 검색&lt;/li&gt;
&lt;li&gt;예시 코드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PDF Read &amp;rarr; 임베딩하여 인메모리 벡터 DB에 저장&lt;/li&gt;
&lt;li&gt;체인을 활용해 LLM과 백터DB를 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;from langchain.document_loaders import PyPDFLoader
from langchain.vectorstores import FAISS
from langchain.embeddings import OpenAIEmbeddings
from langchain.chat_models import ChatOpenAI
from langchain.chains import RetrievalQA

loader = PyPDFLoader(&quot;path/to/pdf&quot;)
document = loader.load()

# OpenAI 임베딩 사용. 벡터만 다루고자 하면 embeddings 객체 활용 가능
embeddings = OpenAIEmbeddings()

# 인메모리DB FAISS 에 임베딩 벡터 저장
# 검색 도구 (Retriever 로 사용)
db = FAISS.from_documents(document, embeddings)
retriever = db.as_retriever()

llm = ChatOpenAI(temperature = 0, model_name = 'gpt-4')
qa = RetrievalQA.from_chain_type(
    llm = llm, 
    chain_type = &quot;stuff&quot;, 
    retriever = retriever
)

qa({ &quot;query&quot;: &quot;PDF 파일에 내용을 검색하면 잘 답변해준다.&quot; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 체인&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Langchain 내 여러 컴포넌트들을 묶어 하나의 행위를 만들어주는 역할&lt;/li&gt;
&lt;li&gt;기본적인 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;from langchain.chains import LLMChain
from langchain import PromptTemplate
from langchain.chat_models import ChatOpenAI

llm = ChatOpenAI(temperature = 0, model_name = 'gpt-4')

prompt = PromptTemplate(input_variables = [&quot;country&quot;], template = &quot;{country}의 수도는 어디야?&quot;)

# LLM 과 프롬프트를 연결한 예시
chain = LLMChain(llm = llm, prompt = prompt)
chain.run(&quot;미국&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 체인 연결하여 사용하는 예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 체인의 출력값을 다음 체인의 입력값으로 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;from langchain.chains import LLMChain, SequentialChain
from langchain import PromptTemplate
from langchain.chat_models import ChatOpenAI
import os

os.environ[&quot;OPENAI_API_KEY&quot;] = &quot;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&quot;

# LLM 생성
llm = ChatOpenAI(temperature = 0, model_name = 'gpt-4')

# 번역체인 생성 (번역 프롬프트 + LLM)
translatePrompt = PromptTemplate.from_template(&quot;다음 문장을 한글로 번역하세요.\\n\\n{sentence}&quot;)
translateChain = LLMChain(llm=llm, prompt=translatePrompt, output_key=&quot;translation&quot;)

# 요약체인 생성 (요약 프롬프트 - 번역체인 결과값 사용 + LLM)
summaryPrompt = PromptTemplate.from_template(&quot;다음 문장을 한 문장으로 요약하세요.\\n\\n{translation}&quot;)
summaryChain = LLMChain(llm=llm, prompt=summaryPrompt, output_key=&quot;summary&quot;)

# 최종 체인 (번역체인 -&amp;gt; 요약체인 순으로 수행)
aggregateChain = SequentialChain(
    chains=[translateChain, summaryChain], 
    input_variables=['sentence'], 
    output_variables=['translation', 'summary']
)

sentence=&quot;&quot;&quot;
Ukraine&amp;rsquo;s President Volodymyr Zelenskyy, who has a checkered relationship with Trump and was not invited to the talks, will be nervous as they get underway. On Friday, the president commented on X that &amp;ldquo;it is time to end the war, and the necessary steps must be taken by Russia. We are counting on America.&amp;rdquo;
Both he and his European allies fear the U.S. leader could capitulate to skilled negotiator Putin&amp;rsquo;s likely demands for Moscow to retain occupied Ukrainian territory and cut short Ukraine&amp;rsquo;s NATO membership aspirations, in return for halting its military offensive.
As he headed off to Alaska on Air Force One, Trump told reporters that &amp;ldquo;something is going to come of it [the meeting].&amp;rdquo; When asked about Russia&amp;rsquo;s ongoing attacks on Ukraine, the president said Putin &amp;ldquo;thinks that gives him strength in talks, I think it hurts him,&amp;rdquo; in comments reported by Reuters.
Trump also insisted that Ukraine has to decide about its territory and that security guarantees were &amp;ldquo;possible along with Europe,&amp;rdquo; without giving further detail.
What&amp;rsquo;s the schedule?
On Friday, the White House confirmed that the U.S. delegation includes Secretary of State Marco Rubio, Treasury and Commerce Secretaries Scott Bessent and Howard Lutnick, CIA Director John Ratcliffe and Special Envoy Steven Witkoff, as well as other senior U.S. officials.
Moscow released more details about the summit earlier than Washington, however, which only this week confirmed that the presidents&amp;rsquo; talks would be a &amp;ldquo;one-on-one&amp;rdquo; meeting.
&quot;&quot;&quot;

aggregateChain.invoke(sentence)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 메모리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LLM은 기본적으로 채팅 기록을 장기적으로 보관하지 않고 따로 저장해놔야하는데 랭체인에서 제공하는 컴포넌트를 사용하면 쉽게 구현할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;from langchain import ConversationChain
from langchain.chat_models import ChatOpenAI
import os

os.environ[&quot;OPENAI_API_KEY&quot;] = &quot;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&quot;

llm = ChatOpenAI(temperature = 0, model_name = 'gpt-4')

conversation = ConversationChain(llm = llm, verbose=True)

# 아래 대화 내용들이 메모리에 저장되어 LLM 이 기억함
conversation.predict(input = &quot;진희는 강아지를 한마리 키우고 있습니다.&quot;)
conversation.predict(input = &quot;영수는 고양이를 두마리 키우고 있습니다.&quot;)
conversation.predict(input = &quot;진희와 영수가 키우는 동물은 총 몇마리?&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 에이전트 / 툴&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;학습되지 않은 정보에 대해서는 전혀 모르는 LLM의 한계를 해소하기 위한 것이다.&lt;/li&gt;
&lt;li&gt;에이전트도 주어진 프롬프트에 답변하는것이 목적으로 LLM과 유사하지만 방식이 LLM 과 크게 다르다&lt;/li&gt;
&lt;li&gt;에이전트는 LLM 을 이용해서 어떤 작업을 어떤 순서로 수행할지 결정하는데, &amp;ldquo;어떤 작업&amp;rdquo;에 툴을 사용한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때 에이전트 유형(AgentType)에 따라 LLM이 어떻게 툴을 결정하도록 할 지 지정할 수 있음&lt;/li&gt;
&lt;li&gt;ZERO_SHOT_REACT_DESCRIPTION : 스스로 선택 X. 언제 툴을 쓸 지 설명 필요&lt;/li&gt;
&lt;li&gt;REACT_DOCSTORE : 질문에 답하기 위해 검색 도구가 필요함&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;툴은 특정 작업을 수행하기 위한 도구로 LLM 이외 다른 리소스를 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import os
from langchain.chat_models import ChatOpenAI
from langchain.agents import load_tools, initialize_agent, AgentType

os.environ[&quot;OPENAI_API_KEY&quot;] = &quot;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&quot;

llm = ChatOpenAI(temperature = 0, model_name = 'gpt-4')

# wikipedia, llm-math 툴 사용
tools = load_tools([&quot;wikipedia&quot;, &quot;llm-math&quot;], llm=llm)

# 위 툴을 이용하는 Agent 생성
agent = initialize_agent(tools, llm, agent = AgentType.ZERO_SHOT_REACT_DESCRIPTION, description='계산이 필요할 때 사용', verbose=True)

agent.run(&quot;에드 시런이 태어난 해는? 2025년도 현재 에드 시런은 몇 살?&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에이전트는 결국 LLM이 &quot;생각 &amp;rarr; 도구 선택 &amp;rarr; 실행 &amp;rarr; 결과 결합&quot; 과정을 통해 외부 세계와 상호작용하면서 답하는 기능을 묶어둔 것이다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI &amp;amp; LLM</category>
      <category>AI</category>
      <category>GPT</category>
      <category>langchain</category>
      <category>LLM</category>
      <category>랭체인</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/134</guid>
      <comments>https://do-study.tistory.com/134#entry134comment</comments>
      <pubDate>Sun, 24 Aug 2025 21:50:38 +0900</pubDate>
    </item>
    <item>
      <title>LLM 기본 개념</title>
      <link>https://do-study.tistory.com/133</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;LLM 정의&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;언어 모델링 (Language Modeling) 은 NLP의 하위 분야&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모델링 방법
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;li&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;텍스트 생성에 이상적이며 GPT가 자기회귀&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LLM 주요 특징&lt;/h3&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;인코더: 원시 텍스트를 &amp;rarr; 핵심 구성 요소로 분리 &amp;rarr; 벡터로 변환, 어텐션을 사용하여 맥락 이해&lt;/li&gt;
&lt;li&gt;디코더: 어텐션을 사용하여 다음에 올 최적의 토큰을 예측하여 텍스트를 생성&lt;/li&gt;
&lt;li&gt;인코더로 텍스트를 잘 이해하고, 디코더로 잘 생성해내는 구조&lt;/li&gt;
&lt;li&gt;GPT 계열 LLM은 자기회귀 모델링으로 학습, 디코더만 있는 모델로 텍스트 생성에 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LLM 작동 원리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사전 훈련 (Pre-Traning)
&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;BERT(자동 인코딩 모델)의 경우 마스크된 언어 모델링 (MLM) 및 다음 문장 예측 (NSP)로 학#습&lt;/li&gt;
&lt;li&gt;MLM은 하나의 문장에서 토큰의 상호작용 인식하도록 함&lt;/li&gt;
&lt;li&gt;NSP는 문장들 사이에 토큰이 어떻게 상호작용하는지 이해하도록 함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;# MLM
이스탄불은 방문하기에 훌륭한 [마스크] &amp;lt;&amp;lt; 추측하도록 함

# NSP
A: 이스탄불은 방문하기에 훌륭한 도시입니다.
B: 나는 거기 가봤습니다.

문장 B는 문장의 A의 바로 다음에 왔습니까? Y or N
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GPT 계열은 다른 데이터 셋에, 다른 방식으로 사전 훈련함&lt;/li&gt;
&lt;li&gt;이런 사전 훈련을 뭐로 하냐에 따라 어떤 LLM이 되냐가 갈리게된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전이학습 (Transfer Learning)
&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;LLM 에서는 말뭉치 (텍스트를 목적에 맞는 포맷으로 데이터화 해둔것) 를 기반으로 사전 훈련 (말뭉치 전이학습) 되고, 이보다 작은 데이터셋을 통해 파인튜닝 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파인튜닝
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사전 훈련된 LLM을 특정 도메인에 특화 시키는 과정&lt;/li&gt;
&lt;li&gt;사전 훈련이 기본적이고 일반적인 개념을 가르치는 과정이라면, 파인튜닝은 해당 분야 전문 지식을 가르치는 것&lt;/li&gt;
&lt;li&gt;적절한 학습률, 에포크 설정이 필요&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&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;LLM이 중요한 것을 판가름할 수 있는데에 도움을 줌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;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;전통적인 NLP에서 불용어 (the, a, an) 제거 등은 하지 않는다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LLM은 사람이 사용하는 언어의 본질을 이해 시키는 게 목적이기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LLM이 사전에 없는 단어 (OOV - Out Of Vocaburary)를 발견하면 자신이 알고 있는 가장 작은 단어로 나눈다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예시 사전: [&amp;rdquo;Sin&amp;rdquo;], 문장 &amp;ldquo;Sinan&amp;rdquo; 일 경우 [&amp;rdquo;Sin&amp;rdquo;, &amp;ldquo;##an&amp;rdquo;] 이라는 2 토큰으로 나뉨&lt;/li&gt;
&lt;li&gt;##은 부분 단어임을 의미 (LLM이 구분할 수 있도록)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현재 많이 사용되는 LLM&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BERT
&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이해를 잘하고 생성은 좀 못한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;텍스트 생성 보단 분류 등 특정 NLP 작업을 위한 사전 훈련 모델로 이용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GPT-4, ChatGPT
&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이해는 잘 못하지만 생성은 잘한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GPT 기반 모델은 큰 문맥을 주었을 때 텍스트를 잘 생성해냄&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;T5
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파인튜닝 없이 준수한 성능 자랑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI &amp;amp; LLM</category>
      <category>AI</category>
      <category>GPT</category>
      <category>LLM</category>
      <category>openai</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/133</guid>
      <comments>https://do-study.tistory.com/133#entry133comment</comments>
      <pubDate>Sun, 24 Aug 2025 21:33:59 +0900</pubDate>
    </item>
    <item>
      <title>SSE를 활용해 웹에서 실시간 데이터 구독 기능 구현</title>
      <link>https://do-study.tistory.com/132</link>
      <description>&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;br /&gt;서버 -&amp;gt; 클라이언트 방향으로의 단방향만 지원하면 돼서 다른 기술을 찾아보다가 SSE를 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE는 Server Send Events의 약자로 HTML5부터 사용하능한 표준 스펙이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 서버에서 주기적으로 발행하는 이벤트를 클라이언트(브라우저)에서 구독할 수 있는 기술로 접속연결만 HTTP로 하고 자체 프로토콜로 통신하는 웹소켓과 달리 전체 과정을 HTTP로 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하는 방법은 간단하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 원리를 알기 위해 SSE 스펙에 맞춰 이벤트를 발행하는 서버 코드를 작성해보자. (스프링에서 쉽게 적용하는 부분은 글 뒷 부분에 작성)&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1652545399701&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
@RequestMapping(&quot;/sse&quot;)
public class SseController {

    @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public void publish(HttpServletResponse response) throws Exception {    
	    response.setCharacterEncoding(&quot;UTF-8&quot;);
        
        PrintWriter writer = response.getWriter(); 
        for(int i = 1; i &amp;lt;= 10; i++) { 
        	writer.write(&quot;data: { \&quot;message\&quot; : \&quot;number : &quot;+ i + &quot;\&quot; }\n\n&quot;); 
            try { 
            	Thread.sleep(1000); 
            } catch (InterruptedException e) { 
            	e.printStackTrace(); 
            } 
        } 
        writer.close();
    }

}&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;먼저 Content-Type을 &quot;text/event-stream&quot;로 지정하고, 응답하는 데이터형식은 &quot;data:실제응답할데이터\n\n&quot;와 같이 맞춰 응답하면 된다. 그러면 보통 모든 작업이 끝난 후 response를 한번에 보내는것과 달리 데이터형식에 맞는 데이터가 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;클라이언트 코드는 간단하다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1652545740518&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const eventSource = new EventSource(&quot;SSE API URL&quot;);
eventSource.onmessage = event =&amp;gt; {
    console.log(event.data);
};

eventSource.onerror = error =&amp;gt; {
    eventSource.close();
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EventSource라는 SSE를 쉽게 다룰수 있는 클래스를 활용하면 정말 간단하게 구독 처리를 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 스프링 프레임워크에서도 마찬가지로 SSE를 쉽게 적용할 수 있는 구현체를 제공한다.&lt;/p&gt;
&lt;pre id=&quot;code_1652545966125&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
public SseController {

    private TaskExecutor = Executors.newSingleThreadExecutor();

    @GetMapping
    public SseEmitter greeting(@PathVariable final int count) {
        final SseEmitter emitter = new SseEmitter();
        taskExecutor.execute(() -&amp;gt; {
            try {
                for (int i = 0; i &amp;lt; 10; i++) {
                    Thread.sleep(1000);
                    emitter.send(&quot;test data &quot; + i);
                }
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        });
        return emitter;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예시에서는 이벤트 발행을 비동기로 처리하기 위해 별도 스레드를 통해 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한건 &quot;SseEmitter&quot;라는 클래스를 제공하며 send 메소드로 데이터를 전송하고 전송이 끝나면 complete 메소드를 호출해주면 끝이다. 위 예시처럼 직접 Content-Type을 지정해주거나 데이터포맷을 맞출 필요 없이 편하게 사용할 수 있다.&lt;/p&gt;</description>
      <category>Back-End/Spring framework</category>
      <category>html5</category>
      <category>Spring</category>
      <category>SSE</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/132</guid>
      <comments>https://do-study.tistory.com/132#entry132comment</comments>
      <pubDate>Sun, 15 May 2022 01:35:02 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot Managed Dependency 버전 변경</title>
      <link>https://do-study.tistory.com/131</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 Spring Framework와 달리 프레임워크 빌드, 런타임 등에 필요한 의존성을 자동으로 관리해주는게 큰 장점중에 하나인데요.&lt;/p&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 Boot에서는 관련 있는 기능 및 의존성을 묶어 하나의 의존성으로 제공하고 개발자들은 필요한 의존성만 선택하여 사용하면 됩니다.&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;이를 Spring Boot 진영에서는 Managed Dependency라고 부르며 아래 링크에서 어떤 의존성들을 관리하고 있는지 볼 수 있습니다.&lt;/p&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.spring.io/spring-boot/docs/current/reference/html/dependency-versions.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-boot/docs/current/reference/html/dependency-versions.html&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot managed Dependency에선 특정 라이브러리 버전을 커스터마이즈 할 수 있는 방법을 제공하는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 Version Properties를 제공해주어 원하는 버전으로 변경할 수 있게 해줍니다.&lt;/p&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 에서는 아래와 같이 properties로 선언해주면 됩니다. 변경가능한 Property명은 &lt;a title=&quot;이곳&quot; href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/dependency-versions.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-boot/docs/current/reference/html/dependency-versions.html&lt;/a&gt; 의 Version Proerties에 정의되어 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1649416353375&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;properties&amp;gt;
    &amp;lt;srping-framework.version&amp;gt;5.3.18&amp;lt;/srping-framework.version&amp;gt;
&amp;lt;/properties&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;Gradle의 경우엔 아래와 같이 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1649416464038&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ext['spring-framework.version] = '5.3.18'​&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;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;</description>
      <category>Back-End/Spring framework</category>
      <category>Gradle</category>
      <category>maVen</category>
      <category>Spring</category>
      <category>spring-boot</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/131</guid>
      <comments>https://do-study.tistory.com/131#entry131comment</comments>
      <pubDate>Fri, 8 Apr 2022 20:16:00 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot Tomcat Access Log 필터링</title>
      <link>https://do-study.tistory.com/130</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot Embedded Tomcat을 사용하면 Access Log를 남길수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #000000;&quot;&gt;기본적으로는 비활성화 되어있고 server.tomcat.accesslog.enabled 옵션을 true로 주면 활성화 할 수 있습니다.&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;Access Log는 해당 서버로 들어오는 모든 요청들을 로깅하는데 이 중 로그로 남기고 싶지 않은 건들이 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 경우엔 외부 모니터링 서버에서 헬스체크를 위해 헬스체크 URL을 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;톰캣 Access Log 스펙 중에, HttpServletRequest 객체의 Attribute 내 특정 키가 포함되있거나, 되있지 않을 경우 해당 요청에 대해서는 로깅을 하지 않도록 설정할 수 있는 기능이 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 문서를 통해 톰캣 Access Log 동작 관련 설정 방식을 자세하게 알아볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tomcat.apache.org/tomcat-8.5-doc/config/valve.html#Access_Log_Valve/Attributes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://tomcat.apache.org/tomcat-8.5-doc/config/valve.html#Access_Log_Valve/Attributes&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;이 포스트에서는 스프링 부트 옵션을 통해 톰캣 Access Log 설정을 하고, 필터를 통해 구현한 사례를 소개합니다.&lt;/p&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. Spring Boot 옵션 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- server.tomcat.accesslog.enabled: 액세스로그 활성화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- server.tomcat.accesslog.condition-unless: 로그를 남기지 않을 대상 Request Attribute의 key 값.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 request.getAttribute(&quot;NO-LOG&quot;) != null 인 요청은 로그를 남기지 않음&lt;/p&gt;
&lt;pre class=&quot;brush: shell&quot;&gt;&lt;code&gt;server:
  tomcat:
    accesslog:
      enabled: true
      condition-unless: NO-LOG&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;2. Filter 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Http 요청 객체의 Attribute를 추가하여 톰캣이 액세스 로그를 남기지 않도록하는 필터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 파라미터로 전달받는 conditionUnlessKey는 server.tomcat.accesslog.condition-unless에 설정해둔 값 (NO-LOG) 이 전달되도록 외부에서 주입 예정&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;public class NonLogingMarkingFilter implement Filter {

    private String conditionUnlessKey;

    @Override
    public void init(FilterConfig config) {
        conditionUnlessKey = config.getInitParam(&quot;conditionUnlessKey&quot;);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException,
            IOException {
        request.setAttribute(conditionUnlessKey, conditionUnlessKey);
        chain.doFilter(request, response);
    }

}&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;3. 필터 등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 설정 파일 값을 주입받아 FilterRegistrationBean 통해 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- UrlPatterns에 로깅하지 않을 URL 목록을 설정하여 해당 요청들만 필터를 타도록 함&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
    
    @Value(&quot;${server.tomcat.accesslog.condition-unless}&quot;)
    private String conditionUnlessKey;

    @Bean
    public FilterRegistrationBean&amp;lt;NonLogingMarkingFilter&amp;gt; filterBean() {
        FilterRegistrationBean&amp;lt;NonLogingMarkingFilter&amp;gt; filterBean = new FilterRegistrationBean&amp;lt;&amp;gt;(new NonLogingMarkingFilter());
        filterBean.setOrder(Integer.MIN_VALUE);
        filterBean.setUrlPatterns(List.of(&quot;url&quot;, &quot;to&quot;, &quot;ignore&quot;));
        filterBean.addInitParameter(&quot;conditionUnlessKey&quot;, conditionUnlessKey);
        return filterBean;
    }
}&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;위와 같이 구현하면 특정 URL의 요청들만 NonLogingMarkingFilter 타게 되어 Request Attribute에 conditionUnless 값이 세팅되고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣 설정에 의해 해당 요청들은 엑세스 로그가 남지 않게됩니다.&lt;/p&gt;</description>
      <category>Back-End/Spring framework</category>
      <category>access log</category>
      <category>Filter</category>
      <category>spring boot</category>
      <category>tomcat</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/130</guid>
      <comments>https://do-study.tistory.com/130#entry130comment</comments>
      <pubDate>Wed, 9 Mar 2022 00:25:01 +0900</pubDate>
    </item>
    <item>
      <title>[Svelte 시리즈] 컴포넌트 생명주기 (Lifecycle) 와 훅(Hook)</title>
      <link>https://do-study.tistory.com/129</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트는 &lt;b&gt;HEROPY 님 유튜브 Svelte 강좌를 보고 공부한 내용을 정리한 포스트입니다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Svelte에서 컴포넌트는 화면에 랜더링되거나 사라질 때를 나타내는 컴포넌트의 상태가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태의 흐름을 컴포넌트 생명주기라 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mounted: 컴포넌트가 화면 또는 다른 컴포넌트에 랜더링된 (연결된) 상태. 이 전에는 화면 요소에 접근할 수 없음&lt;/li&gt;
&lt;li&gt;destroyed: 컴포넌트가 화면 또는 다른 컴포넌트에서 사라진 (연결이 해제된) 상태&lt;/li&gt;
&lt;li&gt;before update: 컴포넌트의 반응성 데이터 변경에 의해 재랜더링 되기 전 상태&lt;/li&gt;
&lt;li&gt;after update: 컴포넌트의 반응성 데이터 변경에 의해 재랜더링 된 후 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상태들은 컴포넌트가 화면(DOM)상에서 어떻게 다뤄지냐에 따라 다양하게 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 특정 컴포넌트가 화면에 최초로 랜더링(연결될) 될 때 before update &amp;rarr; mount &amp;rarr; after update 의 주기로 진행된다. (destroyed는 해당 컴포넌트가 사라질 때)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 화면에 그려져있는(연결되있던) 컴포넌트의 반응성 데이터가 변경되어 화면이 갱신될 때 before update &amp;rarr; after update 순으로 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 화면에 그려져있던(연결되있던) 컴포넌트가 사라질 때 destroyed 상태로 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 상태는 훅(Hook)을 통해 접근하고 해당 상태일 때 처리해야할 로직을 수행할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mounted: onMount&lt;/li&gt;
&lt;li&gt;destroyed: onDestroy&lt;/li&gt;
&lt;li&gt;before update: beforeUpdate&lt;/li&gt;
&lt;li&gt;after update: afterUpdate&lt;/li&gt;
&lt;li&gt;App.svelte
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 Hook은 svelte 통해 import할 수 있고 해당 주기 때 수행할 로직을 전달할 수 있다.&lt;/li&gt;
&lt;li&gt;아래와 같은 코드를 작성하고 언제 어떤 Hook이 실행되는지 확인해보자&lt;/li&gt;
&lt;li&gt;참고로 과거엔 onDestroy 가 없었고 onMount 함수에서 함수를 반환하는 형태로 destroyed hook을 구현했었다. 지금도 두 방법 모두 가능하지만 onDestroy를 사용하는게 보다 더 직관적으로 보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
	import { onMount, onDestroy, beforeUpdate, afterUpdate } from 'svelte';
	let count = 0;
	onMount(() =&amp;gt; {
		console.log('Mounted!!');
	});
	onDestroy(() =&amp;gt; {
		console.log('Destroyed');
	});
	beforeUpdate(() =&amp;gt; {
		console.log('before update');
	})
	afterUpdate(() =&amp;gt; {
		console.log('after update');
	});
&amp;lt;/script&amp;gt;

&amp;lt;h1&amp;gt;Something {count}&amp;lt;/h1&amp;gt; 
&amp;lt;button type=&quot;button&quot; on:click={() =&amp;gt; count++}&amp;gt;+&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Front-End</category>
      <category>svelte</category>
      <category>스벨트</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/129</guid>
      <comments>https://do-study.tistory.com/129#entry129comment</comments>
      <pubDate>Sun, 20 Feb 2022 00:59:51 +0900</pubDate>
    </item>
    <item>
      <title>[Svelte 시리즈] 컴포넌트와 스토어</title>
      <link>https://do-study.tistory.com/128</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트는 &lt;b&gt;HEROPY 님 유튜브 Svelte 강좌를 보고 공부한 내용을 정리한 포스트입니다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴포넌트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Svelte에서는 Single File Component를 정의하고 사용할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Name.svelte
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트 사용할 때 전달받을 데이터 (props)는 export 구문을 통해 선언한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
export let name
&amp;lt;/script&amp;gt;

&amp;lt;h1&amp;gt;{name}&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;App.svelte
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용 측에서 import 하여 컴포넌트를 가져온다.&lt;/li&gt;
&lt;li&gt;props는 해당 컴포넌트 attribute를 통해 전달한다. (컴포넌트에서 export된 이름에 맞춤)&lt;/li&gt;
&lt;li&gt;전달하려는 prop의 이름과 데이터 변수명이 같다면 생략하여 작성할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
import Name from './Name.svelte';
let names = ['Tom', 'Mike', 'Elsa'];
let name = 'Another Tom';
&amp;lt;/script&amp;gt;

{#each names as name}
&amp;lt;Name name={name} /&amp;gt;
{/each}

&amp;lt;Name {name} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트내에서 데이터를 가공할 땐 원본 데이터에 영향이 없도록 작성하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 컴포넌트 내에서 데이터를 변경해야한다면 bind 키워드를 사용해 양방향 바인딩을 해주면 전달받은 prop을 변경할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스토어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 아래와 같은 상황을 가정해보자&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;App.svelte&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
import Parent from './Parent.svelte';
let name = 'Tom';
&amp;lt;/script&amp;gt;

&amp;lt;Parent name={name} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Parent.svelte&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
import Child from './Child.svelte';
export let name;
&amp;lt;/script&amp;gt;

&amp;lt;div&amp;gt;
  Parent
&amp;lt;/div&amp;gt;
&amp;lt;Child name={name} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Child.svelte&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
export let name;
&amp;lt;/script&amp;gt;

&amp;lt;div&amp;gt;
  Child {name}
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App &amp;gt; Parent &amp;gt; Child 순으로 컴포넌트가 배치되어 있고, App에서 Child 컴포넌트로 데이터를 전달해야한다. 그럼 아래와 같은 코드가 되는데, 이 때 Parent 컴포넌트에서는 Child로 데이터 (name)을 전달하기 위해 본인은 사용하지않는 데이터를 선언해두고 받아서 전달하는 매개체 같은 역할을 해주어야한다.&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;이 부분을 해결하기 위해 먼저 Svelte에서 제공하는 Store를 활용해 스토어를 작성해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;store.js
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스토어에서 export될 때의 이름으로 외부에서 참조할 수 있다.&lt;/li&gt;
&lt;li&gt;외부에서 값 변경이 가능하게 하려면 writable 함수를 사용해주어야한다.&lt;/li&gt;
&lt;li&gt;writable 함수를 사용하고 초기값을 주면 name은 외부에서 값 참조, 변경이 가능한 스토어 객체가 된다.&lt;/li&gt;
&lt;li&gt;Store가 외부라이브러리로 구성되어있는 타 프레임워크와 다르게 Svelte는 내장되어 있기 때문에 별도 의존성을 설치할 필요 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush: javascript&quot;&gt;&lt;code&gt;import { writable } from 'svelte/store';
export let name = writable('');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 export된 name은 writable함수에 의해 스토어 객체로 Wrapping된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스토어 객체는 set update subscribe 3가지 함수를 갖고 있고 이를 통해 값을 변경하고 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;set 함수는 스토어 객체에 값을 할당하는 함수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;update함수는 함수를 통해 스토어 객체의 값을 현재 값을 변경하는 함수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;subscribe는 현재 스토어의 값을 사용하는 함수 전달받아를 소비(consumer)시켜주는 함수이다.&lt;/p&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
import { name } from './store.js';

let nameValue;
// name 스토어의 값을 현재 컴포넌트의 nameValue에 할당시킴
name.subscribe(value =&amp;gt; {
  nameValue = value;
});

// name 스토어의 값에 Hello World 문자열을 할당한다.
name.set('Hello World');

// name 스토어의 현재값에 !! 문자열을 붙힌다.
name.update(value =&amp;gt; value + '!!');
&amp;lt;/script&amp;gt;

&amp;lt;h1&amp;gt;{ nameValue }&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 함수들을 사용할 수 있지만 다소 불편한 부분이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Svelte에선 $ 키워드를 통해 스토어 데이터를 쉽게 다룰수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시를 스토어를 사용한 코드로 변경해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;App.svelte
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스토어의 변수를 참조할 땐 스토어 파일의 export된 이름으로 참조한다.&lt;/li&gt;
&lt;li&gt;상술했듯 name은 일반 자료형 데이터가 아닌 스토어 객체이고 set, update, subscribe 함수를 사용해야하지만 $키워드를 통해 일반 변수처럼 사용할 수도 있다.&lt;/li&gt;
&lt;li&gt;이를 Svelte에선 Auto-Subscribe라고 부른다.&lt;/li&gt;
&lt;li&gt;이 때문에 Svelte에서는 변수앞에 $가 붙어있으면 스토에어 의해 관리되고 있는 데이터라는 의미이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
import { name } from './store.js';
import Parent from './Parent.svelte';

$name = 'Tom';

&amp;lt;/script&amp;gt;

&amp;lt;Parent /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Parent.svelte
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 코드에서 App에서 데이터를 받아 Child로 전달해주는 코드가 없어졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
import Child from './Child.svelte';
&amp;lt;/script&amp;gt;

&amp;lt;div&amp;gt;
  Parent
&amp;lt;/div&amp;gt;
&amp;lt;Child /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Child.svelte
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;props을 선언하는 코드 대신, 스토어에서 name 객체를 가져온다.&lt;/li&gt;
&lt;li&gt;$name 으로 필요한 곳에 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Front-End</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/128</guid>
      <comments>https://do-study.tistory.com/128#entry128comment</comments>
      <pubDate>Sat, 19 Feb 2022 00:58:00 +0900</pubDate>
    </item>
    <item>
      <title>[Svelte 시리즈] 데이터 바인딩, 조건문, 반복문, 이벤트 핸들링</title>
      <link>https://do-study.tistory.com/127</link>
      <description>&lt;h1&gt;[Svelte 시리즈] 데이터 바인딩, 조건문, 반복문, 이벤트 핸들링&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트는 &lt;b&gt;&lt;code&gt;HEROPY&lt;/code&gt; 님 유튜브 Svelte 강좌를 보고 공부한 내용을 정리한 포스트입니다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 단/양방향 바인딩&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Svelte는 기본적으로 단방향 데이터 바인딩이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예제에서 input에 name을 바인딩 했지만, 값을 변경한다고 name 변수의 값은 바뀌지 않는다.&lt;/p&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
let name = 'world';
&amp;lt;/script&amp;gt;

&amp;lt;h1&amp;gt;Hello {name}&amp;lt;/h1&amp;gt;
&amp;lt;input type=&quot;text&quot; value={name} /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양방향 바인딩을 하려면 &lt;code&gt;bind&lt;/code&gt; 키워드를 사용해 아래와 같이 작성해주면 된다.&lt;/p&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
let name = 'world';
&amp;lt;/script&amp;gt;

&amp;lt;h1&amp;gt;Hello {name}&amp;lt;/h1&amp;gt;
&amp;lt;input type=&quot;text&quot; bind:value={name} /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;조건문&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마크업 영역에서 조건문을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;{#if 조건} 조건문 내용 {/if}&lt;/code&gt; 와 같이 조건문 영역을 잡을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;{:else}&lt;/code&gt;, &lt;code&gt;{:else if 조건}&lt;/code&gt; 를 통해 else / else if 구문을 작성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Svelte의 대부분 구문은 시작할 때 &lt;code&gt;#&lt;/code&gt;, 중간단계는 &lt;code&gt;:&lt;/code&gt;, 종료단계는 &lt;code&gt;/&lt;/code&gt;인 것이 대부분이다.&lt;/p&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
let toggle = false;
&amp;lt;/script&amp;gt;

{#if toggle}
    &amp;lt;h1&amp;gt;Hello {name}&amp;lt;/h1&amp;gt;
{:else}
    &amp;lt;div&amp;gt;No name!&amp;lt;/div&amp;gt;
{/if}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;반복문&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적으로 for each 문은 &lt;code&gt;{:each array as item} 반복문 내용 {/each}&lt;/code&gt; 으로 작성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;array는 배열 변수이며 as 뒤에는 반복문 내용에서 접근할 각 배열의 원소에 대한 이름을 지정한 것이다.&lt;/p&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    let fruits = ['Apple', 'Banana', 'Cherry', 'Orange', 'Mango'];
&amp;lt;/script&amp;gt;

&amp;lt;ul&amp;gt;
{#each fruits as fruit}
    &amp;lt;li&amp;gt;{fruit}&amp;lt;/li&amp;gt;
{/each}
&amp;lt;/ul&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고할 점은 Svelte는 기본적으로 단방향이기 때문에 아래와 같은 코드는 fruits가 변경되더라도 화면 랜더링에는 변화가 없다. 화면 랜더링에 변화를 주려면 fruits를 변경한 후 다시 재할당 해주어야한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이벤트 핸들링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예제는 &lt;code&gt;box&lt;/code&gt; 클래스로 지정된 div를 클릭하면 배경색이 변경되도록 작성한 예시이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;svelte에서 import한 &lt;code&gt;onMount&lt;/code&gt; 함수는 document.ready와 같이 화면쪽 컴포넌트 들이 모두 구성이 완료된 후 실행될 콜백을 세팅해둘수 있는 함수이다.&lt;/p&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    import { onMount } from 'svelte';
    let isRed = false;
    onMount(() =&amp;gt; {
         const box = document.querySelector('.box');
         box.addEventListener('click', () =&amp;gt; { isRed = !isRed });    
    });    
&amp;lt;/script&amp;gt;

&amp;lt;div class=&quot;box&quot; 
     style=&quot;background-color: {isRed ? 'red' : 'orange'}&quot;&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;style&amp;gt;
    .box {
        width: 300px;
        height: 150px;
        background-color: orange;
    }
&amp;lt;/style&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;svelte에서 제공해주는 방식대로 이벤트를 추가하려면 아래와 같이 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;on:&lt;/code&gt;이벤트명 과 함수를 전달하면 된다.&lt;/p&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    let isRed = false;
&amp;lt;/script&amp;gt;

&amp;lt;div class=&quot;box&quot; 
     style=&quot;background-color: {isRed ? 'red' : 'orange'}&quot;
     on:click={() =&amp;gt; isRed = !isRed}&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;style&amp;gt;
    .box {
        width: 300px;
        height: 150px;
        background-color: orange;
    }
&amp;lt;/style&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 예시를 더보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시는 input 태그에 input 이벤트 발생 시 해당 텍스트를 &lt;code&gt;text&lt;/code&gt; 변수에 할당해주는 이벤트를 걸어두고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼 클릭 시 &lt;code&gt;text&lt;/code&gt;변수를 &amp;lsquo;clicked!&amp;rsquo; 로 변경해주는 이벤트를 걸어둔 예시이다.&lt;/p&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    let text = '';
&amp;lt;/script&amp;gt;

&amp;lt;h1&amp;gt;
    {text}
&amp;lt;/h1&amp;gt;
&amp;lt;input type=&quot;text&quot; 
       on:input={(e) =&amp;gt; {text = e.target.value}} /&amp;gt;
&amp;lt;button on:click={() =&amp;gt; {text = 'clicked!'}}&amp;gt;
    Change Text
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시는 input의 값을 변경하면 값이 h1 태그 통해 출력되는 text 변수 값이 바뀌는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 버튼을 클릭하면 h1 통해 출력되는 값은 바뀌지만, input 태크에 입력되어있는 값은 바뀌지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 위 input 태그에 값을 바인딩을 해두지 않아서이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 input 태그에 value로 값을 바인딩해주면 버튼 클릭으로도 input 태그 값이 바뀌는 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;brush: html xml&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    let text = '';
&amp;lt;/script&amp;gt;

&amp;lt;h1&amp;gt;
    {text}
&amp;lt;/h1&amp;gt;
&amp;lt;input type=&quot;text&quot; 
       on:input={(e) =&amp;gt; {text = e.target.value}} 
       value={text}/&amp;gt;
&amp;lt;button on:click={() =&amp;gt; {text = 'clicked!'}}&amp;gt;
    Change Text
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방법은 양방향 바인딩으로도 해결해줄수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;input type=&quot;text&quot; bind:value={text} /&amp;gt;&lt;/code&gt; 라고 bind 키워드를 사용해 양방향 바인딩을 해주는 것이 위와 똑같은 역할을 해주고 있는 것이다.&lt;/p&gt;</description>
      <category>Front-End</category>
      <category>svelte</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/127</guid>
      <comments>https://do-study.tistory.com/127#entry127comment</comments>
      <pubDate>Thu, 17 Feb 2022 23:52:44 +0900</pubDate>
    </item>
    <item>
      <title>Docker Compose 를 이용해 Spring Boot + MySQL 서비스 구축 (기초)</title>
      <link>https://do-study.tistory.com/126</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;이 글은 개인적으로 공부를 목적으로 여러 블로그, 문서를 참조하며 작성하여 부정확하거나 내용이 부실할 수 있는점 양해드리며&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&amp;nbsp;잘못된 부분이 있다면 댓글로 알려주시면 감사하겠습니다 :)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;간단하면서 일반적으로, 백엔드 애플리케이션 + DB와 프론트에 Nginx를 두는 구조를 많이 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker 환경에서는 백엔드 애플리케이션, DB, Nginx를 각각 설정하고 컨테이너화 하여 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 서버 장비가 증설된다면 각 컨테이너 설정을 다시 해주어야하는 번거로움이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose는 한 서비스를 위한 여러 컨테이너를 관리를 하나의 파일에서 할 수 있고 여러 컨테이너들을 동시에 실행시킬 수 있다. 이를 통해 위와 같이 매번 컨테이너를 설정해야하는 반복작업을 줄이며 서비스를 위한 컨테이너들의 관리를 쉽게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예시에서는 아주 간단한 Spring Boot 애플리케이션 하나와, MySQL DB가 연동된 구조를 Docker Compose로 구성해보겠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Spring Boot 애플리케이션 구축&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://start.spring.io/&quot;&gt;https://start.spring.io/&lt;/a&gt; 에서 spring-boot-starter-data-jpa, mysql 의존성을 추가해 프로젝트 생성&lt;/li&gt;
&lt;li&gt;빌드시스템은 Gradle 사용&lt;/li&gt;
&lt;li&gt;간단한 REST API 하나 제공하는 서비스 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker Compose 관련이 핵심이기 때문에, 예시코드가 좀 부실하더라도 양해 바랍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// 엔티티 정의
@Entity
public class User {
    @Id
    private Long id;

    @Column
    private String name;

    // getters, setters
}

// 레파지토리 정의
@Repository
public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {
}

// 컨트롤러 정의
@RestController
@RequestMapping(&quot;/users&quot;)
public class UserController {

    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping
    public List&amp;lt;User&amp;gt; getUsers() {
        return this.userRepository.findAll();
    }

    @GetMapping(&quot;/{id}&quot;)
    public User getUsers(@PathVariable Long id) {
        return this.userRepository.findById(id).orElse(null);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application.yml에 설정 진행
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL서버가 컨테이너 환경이 아닌 외부에 떠있는 경우엔 아래와같이 설정파일에 DB 설정을 해둔다.&lt;/li&gt;
&lt;li&gt;하지만 MySQL 서버가 컨테이너로 구성되어있는 경우엔 설정할 수 있는 방벙이 다양하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot 설정값 외부 주입을 이용하는 방법
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정파일내에서 ${} 로 외부 환경변수 값을 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;설정값을 외부에서 주입하지 않고 도커 서비스명으로 참조하는 방법
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker Compose로 실행시키는 컨테이너들은 동일 네트워크로 묶여 상호 통신이 가능&lt;/li&gt;
&lt;li&gt;도커 네트워크에 대한 내용은 별도 포스트 참조&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Spring Boot Relaxed Binding을 이용하면, application.yml에 datasource 설정이 없어도됨
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties.relaxed-binding.environment-variables&quot;&gt;https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties.relaxed-binding.environment-variables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 아래는 MySQL이 컨테이너가 아닌 경우에 대한 설정 예시
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/userdb
    username: user
    password: userpwd
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    database-platform: org.hibernate.dialect.MySQL5Dialect
    show-sql: true
    hibernate:
      ddl-auto: none

# 외부 환경변수를 주입받아 설정되도록 할 경우에 대한 예시
spring:
  datasource:
    url: ${DB_CONNECTION_URL}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    database-platform: org.hibernate.dialect.MySQL5Dialect
    show-sql: true
    hibernate:
      ddl-auto: none

# 외부 환경변수를 주입받아 설정되도록 할 경우에 대한 예시
# 호스트네임의 &quot;db&quot;는 도커 네트워크상 참조가능한 이름으로, 이후 docker-compose 설정할 때 다시 다루겠습니다.
spring:
  datasource:
    url: jdbc:mysql://database:3306/userdb 
    username: user
    password: userpwd
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    database-platform: org.hibernate.dialect.MySQL5Dialect
    show-sql: true
    hibernate:
      ddl-auto: none
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이후 프로젝트를 빌드하면 프로젝트경로/build/libs 밑에 jar 파일이 생성됨&lt;/li&gt;
&lt;li&gt;백엔드 애플리케이션 Dockerfile 작성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dockerfile 위치는 프로젝트 루트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM openjdk:11
ADD build/libs/app.jar app.jar
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Docker Compose 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose는 docker-compose.yml 이라는 파일을 통해 설정한다.&lt;/p&gt;
&lt;pre class=&quot;yml yaml&quot;&gt;&lt;code&gt;version: &quot;3&quot;
services:
  database:
    image: mysql
    environment:
      MYSQL_DATABASE: userdb
      MYSQL_USER: user      
      MYSQL_PASSWORD: userpwd
      MYSQL_ROOT_HOST: '%'
      MYSQL_ROOT_PASSWORD: rootpwd
    volumns:
      - ./db/data:/var/lib/mysql
    ports:
      - 3306:3306
    restart: always
  application:
    build: .
    depands_on:
      - database
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://localhost:3306/userdb
      SPRING_DATASOURCE_USERNAME: user
      SPRING_DATASOURCE_PASSWORD: userpwd
    restart: always
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정파일을 살펴보면 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;version
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Comose 파일 버전로 도커 엔진 버전별 사용가능한 버전이 존재&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;services
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker Compose로 하나로 관리할 서비스들을 정의. 이들이 각각 컨테이너가 된다.&lt;/li&gt;
&lt;li&gt;services 아래 설정되는 각 서비스의 이름으로 컨테이너간 통신할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 DB 서비스을 보면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;database
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;database&amp;rdquo;라는 이름의 서비스&lt;/li&gt;
&lt;li&gt;백엔드에서 DB 설정을 한다면 hostname에 이 값을 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;image
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mysql 기반 이미지 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;environment
&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;mysql 이미지는 컨테이너의 환경변수를 참조하여 MySQL DB, 유저 정보등을 구성함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;volumns
&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;DB 데이터 유지를 위해 필수적으로 설정되야함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ports
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호스트의 포트와 컨테이너의 포트를 매핑함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;restarts
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오류 등으로 컨테이너가 종료됐을때 재시작 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 Spring Boot 애플리케이션 컨테이너에 대한 설정이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;application&amp;rdquo;라는 이름의 서비스&lt;/li&gt;
&lt;li&gt;build
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dockerfile을 이용해 컨테이너화 할 때 사용하는 옵션&lt;/li&gt;
&lt;li&gt;위 database 컨테이너와 달리 Dockerfile을 빌드하여 컨테이너화 해야하기 때문에 사용&lt;/li&gt;
&lt;li&gt;Dockerfile이 docker-compose.yml 파일과 다른 경로에 있을경우엔 &amp;ldquo;.&amp;rdquo; 대신 해당 경로 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;depends_on
&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;이 예시에선 DB가 먼저 올라가야하기 때문에 &amp;ldquo;database&amp;rdquo; 서비스에 의존성을 갖게함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;environment
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;database 설정과 동일하게, 환경변수를 컨테이너 내부에 세팅해주기 위해 사용한다.&lt;/li&gt;
&lt;li&gt;프로젝트에서 채택한 방식에 따라 적절하게 설정해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;restarts
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 동일&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 docker-compose.yml 파일까지 작성이 완료되면 이제 실행만 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 docker-compose 기본적인 명령어들이다. 전체적으로 Docker 명령어와 유사하다&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# 컨테이너화 + 데몬모드로 각 컨테이너 실행
docker-compose up -d 

# 상태 체크
docker-compose ps

# 컨테이너 실행 / 종료
docker-compose start
docker-compose stop

# Docker Compose 제거 (volumn, network 포함 제거)
docker-compose down

# 로그 확인
docker-compose logs
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Back-End/Spring framework</category>
      <category>docker</category>
      <category>docker-compose</category>
      <category>MySQL</category>
      <category>spring-boot</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/126</guid>
      <comments>https://do-study.tistory.com/126#entry126comment</comments>
      <pubDate>Mon, 17 Jan 2022 00:46:09 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 애플리케이션 Docker 컨테이너로 배포 (기초)</title>
      <link>https://do-study.tistory.com/125</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;명령어 간략 정리&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;docker images            # 이미지 목록 조회
docker ps                # 실행중인 컨테이너 목록 조회 (-a 옵션 시 종료된 컨테이너 목록도 보여줌)
docker kill CONTAINER_ID # 컨테이너 종료 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도커 이미지 빌드 명령어&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker build --tag [repo:tag] [Dockerfile path]

e.g.
docker build --tag myservice:0.1 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨테이너 실행 관련 명령어&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;docker run IMAGE_ID       # 이미지ID 기반 새 컨테이너 생성하여 실행
docker run repo:tag       # 레파지토리&amp;amp;태그에 해당하는 이미지 기반 새 컨테이너 생성하여 실행

docker start CONTAINER_ID # 기존 생성된 특정 컨테이너를 실행시킴
docker stop CONTAINER_ID  # 기존 실행중인 특정 컨테이너를 종료시킴

docker rm CONTAINER_ID    # 컨테이너 삭제
docker rmi IMAGE_ID       # 이미지 삭제
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SpringBoot Dockerfile 예시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dockerfile을 작성한다.
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDK11 환경 사용&lt;/li&gt;
&lt;li&gt;build/libs/app.jar 파일 (Dockerfile 기준 상대경로)을 app.jar라는 이름으로 컨테이너 내부에 추가&lt;/li&gt;
&lt;li&gt;&amp;ldquo;java -jar app.jar&amp;rdquo; 명령어 실행 (각 명령어 토큰을 배열 형태로 설정)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM openjdk:11
ADD build/libs/app.jar app.jar
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dockerfile 이미지로 빌드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 디렉토리 내 Dockerfile을 app Repository, 0.1 Tag로 빌드한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;docker build -t app:0.1 .
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너 실행
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;-d 옵션 : 데몬 모드로 실행 (백그라운드에서 동작)&lt;/li&gt;
&lt;li&gt;-p 옵션 : 호스트 포트로 들어오는 요청을 도커 컨테이너 내부 포트로 포워딩해줌 (호스트:컨테이너)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래 예시에선 5000번 포트로 들어오는 요청이 도커 컨테이너의 8080포트로 포워딩된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;실행할 이미지 : 이미지ID 또는 repo:tag 형태의 이미지 식별자 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;docker run -d -p 5000:8080 app:0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너 접속하여 잘 실행되고 있는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;docker exec -it /bin/bash
ps -ef | grep java
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Back-End/Spring framework</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/125</guid>
      <comments>https://do-study.tistory.com/125#entry125comment</comments>
      <pubDate>Sun, 16 Jan 2022 03:44:03 +0900</pubDate>
    </item>
    <item>
      <title>Java / Servlet 비동기 기술 흐름 with Spring</title>
      <link>https://do-study.tistory.com/124</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Servlet 3.0 이전&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 알고 있었던과 같이 1 Request per 1 Thread 할당 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오래걸리는 무거운 작업은 @Async 를 활용해 비동기로 처리할 수 있었지만 이는 말 그대로 해당 스레드 내부에서 유효할 뿐 해당 작업이 끝나기 까지 그 스레드가 반환되지 못하는건 매한가지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣의 NIO 지원은 HTTP Connection 관련 부분을 비동기로 처리하는 것 뿐 서블릿 동작과는 별개의 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 문제점은 짧게 끝나는 작업이 오래 걸리는 작업이 스레드를 오래 점유하여 덩달아 오래걸리게 되는 문제가 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Servlet 3.0&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet 3.0 부터는 이런 문제점을 어느정도 해결하여 오래 걸리는 작업을 별로 스레드에 할당하여 처리하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 스레드는 빨리 반환하여 다른 요청을 받을 수 있도록 개선하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 HTTP Request, Response를 담당하는 스레드는 서블릿 스레드, 작업을 처리하는 스레드는 작업 스레드(워커 스레드)로 구분된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 스레드 내부에서 비동기 작업(별도 스레드)이 실행되면 해당 작업은 작업 스레드가 처리하고, 현재 서블릿 스레드는 즉시 풀에 반납된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 에선 컨트롤러 레이어에서 Callable 등이 반환되면 해당 작업이 작업 스레드에서 시작된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Servlet 3.1&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet 3.0에선 서블릿 스레드의 Request Read, Response Write 작업은 동기방식으로 진행됐었는데, 3.1 부턴 이 작업까지 비동기로 진행하도록 개선되었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. DeferredResult와 Queue 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DefferedResult&amp;lt;T&amp;gt; 는 네이밍 그대로 지연된 결과 라는 뜻을 담고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 컨트롤러 레이어에서 DefferedResult가 반환되면 해당 객체에 결과가 세팅될 때 까지 응답을 보내지 않고 기다린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 한 곳에서 요청하면 (풀링 개념) DefferedResult 객체를 큐에 저장하고 외부에서 이벤트를 주었을 때 해당 객체에 값이 설정되게 하며 풀링하고 있던 클라이언트에선 그 즉시 결과를 받도록 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DefferedResult의 특징은 워커 스레드를 사용하지 않으며 서블릿 스레드도 즉시 반환한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DefferedResult 객체만 메모리에 유지되면 결과가 세팅되는 시점에 바로 응답을 보낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 아주 간단한 사용사례이다.&lt;/p&gt;
&lt;pre class=&quot;brush:java arduino&quot;&gt;&lt;code&gt;@RestController
public static class MyController {
	Queue&amp;lt;DeferredResult&amp;lt;String&amp;gt;&amp;gt; results = new ConcurrentLinkedQueue&amp;lt;&amp;gt;();
	
	// 아래 코드를 통해 클라이언트가 풀링하는 API
	@GetMapping(&quot;/dr&quot;)
	public DeferredResult&amp;lt;String&amp;gt; deferredResult() {
		DeferredResult&amp;lt;String&amp;gt; dr = new DeferredResult&amp;lt;&amp;gt;();
		results.add(dr);
		return dr;
	}

	@GetMapping(&quot;/dr/count&quot;)
	public String drCount() {
		return String.valueOf(results.size());
	}

	// 외부에서 이벤트 발생시키는 API 
	@GetMapping(&quot;/dr/event&quot;)
	public String drEvent(String msg) {
		for (DeferredResult&amp;lt;String&amp;gt; dr :results) {
			dr.setResult(&quot;Hello &quot; + msg); // 이 때 결과가 세팅되며, 이 객체를 풀링하고 있는 곳에선 결과를 받을수있음
			results.remove(dr);
		}
		return &quot;OK&quot;;
	}

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 작업 수행 후 결과를 처리할 때도 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;brush:java arduino&quot;&gt;&lt;code&gt;public class MyController {

    @GetMapping
    public ListenableFuture&amp;lt;String&amp;gt; api() {
        ListenableFuture&amp;lt;String&amp;gt; future = doAsyncTask();
				// future.get(); =&amp;gt; 이 코드가 호출되는 순간 서블릿 스레드가 점유되어 비동기 의미가 없어짐
				return future;
    }

	  @GetMapping
    public DeferredResult&amp;lt;String&amp;gt; api() {
        DeferredResult&amp;lt;String&amp;gt; dr = new DeferredResult&amp;lt;&amp;gt;();
        ListenableFuture&amp;lt;String&amp;gt; future = doAsyncTask();
				// 콜백을 추가하고 콜백 내에서 DeferredResult에 값을 설정해줌
				future.addCallback(res -&amp;gt; {
						dr.setResult(res);
				}, err -&amp;gt; {
						dr.setErrorResult(err);
				});				
				return dr;
    }
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. ResponseBodyEmitter&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE (Server Sent Event)는 HTTP 기반 서버 사이드 스트리밍 기술이라 생각하면 된다. (&lt;a href=&quot;https://hamait.tistory.com/792&quot;&gt;https://hamait.tistory.com/792&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket이 서버/클라이언트간 양방향 통신을 지원한다면 SSE는 서버 &amp;rarr; 클라이언트로만 데이터를 스트리밍하면 되는 시나리오에 유용하게 사용될 수 있다. (이전의 ajax 롱 풀링, 푸시 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 구현하려면 꽤 복잡한 구현이 필요하지만 Spring의 ResponseBodyEmitter 를 사용하면 복잡한 HTTP 레벨 구현 없이 쉽게 스트리밍 API를 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예제는 1초에 1건씩 응답을 보내는 Emitter를 구현한 것이다.&lt;/p&gt;
&lt;pre class=&quot;brush:java&quot;&gt;&lt;code&gt;@RestController
public static class MyController {

	@GetMapping(&quot;/streaming&quot;)
	public ResponseBodyEmitter streaming() {
		ResponseBodyEmitter emitter = new ResponseBodyEmitter();
		Executors.newSingleThreadExecutor().execute(() -&amp;gt; {
			for (int i = 0; i &amp;lt;= 50; i++) {
				try {
					emitter.send(&quot;&amp;lt;p&amp;gt; Stream &quot; + i + &quot;&amp;lt;/p&amp;gt;&quot;); // 클라이언트로 이벤트 보냄
					Thread.sleep(1000);
				} catch (Exception e) {}
			}
		});
		return emitter;
	}

}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Back-End/Reactive Programming</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/124</guid>
      <comments>https://do-study.tistory.com/124#entry124comment</comments>
      <pubDate>Thu, 13 Jan 2022 03:14:51 +0900</pubDate>
    </item>
    <item>
      <title>Java CompletableFuture 개념 및 간단한 활용 사례</title>
      <link>https://do-study.tistory.com/123</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Future 는 비동기 작업 수행의 결과를 담고있는 자바의 인터페이스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ListenableFuture 는 Spring 에서 제공하는 인터페이스로 Future에 콜백을 등록해 사용할 수 있도록 한 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 8 때 소개된 CompletableFuture 는 여러 비동기 작업을 결합하고 처리하는데 기존 방식에 비해 훨씬 편하게 수행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 간단히 반환값이 없는 2개 비동기 작업을 수행하는 예시이다.&lt;/p&gt;
&lt;pre class=&quot;brush:java livescript&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;CompletableFuture
	.runAsync(() -&amp;gt; log.info(&quot;runAsync&quot;))
	.thenRun(() -&amp;gt; log.info(&quot;thenRun&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환값이 있는 비동기 작업은 아래와 같이 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;brush:java reasonml&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;CompletableFuture
	.supplyAsync(() -&amp;gt; 1)  // 비동기적으로 값을 제공 (외부 API 호출 등)
	.thenApply(i -&amp;gt; i + &quot;hi&quot;)  // 값을 가공
	.exceptionally(e -&amp;gt; e.getMessage()) // 작업 중 오류가 발생했을 경우 사용될 값
	.thenAccept(i -&amp;gt; System.out.println(i)); // 값을 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CompletableFuture 에서 할 수 있는 작업들은 CompletionStage 에 정의되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CompletableFuture 에서 수행되는 작업은 내부에 관리되는 ForkJoinPool에 의해 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 ForkJoinPool의 병렬도가 1 이상이면 ForkJoinPool, 아니면 각 작업마다 하나의 스레드가 할당되는 ThreadPerTaskExecutor가 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(풀 병렬도는 ForkJoinPool.getCommonPoolParallelism() 메소드로 가져오는데,, 이게 정확히 무슨 값을 의미하는지는 잘 모르겠음. 아시는 분 댓글 남겨주시길 바랍니다)&lt;/p&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 로 호출되는 메소드는 두 번째 인자로 특정 스레드풀에서 실행되게 할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 어떤 작업 내에서 리턴값이 CompletableFuture가 되어야하는 경우 thenCompose 를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 또다른 비동기 작업을 수행하되, CompletableFuture 타입이 아닌경우 CompletableFuture로 변환하여 리턴할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시는 스프링의 ListenableFuture를 중간작업으로 수행하는데 CompletableFuture 체인에 연결한 예시이다.&lt;/p&gt;
&lt;pre class=&quot;brush:java reasonml&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// Callback 방식의 ListenableFuture를 CompletableFuture로 변환해주는 메소드
// ListenableFuture 콜백에 CompletableFuture 방식의 메소드를 전달한다.
public static &amp;lt;T&amp;gt; toCompletableFuture(ListenableFuture&amp;lt;T&amp;gt; lf) {
    CompletableFuture&amp;lt;T&amp;gt; cf = new CompletableFuture&amp;lt;&amp;gt;();
    lf.addCallback(cf::complete, cf::completeExceptionally);
    return cf;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;brush:java xl&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;CompletableFuture
        .supplyAsync(() -&amp;gt; 1)
        .thenCompose(i -&amp;gt; {
            ListenableFuture&amp;lt;Integer&amp;gt; lf = doListenableFutureJob();            
            return toCompletableFuture(lf); // CompletableFuture 체인에 연결해주기위해 바꿔줌
        })
        .thenAccept(i -&amp;gt; System.out.println(i));
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Back-End/Reactive Programming</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/123</guid>
      <comments>https://do-study.tistory.com/123#entry123comment</comments>
      <pubDate>Wed, 12 Jan 2022 02:11:24 +0900</pubDate>
    </item>
    <item>
      <title>[짧은정리] Gradle Multi Module 구성 시 참고사항</title>
      <link>https://do-study.tistory.com/122</link>
      <description>&lt;p&gt;멀티 모듈 구성 시 보통 common 모듈을 두고 나머지 모듈 (e.g. API 모듈, Batch 모듈, ..) 에서 common 모듈을 참조하도록 구성한다.&lt;/p&gt;
&lt;p&gt;common 모듈에 작성한 공통 로직 + 의존성을 타모듈들에서 중복없이 사용하기 위함이다.&lt;/p&gt;
&lt;p&gt;이 때 common 모듈에 dependency를 &lt;code&gt;implementation&lt;/code&gt;으로 잡을 경우 common을 참조하는 타 모듈에선 common의 코드들만 노출되고 common의 dependency들은 노출되지 않는다. &lt;/p&gt;
&lt;p&gt;예를 들어 common 모듈에 JPA Starter 의존성을 &lt;code&gt;implementation&lt;/code&gt;으로 잡으면 api, batch 모듈에선 JPA 관련 코드가 노출이 안되는 것이고 JPARepository 등을 상속받아 구현된 Repository들에 findAll, save 등 메소드를 사용할 수 없다는 뜻이다.&lt;/p&gt;
&lt;p&gt;이를 해결하기 위해 의존성을 &lt;code&gt;api&lt;/code&gt;로 잡으면 이 부분이 해결된다. (Gradle 6 이하에선 compile, 7 이상에선 api)&lt;/p&gt;</description>
      <category>Back-End/Spring framework</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/122</guid>
      <comments>https://do-study.tistory.com/122#entry122comment</comments>
      <pubDate>Sat, 11 Dec 2021 03:39:45 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 스키마 추출 (DDL Export)</title>
      <link>https://do-study.tistory.com/121</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 특정 DB DDL 추출하는 법 정리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mysqldump 프로그램을 사용하고, --no-data 옵션으로 데이터를 제외한 나머지 오브젝트들만 export한다.&lt;/p&gt;
&lt;pre id=&quot;code_1637430787337&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysqldump -h [DB서버IP] -u root -p --no-data [대상DB] &amp;gt; [추출파일명]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 AUTO_INCREMENT 값이 유지되기 때문에 유지를 원하지 않는 경우 1로 변경해두어야 한다.&lt;/p&gt;</description>
      <category>IT기본</category>
      <category>DDL</category>
      <category>MySQL</category>
      <category>mysql dump</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/121</guid>
      <comments>https://do-study.tistory.com/121#entry121comment</comments>
      <pubDate>Sun, 21 Nov 2021 02:54:38 +0900</pubDate>
    </item>
    <item>
      <title>Kotlin 프로퍼티 (Properties and Fields)</title>
      <link>https://do-study.tistory.com/120</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;보통 객체지향 프록그래밍에서 클래스를 정의할 때 객체의 상태는 멤버변수 (필드)로 행위는 메소드로 표현합니다.&lt;br /&gt;그리고 보통 멤버변수가 있으면 객체의 값을 설정하고 가져오는 (setter / getter) 메소드가 동반되는 경우가 많은데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 이런 요소를 아우르는 &lt;code&gt;프로퍼티&lt;/code&gt;라는 개념이 제공됩니다.&lt;br /&gt;즉 프로퍼티는 getter, setter와 같은 접근자를 포함하고있는 필드입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로퍼티 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로퍼티를 정의하는 전체 문법은 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;(var/val) &amp;lt;propertyName&amp;gt;[: &amp;lt;PropertyType&amp;gt;] [= &amp;lt;property_initializer&amp;gt;]
          [&amp;lt;getter&amp;gt;]
          [&amp;lt;setter&amp;gt;]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;var / val : 프로퍼티 선언을 위한 예약어. var는 초기화 후 값 변경이 가능한 프로퍼티, val은 초기화 후 값 변경이 불가능한 프로퍼티&lt;/li&gt;
&lt;li&gt;propertyName: 프로퍼티명&lt;/li&gt;
&lt;li&gt;PropertyType: 프로퍼티 타입 (타입 추론이 가능한 경우 생략 가능)&lt;/li&gt;
&lt;li&gt;property_initializer: 프로퍼티 값 초기화 (초기화가 불필요한 경우 생략 가능)&lt;/li&gt;
&lt;li&gt;getter / setter : 해당 프로퍼티에 대한 getter / setter를 정의 (생략할 경우 default getter, setter 적용) &lt;code&gt;val&lt;/code&gt; 프로퍼티는 setter를 가질수없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로퍼티와 getter, setter는 접근제한자 (public, internal, protected, private) 적용이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 프로퍼티가 자바로 변환되면 어떻게 적용되는지 살펴보겠습니다.&lt;br /&gt;아래 name, age은 생성자 인자이면서 프로퍼티입니다.&lt;/p&gt;
&lt;pre class=&quot;brush:kotlin angelscript&quot;&gt;&lt;code&gt;class Person(var age: Int, val name: String)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드가 자바로 변환되면 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;brush:java&quot;&gt;&lt;code&gt;public static final class Person {
    private int age;
    @NotNull
    private final String name;

    public final int getAge() {
        return this.age;
    }

    public final void setAge(int var1) {
        this.age = var1;
    }

    @NotNull
    public final String getName() {
        return this.name;
    }

    public Person(int age, @NotNull String name) {
        Intrinsics.checkNotNullParameter(name, &quot;name&quot;);
        super();
        this.age = age;
        this.name = name;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;var&lt;/code&gt;로 선언한 &lt;code&gt;age&lt;/code&gt;의 경우 &lt;code&gt;getAge&lt;/code&gt;, &lt;code&gt;setAge&lt;/code&gt;와 같이 getter, setter가 자동으로 생성되었습니다.&lt;br /&gt;반면 &lt;code&gt;val&lt;/code&gt;로 선언한 &lt;code&gt;name&lt;/code&gt;의 경우 값 변경이 불가능하고 setter를 정의할 수 없기 때문에 당연하게 &lt;code&gt;setter&lt;/code&gt;가 생성되지 않은 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 자바에서는 getter / setter는 필드값 반환, 세팅의 역할이 주였는데요.&lt;br /&gt;코틀린에서의 getter / setter는 보다 다양하게 활용이 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;brush:kotlin angelscript&quot;&gt;&lt;code&gt;class Product(var size: Int) {
    val isEmpty
        get() = this.size == 0
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작성한 코드는 자바로 다음과 같이 변환됩니다.&lt;/p&gt;
&lt;pre class=&quot;brush:java arduino&quot;&gt;&lt;code&gt;public static final class Product {
  private int size;

  public final boolean isEmpty() {
      return this.size == 0;
  }

  public final int getSize() {
      return this.size;
  }

  public final void setSize(int var1) {
      this.size = var1;
  }

  public Product(int size) {
      this.size = size;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 다른 필드를 활용한 계산된 (computed) 속성으로도 활용이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 커스텀 setter를 지정하여 프로퍼티 값 설정 전 유효성 검사, 혹은 다른 동작도 지정할 수 있습니다.&lt;br /&gt;아래 예시에서는 1~3 값만 설정가능한 &lt;code&gt;grade&lt;/code&gt; 프로퍼티를 정의했습니다.&lt;br /&gt;setter 로직 중 &lt;code&gt;field&lt;/code&gt;가 실제 프로퍼티의 값인데 코틀린에서는 이를 &lt;code&gt;Backing Fields&lt;/code&gt;라고 합니다.&lt;/p&gt;
&lt;pre class=&quot;brush:kotlin cs&quot;&gt;&lt;code&gt;class Person {
    var grade = 1
        set(value) {
            if (value in 1..3) {
                field = value
            } else {
                throw IllegalArgumentException(&quot;Grade 는 1 ~ 3 사이여야합니다.&quot;)
            }
        }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Back-End/Kotlin</category>
      <category>FieLDS</category>
      <category>Kotlin</category>
      <category>properties</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/120</guid>
      <comments>https://do-study.tistory.com/120#entry120comment</comments>
      <pubDate>Tue, 7 Sep 2021 02:47:24 +0900</pubDate>
    </item>
    <item>
      <title>Kotlin 생성자 개념과 사용법 정리</title>
      <link>https://do-study.tistory.com/119</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서 생성자를 정의하는 여러가지 방법에 대해 정리합니다.&lt;br /&gt;코틀린 생성자는 크게 주 생성자(primary constructor)와 부 생성자(secondary constructor)로 나뉘고 각각 제약이 조금씩 다릅니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 주 생성자 (Primary constructor)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 &lt;code&gt;constructor&lt;/code&gt; 키워드를 통해 생성자를 정의할 수 있습니다.&lt;br /&gt;&lt;code&gt;constructor&lt;/code&gt; 키워드 앞에 접근 제한자를 지정할 수 있습니다.&lt;br /&gt;&lt;code&gt;constructor&lt;/code&gt; 키워드 자체를 생략할 수도 있습니다. 단, 이경우엔 접근 제한자는 지정할 수 없습니다.&lt;br /&gt;이렇게 선언하는 생성자를 &lt;code&gt;주 생성자&lt;/code&gt;라고 합니다.&lt;/p&gt;
&lt;pre class=&quot;brush:kotlin angelscript&quot;&gt;&lt;code&gt;class Person constructor(name: String, age: Int)

// 접근 제한자 지정
class Person private constructor(name: String, age: Int)

// constructor 키워드 생략 가능
class Person(name: String, age: Int)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 경우엔 생성자 시그니쳐만 정의할 수 있고 초기화 로직 (내부 프로퍼티에 할당)을 작성할 수 가 없습니다.&lt;br /&gt;즉 위 코드만 가지고는 객체 생성 시 전달되는 &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;age&lt;/code&gt;를 클래스 내부에서 사용할 수 없는 것입니다.&lt;br /&gt;이러한 상황에 대비해 &lt;code&gt;init&lt;/code&gt;이란 키워드가 제공됩니다. 초기화 로직을 포함 추가로 필요한 로직이 있다면 호출할 수 있습니다.&lt;br /&gt;혹은 프로퍼티를 정의하면서 바로 대입해줄 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;brush:kotlin&quot;&gt;&lt;code&gt;class Person(name: String, age: Int) {
    val name: String
    val age: Int

    // 아래 init 블록 통해 초기화 가능
    init {
      this.name = name
      this.age = age
    }

    // 혹은 init 블록 사용 없이 프로퍼티 선언 후 대입
    val name = name
    val age = age
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 매번 이런식으로 프로퍼티에 값을 할당해주는 것은 상당히 번거로운 일이 될 수 있습니다.&lt;br /&gt;코틀린에서는 생성자 시그니쳐를 작성할 때 인자 선언과 동시에 프로퍼티로 할당해주는 문법을 지원합니다.&lt;br /&gt;아래와 같이 인자 선언할 때 &lt;code&gt;val / var&lt;/code&gt; 을 주면 됩니다.&lt;br /&gt;또한&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class Person(val name: String, val age: Int)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 부 생성자 (Secondary constructor)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;주 생성자&lt;/code&gt; 이외에 추가로 생성하는 생성자들은 모두 &lt;code&gt;부 생성자&lt;/code&gt;가 됩니다.&lt;br /&gt;부 생성자는 주 생성자에 비해 몇가지 제약이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;부 생성자&lt;/code&gt;는 &lt;code&gt;주 생성자&lt;/code&gt;를 반드시 상속해야합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;부 생성자&lt;/code&gt;에는 인자 선언과 동시에 프로퍼티 할당을 할 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:kotlin delphi&quot;&gt;&lt;code&gt;class Person(val name: String, val age: Int) {

    constructor(grade: Int) {}  // 오류 발생

    constructor(name: String, age: Int, grade: Int): this(name, age) {  // 이렇게 주 생성자를 상속해줘야함

    }

    constructor(name: String, age: Int, val grade: Int): this(name, age) {  // 부 생성자에서 var / val 사용 불가
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;주 생성자&lt;/code&gt;가 없는 클래스가 존재할 수도 있다. 이 경우엔 아래와 같이 부 생성자를 자유롭게 선언해둘 수 있다.&lt;/p&gt;
&lt;pre class=&quot;brush:kotlin delphi&quot;&gt;&lt;code&gt;class Person {

    constructor(name: String) { 
    }

    constructor(name: String, age: Int) { 
    }

    constructor(name: String, age: Int, grade: Int) { 
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이렇게 할 일이 거의 없는게.. 주 / 부 생성자 상관 없이 각 인자에 기본값(default)를 정의해 줄 수 있기 때문에&lt;br /&gt;이를 활용해 대부분 주 생성자 하나로 해당 클래스의 객체 생성 방법을 대부분 다 표현해낼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;brush:kotlin angelscript&quot;&gt;&lt;code&gt;class Person(val name: String, val age: Int, val grade: Int = 1)

fun main(args: Array&amp;lt;String&amp;gt;) {
    val p1 = Person(&quot;p1&quot;, 20)
    val p2 = Person(&quot;p2&quot;, 21, 2)
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Back-End/Kotlin</category>
      <category>Kotlin</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/119</guid>
      <comments>https://do-study.tistory.com/119#entry119comment</comments>
      <pubDate>Tue, 7 Sep 2021 01:42:57 +0900</pubDate>
    </item>
    <item>
      <title>Kotlin 클래스 주요 개념 with Java class와 차이점</title>
      <link>https://do-study.tistory.com/118</link>
      <description>&lt;h1&gt;클래스 정의에 대한 Java와 Kotlin 차이점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트에서는 코틀린에서의 클래스가 자바와 어떻게 다른지 대략적으로 정리한 내용을 다룹니다.&lt;br /&gt;공부하면서 정리한 포스트이기에 잘못된 내용이나 부족한 부분이 있을 수 있습니다.&lt;br /&gt;댓글로 일러주시면 감사하겠습니다 :)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. class 키워드에 대한 차이점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 자바에서는 &lt;code&gt;class&lt;/code&gt; 키워드를 통해 클래스를 정의합니다.&lt;/p&gt;
&lt;pre class=&quot;brush:java angelscript&quot;&gt;&lt;code&gt;class SomeClass {
    // 속성, 메서드 선언
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 이렇게 선언하면 기본적으로 클래스를 포함한 모든 멤버가 &lt;code&gt;final&lt;/code&gt;로 정의됩니다. (상속 불가)&lt;/p&gt;
&lt;pre class=&quot;brush:kt angelscript&quot;&gt;&lt;code&gt;class SomeClass {
    // 속성, 메서드 선언
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 코틀린에는 상속 및 재정의가 가능한 요소로 만들어주는 &lt;code&gt;open&lt;/code&gt;이라는 키워드가 있습니다.&lt;br /&gt;클래스, 속성, 메서드 모든 곳에 사용이 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;brush:kt kotlin&quot;&gt;&lt;code&gt;// 상속 가능
open class SomeClass {
    // final로 선언되어 상속받는 클래스에서 재정의 불가
    fun someFunction(): Int {
        return 1
    }

    // 상속받는 클래스에서 재정의 가능
    open fun otherFunction(): Int {
        return 2
    } 
}

// 이렇게 open 클래스인 SomeClass를 상속받을 수 있다.
class ExtendSomeClass(): SomeClass() {
    override fun otherFunction(): Int {
        return 4
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 추상 클래스에서도 동일하게 적용됩니다.&lt;/p&gt;
&lt;pre class=&quot;brush:kt kotlin&quot;&gt;&lt;code&gt;abstract class Machine(val name: String) {    
    // 반드시 재정의해야하는 메서드
    abstract fun connect()

    // 선택적으로 재정의할 수 있는 메서드
    open fun prepare() {
        println(&quot;Prepare machine&quot;)
    }

    // 재정의 불가
    fun start() {
        connect()
        parepare()
        println(&quot;Start machine - $name&quot;)
    }
}

class LocalMachine(name: String): Machine(name) {
    override fun connect() {
        println(&quot;Connect machine - $name&quot;)
    }

    override fun prepare() {
        println(&quot;Prepare machine on local&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약해보자면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kotlin에서 &lt;code&gt;class&lt;/code&gt; 키워드는 기본적으로 멤버들을 final로 정의된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open&lt;/code&gt; 키워드를 사용하면 각 멤버 (클래스, 속성, 메서드)의 상속 및 재정의 여부를 제어할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. data class&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;data class는 용어 그대로 데이터를 보관하는 목적의 클래스들을 정의할 때 사용합니다.&lt;br /&gt;&lt;code&gt;data&lt;/code&gt; 키워드를 클래스 선언 앞에 붙혀주면 &lt;code&gt;toString()&lt;/code&gt;, &lt;code&gt;hashCode()&lt;/code&gt;, &lt;code&gt;equals()&lt;/code&gt;, &lt;code&gt;copy()&lt;/code&gt; 메소드를 자동으로 생성해줍니다.&lt;/p&gt;
&lt;pre class=&quot;brush:kt angelscript&quot;&gt;&lt;code&gt;data class Machine(val name: String, val version: Long)

fun main(args: Array&amp;lt;String&amp;gt;) {
    val aMachine = Machine(&quot;A-Machine&quot;, 1)
    println(&quot;machine info ${aMachine}&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Data Class인 Machine를 자바 코드로 변환해보면 아래와 같습니다. (Intellij Decompile 기능 사용)&lt;br /&gt;Lombok의 &lt;code&gt;@Data&lt;/code&gt;, &lt;code&gt;@Getter&lt;/code&gt;, &lt;code&gt;@EqaulsAndHashCode&lt;/code&gt;, &lt;code&gt;@ToString&lt;/code&gt; 을 &lt;code&gt;data&lt;/code&gt; 키워드 멋지게 해주는 것 같습니다.&lt;/p&gt;
&lt;pre class=&quot;brush:java kotlin&quot;&gt;&lt;code&gt;public final class Machine {
   @NotNull
   private final String name;
   private final long version;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final long getVersion() {
      return this.version;
   }

   public Machine(@NotNull String name, long version) {
      Intrinsics.checkNotNullParameter(name, &quot;name&quot;);
      super();
      this.name = name;
      this.version = version;
   }

   @NotNull
   public final String component1() {
      return this.name;
   }

   public final long component2() {
      return this.version;
   }

   @NotNull
   public final Machine copy(@NotNull String name, long version) {
      Intrinsics.checkNotNullParameter(name, &quot;name&quot;);
      return new Machine(name, version);
   }

   // $FF: synthetic method
   public static Machine copy$default(Machine var0, String var1, long var2, int var4, Object var5) {
      if ((var4 &amp;amp; 1) != 0) {
         var1 = var0.name;
      }

      if ((var4 &amp;amp; 2) != 0) {
         var2 = var0.version;
      }

      return var0.copy(var1, var2);
   }

   @NotNull
   public String toString() {
      return &quot;Machine(name=&quot; + this.name + &quot;, version=&quot; + this.version + &quot;)&quot;;
   }

   public int hashCode() {
      String var10000 = this.name;
      return (var10000 != null ? var10000.hashCode() : 0) * 31 + Long.hashCode(this.version);
   }

   public boolean equals(@Nullable Object var1) {
      if (this != var1) {
         if (var1 instanceof Machine) {
            Machine var2 = (Machine)var1;
            if (Intrinsics.areEqual(this.name, var2.name) &amp;amp;&amp;amp; this.version == var2.version) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 자바 코드를 보다보면 &lt;code&gt;component1()&lt;/code&gt;, &lt;code&gt;component2()&lt;/code&gt; 이라는 정의한 적이 없는 메소드가 생성 되어있는데요.&lt;br /&gt;이는 Kotlin의 Destructuring Declaration (구조분해할당)과 관련이 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 말해 아래와 같이 클래스 내부 속성을 여러 변수로 나누어 할당할 수 있는 Kotlin의 기능입니다.&lt;/p&gt;
&lt;pre class=&quot;brush:kotlin&quot;&gt;&lt;code&gt;val machine = Machine(&quot;My Machine&quot;, 1)
val (name, version) = machine
println(&quot;name: $name, version: $version&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 자바로 표현하면 아래와 같이 되겠죠&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Machine machine = new Machine(&quot;My Machine&quot;, 1);
String name = machine.component1();
long version = machine.component2();
System.out.println(String.format(&quot;name: %s, version: %d&quot;, name, version));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇기 때문에 &lt;code&gt;data class&lt;/code&gt;에서는 component1, component2 같은 메소드를 자동으로 생성하는 것입니다.&lt;br /&gt;이는 일반 클래스에서도 가능합니다만 &lt;code&gt;componentN&lt;/code&gt; 메소드들을 직접 구현해주어야 합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약해보자면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kotlin에서는 데이터 보관 목적의 클래스를 정의하기 위한 Data Class를 지원하며 키워드는 &lt;code&gt;data class&lt;/code&gt; 이다.&lt;/li&gt;
&lt;li&gt;Data Class는 &lt;code&gt;toString()&lt;/code&gt;, &lt;code&gt;hashCode()&lt;/code&gt;, &lt;code&gt;equals()&lt;/code&gt;, &lt;code&gt;copy()&lt;/code&gt;를 자동으로 생성해준다.&lt;/li&gt;
&lt;li&gt;구조분해할당을 위한 &lt;code&gt;componentN()&lt;/code&gt; 메서드를 자동으로 생성해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. object 클래스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin에서 &lt;code&gt;class&lt;/code&gt; 대신 &lt;code&gt;object&lt;/code&gt; 키워드를 통해 클래스를 정의할 수 도 있는데요.&lt;br /&gt;이 키워드는 해당 클래스를 싱글톤으로 만들어주는 기능을 합니다.&lt;/p&gt;
&lt;pre class=&quot;brush:kotlin&quot;&gt;&lt;code&gt;object MachineFactory {
    val machines = mutableListOf&amp;lt;Machine&amp;gt;()
    fun createMachine(name: String): Machine {
        val machine = Machine(name)
        machines.add(machine)
        return machine
    }
}

class Machine(val name: String)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;code&gt;object&lt;/code&gt; 키워드만 사용했을뿐 기존 자바에서 싱글톤을 구현하기위해 했던 부수적인 코드들은 하나도 없습니다.&lt;br /&gt;하지만 아래와같이 &lt;code&gt;getInstance()&lt;/code&gt; 메소드는 없지만 싱글톤 클래스로 사용 / 동작하게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;brush:kt reasonml&quot;&gt;&lt;code&gt;fun main(args: Array&amp;lt;String&amp;gt;) {
    val aMachine = MachineFactory.createMachine(&quot;A-Machine&quot;)
    println(MachineFactory.machines.size)
    val bMachine = MachineFactory.createMachine(&quot;B-Machine&quot;)
    println(MachineFactory.machines.size)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 자바코드로 변환해보면 아래와 같이 &lt;code&gt;INSTANCE&lt;/code&gt;라는 변수가 &lt;code&gt;static&lt;/code&gt; 블록에서 한번 초기화 되며 &lt;code&gt;MachineFactory.INSTANCE&lt;/code&gt;와 같이 해당 객체만 참조됨을 알 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;brush:java reasonml&quot;&gt;&lt;code&gt;public final class MachineFactory {
   @NotNull
   private static final List machines;
   @NotNull
   public static final MachineFactory INSTANCE;

   @NotNull
   public final List getMachines() {
      return machines;
   }

   @NotNull
   public final Machine createMachine(@NotNull String name) {
      Intrinsics.checkNotNullParameter(name, &quot;name&quot;);
      Machine machine = new Machine(name);
      machines.add(machine);
      return machine;
   }

   private MachineFactory() {
   }

   static {
      MachineFactory var0 = new MachineFactory();
      INSTANCE = var0;
      boolean var1 = false;
      machines = (List)(new ArrayList());
   }
}

public static final void main(@NotNull String[] args) {
    Intrinsics.checkNotNullParameter(args, &quot;args&quot;);
    Machine aMachine = MachineFactory.INSTANCE.createMachine(&quot;A-Machine&quot;);
    int var2 = MachineFactory.INSTANCE.getMachines().size();
    boolean var3 = false;
    System.out.println(var2);
    Machine bMachine = MachineFactory.INSTANCE.createMachine(&quot;B-Machine&quot;);
    int var6 = MachineFactory.INSTANCE.getMachines().size();
    boolean var4 = false;
    System.out.println(var6);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 Kotlin에서는 &lt;code&gt;static&lt;/code&gt; 키워드가 없는대신 &lt;code&gt;companion object&lt;/code&gt;라는 키워드가 있는데요.&lt;br /&gt;아래와 같이 클래스 내에 &lt;code&gt;companion object&lt;/code&gt; 블록 내에 변수와 함수를 선언하면, 이것들이 static 멤버로 동작하게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;brush:kotlin&quot;&gt;&lt;code&gt;class Config {
    companion object {
        var PREFIX = &quot;config.&quot;
        fun getConfig(name: String): String {
            // do something
        }
    }
}

fun main(args: Array&amp;lt;String&amp;gt;) {
    println(Config.PREFIX)
    val someConfigValue = Config.getConfig(&quot;some-config&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 자바 클래스와 코틀린 클래스의 멤버 가시성 차이 (Visibility)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린과 자바는 멤버 가시성에도 차이가 있는데요.&lt;br /&gt;큰 차이점으로는&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 접근제어자(아무것도 적지 않을경우)는 &lt;code&gt;public&lt;/code&gt; 이다.&lt;/li&gt;
&lt;li&gt;패키지 접근 제어성이 없는 대신 모듈에 대한 접근 제어성이 있다.&lt;br /&gt;프로젝트 하위 패키지 상위 개념으로, 한꺼번에 컴파일되는 코틀린 파일들의 모임이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;public: 모든 곳에서 접근 가능&lt;/li&gt;
&lt;li&gt;protected: 같은 패키지  내이거나 파생된 클래스에서 접근 가능&lt;/li&gt;
&lt;li&gt;default: 같은 패키지 내에서만 접근 가능 (기본)&lt;/li&gt;
&lt;li&gt;private: 같은 클래스 내에서만 접근 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;코틀린
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;public: 모든 곳에서 접근 가능 (기본)&lt;/li&gt;
&lt;li&gt;internal: 같은 모듈 내에서만 접근 가능&lt;/li&gt;
&lt;li&gt;protected: 해당 클래스와 해당 클래스를 상속받은 클래스에서만 접근 가능. 자바와 달리 패키지가 같다고해서 접근할 수 없음&lt;/li&gt;
&lt;li&gt;private: 같은 클래스 안에서만 접근 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@conatuseus/Kotlin-%ED%82%A4%EC%9B%8C%EB%93%9C-%EC%A0%95%EB%A6%AC-open-internal-companion-data-class-%EC%9E%91%EC%84%B1%EC%A4%91&quot;&gt;https://velog.io/@conatuseus/Kotlin-%ED%82%A4%EC%9B%8C%EB%93%9C-%EC%A0%95%EB%A6%AC-open-internal-companion-data-class-%EC%9E%91%EC%84%B1%EC%A4%91&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://codechacha.com/ko/data-classes-in-kotlin/&quot;&gt;https://codechacha.com/ko/data-classes-in-kotlin/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://codechacha.com/ko/kotlin-object-vs-class/&quot;&gt;https://codechacha.com/ko/kotlin-object-vs-class/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Back-End/Kotlin</category>
      <category>Kotlin</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/118</guid>
      <comments>https://do-study.tistory.com/118#entry118comment</comments>
      <pubDate>Sun, 29 Aug 2021 18:16:36 +0900</pubDate>
    </item>
    <item>
      <title>리액티브 프로그래밍 시리즈 3 - 스레드 스케쥴링 (Thread scheduling)</title>
      <link>https://do-study.tistory.com/117</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트 시리즈는 Reactive Programming은 토비의 스프링 저자 이일민님의 리액티브 프로그래밍 유튜브 강좌를 공부하며 정리한 내용입니다.&lt;/p&gt;
&lt;h1&gt;1. 표준 Reactive streams의 문제점&lt;/h1&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Publisher가 Blocking I/O를 사용하거나 데이터를 준비하는데 시간이 오래걸릴 경우 그걸 다 기다려야하기 때문이다.&lt;/li&gt;
&lt;li&gt;반대로 Publisher의 데이터 생성은 굉장히 빠른데, Subscriber의 데이터 처리가 늦을 경우도 마찬가지다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Reactor에서는 &lt;code&gt;Scheduler&lt;/code&gt;를 스레드 개념의 오퍼레이터를 활용해 이부분을 해결한다.&lt;/li&gt;
&lt;li&gt;여기에서는 이 개념을 직접 구현한 코드를 보겠다.&lt;/li&gt;
&lt;li&gt;아래 기본적인 Publisher와 Subscriber가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java&quot;&gt;&lt;code&gt;public class App {

    private static final Logger log = LoggerFactory.getLogger(App.class);

    public static void main(String[] args) {
        Publisher&amp;lt;Integer&amp;gt; pub = (sub) -&amp;gt; {
            sub.onSubscribe(new Subscription() {
                @Override
                public void request(long n) {
                    for (long i = 0; i &amp;lt; n; i++) {
                        Integer data = fetchData(); // 이 작업이 아주 오래걸리는 작업일 경우
                        sub.onNext(data);
                    }
                    sub.onComplete();
                }

                @Override
                public void cancel() {
                }
            });
        };

        pub.subscribe(new Subscriber&amp;lt;&amp;gt;() {
            @Override
            public void onSubscribe(Subscription s) {
                log.debug(&quot;onSubscribe&quot;);
                s.request(Long.MAX_VALUE);
            }

            @Override
            public void onNext(Integer i) {
                log.debug(&quot;onNext: {}&quot;, i);
            }

            @Override
            public void onError(Throwable t) {
                log.debug(&quot;onError: {}&quot;, t);
            }

            @Override
            public void onComplete() {
                log.debug(&quot;onComplete&quot;);
            }
        });

        log.info(&quot;exit&quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 코드를 실행하면 subscribe 후 모든 일이 main 스레드에서 실행되기 때문에 subscribe -&amp;gt; onSubscribe -&amp;gt; onNext* -&amp;gt; onComplete 후 &quot;exit&quot; 로그가 찍힌다.&lt;/li&gt;
&lt;li&gt;Publisher에서 데이터를 가져오는 &lt;code&gt;fetchData()&lt;/code&gt; 메소드가 오래걸리기때문에 이 작업을 전부 다할때까지 기다려야한다.&lt;/li&gt;
&lt;li&gt;보통은 이러한 작업은 백그라운드로 실행되어야 메인 프로그램 실행 흐름에 영향을 미치지 않고 처리할 수 있기 때문에 스레드를 활용한다.&lt;/li&gt;
&lt;li&gt;여기 Publisher에 subscribe를 스레드에서 실행하는 오퍼레이터를 심은 Publisher를 만들어보면 아래와 같이 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java perl&quot;&gt;&lt;code&gt;Publisher&amp;lt;Integer&amp;gt; pub = normalPub();

Publisher&amp;lt;Integer&amp;gt; subOnPub = (sub) -&amp;gt; {
    // 별도 스레드를 생성하여 원래 처리할 publisher에게 subscriber 위임
    ExecutorService es = Executors.newSingleThreadExecutor();
    es.execute(() -&amp;gt; {
        pub.subscribe(sub);
    });
};

subOnPub.subscribe(subscriber());&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이렇게 되면 메인 스레드와 별도의 스레드에서 기능이 실행되며 이후 프로그램 실행에는 영향을 주지 않게된다.&lt;/li&gt;
&lt;li&gt;Reactor에서는 이를 &lt;code&gt;subscribeOn&lt;/code&gt; 메소드로 해결한다.&lt;/li&gt;
&lt;li&gt;반대로 Subscriber에서 데이터를 처리하는게 오래걸리는 경우도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java perl&quot;&gt;&lt;code&gt;Publisher&amp;lt;Integer&amp;gt; pubOnSub = sub -&amp;gt; {
    pub.subscribe(new Subscriber&amp;lt;Integer&amp;gt;() {
        ExecutorService es = Executors.newSingleThreadExecutor();

        @Override
        public void onSubscribe(Subscription s) {
            sub.onSubscribe(s);
        }

        @Override
        public void onNext(Integer integer) {
            es.execute(() -&amp;gt; {
                sub.onNext(integer);
            });
        }

        @Override
        public void onError(Throwable t) {
            es.execute(() -&amp;gt; {
                sub.onError(t);
            });
        }

        @Override
        public void onComplete() {
            es.execute(() -&amp;gt; {
                sub.onComplete();
            });
        }
    });
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 같이 Subscribe를 중개하면서 각 작업을 스레드에서 실행되게 wrapping하면 무거운 작업을 별도 스레드에서 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;Reactor에서는 이를 &lt;code&gt;publishOn&lt;/code&gt;이라는 메소드로&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;2. Reactor publishOn, subscribeOn&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 Reactor는 비동기를 강제하지는 않는다. (별도 스레드로 실행되는게 기본이 아니라는 뜻)&lt;/li&gt;
&lt;li&gt;위에서 얘기한 내용 토대로 실제 Reactor에서 어떠한 식으로 구현했고 사용했는지 보자.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;publishOn&lt;/code&gt; 오퍼레이터는 데이터를 소비(consume)하는게 느릴 경우 사용한다.&lt;/li&gt;
&lt;li&gt;Subscriber쪽 코드가 별도 스레드에서 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java xl&quot;&gt;&lt;code&gt;Flux.range(1, 10)
    .publishOn(Schedulers.newSingle(&quot;pub&quot;))
    .subscribe(data -&amp;gt; log.info(&quot;{}&quot;, data))
;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;subscribeOn&lt;/code&gt;은 데이터 생성 로직이 느릴 경우 사용한다.&lt;/li&gt;
&lt;li&gt;Publisher쪽 코드가 별도 스레드에서 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java xl&quot;&gt;&lt;code&gt;Flux.range(1, 10)
  .subscribeOn(Schedulers.newSingle(&quot;sub&quot;))
  .subscribe(data -&amp;gt; log.info(&quot;{}&quot;, data))
;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;혹은 아래와 같이 동시에 사용해도 문제없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java reasonml&quot;&gt;&lt;code&gt;Flux.range(1, 10)
    .publishOn(Schedulers.newSingle(&quot;pub&quot;))
    .subscribeOn(Schedulers.newSingle(&quot;sub&quot;))
    .subscribe(data -&amp;gt; log.info(&quot;{}&quot;, data))
;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3. 기본적으로 별도 스레드를 사용하는 Operators&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 말했든 Reactor는 일반적으로 비동기를 강제하지 않는다.&lt;/li&gt;
&lt;li&gt;하지만 몇몇 Operators는 기본적으로 비동기로 동작하는 Operator들이 있다.&lt;/li&gt;
&lt;li&gt;아래 예시코드를 보자&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java reasonml&quot;&gt;&lt;code&gt;Flux.intervel(Duration.ofMillis(200))   // 200ms 간격(interval)으로 데이터를 생성
    .take(5)                            // 최대 5개까지만 데이터를 수용함
    .subscribe(e -&amp;gt; log.info(&quot;{}&quot;, e)); // 데이터를 수신하면 로그를 출력함&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 코드를 pure reactive streams 로 구현해보면 아래와 같이 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java perl&quot;&gt;&lt;code&gt;// 일정 주기로 데이터를 발행하는 퍼블리셔
Publisher&amp;lt;Integer&amp;gt; pub = (sub) -&amp;gt; {
    sub.onSubscribe(new Subscription() {
        int value = 0;
        boolean cancelled = false;

        @Override
        public void request(long max) {
            // 아래 Executor는 일정 주기로 동작하는 작업를 구현할 때 용이함
            ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();

            // 초기 딜레이 0, 작업간 딜레이 200ms 간격으로 스케쥴링
            exec.scheduleAtFixedRate(() -&amp;gt; {
                // 작업이 종료되었으면 (cancel이 호출된 경우) 스케쥴링 종료
                if (cancelled) {
                    exec.shutdown();                    
                    return;
                }
                // 데이터 발생
                sub.onNext(value++);
            }, 0, 200, TimeUnit.MILLISECONDS);
        }

        @Override
        public void cancel() {
            this.cancelled = true;
        }
    });
};

// 일정 갯수가 발행되면 발행을 중단시키는 퍼블리셔
// 내부적으로 기존 subscriber를 사용하되 일정 갯수가 되면 cancel을 날리는 Subscriber를 새로 생성
Publiser&amp;lt;Integer&amp;gt; takePub = (sub) -&amp;gt; {
    pub.subscribe(new Subscriber() {
        int count = 0;
        Subscription s;

        @Override
        public void onSubscribe(Subscription s) {
            sub.onSubscribe(s);
            this.s = s;
        }

        @Override
        public void onNext(Integer integer) {            
            // 기존 Subscriber로 처리 위임 후 카운트 증가
            sub.onNext(integer);
            count++;

            // 정해진 횟수를 넘어서면 cancel 호출
            if (count &amp;gt;= 5) {
                s.cancel();
            }
        }

        @Override
        public void onError(Throwable t) {
            sub.onError(t);
        }

        @Override
        public void onComplete() {
            sub.onComplete();
        }
    });
};

Subscriber&amp;lt;Integer&amp;gt; logSub = new Subscriber&amp;lt;&amp;gt;() {
    @Override
    public void onSubscribe(Subscription s) {
        log.info(&quot;onSubscribe&quot;);
        s.request(Long.MAX_VALUE);
    }

    @Override
    public void onNext(Integer integer) {
        log.info(&quot;onNext: {}&quot;, integer);
    }

    @Override
    public void onError(Throwable t) {
        log.info(&quot;onError: {}&quot;, t);
    }

    @Override
    public void onComplete() {
        log.info(&quot;onComplete&quot;);
    }
};

takePub.subscribe(logSub);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Back-End/Reactive Programming</category>
      <category>reactive programming</category>
      <category>Reactive Streams</category>
      <category>Reactor</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/117</guid>
      <comments>https://do-study.tistory.com/117#entry117comment</comments>
      <pubDate>Wed, 25 Aug 2021 00:06:34 +0900</pubDate>
    </item>
    <item>
      <title>리액티브 프로그래밍 시리즈 2 - Operators와 Reactor 맛보기</title>
      <link>https://do-study.tistory.com/116</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트 시리즈는 Reactive Programming은 토비의 스프링 저자 이일민님의 리액티브 프로그래밍 유튜브 강좌를 공부하며 정리한 내용입니다.&lt;/p&gt;
&lt;h1 data-ke-size=&quot;size26&quot;&gt;0. Tips&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stream 인터페이스에 iterate 라는 메소드가 있다.&lt;br /&gt;이 메소드는 어떠한 데이터 스트림을 쉽게 만들어 낼 수 있는 메소드이다.&lt;/p&gt;
&lt;pre class=&quot;brush:java reasonml&quot;&gt;&lt;code&gt;public List&amp;lt;Integer&amp;gt; createSampleIntegerList(int count) {
    return Stream.iterate(1, e -&amp;gt; e + 1).limit(count).collect(Collectors.toList());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 1 ~ 10까지 정수를 담은 리스트를 만든다.&lt;br /&gt;핵심은 &lt;code&gt;iterate(시작값, 값의 변화 함수)&lt;/code&gt; 메소드인데, 위 예시에서 시작값은 1, 변화는 1씩 증가시킨다라는 뜻이다.&lt;br /&gt;그리고 &lt;code&gt;limit(10)&lt;/code&gt;을 통해 10개만이라는 제한을 걸어주었다. (걸지 않으면 무한하게 증가하기 때문에 collect를 할 수 없다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종종 활용할 곳이 있을것 같아 기록해둔다.&lt;/p&gt;
&lt;h1 data-ke-size=&quot;size26&quot;&gt;1. Operators 직접 구현해보기&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reactive Streams의 핵심 개념은 Publisher -&amp;gt; Data -&amp;gt; Subscriber의 흐름으로 데이터가 전달된다는 것이다.&lt;/li&gt;
&lt;li&gt;아래에서 &lt;code&gt;&amp;lt;-&lt;/code&gt; 방향으로의 흐름을 업스트림(Upstream)이라 하고 &lt;code&gt;-&amp;gt;&lt;/code&gt; 방향으로의 흐름을 다운스트림(Downstream)이라 한다.&lt;/li&gt;
&lt;li&gt;그리고 데이터는 업스트림에서 다운스트림 방향으로 (&lt;code&gt;-&amp;gt;&lt;/code&gt;) 흘러간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;Publisher -&amp;gt; Data -&amp;gt; Subscriber
                &amp;lt;- subscribe(Subscriber)
                -&amp;gt; onSubscribe(Subscription)
                -&amp;gt; onNext
                -&amp;gt; onNext
                -&amp;gt; ...
                -&amp;gt; onComplete                  
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reactive Streams에서는 이 과정에서 Publisher -&amp;gt; [Data1] -&amp;gt; Operator -&amp;gt; [Data2] -&amp;gt; Operator2 -&amp;gt; [Data3] -&amp;gt; Subscriber 이런식으로 데이터를 가공하는 Operator를 적용할 수 있다.&lt;/li&gt;
&lt;li&gt;아래와 같은 Publisher와 Subscriber가 있다고 해보자. 1부터 10까지 정수 데이터가 발생하고, Subscriber는 화면에 출력하고 프로그램은 종료된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java&quot;&gt;&lt;code&gt;public class PubSub {

    public static void main(String[] args) {
        Publisher&amp;lt;Integer&amp;gt; pub = iterPub(Stream.iterate(1, e -&amp;gt; e + 1).limit(10).collect(Collectors.toList()));
        pub.subscribe(logSub());
    }

    /**
     * Iterator의 데이터를 발생시키는 Publisher 
     * @param iter 데이터 소스
     * @return Publisher
     */
    private static Publisher&amp;lt;Integer&amp;gt; iterPub(Iterable&amp;lt;Integer&amp;gt; iter) {
        return new Publisher&amp;lt;&amp;gt;() {
            @Override
            public void subscribe(Subscriber&amp;lt;? super Integer&amp;gt; subscriber) {
                subscriber.onSubscribe(new Subscription() {
                    @Override
                    public void request(long n) {
                        // 이 예시 코드에선 Subscriber의 요청갯수인 'n' 인자에 대한 구현은 안되어있다.
                        try {
                            iter.forEach(subscriber::onNext);
                            subscriber.onComplete();
                        } catch (Exception e) {
                            subscriber.onError(e);
                        }
                    }

                    @Override
                    public void cancel() {
                        // Subscriber가 어떠한 경우로든 Publisher에게 데이터를 그만보내라고 요청하는것
                        // 그에 대비해 Publisher는 이 메소드안에 적절한 내용을 구현해두어야함
                        // 일반적으로 flag를 두고 request쪽에서 데이터를 더 보내지 않도록 함
                    }
                });
            }
        };
    }

    /**
     * Publisher에서 수신한 데이터를 출력하는 Subscriber
     * @return Subscriber
     */
    private static Subscriber&amp;lt;Integer&amp;gt; logSub() {
        return new Subscriber&amp;lt;&amp;gt;() {
            @Override
            public void onSubscribe(Subscription subscription) {
                System.out.println(&quot;onSubscribe&quot;);
                subscription.request(Long.MAX_VALUE);
            }

            @Override
            public void onNext(Integer integer) {
                System.out.println(&quot;onNext: &quot; + integer);
            }

            @Override
            public void onError(Throwable throwable) {
                System.out.println(&quot;onError: &quot; + throwable);
            }

            @Override
            public void onComplete() {
                System.out.println(&quot;onComplete&quot;);
            }
        };
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기에서, 데이터를 변환하는 과정을 넣어본다면 아래와 같이 구현할 수 있다.&lt;/li&gt;
&lt;li&gt;마치 기존 Publisher를 한 겹 감싸고, 데이터를 전달하는 부분인 onNext 메소드에 특정 작업을 심은 새로운 Publisher를 생성하는 것이다.&lt;/li&gt;
&lt;li&gt;아래 &lt;code&gt;mapPub&lt;/code&gt; 함수에서 스트림의 흐름을 이해하기 쉽게 방향과 대조해보면
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인자로 전달된 publisher: 업스트림 (현재 스트림보다 상위에 있는 스트림)&lt;/li&gt;
&lt;li&gt;새로 생성되어 return되는 Publisher: 중개 스트림 (현재 스트림)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 안에서 구현하는 subscribe함수에 인자로 전달되는 subscriber: 다운스트림 (현재 스트림보다 하위에 있는 스트림)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;publisher.subscribe 안에 새로 생성하는 Subscriber: 중개 스트림 (현재 스트림)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;업스트림 -&amp;gt; 다운스트림으로의 흐름에 무언가 중개하는 새로운 흐름이 끼어든 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java reasonml&quot;&gt;&lt;code&gt;public class PubSub {

    public static void main(String[] args) {
        Publisher&amp;lt;Integer&amp;gt; pub = iterPub(Stream.iterate(1, e -&amp;gt; e + 1).limit(10).collect(Collectors.toList()));
        Publisher&amp;lt;Integer&amp;gt; mapPub = mapPub(pub, e -&amp;gt; e * 10); // 기존 값에 10을 곱하는 함수 전달
        mapPub.subscribe(logSub());
    }

    private static Publisher&amp;lt;Integer&amp;gt; mapPub(Publisher&amp;lt;Integer&amp;gt; publisher, Function&amp;lt;Integer, Integer&amp;gt; mappingFunc) {
        return new Publisher&amp;lt;Integer&amp;gt;() {
            @Override
            public void subscribe(Subscriber&amp;lt;? super Integer&amp;gt; subscriber) {
                // 인자로 전달된 기존 publisher.subscribe의 인자에 새 Subscriber를 생성하여 넘겨준다.
                // 이 때, 새 Subscriber에 기존 Subscriber의 기능들을 연결시키고, onNext에 데이터를 가공하는 기능만 부가적으로 추가해준다.
                publisher.subscribe(new Subscriber&amp;lt;&amp;gt;() {
                    @Override
                    public void onSubscribe(Subscription subscription) {
                        subscriber.onSubscribe(subscription);
                    }

                    @Override
                    public void onNext(Integer integer) {
                        // 이전 퍼블리셔에서 발생된 데이터에 'mappingFunc' 적용하여 전달
                        subscriber.onNext(mappingFunc.apply(integer));
                    }

                    @Override
                    public void onError(Throwable throwable) {
                        subscriber.onError(throwable);
                    }

                    @Override
                    public void onComplete() {
                        subscriber.onComplete();
                    }
                });
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 기존 스트림(Publisher) 내 모든 값을 합한 값을 발생시키는 Publisher를 구현한다면 아래와 같이 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java&quot;&gt;&lt;code&gt;public class PubSub {

    public static void main(String[] args) {
        Publisher&amp;lt;Integer&amp;gt; pub = iterPub(Stream.iterate(1, e -&amp;gt; e + 1).limit(10).collect(Collectors.toList()));
        Publisher&amp;lt;Integer&amp;gt; sumPub = sumPub(pub);
        sumPub.subscribe(logSub());
    }

    private static Publisher&amp;lt;Integer&amp;gt; sumPub(Publisher&amp;lt;Integer&amp;gt; publisher, Function&amp;lt;Integer, Integer&amp;gt; mappingFunc) {
        return new Publisher&amp;lt;Integer&amp;gt;() {
            @Override
            public void subscribe(Subscriber&amp;lt;? super Integer&amp;gt; subscriber) {
                publisher.subscribe(new Subscriber&amp;lt;&amp;gt;() {
                    // 업스트림에서 전달되는 값을 합해둘 멤버변수 선언
                    private int sum = 0;

                    @Override
                    public void onSubscribe(Subscription subscription) {
                        subscriber.onSubscribe(subscription);
                    }

                    @Override
                    public void onNext(Integer integer) {
                        // 업스트림에서 데이터가 전달되면 바로 전달하지 않고 값을 더해둠
                        this.sum += integer;
                    }

                    @Override
                    public void onError(Throwable throwable) {
                        subscriber.onError(throwable);
                    }

                    @Override
                    public void onComplete() {
                        // 업스트림에서 데이터 전달이 끝나면 더해둔 값을 다운스트림으로 보냄
                        subscriber.onNext(sum);
                        // 그리고 현재 스트림은 끝났음을 통지
                        subscriber.onComplete();
                    }
                });
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 &lt;code&gt;mapPub&lt;/code&gt; 메소드에 제네릭을 적용하면 아래와 같이 작성할 수 있다.&lt;/li&gt;
&lt;li&gt;T타입 데이터 흐름을 R타입 데이터 흐름으로 변경한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java&quot;&gt;&lt;code&gt;private static &amp;lt;T, R&amp;gt; Publisher&amp;lt;R&amp;gt; mapPub(Publisher&amp;lt;T&amp;gt; publisher, Function&amp;lt;T, R&amp;gt; mappingFunc) {
    return new Publisher&amp;lt;R&amp;gt;() {
        @Override
        public void subscribe(Subscriber&amp;lt;? super R&amp;gt; subscriber) {
            publisher.subscribe(new Subscriber&amp;lt;T&amp;gt;() {
                @Override
                public void onSubscribe(Subscription subscription) {
                    subscriber.onSubscribe(subscription);
                }

                @Override
                public void onNext(T integer) {
                    subscriber.onNext(mappingFunc.apply(integer));
                }

                @Override
                public void onError(Throwable throwable) {
                    subscriber.onError(throwable);
                }

                @Override
                public void onComplete() {
                    subscriber.onComplete();
                }
            });
        }
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1 data-ke-size=&quot;size26&quot;&gt;2. Reactor 맛보기&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reactor는 reactive streams 구현체중 하나로 Spring Webflux 엔진으로도 사용된다.&lt;/li&gt;
&lt;li&gt;Reactive Streams 표준을 아주 쉽게 다룰수 있게 해두었다.&lt;/li&gt;
&lt;li&gt;디펜던시
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'io.projectreactor:reactor-core:3.4.9'&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Reactor를 사용한 심플한 예제를 하나 보자&lt;/li&gt;
&lt;li&gt;Flux는 Publisher의 구현체 중 하나이다. (데이터를 생성하는 역할이라는 뜻)&lt;/li&gt;
&lt;li&gt;create 메소드엔 데이터 생성하는 로직이 들어가는데, next, complete 등 마치 Publisher 의 subscribe 메소드를 구현할 때 하는 작업을 편리하게 할 수 있다.&lt;/li&gt;
&lt;li&gt;map 메소드와 같이 생성 -&amp;gt; 구독 사이에 여러 Operators를 체이닝 방식으로 쉽게 걸어줄 수 있다.&lt;/li&gt;
&lt;li&gt;위에 Publisher, Subscriber 직접 생성하고 연결하는 코드들은 모두 내부에 감춰져있고 아래와 같은 간단한 코드로 하나의 흐름을 만들수있다.
&lt;pre class=&quot;brush:java livescript&quot;&gt;&lt;code&gt;Flux.&amp;lt;Integer&amp;gt;create(e -&amp;gt; {
  e.next(1);
  e.next(2);
  e.next(3);
  e.complete();
})
.map(e -&amp;gt; e * 10)
.reduce(0, (a, b) -&amp;gt; a + b)        
.subscribe(s -&amp;gt; System.out.println(s));&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Back-End/Reactive Programming</category>
      <category>reactive programming</category>
      <category>Reactive Streams</category>
      <category>Reactor</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/116</guid>
      <comments>https://do-study.tistory.com/116#entry116comment</comments>
      <pubDate>Sat, 21 Aug 2021 02:21:30 +0900</pubDate>
    </item>
    <item>
      <title>리액티브 프로그래밍 시리즈 1 - Reactive Streams 개요</title>
      <link>https://do-study.tistory.com/115</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트 시리즈는 Reactive Programming은 토비의 스프링 저자 이일민님의 리액티브 프로그래밍 유튜브 강좌를 공부하며 정리한 내용입니다.&lt;/p&gt;
&lt;h1 data-ke-size=&quot;size26&quot;&gt;1. Iterable과 Observable의 차이점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Iterable과 Observable은 정반대의 동작방식으로 같은 목적의 문제를 해결한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.1. Iterable 개념&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 연속적인 데이터 구조를 표현할 때 &lt;code&gt;List&lt;/code&gt;를 주로 사용한다.&lt;br /&gt;그리고 주로 아래와 같이 &lt;code&gt;for-each&lt;/code&gt; 구문을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;brush:java angelscript&quot;&gt;&lt;code&gt;import java.util.List;
import java.util.Arrays;

List&amp;lt;Integer&amp;gt; list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer i : list) {
  System.out.println(i);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 &lt;code&gt;List&lt;/code&gt; 인터페이스가 &lt;code&gt;Iterable&lt;/code&gt; 인터페이스를 상속받고 있기 때문이다.&lt;br /&gt;자바에서 &lt;code&gt;Iterable&lt;/code&gt; 인터페이스를 구현한 타입은 &lt;code&gt;for-each&lt;/code&gt; 루프 사용이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Iterable&lt;/code&gt; 인터페이스는 데이터를 꺼내올 수 있는 &lt;code&gt;iterator()&lt;/code&gt;를 생성하도록 하는 메소드가 있다.&lt;br /&gt;&lt;code&gt;Iterator&lt;/code&gt;는 &lt;code&gt;next()&lt;/code&gt; 메소드를 통해 데이터를 제공하고 &lt;code&gt;hasNext()&lt;/code&gt; 메소드를 통해 데이터가 더 남아있는지 여부를 반환한다.&lt;br /&gt;즉 &lt;code&gt;Iterable&lt;/code&gt; &amp;gt; &lt;code&gt;Iterator&lt;/code&gt; &amp;gt; &lt;code&gt;Iterator.next()&lt;/code&gt; 순으로 데이터를 가져오는 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.2. Observable 개념&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 Java9에서 deprecate 됨&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Observable: 데이터를 만들어내는 Source, Event를 발생하여 &lt;code&gt;Observer&lt;/code&gt;에게 전달, 옵저버는 여려개가 될 수 있음&lt;/li&gt;
&lt;li&gt;Observer: Observable에서 발생하는 이벤트를 수신하여 동작하는 오브젝트&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java&quot;&gt;&lt;code&gt;import java.util.Observable;
import java.util.Observer;

class IntObservable extends Observable implements Runnable {
  @Override
    public void run() {
      for (int i = 0; i &amp;lt; 10; i++) {
        // Observable에 변화가 생겼음을 알리는 메소드
        setChanged();
        // Observer들에게 변화를 알림 (값 포함)
        notifyObservers(i);
      }
    }
}

class Program {
  public static void main(String[] args) {
    Observer observer = new Observer() {
      @Override
      public void update(Observable observable, Object o) {
        // 옵저버의 변화로 이벤트가 발생하면 아래 로직 수행
        System.out.println(o);
      }
    };
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.3. Iterable VS Observable&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 전달하고 전달받는 방식이 정반대이다. (쌍대성, duality)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Iterable은 Pull이다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Iterator.next()를 통해 데이터를 지속적으로 당겨옴 (pull)&lt;/li&gt;
&lt;li&gt;next() 메소드는 리턴값이 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Observable은 Push다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Observable.notifyObservers(Object)를 통해 데이터를 밀어줌 (push)&lt;/li&gt;
&lt;li&gt;notifyObservers 메소드는 리턴값이 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.4. Observable의 문제점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;끝났다 라는 개념이 없다. (Complete)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용할 때 직접 이 개념을 정해놓고 써야했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 data-ke-size=&quot;size26&quot;&gt;2. Reactive Streams의 표준&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.reactive-streams.org/&quot;&gt;https://www.reactive-streams.org/&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정식(?)은 아닌것 같지만 리액티브 프로그래밍의 대략적인 표준을 제시하고 있다.&lt;/li&gt;
&lt;li&gt;Java9 부터 &lt;code&gt;java.util.concurrent.Flow&lt;/code&gt; 에 포함되어 있으며, 하위 버전에서는 디펜던시를 추가하여 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;Processor, Publiser, Subscriber, Subscription 4가지 간단한 &lt;a href=&quot;https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/org/reactivestreams/package-summary.html&quot;&gt;API&lt;/a&gt; 가 있고,&lt;br /&gt;이를 reactive streams에서 제시하는 &lt;a href=&quot;https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.3/README.md#specification&quot;&gt;스펙&lt;/a&gt; 을 준수하여 구현하면 된다.&lt;/li&gt;
&lt;li&gt;이렇게 구현된 리액티브 엔진이 ReactiveX (RxJava), Reactor 등이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.1. Publisher / Subscriber 간단 스펙&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;옵저버 패턴의 Observable 역할&lt;/li&gt;
&lt;li&gt;일련의 시퀀스를 가진 요소를 제공해야한다.&lt;/li&gt;
&lt;li&gt;Publisher.subscribe(Subscriber)를 통해 등록&lt;/li&gt;
&lt;li&gt;아래 프로토콜을 따라야한다.&lt;br /&gt;```&lt;br /&gt;onSubscribe onNext* (onError | onComplete)?&lt;/li&gt;
&lt;li&gt;onSubscribe는 subscribe 될 시 반드시 1번 호출되야함&lt;/li&gt;
&lt;li&gt;onNext 0 ~ 무한대&lt;/li&gt;
&lt;li&gt;onError, onComplete 0~1번 (optional)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;onError 호출 시 onComplete 호출불가&lt;/li&gt;
&lt;li&gt;onComplete 호출 시 onError 호출불가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.2. 간단한 Publisher &amp;amp; Subscriber&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&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;code&gt;Publisher&lt;/code&gt;와 &lt;code&gt;Subscriber&lt;/code&gt; 스펙을 모두 구현한 것은 아니지만 기본적인 동작방식은 표현되어있다.&lt;/li&gt;
&lt;li&gt;옵저버 패턴과 유사하다&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush:java&quot;&gt;&lt;code&gt;import java.util.Arrays;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Publisher;
import java.util.concurrent.Flow.Subscription;

public class IntPublisher implements Publisher&amp;lt;Integer&amp;gt; {

  private Iterator&amp;lt;Integer&amp;gt; datasource;

  public IntPublisher() {
      this.datasource = Arrays.asList(1, 2, 3, 4, 5).iterator();
  }

  @Override
  public void subscribe(Subscriber&amp;lt;? super Integer&amp;gt; subscriber) {
      Subscription subscription = new Subscription() {
          @Override
          public void request(long n) {
              try {
                  while (n-- &amp;lt; 0) {
                    if (datasource.hasNext()) {
                        // 데이터가 있을 경우 onNext 호출
                        subscriber.onNext(datasource.next());
                    } else {
                        // 데이터가 더이상 없을 경우 onComplete 호출
                        subscriber.onComplete();
                        break;
                    }
                }
              } catch (Exception e) {
                  // 예외 발생 시 onError 호출
                  subscriber.onError(e);
              }
          }
      };
      subscriber.onSubscribe(subscription);
  }
  
  @Override
  public void cancel() {
  }
}

public class IntSubscriber implements Subscriber&amp;lt;Integer&amp;gt; {
    private Subscription subscription;
    
    @Override
    public void onSubscribe(Subscription subscription) {
        System.out.println(&quot;onSubscribe&quot;);
    	  this.subscription = subscription;
    	  // subscribe 완료 후 데이터 1개 요청
        subscription.request(1);
    }

    @Override
    public void onNext(Integer integer) {
        // 데이터 1개 받은 후 1개 요청
        System.out.println(&quot;onNext &quot; + integer);
        this.subscription.request(1);
    }

    @Override
    public void onComplete() {
        System.out.println(&quot;onComplete&quot;);
    }

    @Override
    public void onError(Throwable throwable) {
        System.out.println(&quot;onError &quot; + throwable.getMessage());
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Back-End/Reactive Programming</category>
      <category>reactive programming</category>
      <category>Reactive Streams</category>
      <category>비동기</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/115</guid>
      <comments>https://do-study.tistory.com/115#entry115comment</comments>
      <pubDate>Tue, 17 Aug 2021 01:07:13 +0900</pubDate>
    </item>
    <item>
      <title>Spring Cloud Gateway 기본 활용법</title>
      <link>https://do-study.tistory.com/114</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. Spring Cloud Gateway 란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netflix OSS의 API Gateway 컴포넌트인 &lt;code&gt;Zuul&lt;/code&gt;을 Spring 진영에서 직접 만든 API Gateway 입니다.&lt;br /&gt;Zuul은 기본적으로 블록킹 방식으로 동작했었는데요. (Zuul 1.x) 이를 개선하기 위해 Zuul 2.x에서 논블록킹 방식을 도입했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 진영에서는 Zuul의 동기방식이었던 단점을 보완하며 Spring 생태계에 더 적합한 형태의 비동기 API Gateway를 만든것이 바로 &lt;code&gt;Spring Cloud Gateway&lt;/code&gt; 입니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency.&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot 2.4.x 이상부터 Zuul을 사용할 수 없습니다.&lt;/li&gt;
&lt;li&gt;spring-webmvc 위에서 동작하던 Zuul과 달리 Spring Cloud Gateway는 spring-webflux + Reactor 기반으로 동작합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;때문에 spring-webmvc, spring-data, spring-security 등 동기방식 기반 프로젝트들과 함께 실행하면 문제가 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Spring Cloud Gateway 의존성 추가&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;build.gradle&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush: groovy&quot;&gt;&lt;code&gt;plugins {
  id 'org.springframework.boot' version '2.5.2'
  id 'io.spring.dependency-management' version '1.0.11.RELEASE'
  id 'java'
}
group = 'com.sample.project'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
    mavenCentral()
}

dependencyManagement {
    imports {
        mavenBom &quot;org.springframework.cloud:spring-cloud-dependencies:2020.0.3&quot;
    }
}

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 라우팅 설정 추가&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application.yml&lt;/li&gt;
&lt;li&gt;기본적으로 라우팅 정보, 라우팅 대상 지정 (predicates), 라우팅 시 동작 추가 (filters)를 할 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;predicates를 통해 게이트웨이로 들어온 요청 중 어떤 요청들을 해당 라우트에서 처리할 지 조건을 기술합니다.
&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;a href=&quot;https://cloud.spring.io/spring-cloud-gateway/reference/html/#gateway-request-predicates-factories&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://cloud.spring.io/spring-cloud-gateway/reference/html/#gateway-request-predicates-factories&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;filters를 통해 게이트웨이로 들어온 요청이 해당 라우트를 거치며 수행될 추가 동작들을 선언할 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;헤더 추가, 쿼리 스트링 추가, MSA 환경의 경우 써킷 브레이커 추가, Path 재설정 등등 다양합니다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.spring.io/spring-cloud-gateway/reference/html/#gatewayfilter-factories&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://cloud.spring.io/spring-cloud-gateway/reference/html/#gatewayfilter-factories&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;아래 설정은 게이트웨이로 들어오는 요청을 아래와 같이 처리합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`/api/monitoring` 로 시작하는 요청만을 대상으로 하며&lt;/li&gt;
&lt;li&gt;Path를 `/api/monitoring/*` =&amp;gt; `/monitor/*`로 변경하여&lt;/li&gt;
&lt;li&gt;uri에 설정된 서버로 라우팅&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;
spring:
  cloud:
    gateway:
      routes:
        - id: 서비스 아이디
          uri: 라우팅할 서비스 URI
          predicates:
            - Path=/api/monitoring/**
          filters:
            - RewritePath=/api/monitoring, /monitor&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Back-End/Spring framework</category>
      <category>spring-boot</category>
      <category>spring-cloud</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/114</guid>
      <comments>https://do-study.tistory.com/114#entry114comment</comments>
      <pubDate>Mon, 9 Aug 2021 23:44:13 +0900</pubDate>
    </item>
    <item>
      <title>Java에서 파일의 Mime type을 판별하는 방법</title>
      <link>https://do-study.tistory.com/113</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;얼마전, 로컬 파일 시스템에 저장되어 있는 파일을 바이너리 형태로 내려주는 REST API를 작성할 일이 있었습니다.&lt;br /&gt;파일을 내려줄 때 &lt;code&gt;Content-Type&lt;/code&gt; 헤더에 Mime Type을 알맞게 지정해줘야 하는데요. 찾아보니 다음과 같은 방법으로 할 수 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;brush:java reasonml&quot;&gt;&lt;code&gt;Path filePath = Paths.get(&quot;file/save/path&quot;);
String fileContentType = Files.probeContentType(path);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템에 따라 파일 타입을 감지하지 못하는 경우도 있는데요. 제 경우엔 Mac OSX에서 파일 타입을 감지하지 못하는 경우가 있었습니다.&lt;br /&gt;때문에 알아보니 URLConnection을 이용하는 방법도 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;brush:java reasonml&quot;&gt;&lt;code&gt;URLConnection.guessContentTypeFromName(&quot;file/save/path&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 2가지 방법에 기반하여 아래와 같이 유틸 메소드를 작성했습니다.&lt;/p&gt;
&lt;pre class=&quot;brush:java reasonml&quot;&gt;&lt;code&gt;/**
 * 파일 Mime Type 탐지
 * @param filePath 파일 저장 경로
 * @return 파일 Mime Type
 */
public String detectFileMimeType(String filePath) {
  Path path = Paths.get(filePath);
  String detectedMimeType = null;
  try {
    detectedMimeType = Files.probeContentType(path);
  } catch (IOException e) {
    // ignore
  }
  return detectedMimeType != null ? detectedMimeType : URLConnection.guessContentTypeFromName(filePath);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황에 맞춰 사용하면 될 것 같습니다.&lt;/p&gt;</description>
      <category>Back-End/Java</category>
      <category>Content-type</category>
      <category>Java</category>
      <category>Mime-type</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/113</guid>
      <comments>https://do-study.tistory.com/113#entry113comment</comments>
      <pubDate>Wed, 4 Nov 2020 21:36:40 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot Redis Connection Pool + Cluster 설정</title>
      <link>https://do-study.tistory.com/112</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 Redis Cluster에 접속하는 방법을 정리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Boot 설정&lt;/h2&gt;
&lt;pre class=&quot;brush: bash&quot;&gt;&lt;code&gt;spring:
    redis:
        cluster:
            nodes:
                1.1.1.1:1234
                1.1.1.2:1234
                1.1.1.3:1234    &lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;@Configuration
public class RedisConfiguration {

    @Value(&quot;${spring.redis.cluster.nodes}&quot;)
    private String[] clusterNodes;

    /**
     * Jedis Connection Pool
     */
    @Bean
    public JedisPoolConfig jedisPoolConfig() {
        return new JedisPoolConfig();
    }

    /**
     * Redis Connection Factory (커넥션풀링 + 클러스터링)
     */
    @Bean
    public RedisConnectionFactory redisConnectionFactory(JedisPoolConfig jedisPoolConfig) {
        RedisClusterConfiguration redisClusterConfigurations = new RedisClusterConfiguration(clusterNodes);
        return new JedisConnectionFactory(redisClusterConfigurations, jedisPoolConfig);
    }

    /**
     * 어플리케이션에서 사용할 RedisTemplate 설정
     */
    @Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate = new RedisTemplate&amp;lt;&amp;gt;();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 후 사용은 보통 RedisTemplate 혹은 Repository 사용하듯이 사용하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 (Redis 클러스터 모드로 접속)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-c 옵션으로 클러스터 모드로 접속&lt;/p&gt;
&lt;pre class=&quot;brush: bash&quot;&gt;&lt;code&gt;redis-cli -c -h 1.2.3.4 -p 1234&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Back-End</category>
      <category>clustering</category>
      <category>in memory</category>
      <category>NoSQL</category>
      <category>Redis</category>
      <category>redis cluster</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/112</guid>
      <comments>https://do-study.tistory.com/112#entry112comment</comments>
      <pubDate>Tue, 21 Jul 2020 06:52:26 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot에서 Redis 활용</title>
      <link>https://do-study.tistory.com/111</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정&lt;/h2&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;/ul&gt;
&lt;pre class=&quot;brush: xml&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-data-redis&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정 클래스 작성&lt;br /&gt;아래와 같이 설정하면 default 설정에 의해 localhost:6379 로 연결합니다.&lt;br /&gt;변경하려면 설정파일에 &lt;code&gt;spring.redis.host&lt;/code&gt;, &lt;code&gt;spring.redis.port&lt;/code&gt;로 지정할 수 있습니다.&lt;br /&gt;Key / Value Serializer를 설정해주는 이유는 RedisTemplate에서 Spring ~ Redis간 데이터 직, 역직렬화 시 사용하는 방식이 Jdk 직렬화 방식이기 때문입니다. 동작에는 문제가 없지만 redis-cli를 통해 직접 데이터를 보려고 할 때 알아볼수 없는 형태로 출력되기 때문에 Serializer를 변경해준 것입니다. &lt;a href=&quot;https://stackoverflow.com/questions/31608394/get-set-value-from-redis-using-redistemplate&quot;&gt;참고 링크&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한글의 경우 이 설정으로도 깨질수가 있는데, cli 접속 시 &lt;code&gt;redis-cli --raw&lt;/code&gt; 이렇게 옵션을 주고 접속하면 정상적으로 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;@Configuration
public class RedisConfiguration {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        // 사용하는 Redis Client 팩토리 지정
        // Pool 사용 시 이곳에서 설정 가능
        return new JedisConnectionFactory(); 
    }

    @Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate() {
        RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate = new RedisTemplate&amp;lt;&amp;gt;();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        // Hash Operation 사용 시
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());

        // 혹은 아래 설정으로 모든 Key / Value Serialization을 변경할 수 있음
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());

        return redisTemplate;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. RedisTemplate 사용한 방법&lt;/h3&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;@Service
public RedisTestService {

    // 기본 
    @Autowired
    private RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

    /**
     * String Operation의 set 커맨드
     * @param key Redis Key
     * @param value key에 저장될 value
     */
    public void setStringValue(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * String Operation의 get 커맨드
     * @param key Redis Key
     * @param value key에 저장될 value
     */
    public String getStringValue(String key) {
        return (String) redisTemplate.opsForValue().get(key);
    } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisTemplate의 &lt;code&gt;opsFor*&lt;/code&gt; 메소드들은 특정 컬렉션의 커맨드(Operation)을 호출할 수 있는 기능을 모아둔&lt;br /&gt;&lt;code&gt;*Operations&lt;/code&gt; 인터페이스를 반환합니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;center&quot;&gt;메소드명&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;반환 오퍼레이션&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;관련 Redis 자료구조&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;opsForValue()&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;ValueOperations&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;String&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;opsForList()&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;ListOperations&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;List&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;opsForSet()&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;ListOperations&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;List&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;opsForList()&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;SetOperations&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;opsForZSet()&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;ZSetOperations&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;Sorted Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;opsForHash()&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;HashOperations&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;Hash&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 이 &lt;code&gt;*Operations&lt;/code&gt;는 아래와 같은 방식으로도 주입받을 수 있습니다.&lt;br /&gt;이런 경우 프레임워크에서 자동으로 판단하여 주입해준다고 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;For cases where a certain template view is needed, declare the view as a dependency and inject the template: the container will automatically perform the conversion eliminating the opsFor[X] calls:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;@Service
public RedisTestService {

    // 기본 
    @Autowired
    private RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

    // 특정 오퍼레이션 직접 주입
    @Resource(name =&quot;redisTemplate&quot;)
    private ValueOperations&amp;lt;String, Object&amp;gt; valueOps;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, Key Bound Operation을 제공하는데, 이건 특정 Redis Key 한정으로 오퍼레이션을 수행할 수 있는 인터페이스입니다. 즉, &lt;code&gt;redisTemplate.opsForValue().set(&quot;key~~&quot;, &quot;string value&quot;)&lt;/code&gt; 이렇게 할 것을 &lt;code&gt;redisTemplate.boundValueOps(&quot;key~~&quot;).set(&quot;string value&quot;)&lt;/code&gt; 이렇게 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로, &lt;code&gt;StringRedisTemplate&lt;/code&gt; 라는 구현체도 있습니다.&lt;br /&gt;Serializer가 String으로 설정되어있는 &lt;code&gt;RedisTemplate&amp;lt;String, String&amp;gt;&lt;/code&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 여러가지 방식이 있으니 필요에 따라서 사용하면 좋을듯 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Spring Data CrudRepository 를 통한 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 사용해 RDB와 통신할 때 &lt;code&gt;org.springframework.data.repository&lt;/code&gt; 밑에 있는 Repository 인터페이스를 상속받으면 findAll, findById, save 등 자동으로 여러 기능들을 바로 사용할 수 있듯이 Redis 또한 이 방식을 사용할 수 있습니다. (Redis의 Hash 자료구조 한정)&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;엔티티(모델) 클래스 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@RedisHash&lt;/code&gt; 어노테이션의 &lt;b&gt;&quot;testModel&quot;&lt;/b&gt; 을 Redis 키 prefix로 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Id&lt;/code&gt;는 JPA와 동일한 역할을 수행합니다. &lt;b&gt;&quot;testModel:{id}&quot;&lt;/b&gt; 의 id 위치에 자동 generate 값이 들어갑니다.&lt;/li&gt;
&lt;li class=&quot;brush: java&quot;&gt;@RedisHash(&quot;testModels&quot;) public class TestModel { @Id private Long id; private String name; private int age; // getter, setters, ... }&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Repository 작성&lt;/li&gt;
&lt;li class=&quot;brush: java&quot;&gt;public interface TestModelRedisRepository extends CrudRepository&amp;lt;TestModel, Long&amp;gt; { }&lt;/li&gt;
&lt;li&gt;사용&lt;br /&gt;간단합니다. JPA에서 save를 호출하면 insert 문이 호출되듯, hmset, hset 등이 자동으로 호출됩니다.&lt;/li&gt;
&lt;li class=&quot;brush: java&quot;&gt;@Service public class RedisTestService { @Autowired private TestModelRedisRepository testModelRedisRepository; public void addModel(String name, int age) { TestModel testModel = createModel(name, age); testModelRedisRepository.save(testModel); }&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;}&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redis-cli에서 `keys *`로 확인해보면 추가된 것이 확인됩니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;&amp;gt; keys *
$ testModels
$ testModels:1564644222311766174
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`hgetall testModels:1564644222311766174`로 값이 잘 들어갔는지 확인해보면&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;_class
com.testprj.entity.redis.TestModel
id
1564644222311766174
name
이현규
age._class
java.lang.Integer
age
99&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같이 우리가 저장하고자 하는 실제 필드 (id, name, age)와 Java Type (Redis - Spring Boot간 매핑에 사용되는 듯한)이 저장되있음을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에 &lt;code&gt;findOne&lt;/code&gt;, &lt;code&gt;findAll&lt;/code&gt; 또한 모두 잘 동작합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/31608394/get-set-value-from-redis-using-redistemplate&quot;&gt;https://stackoverflow.com/questions/31608394/get-set-value-from-redis-using-redistemplate&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://yookeun.github.io/java/2017/05/21/spring-redis/&quot;&gt;https://yookeun.github.io/java/2017/05/21/spring-redis/&lt;/a&gt;&lt;/p&gt;</description>
      <category>Back-End</category>
      <category>In-Memory</category>
      <category>Redis</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/111</guid>
      <comments>https://do-study.tistory.com/111#entry111comment</comments>
      <pubDate>Mon, 20 Jul 2020 16:19:09 +0900</pubDate>
    </item>
    <item>
      <title>Redis 개념과 설치, 활용방안</title>
      <link>https://do-study.tistory.com/110</link>
      <description>&lt;h1&gt;Redis란?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 REmote Dictionary Server의 약자로 &quot;key-value&quot; 기반 인메모리비 관계형 데이터 베이스다.&lt;br /&gt;모든 데이터를 메모리에 저장하고 조회하기 때문에 빠른 Read, Write 속도를 보장한다.&lt;br /&gt;다양한 value에 다양한 자료구조를 지원해 사용자 애플리케이션 개발 시 활용도가 높다.&lt;/p&gt;
&lt;h1&gt;Redis vs Memcached&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 검색해보면 개념 설명과 함께 Memcached와의 비교글을 많이 볼 수 있다.&lt;br /&gt;Memcached는 메모리 기반이라 처리속도가 빠르고 데이터에 만료 시간을 지정할 수 있고, 저장소 공간이 없으면 LRU 알고리즘에 의해&lt;br /&gt;삭제되는 특징이 있어, 대형 포털에서 Static Page나 검색 결과 등 캐싱 용도로 많이 사용된다.&lt;br /&gt;다만 프로세스가 죽거나 장비가 shutdown되면&lt;br /&gt;저장된 데이터가 사라지기 때문에 중요 데이터를 저장하면 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 메모리 기반 빠른 처리 속도 등의 Memcached와 차이 없는 처리속도를 보장함과 동시에, 데이터를 디스크에도 저장해 영속화가 가능(데이터가 유실될 위험이 없음)하다.&lt;br /&gt;또한 문자열만 저장할 수 있는 Memcached와 달리 List, Set, Sorted Set, Hash 등 여러 자료 구조를 지원한다는 장점이 있다.&lt;/p&gt;
&lt;h1&gt;Mac에 Redis 설치하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Homebrew를 이용해 redis 서버 설치&lt;/p&gt;
&lt;pre class=&quot;brush: bash&quot;&gt;&lt;code&gt;brew install redis&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 커맨드&lt;/p&gt;
&lt;pre class=&quot;brush: bash&quot;&gt;&lt;code&gt;# redis 서버 시작 (기본 설정으로)
redis-server

# redis 서버 시작
redis-server [설정 파일 경로]

# redis 서버 백그라운드로 실행
redis-server --daemonize yes

# redis 서버 실행상태 확인
redis-cli ping # PONG 응답이 오면 정상 실행

# redis 서버 중지
redis-cli shutdown&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 설정파일 내용 확인&lt;/p&gt;
&lt;pre class=&quot;brush: bash&quot;&gt;&lt;code&gt;cat /usr/local/etc/redis.conf&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Redis 자료구조와 커맨드&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. String&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 자료 구조, 쉽게 생각하면 문자열에 다른 문자열을 매핑하는 것.&lt;br /&gt;전체 커맨드: &lt;a href=&quot;https://redis.io/commands#string&quot;&gt;https://redis.io/commands#string&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;brush: bash&quot;&gt;&lt;code&gt;&amp;gt; set hello world
OK

&amp;gt; get hello
&quot;world&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. List&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 연결 리스트의 특성을 갖고 있음.&lt;br /&gt;LPUSH / LPOP / RPUSH / RPOP 등 명령어로 양 끝단 엘리먼트 조작을 빠르게 할 수 있고.&lt;br /&gt;LSET / LINDEX 등 명령어로 특정 인덱스의 엘리먼트에 대한 조작도 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pub-Sub(생산자-소비자)와 잘 어울리는 자료구조. (생산자가 List에 PUSH하면 소비자가 POP)&lt;br /&gt;Blocking 연산도 지원해 소비자가 POP 시도 시 List에 원소가 없으면 들어올 때 까지 대기하게 할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 커맨드: &lt;a href=&quot;https://redis.io/commands#list&quot;&gt;https://redis.io/commands#list&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Hash&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Key하나에 여러 field-value 쌍을 저장할 수 있는 자료구조로 RDB의 테이블과 유사하며 비교해보면 아래와 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RDB&lt;/th&gt;
&lt;th&gt;Redis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PK&lt;/td&gt;
&lt;td&gt;Key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Column&lt;/td&gt;
&lt;td&gt;Hash Field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value&lt;/td&gt;
&lt;td&gt;Hash Value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Redis Key하나 당 RDB Table의 row라고 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 아래와 같은 구조를 같는 User 테이블이 있다고하면&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Age&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Kim&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Lee&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 Redis Hash 구조에서는 아래와 같이 표현된다.&lt;br /&gt;User-1 (HashKey)&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Kim&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Age&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User-2 (HashKey)&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Lee&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Age&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 커맨드: &lt;a href=&quot;https://redis.io/commands#hash&quot;&gt;https://redis.io/commands#hash&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Set&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬되지 않는 문자열의 모음으로, Set인만큼 원소의 중복은 불가하다.&lt;br /&gt;집합연산 (교집합, 합집합, 차집합) 연산을 지원하기 때문에 객체관의 관계를 표현하는데 용이하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 어떤 블로그 포스트에 연결된 태그 ID [1, 2, 3, 4, 5]를 저장해야하는 경우가 있다면&lt;/p&gt;
&lt;pre class=&quot;brush: bash&quot;&gt;&lt;code&gt;sadd post:1234:tags 1 2 3 4 5

smembers post:1234:tags&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 추가할 수 있다. (참고로 Key &quot;post:1234:tags&quot;는 설계하기 나름이지만 여기서는 1234번 포스트의 태그라는 의미로 저렇게 부여함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 집합연산은 SINTER (교집합), SUNION (합집합), SDIFF (차집합) 커맨드로 수행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 커맨드: &lt;a href=&quot;https://redis.io/commands#set&quot;&gt;https://redis.io/commands#set&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Sorted Set&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Set과 마찬가지로 Key하나에 중복되지않는 여러 엘리먼트를 저장할 수 있는 자료구조이다.&lt;br /&gt;단, 원소 추가 시 score를 같이 입력해주고, 내부적으로 이 score를 기준으로 정렬을 유지시키는 특징을 갖고 있다.&lt;br /&gt;ZRANGE (인덱스로 범위 검색), ZRANGEBYSCORE (스코어로 범위검색) 등 연산을 지원한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis 키 설계와 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 키는 문자열이기 때문에 공백문자 (&quot;&quot;)부터 JPEG 파일 등 이진형태로 표현되는 모든 값을 키로 사용할 수 있다. (최대 512MB)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키는 너무 길지 않게 설계하는 것을 권장(조회 성능때문), 너무 길어지면 차라리 Hash를 이용하는 것이 나을수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키는 데이터 스키마를 기준으로 &quot;object-type:id&quot; 설계하는 것을 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들면, &quot;user:1234&quot;라는 키는 User 타입의 ID가 1234인 오브젝트를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 사용하는 부호로는 &lt;code&gt;:&lt;/code&gt;, &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들면 &quot;post:123:comments&quot;라는 키는 ID가 123인 포스트(post)의 댓글목록(comments)를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키 관련 대표적인 명령어&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SORT&lt;/td&gt;
&lt;td&gt;해당 키의 Value를 정렬해서 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXISTS&lt;/td&gt;
&lt;td&gt;해당 키가 존재하는지 여부 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DEL&lt;/td&gt;
&lt;td&gt;해당 키를 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TYPE&lt;/td&gt;
&lt;td&gt;해당 키의 Value가 어떤 자료구조인지 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Redis와 Expire 기능&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://redis.io/commands/expire&quot;&gt;https://redis.io/commands/expire&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 인메모리 DB인 만큼 저장할 수 있는 데이터량이 한정적.&lt;br /&gt;메모리에 더 이상 저장할 수 없는 경우 가장 먼저 들어온 데이터를 삭제하거나 가장 오랫동안 사용하지 않은 데이터를 삭제. 혹은, 꽉 차서 더 이상 입력이 불가능해질수도 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 좋은 방법은 삭제 / 만료를 직접 설정하는 것이다.&lt;br /&gt;키에 timeout을 걸거나, 유닉스 타임스태프를 지정해 해당 시간에 자동으로 삭제되도록 할 수 있다. (DEL 명령어 사용과 같은 효과)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;timeout의 경우 동일한 키의 변경이 있을 경우 갱신된다.&lt;br /&gt;기본적으로 모든 데이터에 Expire 처리를 할 것을 권장하며, 너무 짧으면 레디스에 부하가, 너무 길면 의미가 없어지니 적절한 시간을 찾는것이 중요하다.&lt;/p&gt;
&lt;h1&gt;참고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/garimoo/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%A0%88%EB%94%94%EC%8A%A4-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-01-92aaa24ca8cc&quot;&gt;https://medium.com/garimoo/개발자를-위한-레디스-튜토리얼-01-92aaa24ca8cc&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://redis.io/&quot;&gt;https://redis.io/&lt;/a&gt;&lt;/p&gt;</description>
      <category>Back-End</category>
      <category>In-Memory</category>
      <category>Redis</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/110</guid>
      <comments>https://do-study.tistory.com/110#entry110comment</comments>
      <pubDate>Fri, 17 Jul 2020 15:13:32 +0900</pubDate>
    </item>
    <item>
      <title>[Cloud Series] 클라우드란? - 2</title>
      <link>https://do-study.tistory.com/109</link>
      <description>&lt;h2 id=&quot;클라우드의-정의와-특징&quot;&gt;클라우드의 정의와 특징&lt;/h2&gt;
&lt;p&gt;미국 국립 표준 기술연구소 NIST에서는 클라우드를 아래와 같이 정의하고 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;클라우드 컴퓨팅이란, 공유 구성이 가능한 컴퓨팅 리소스(네트워크, 서버, 스토리지, 애플리케이션 서비스)의 통합을 통해 어디서나 간편하게, 요청에 따라 네트워크를 통해 접근하는 것을 가능하게 하는 모델이다. 이는 최소한의 이용 절차 또는 서비스 공급자의 상호 작용을 통해, 신속히 할당되어 제공된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;또한, 클라우드의 특징으로 아래 5가지를 들었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;주문형 셀프 서비스 사업자와 직접 상호 작용하지 않고, 사용자의 개별 관리화면(콘솔 등)을 통해 서비스를 이용할 수 있다.&lt;/li&gt;
&lt;li&gt;광범위한 네트워크 접속 모바일 기기 등의 다양한 디바이스를 통해 서비스에 접속할 수 있다.&lt;/li&gt;
&lt;li&gt;리소스의 공유 사업자의 컴퓨팅 리소르를 여러 사용자가 공유하는 형태로 이용한다. 사용자마다 리소스가 할당되지만 사용자는 시스템의 어느 부분에 할당, 접속하였는지 알 수도, 알 필요도 없다.&lt;/li&gt;
&lt;li&gt;신속한 확장성 필요에 따라, 필요한 만큼의 스케일링이 가능하다. 스케일링 방법으로는 아래와 같은 종류가 있습니다.&lt;/li&gt;
&lt;li&gt;스케일 업: 노드의 스펙을 상향하여 처리량을 높히는 것&lt;/li&gt;
&lt;li&gt;스케일 다운: 노드의 스펙을 하향하여 처리량을 낮추는 것&lt;/li&gt;
&lt;li&gt;스케일 아웃: 노드의 수를 늘려 처리량을 높히는 것&lt;/li&gt;
&lt;li&gt;스케일 인: 노드의 수를 줄여 처리량을 낮추는 것.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;스케일 업 / 다운 방식은 노드 스펙 상한이 처리량의 한계가 되는 단점이 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;측정 가능한 서비스 이용한 만큼 요금이 부과되는 종량제 서비스&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이외에도 SaaS(Software as a Service), PaaS(Platform as a Service), IaaS(Infrastructure as a Service)와 같은 서비스 모델 프라이빗 클라우드, 퍼블릭 클라우드와 같은 이용 모델로 구성됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;그림으로 배우는 클라우드(하야시 마사유키 저)&amp;rdquo; 책을 읽고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;클라우드의-장점&quot;&gt;클라우드의 장점&lt;/h2&gt;
&lt;p&gt;클라우드는 기존 자체 시스템을 구축하는 &amp;ldquo;온 프레미스&amp;rdquo; 방식과 비교해봤을 때 여러 장점이 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;경제성 온 프레미스 방식에서는 피크 타임을 예상하여 그를 처리할 수 있는 하드웨어와 소프트웨어를 준비해야합니다. 반면 클라우드는 유연한 스케일링이 가능하여 그 때 그 때에 맞는 비용으로 상황에 대응할 수 있습니다. 또한 부서별 / 사업장별 관리하던 소프트웨어와 데이터를 클라우드에서 통합 관리하여 관리 효율을 증대하고 비용을 절감할 수 있습니다.&lt;/li&gt;
&lt;li&gt;유연성 위 &amp;ldquo;경제성&amp;rdquo;이 가능한 이유입니다. 유연한 스케일링을 통해 큰 노력과 비용을 들이지 않기 때문에 굉장히 용이합니다.&lt;/li&gt;
&lt;li&gt;가용성 온 프레미스 방식에서는 서버 장애에 대처하기 위한 이중화, 백업을 직접 조치해야합니다. 반면 클라우드는 재해에 강하게 구축된 데이터 센터에 보관되며 일부 하드웨어에 장애가 있더라도 서비스를 계속해서 사용할 수 있도록 구성되어있기 때문에 온 프레미스 방식보다 낮은 비용으로 가용성이 높은 환경을 구축할 수 있습니다.&lt;/li&gt;
&lt;li&gt;빠른 구축 속도 온 프레미스 방식의 경우 설계 후 하드웨어와 소프트웨어를 조달하여 배치하기까지 시간이 걸립니다. 클라우드는 클라우드가 제공하는 준비된 인프라에서 하드웨어와 소프트웨어를 이용하여 시스템을 신속하게 구축할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;도서 정보:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;http://www.yes24.com/Product/Goods/41171236&quot;&gt;http://www.yes24.com/Product/Goods/41171236&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>클라우드</category>
      <category>cloud</category>
      <category>스케일링</category>
      <category>온프레미스</category>
      <category>클라우드</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/109</guid>
      <comments>https://do-study.tistory.com/109#entry109comment</comments>
      <pubDate>Thu, 16 Jul 2020 23:20:43 +0900</pubDate>
    </item>
    <item>
      <title>[Cloud Series] 클라우드란? - 1</title>
      <link>https://do-study.tistory.com/108</link>
      <description>&lt;p style=&quot;text-align: center;&quot;&gt;금일부터 클라우드 기반 지식 정리를 공부하고 내용을 정리해보고자 합니다.&lt;/p&gt;
&lt;hr style=&quot;box-sizing: content-box; height: 1px; overflow: visible; margin: 0px 0px calc(0.8125rem - 1px); padding: 0px; background: rgba(0, 0, 0, 0.2); border: none; color: rgba(0, 0, 0, 0.8); font-family: 'Noto Sans KR', sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; /&gt;
&lt;h2 id=&quot;1-클라우드-컴퓨팅이란&quot;&gt;1. 클라우드 컴퓨팅이란?&lt;/h2&gt;
&lt;p&gt;&lt;b&gt;클라우드 컴퓨팅&lt;/b&gt;(이하 클라우드)이란 컴퓨터를 사용한 정보의 처리를 자신의 PC가 아닌 인터넷 너머에 존재하는 컴퓨터를 처리하는 서비스를 의미합니다. 아마존의 AWS, MS의 Azure는 이러한 클라우드 서비스를 제공하는 대표적인 클라우드 사업자입니다.&lt;/p&gt;
&lt;p&gt;클라우드는 네트워크나 인터넷에 연결되어있는 여러 장치들이 마치 구름과 같이 표현된다는 것에서 유래되었다고 하는데요. 말 그대로 이 구름과 같이 연결되어 있는 네트워크에서 제공하는 서비스를 통해 개인 혹은 기업이 하고자 하는 일을 처리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;중요한 점은 개인이 소유한 자원이 아닌 인터넷에 연결되어있는 클라우드 환경에서 제공되는 컴퓨팅 파워, 소프트웨어를 이용한다는 부분입니다.&lt;/p&gt;
&lt;p&gt;클라우드는 &amp;ldquo;은행예금&amp;rdquo;과도 비유할 수 있습니다. &amp;ldquo;은행예금&amp;rdquo;은 금융 자산을 직접 보관하는 것이 아닌 금융 기관에 맡기고 필요할 때 ATM이 있는 곳이라면 어디에서든 입금, 출금, 이체 등 금융 서비스를 이용할 있습니다. 클라우드도 이와 비슷합니다. 우리가 보유한&lt;span&gt;&amp;nbsp;&lt;/span&gt;정보가&lt;span&gt;&amp;nbsp;&lt;/span&gt;금융자산이고&lt;span&gt;&amp;nbsp;&lt;/span&gt;클라우드 사업자가&lt;span&gt;&amp;nbsp;&lt;/span&gt;금융 기관이라고 할 수 있습니다. 우리는 정보를 클라우드에 위임하고, 우리가 필요한 각종 서비스를 클라우드를 통해 어디서든 수행할 수 있습니다.&lt;/p&gt;
&lt;p&gt;우리가 금융 기관에 금융 자산을 맡기는 것이 당연하다고 느끼듯, 입증된 클라우드 사업자에게 정보 자산을 맡기고 서비스를 운용하는 것 또한 점차 당연해지고 있고 사례도 늘어가고 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;2-클라우드가-등장한-배경&quot;&gt;2. 클라우드가 등장한 배경&lt;/h2&gt;
&lt;p&gt;클라우드가 등장하기 전 IT 환경은 여러 단계에 걸쳐 변화해왔습니다.&lt;/p&gt;
&lt;p&gt;1980년대에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%A9%94%EC%9D%B8%ED%94%84%EB%A0%88%EC%9E%84&quot;&gt;메인 프레임&lt;/a&gt;이라고 하는 대형 범용 컴퓨터가 주류였습니다. 애플리케이션과 데이터를 모두 하나의 대형 컴퓨터에 집중시키고 단말기에서는 입력 / 출력만을 담당하는 구조였습니다.&lt;/p&gt;
&lt;p&gt;1990년대에는 클라이언트 단말기 성능이 향상되며 여러 클라이언트 측에서도 처리 기능을 수행하도록한 분상형 클라이언트 서버 모델로 변화합니다.&lt;/p&gt;
&lt;p&gt;2000년대에는 네트워크가 빨라지고, 웹 및 브라우저 발달이 고속화되며 다시 처리 기능이 중앙 서버로 모이고, 웹 브라우저를 이용한 입 / 출력 처리를 하는 형태로 발전합니다.&lt;/p&gt;
&lt;p&gt;2010년대에 들어서며 가상화, 분산 처리 기술의 발달로 중앙 집중식이 아닌 서버가 여러 개로 분산 배치되고, 필요한 만큼만 사용이 가능한 클라우드 컴퓨팅 모델로 발전되어 현재까지 끊임없이 발전하고 있습니다.&lt;/p&gt;
&lt;p&gt;이렇게 발전하게 요인으로는&lt;/p&gt;
&lt;p&gt;첫 째는 기술의 발전이 있습니다. CPU 성능 향상, 가상화 및 분산 처리 기술의 발달, 빨라지고 저렴해진 네트워크 비용, 거대한 데이터를 구축해 둘 수 있는 데이터 센터 등이 클라이드 환경이 구축될 수 있는 것을 도왔습니다.&lt;/p&gt;
&lt;p&gt;두 번째는 기업의 니즈입니다. 규모가 커질수록 서버 구축, 운용 비용은 늘어가고 예측하기 어려운 트래픽과 필요 자원에 유연하게 대응해야하는 것이 주요 과제 중 하나가 됩니다. 클라우드 컴퓨팅 환경은 이러한 요구사항을 완벽히 해결해 줄 수 있었으며 이러한 환경을 제공하는 클라우드 사업자 또한 효율성 높은 서비스를 제공하고 안정적인 수입을 얻을 수 있다는 점 또한 요인이라 생각합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;그림으로 배우는 클라우드(하야시 마사유키 저)&amp;rdquo; 책을 읽고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;도서 정보:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;http://www.yes24.com/Product/Goods/41171236&quot;&gt;http://www.yes24.com/Product/Goods/41171236&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>클라우드</category>
      <category>cloud</category>
      <category>클라우드</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/108</guid>
      <comments>https://do-study.tistory.com/108#entry108comment</comments>
      <pubDate>Wed, 15 Jul 2020 18:54:56 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security + JWT를 활용한 토큰 기반 인증 구현 (with Spring Boot)</title>
      <link>https://do-study.tistory.com/106</link>
      <description>&lt;p&gt;&lt;img src=&quot;https://deveely-log.netlify.app/static/0ac59b6cec4e1b742b94742813802955/5d675/spring-security.png&quot; alt=&quot;Image1&quot; width=&quot;80%&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Spring Security는 Spring Framework 기반 웹 애플리케이션의 보안을 담당하는 프레임워크입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security is a framework that provides authentication, authorization, and protection against common attacks. With first class support for both imperative and reactive applications, it is the de-facto standard for securing Spring-based applications.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 보안은 기본적으로 요청하는 사용자를 식별하는 인증(Authenticate)과 인증된 사용자가 보호된 리소스에 접근할 권한이 있는지 확인하는 인가(Authorize)이 기본 바탕입니다.&lt;br /&gt;이 글에서는 여러 인증방식 중 REST 서비스 등에서 주로 이용되는 토큰 기반 인증을 소개하고 구현하는 과정을 보여드리고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(개인적으로 공부하면서 해본 것을 기록한 것이라 구현 과정이나 글 내용에 오류가 있을 수 있습니다. 잘못된 부분이 있다면 댓글로 알려주시면 감사하겠습니다!)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토큰 기반 인증? JWT?&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://deveely-log.netlify.app/static/ed3e354747522c4cecb085cf1e9be299/ae92f/1.png&quot; alt=&quot;Image2&quot; width=&quot;80%&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 JWT는 JSON Web Token의 약자로 Json형태로 표현된 정보를 전달하는 하나의 방식으로, 토큰 자체에 모든 정보(토큰 기본정보, 전달할 정보, 검증됐다는 시그니쳐 정보 등)를 스스로 지니고 있다는 것이 큰 특징입니다. (자가 수용적, Self-Contained)&lt;br /&gt;웹 서버의 경우 헤더나 파라미터를 통해 손쉽게 넘길 수 있어 인증이 필요한 REST 서비스에서 주로 활용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 기반 인증은 주로 클라이언트와 연결고리가 없는 모던 웹 서비스에서 사용하는 인증 방식입니다.&lt;br /&gt;백엔드는 상태가 없는(stateless) REST 서비스로 구성하기 때문에 로그인 정보나 기타 정보를 세션에 저장하지 않습니다.&lt;br /&gt;상태가 없이 서버로 들어오는 클라이언트의 요청으로 판단하기 위해 요청에 포함된 토큰을 해석하고, 권한이 있는 유저인지 판단하여 서버의 리소스에 접근하는 것을 허용합니다.&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;클라이언트가 인증 API를 통해 인증 요청&lt;/li&gt;
&lt;li&gt;서버는 인증 진행 후 유효한 경우 토큰 발급 (JWT)&lt;/li&gt;
&lt;li&gt;다음 요청 시 인증 토큰을 요청에 포함시켜 요청&lt;/li&gt;
&lt;li&gt;서버는 요청에 포함된 인증 토큰을 해석해 권한 검사&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Security 핵심 개념과 토큰 기반 인증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 기반 인증을 Spring Security로 구현하기 위해 핵심 개념을 아래와 같이 간략하게 정리해봤습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;접근 주체 (Principal)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보호된 리소스에 접근하려는 주체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인증 (Authenticate, Authentication)
&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;li&gt;인가 (Authorize)
&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;인증을 통해 확인한 사용자가 보유한 권한(Roles)을 확인해 요청한 서비스(API)를 수행할 권한이 있는지 확인하는 과정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 기반 인증은 &lt;code&gt;인증&lt;/code&gt;을 토큰 기반으로 수행한다는 것입니다.&lt;br /&gt;즉 요청에 포함된 토큰을 해석하고, 해당 사용자가 누구인지 확인하고 인증 객체를 생성해 프레임워크에 넘겨주는 과정을 구현해보도록 하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제 진행&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot 2.3.1&lt;/li&gt;
&lt;li&gt;Lombok&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Spring Security 의존성 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Spring Boot 프로젝트에 security 의존성을 추가해줍니다.&lt;/p&gt;
&lt;pre class=&quot;brush: xml&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-starter-security&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Security 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 REST API 서버로 사용하기 위한 설정만 해둔 상태입니다.&lt;br /&gt;이번에 다루는 주요 주제가 Security 설정이 아니기때문에 간략한 설명만 아래 코드에 주석으로 달아두겠습니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    private static final String[] PUBLIC_URI = {
            &quot;/some-public-apis&quot;
    };

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 개발 편의성을 위해 CSRF 프로텍션을 비활성화
            .csrf()
                .disable()
            // HTTP 기본 인증 비활성화
            .httpBasic()
                .disable()
            // 폼 기반 인증 비활성화  
            .formLogin()
                .disable()
            // stateless한 세션 정책 설정  
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            // 리소스 별 허용 범위 설정  
            .authorizeRequests()      
                .antMatchers(PUBLIC_URI)
                    .permitAll()
                .anyRequest()
                    .authenticated()
                .and()
            // 인증 오류 발생 시 처리를 위한 핸들러 추가  
            .exceptionHandling()
                .authenticationEntryPoint(new RestAuthenticationEntryPoint())
        ;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. JWT 라이브러리 추가 및 토큰 인증 클래스 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 설정은 마치고, 본격적으로 토큰 기반 인증을 구현합니다.&lt;br /&gt;먼저 JWT를 사용하기 위해 의존성을 추가해줍니다.&lt;/p&gt;
&lt;pre class=&quot;brush: xml&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;jjwt&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;0.5.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 인증 토큰을 표현한 인터페이스와 구현 클래스를 간단히 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;public interface AuthenticationToken {
    String getToken();
}

@Builder
@Getter
public class JwtAuthenticationToken implements AuthenticationToken {
    private String token;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 인증 토큰 처리 인터페이스 및 구현 클래스 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증에 사용되는 토큰 유형이 달라지거나, 인증 토큰을 획득하는 방식 등 변경 가능성이 있는 부분이 많다고 생각이 들어서 인터페이스를 먼저 정의했습니다.&lt;br /&gt;각 메소드에 대한 설명은 주석으로 대체하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;public interface AuthenticationTokenProvider {

    /***
     * HTTP 요청에서 토큰 취득
     * @param request HTTP 요청
     * @return 토큰
     */
    String parseTokenString(HttpServletRequest request);

    /***
     * 토큰 발급
     * @param userNo 유저 No
     * @return 토큰
     */
    AuthenticationToken issue(Long userNo);

    /***
     * 토큰에서 userNo 취득
     * @param token 토큰
     * @return userNo
     */
    Long getTokenOwnerNo(String token);

    /***
     * 토큰 유효성 검사
     * @param token 토큰
     * @return 유효여부
     */
    boolean validateToken(String token);

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 구현 클래스입니다. 위에서 정의한 &lt;code&gt;AuthenticationTokenProvider&lt;/code&gt;를 impletements하여 구체적인 동작을 정의해줍니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;@Component
public class JwtAuthenticationTokenProvider implements AuthenticationTokenProvider {
    private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationTokenProvider.class);

    // 보통 설정파일에 관리하고 `@Value` 등으로 주입받아 사용하는 것을 추천
    private static final String SECRET_KEY = &quot;SOME_SECRET_KEY&quot;;
    private static final long EXPIRATION_MS = 1000 * 60 * 60 * 24; 

    @Override
    public String parseTokenString(HttpServletRequest request) {
        String bearerToken = request.getHeader(&quot;Authorization&quot;);
        if (bearerToken != null &amp;amp;&amp;amp; bearerToken.startsWith(&quot;Bearer &quot;)) {
            return bearerToken.substring(7);
        }
        return null;
    }

    @Override
    public AuthenticationToken issue(Long userNo) {
        return AuthenticationToken.builder().token(buildToken(userNo)).build();
    }

    // JWT 토큰 생성
    private String buildToken(Long userNo) {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime expiredAt = now.plus(EXPIRATION_MS, ChronoUnit.MILLIS);
        return Jwts.builder()
                .setSubject(String.valueOf(userNo))
                .setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()))
                .setExpiration(Date.from(expiredAt.atZone(ZoneId.systemDefault()).toInstant()))
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                .compact();
    }

    @Override
    public Long getTokenOwnerNo(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
        return Long.parseLong(claims.getSubject());
    }

    @Override
    public boolean validateToken(String token) {
        if (StringUtils.isNotEmpty(token)) {
            try {
                Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
                return true;
            } catch (SignatureException e) {
                logger.error(&quot;Invalid JWT signature&quot;, e);
            } catch (MalformedJwtException e) {
                logger.error(&quot;Invalid JWT token&quot;, e);
            } catch (ExpiredJwtException e) {
                logger.error(&quot;Expired JWT token&quot;, e);
            } catch (UnsupportedJwtException e) {
                logger.error(&quot;Unsupported JWT token&quot;, e);
            } catch (IllegalArgumentException e) {
                logger.error(&quot;JWT claims string is empty.&quot;, e);
            }
        }
        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Spring Security 인증 절차에 추가하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 ~ 4번 과정을 통해 토큰 기반 인증을 수행하는데 필요한 작업을 진행했습니다.&lt;br /&gt;이제 실제 인증을 수행하는 검사기를 만들고, 인증 절차에 추가해주어야 하는데요.&lt;br /&gt;여기서는 Filter를 통해 검사하고 필터체인에 등록하도록 하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;public class TokenAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private AuthenticationTokenProvider authenticationTokenProvider;

    @Autowired
    private UserService userService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token = authenticationTokenProvider.parseTokenString(request);
        if (authenticationTokenProvider.validateToken(token)) {
            Long userNo = authenticationTokenProvider.getTokenOwnerNo(token);
            try {
                User member = (User) userService.loadUserByUsername(userNo.toString());
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(member,
                        member.getPassword(), member.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } catch (UsernameNotFoundException e) {
                throw new ForbiddenException(&quot;유효하지않은 인증토큰 입니다. 인증토큰 회원 정보 오류&quot;);
            }
        }
        filterChain.doFilter(request, response);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증에 필요한 기능을 제공해주는 &lt;code&gt;AuthenticationTokenProvider&lt;/code&gt;를 통해 인증을 수행하는 로직을 구현합니다.&lt;br /&gt;토큰이 유효한지 확인 후 회원정보를 조회 및 인증 객체를 생성해 프레임워크에 전달하는 간단한 로직입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 이 필터를 아래와 같이 추가해 검사가 수행될 수 있도록 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    private static final String[] PUBLIC_URI = {
            &quot;/some-public-apis&quot;
    };

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 개발 편의성을 위해 CSRF 프로텍션을 비활성화
            .csrf()
                .disable()
            // HTTP 기본 인증 비활성화
            .httpBasic()
                .disable()
            // 폼 기반 인증 비활성화  
            .formLogin()
                .disable()
            // stateless한 세션 정책 설정  
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            // 리소스 별 허용 범위 설정  
            .authorizeRequests()      
                .antMatchers(PUBLIC_URI)
                    .permitAll()
                .anyRequest()
                    .authenticated()
                .and()
            // 인증 오류 발생 시 처리를 위한 핸들러 추가  
            .exceptionHandling()
                .authenticationEntryPoint(new RestAuthenticationEntryPoint())
        ;

    // 토큰 인증 필터 추가
    http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

  @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;마치며&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 인증 검사와 Spring Security를 사용해 구현하는 방법을 아주 간단하게 알아봤습니다.&lt;br /&gt;Spring Security는 설계가 굉장히 탄탄하게 되어있고, 인증 절차 전 영역에 걸쳐 개발자가 원하는 지점을 커스터마이징 하기 쉽게 되어있습니다.&lt;br /&gt;다만 이번 공부를 하면서.. &lt;code&gt;Spring Security를 사용해 구현&lt;/code&gt;한다는게 Filter를 사용한 부분밖에 없고, Security의 핵심개념을 잘 살리지 못한 것 같아 아쉬웠습니다.&lt;br /&gt;이 부분은 좀 더 공부해서, 더 나은 방법으로 구현하는 방식을 포스팅해야겠다는 생각이 듭니다.&lt;/p&gt;</description>
      <category>Back-End/Spring framework</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/106</guid>
      <comments>https://do-study.tistory.com/106#entry106comment</comments>
      <pubDate>Tue, 23 Jun 2020 18:34:09 +0900</pubDate>
    </item>
    <item>
      <title>리눅스 zip, unzip 압축, 압축 풀기</title>
      <link>https://do-study.tistory.com/105</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스 환경에서 zip으로 압축 및 .zip파일을 압축해제하는 &lt;code&gt;zip&lt;/code&gt;, &lt;code&gt;unzip&lt;/code&gt; 명령어에 대해 정리합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. zip&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;zip&lt;/code&gt; 명령어를 통해 특정 파일 혹은 디렉토리를 압축할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;brush: bash&quot;&gt;&lt;code&gt;zip [압축 파일명.zip] [압축 대상] [압축 대상]...&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특정 디렉토리 압축&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-r&lt;/code&gt; 옵션은 대상 디렉토리 하위에 또 다른 폴더가 있을경우 전부 포함시키라는 옵션입니다.&lt;/p&gt;
&lt;pre class=&quot;brush: bash&quot;&gt;&lt;code&gt;zip -r [압축 파일명.zip] [압축 대상 디렉토리]

# test 폴더 전체 압축해 test.zip 생성
zip -r test.zip test/*&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. unzip&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;unzip&lt;/code&gt; 명령어를 통해 zip파일 압축 해제를 할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;현재 폴더에 압축 풀기&lt;/h4&gt;
&lt;pre class=&quot;brush: bash&quot;&gt;&lt;code&gt;unzip [압축 파일명.zip]&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특정 디렉토리에 압축 풀기&lt;/h4&gt;
&lt;pre class=&quot;brush: bash&quot;&gt;&lt;code&gt;unzip [압축 파일명.zip] -d [압축 풀 경로]

# test.zip 파일 압축을 /home/test 위치에 품
unzip test.zip -d /home/test&lt;/code&gt;&lt;/pre&gt;</description>
      <category>DevOps/Unix, Linux</category>
      <category>Linux</category>
      <category>unzip</category>
      <category>zip</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/105</guid>
      <comments>https://do-study.tistory.com/105#entry105comment</comments>
      <pubDate>Tue, 2 Jun 2020 18:27:33 +0900</pubDate>
    </item>
    <item>
      <title>Spring Data JPA Audit기능을 활용해 Entity 생성시간 / 수정시간 자동 매핑하기</title>
      <link>https://do-study.tistory.com/104</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;보통 시스템에 필요한 테이블을 설계하면 이력 관리 등의 목적으로 row 생성시간, 수정시간을 저장할 컬럼 두는 경우가 많습니다.&lt;br /&gt;JPA를 통해 해당 테이블들을 엔티티로 매핑하면 여러 테이블에 걸쳐 중복코드가 발생할 수 있습니다.&lt;br /&gt;첫 째로는 각 엔티티에 생성 / 수정 시간을 매핑할 필드를 선언해줘야하고,&lt;br /&gt;두 번째로는 특정 엔티티가 생성, 수정 될 때 해당 필드의 값에 현재시간 값으로 세팅해주는 코드가 필요하게 되는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Spring Data JPA 환경 제공하는 Audit 기능을 통해 이 컬럼들을 효율적으로 매핑할 수 있는 방법들을 소개합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Data JPA Auditing&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Audit 기능은 엔티티에 발생하는 변경사항을 추적하는 기능을 구현하는데 도움을 줍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Auditing 활성화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@EnableJpaAuditing&lt;/code&gt;으로 Auditing 기능을 활성화합니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;@EnableJpaAuditing
public class Application {
  // ... 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. EntityListener 설정 및 BaseEntity 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;EntityListener&lt;/code&gt;는 Entity가 DB로 load / persist되기 전후에 커스텀 로직을 선언하는 인터페이스입니다.&lt;br /&gt;커스텀 리스너를 구현 후 &lt;code&gt;@EntityListeners&lt;/code&gt;를 통해 해당 엔티티에 등록해줄 수 있는데,&lt;br /&gt;이번 예제에서는 Spring에서 제공하는 구현클래스인 &lt;code&gt;AuditingEntityListener&lt;/code&gt;라는 Auditing기능을 수행하는 리스너를 등록했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AuditingEntityListener&lt;/code&gt;는 해당 엔티티에 선언된 &lt;code&gt;CreatedDate&lt;/code&gt;, &lt;code&gt;LastModifiedDate&lt;/code&gt;, &lt;code&gt;CreatedBy&lt;/code&gt;, &lt;code&gt;LastModifiedBy&lt;/code&gt; 어노테이션을&lt;br /&gt;탐색해 엔티티 변경 시 해당값을 자동으로 업데이트 해줍니다. 이 예제에서는 &lt;code&gt;CreatedDate&lt;/code&gt;, &lt;code&gt;LastModifiedDate&lt;/code&gt;만 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@MappedSuperClass&lt;/code&gt;는 해당 클래스가 상속될 속성을 포함하고 있는 SuperClass 라는걸 알리는 마커 어노테이션입니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;@EntityListeners(value = {AuditingEntityListener.class})
@MappedSupperClass
public abstract class BaseEntity {
    @CreatedDate
    @Column(nullable = false)
    private LocalDateTime createdAt;               

    @LastModifiedDate                                   
    @Column(nullable = false)
    private LocalDateTime updatedAt;

    // getter 등 기타 코드 생량
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 실제 엔티티 구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 2번에서 구현한 BaseEntity를 실제 엔티티에 상속받기만 하면 생성시간, 수정시간 속성을 포함하며 해당 값이 자동으로 업데이트되는 기능까지 포함된 효과를 얻게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;user&quot;)
public class UserEntity extends BaseEntity {
    // 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 간단하게 Auditing 기능을 통해 생성시간 수정시간을 자동으로 설정해봤습니다.&lt;br /&gt;상속을 통해 중복되는 속성을 제거했고, AuditingEntityListener를 통해 지루하고 중복되는 값 설정해주는 로직을 제거할 수 있었습니다.&lt;/p&gt;</description>
      <category>Back-End/JPA</category>
      <category>Auditing</category>
      <category>JPA</category>
      <category>SpringBoot</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/104</guid>
      <comments>https://do-study.tistory.com/104#entry104comment</comments>
      <pubDate>Wed, 27 May 2020 15:34:26 +0900</pubDate>
    </item>
    <item>
      <title>IntelliJ Custom Live Template 사용</title>
      <link>https://do-study.tistory.com/103</link>
      <description>&lt;p&gt;이번 포스트에서는 IntelliJ에서 유용하게 사용할 수 있는 커스텀 Live Template을 추가하는 방법을 소개합니다.&lt;/p&gt;
&lt;h4 id=&quot;live-template-기능-소개&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Live Template 기능 소개&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;Live Template 은 간단한 약어로 예약된 코드를 Generate 해주는 유용한 기능인데요.&lt;/p&gt;
&lt;p&gt;예를들어 코드 편집창에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;main이라 치면&lt;/p&gt;
&lt;pre id=&quot;code_1584674534715&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) { 

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 메인 메소드가 Generate됩니다.&lt;/p&gt;
&lt;p&gt;IntelliJ의 Preferences &amp;gt; Editor &amp;gt; Live Templates 에서 제공되는 Live Template들을 확인할 수 있습니다.&lt;/p&gt;
&lt;h4 id=&quot;커스텀-live-template-추가&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;커스텀 Live Template 추가&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;Preferences &amp;gt; Editor &amp;gt; Live Templates 메뉴 우측에 + 버튼을 클릭하여 라이브 템플릿을 추가합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;1456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WCPky/btqCNGkTRXg/WqFkNCwq74hYFJawrqYETK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WCPky/btqCNGkTRXg/WqFkNCwq74hYFJawrqYETK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WCPky/btqCNGkTRXg/WqFkNCwq74hYFJawrqYETK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWCPky%2FbtqCNGkTRXg%2FWqFkNCwq74hYFJawrqYETK%2Fimg.png&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;1456&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 항목들을 설명드리면&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Abbreviation은 &lt;b&gt;Live Template의 약어&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Description은 해당 &lt;b&gt;Live Template에 대한 설명&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Template text는 &lt;b&gt;약어를 통해 Generate할 실제 코드&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;하단부에&lt;span&gt;&amp;nbsp;&lt;/span&gt;Define&lt;span&gt;&amp;nbsp;&lt;/span&gt;버튼을 클릭하여 Live Template이 활성화될 컨텍스트를 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;자바 파일에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;tc라는 약어를 사용해 하나의 테스트 메소드를 generate하는 추가해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1788&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r0SfY/btqCQpbcIBU/NYrgvftUQaAw5xuKQkoK7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r0SfY/btqCQpbcIBU/NYrgvftUQaAw5xuKQkoK7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r0SfY/btqCQpbcIBU/NYrgvftUQaAw5xuKQkoK7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr0SfY%2FbtqCQpbcIBU%2FNYrgvftUQaAw5xuKQkoK7k%2Fimg.png&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1788&quot; data-origin-height=&quot;508&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Template text에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;$END$는 코드가 generate된 후 커서가 이동될 위치를 지정해주는 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 Live Template을 이용해 빈번하게 반복되는 작업을 줄여 보다 생산성을 높힐수 있는거 같습니다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>IDE</category>
      <category>IntelliJ</category>
      <category>Live Template</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/103</guid>
      <comments>https://do-study.tistory.com/103#entry103comment</comments>
      <pubDate>Sun, 22 Mar 2020 13:26:06 +0900</pubDate>
    </item>
    <item>
      <title>리눅스 CentOS Jenkins 설치하기</title>
      <link>https://do-study.tistory.com/102</link>
      <description>&lt;p&gt;Jenkins는 가장 널리 사용되는 CI(Continous Integration)툴 중 하나입니다.&lt;/p&gt;
&lt;p&gt;현 시점에서 많이 사용되는 CI툴로 Travis CI, Jenkins, TeamCity 등이 있습니다.&lt;br&gt;이 중에서도 Jenkins는 오랜 기간 널리 사용되어 오면서 수많은 사용자와 플러그인, 레퍼런스 등이 갖춰져있습니다.&lt;/p&gt;
&lt;p&gt;개인 프로젝트에 적용할 CI툴을 고민해봤는데, Travis CI는 무료정책도 있지만 현재 제 상황에서는 무료정책을 이용할 수 없는 상황이었고, TeamCity도 일정 기준 이상이면 무료로는 사용이 불가하다고하여 우선 무료이면서 가장 보편적인 Jenkins를 구축해보기로 했습니다.&lt;/p&gt;
&lt;h2&gt;설치&lt;/h2&gt;
&lt;p&gt;먼저 아래 명령어로 Jenkins를 설치합니다. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat-stable/jenkins.repo
rpm --import https://jenkins-ci.org/redhat/jenkins-ci.org.key
yum install jenkins&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;설정&lt;/h2&gt;
&lt;h4&gt;기본 설정&lt;/h4&gt;
&lt;p&gt;CentOS의 경우 Jenkins Home 디렉토리나 포트 등의 설정은 &lt;code&gt;/etc/sysconfig/jenkins&lt;/code&gt;에서 진행할 수 있습니다.&lt;br&gt;저의 경우 머신 하나만 가지고 진행하고 있기 때문에 애플리케이션이 사용하는 포트와 겹치지 않게 하기위해 포트를 변경하였습니다. (9000대 포트를 사용한다고 합니다)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;...

## Type:        integer(0:65535)
## Default:     8080
## ServiceRestart: jenkins
#
# Port Jenkins is listening on.
# Set to -1 to disable
#
JENKINS_PORT=&amp;quot;9001&amp;quot;

...&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;JRE 설정&lt;/h4&gt;
&lt;p&gt;Jenkins는 Java 기반으로 제작된 도구이기 때문에 JRE를 필요로 하는데요.&lt;/p&gt;
&lt;p&gt;Jenkins는 기본적으로 아래 경로들을 대상으로 JRE 자동으로 탐색합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;/etc/alternatives/java
/usr/lib/jvm/java-1.8.0/bin/java
/usr/lib/jvm/jre-1.8.0/bin/java
/usr/lib/jvm/java-1.7.0/bin/java
/usr/lib/jvm/jre-1.7.0/bin/java
/usr/lib/jvm/java-11.0/bin/java
/usr/lib/jvm/jre-11.0/bin/java
/usr/lib/jvm/java-11-openjdk-amd64
/usr/bin/java&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만약 수정을 원하면 &lt;code&gt;/etc/init.d/jenkins&lt;/code&gt; 파일에 &lt;code&gt;candidate&lt;/code&gt;목록에 본인 환경에 맞는 경로를 추가해주면 됩니다.&lt;/p&gt;
&lt;p&gt;위 경로에 Java가 설치되어있지 않은 경우 &lt;code&gt;No Such File...&lt;/code&gt; 오류가 발생합니다.  &lt;/p&gt;
&lt;p&gt;경로가 제대로 잡혔더라도 권한 문제로 &lt;code&gt;Permission Denied&lt;/code&gt; 오류가 발생할 수도 있는데요.&lt;br&gt;이경우엔 &lt;code&gt;/etc/sysconfig/jenkins&lt;/code&gt; 에 &lt;code&gt;JENKINS_USER&lt;/code&gt;값을 root로 변경해주면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;...

## Type:        string
## Default:     &amp;quot;jenkins&amp;quot;
## ServiceRestart: jenkins
#
# Unix user account that runs the Jenkins daemon
# Be careful when you change this, as you need to update
# permissions of $JENKINS_HOME and /var/log/jenkins.
#
JENKINS_USER=&amp;quot;root&amp;quot;

...&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;JRE 관련 추가 설정&lt;/h4&gt;
&lt;p&gt;이 설정은 OpenJDK를 사용하는 경우에만 해줘야하는 것으로 예상되는데요.&lt;/p&gt;
&lt;p&gt;저의 경우 Jenkins 실행은 정상적으로 되지만 페이지 접속 시 AWT 관련 오류가 발생했습니다. 정확한 원인은 파악하지 못했지만 예상되는 원인으로는 사용하는 OpenJDK에 몇몇 라이브러리가 누락되어 발생한 것으로 예상했는데요.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;after installing Jenkins for integration with FishEye always get error message below trying to access Jenkins site:
AWT is not properly configured on this server. Perhaps you need to run your container with &amp;quot;-Djava.awt.headless=true&amp;quot;?&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;아래 링크에서 OS별 설치방법 확인해서 필요한 라이브러리 설치하여 해결할 수 있었습니다.&lt;br&gt;&lt;a href=&quot;https://wiki.jenkins.io/display/JENKINS/Jenkins+got+java.awt.headless+problem&quot;&gt;https://wiki.jenkins.io/display/JENKINS/Jenkins+got+java.awt.headless+problem&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Centos 의 경우&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;yum install dejavu-sans-fonts
yum install fontconfig&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설정을 마친 후 젠킨스 서비스를 실행합니다.&lt;/p&gt;
&lt;h2&gt;실행&lt;/h2&gt;
&lt;p&gt;아래 명령어를 통해 젠킨스를 실행한 후 &lt;code&gt;머신주소:설정한포트&lt;/code&gt;로 접속하면 드디어 젠킨스 로그인 페이지를 볼 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;sudo service jenkins start&lt;/code&gt;&lt;/pre&gt;</description>
      <category>DevOps/Unix, Linux</category>
      <category>CentOS</category>
      <category>install</category>
      <category>Jenkins</category>
      <category>Linux</category>
      <category>젠킨스</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/102</guid>
      <comments>https://do-study.tistory.com/102#entry102comment</comments>
      <pubDate>Sat, 21 Mar 2020 13:15:22 +0900</pubDate>
    </item>
    <item>
      <title>리눅스 CentOS Git 설치하기</title>
      <link>https://do-study.tistory.com/101</link>
      <description>&lt;p&gt;CentOS에서 git을 설치해 사용하는 방법을 간략하게 정리합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 설치확인&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;설치 전에 먼저 이미 설치되어있는지 확인합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1584673858775&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git --version&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;command not found 오류가 발생하거나, git이 설치되어있지 않다는 오류 문구가 나면 설치되지 않은 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 설치&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;아래 명령어를 통해 git을 설치할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1584673885113&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yum install git&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위와 같이하면 yum에서 제공되는 git 버전이 자동으로 설치되는데, 만약 원하는 버전이 있다면 아래와 같이 버전을 명시하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1584673893707&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yum install git-&amp;lt;version&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 사용자 설정&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1584673913287&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git config --global user.name &quot;userName&quot; git config --global user.email &quot;email&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 정상 설치 확인&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1584673942861&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git --version&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps/Unix, Linux</category>
      <category>CentOS</category>
      <category>GIT</category>
      <category>install</category>
      <category>Linux</category>
      <category>리눅스</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/101</guid>
      <comments>https://do-study.tistory.com/101#entry101comment</comments>
      <pubDate>Fri, 20 Mar 2020 12:12:44 +0900</pubDate>
    </item>
    <item>
      <title>스프링 부트 (Spring Boot) 와 의존성 관리</title>
      <link>https://do-study.tistory.com/100</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트의 강력한 장점 중 하나는 스프링, 써드파티 라이브러리 의존성을 관리해주는 부분이라고 생각합니다.&lt;br /&gt;의존성 관련 내용에 앞서 스프링 부트가 무엇인지 간략하게 소개하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 부트 (Spring Boot)란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트 공식 홈페이지에서는 다음과 같이 소개하고 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can &quot;just run&quot;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상용 수준의 독립실행형 스프링 애플리케이션을 쉽게 만들 수 있도록 해주는 기술로 대표적으로 다음과 같은 기능을 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;독립 실행형 애플리케이션 제작 Create stand-alone Spring applications&lt;/li&gt;
&lt;li&gt;내장 WAS 지원(Tomcat, Jetty, Undertow)&lt;/li&gt;
&lt;li&gt;빌드 구성을 단순화해주는 &lt;code&gt;starter&lt;/code&gt; 의존성 제공&lt;/li&gt;
&lt;li&gt;스프링 및 써드파티 라이브러리 자동 구성 (Auto Configuration)&lt;/li&gt;
&lt;li&gt;메트릭, 헬스체크, 설정 외부화 등 상용 기능 제공&lt;/li&gt;
&lt;li&gt;XML 설정 등 필요 X&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Boot 의 의존성 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급한대로 스프링 부트는 &lt;code&gt;starter&lt;/code&gt;라 불리는 의존성을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 &lt;code&gt;starter&lt;/code&gt;는 관련있는 의존성들의 묶음으로 하나의 &lt;code&gt;starter&lt;/code&gt; 의존성으로 해당 의존성과 관련된 스프링 &amp;amp; 써드파티 라이브러리 또는 또다른 &lt;code&gt;starter&lt;/code&gt; 의존성까지도 의존성 간 버전, 호환성 걱정 없이 한번에 애플리케이션에 포함시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 링크에서 Spring Boot에서 제공하는 모든 &lt;code&gt;starter&lt;/code&gt; 의존성을 확인할 수 있습니다.&lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-starter&quot;&gt;https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-starter&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 &lt;code&gt;spring-boot-starter-web&lt;/code&gt;은 웹 관련 기능의 집합으로 기존에 &lt;code&gt;sprinb-web&lt;/code&gt;, &lt;code&gt;spring-webmvc&lt;/code&gt;을 포함한 여러 라이브러리를 포함시켜야했던 때와 달리 위 의존성 하나로 웹 관련 의존성을 버전 등이 최적화된 상태로 포함시킬 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;spring-boot-starter-web&lt;/code&gt;의 pom.xml
&lt;pre class=&quot;brush:xml; dust&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
  xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&amp;gt;
  &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
  &amp;lt;parent&amp;gt;
      &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-boot-starters&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;${revision}&amp;lt;/version&amp;gt;
  &amp;lt;/parent&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
  &amp;lt;name&amp;gt;Spring Boot Web Starter&amp;lt;/name&amp;gt;
  &amp;lt;description&amp;gt;Starter for building web, including RESTful, applications using Spring
      MVC. Uses Tomcat as the default embedded container&amp;lt;/description&amp;gt;
  &amp;lt;properties&amp;gt;
      &amp;lt;main.basedir&amp;gt;${basedir}/../../..&amp;lt;/main.basedir&amp;gt;
  &amp;lt;/properties&amp;gt;
  &amp;lt;scm&amp;gt;
      &amp;lt;url&amp;gt;${git.url}&amp;lt;/url&amp;gt;
      &amp;lt;connection&amp;gt;${git.connection}&amp;lt;/connection&amp;gt;
      &amp;lt;developerConnection&amp;gt;${git.developerConnection}&amp;lt;/developerConnection&amp;gt;
  &amp;lt;/scm&amp;gt;
  &amp;lt;dependencies&amp;gt;
      &amp;lt;dependency&amp;gt;
          &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
          &amp;lt;artifactId&amp;gt;spring-boot-starter&amp;lt;/artifactId&amp;gt;
      &amp;lt;/dependency&amp;gt;
      &amp;lt;dependency&amp;gt;
          &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
          &amp;lt;artifactId&amp;gt;spring-boot-starter-json&amp;lt;/artifactId&amp;gt;
      &amp;lt;/dependency&amp;gt;
      &amp;lt;dependency&amp;gt;
          &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
          &amp;lt;artifactId&amp;gt;spring-boot-starter-tomcat&amp;lt;/artifactId&amp;gt;
      &amp;lt;/dependency&amp;gt;
      &amp;lt;dependency&amp;gt;
          &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
          &amp;lt;artifactId&amp;gt;spring-boot-starter-validation&amp;lt;/artifactId&amp;gt;
          &amp;lt;exclusions&amp;gt;
              &amp;lt;exclusion&amp;gt;
                  &amp;lt;groupId&amp;gt;org.apache.tomcat.embed&amp;lt;/groupId&amp;gt;
                  &amp;lt;artifactId&amp;gt;tomcat-embed-el&amp;lt;/artifactId&amp;gt;
              &amp;lt;/exclusion&amp;gt;
          &amp;lt;/exclusions&amp;gt;
      &amp;lt;/dependency&amp;gt;
      &amp;lt;dependency&amp;gt;
          &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
          &amp;lt;artifactId&amp;gt;spring-web&amp;lt;/artifactId&amp;gt;
      &amp;lt;/dependency&amp;gt;
      &amp;lt;dependency&amp;gt;
          &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
          &amp;lt;artifactId&amp;gt;spring-webmvc&amp;lt;/artifactId&amp;gt;
      &amp;lt;/dependency&amp;gt;
  &amp;lt;/dependencies&amp;gt;
&amp;lt;/project&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Boot 의존성의 버전 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 &lt;code&gt;spring-boot-starter-web&lt;/code&gt;의 pom.xml을 보면 각 라이브러리의 버전정보가 적혀있지 않은데요. 그 이유는 버전정보의 경우 별도의 파일에서 관리하고 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 스프링 부트를 사용해 개발하면 Maven이던 Gradle이던 &lt;code&gt;spring-boot-starter-parent&lt;/code&gt;라는 의존성을 상속하게 되는데요. 이 &lt;code&gt;starter&lt;/code&gt; 의존성이 상속하고 있는 &lt;code&gt;spring-boot-dependencies&lt;/code&gt; 라는 의존성에 모든 의존성의 버전정보가 들어있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;spring-boot-dependencies&lt;/code&gt;의 pom.xml
&lt;pre class=&quot;brush:xml; dust&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;
  xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
  xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&amp;gt;
  ....
  &amp;lt;properties&amp;gt;
      &amp;lt;main.basedir&amp;gt;${basedir}/../..&amp;lt;/main.basedir&amp;gt;
      &amp;lt;!-- Dependency versions --&amp;gt;
      &amp;lt;activemq.version&amp;gt;5.15.11&amp;lt;/activemq.version&amp;gt;
      &amp;lt;antlr2.version&amp;gt;2.7.7&amp;lt;/antlr2.version&amp;gt;
      &amp;lt;appengine-sdk.version&amp;gt;1.9.77&amp;lt;/appengine-sdk.version&amp;gt;
      &amp;lt;artemis.version&amp;gt;2.10.1&amp;lt;/artemis.version&amp;gt;
      &amp;lt;aspectj.version&amp;gt;1.9.5&amp;lt;/aspectj.version&amp;gt;
      &amp;lt;assertj.version&amp;gt;3.13.2&amp;lt;/assertj.version&amp;gt;
      &amp;lt;atomikos.version&amp;gt;4.0.6&amp;lt;/atomikos.version&amp;gt;
      &amp;lt;awaitility.version&amp;gt;4.0.2&amp;lt;/awaitility.version&amp;gt;
      &amp;lt;bitronix.version&amp;gt;2.1.4&amp;lt;/bitronix.version&amp;gt;
      &amp;lt;byte-buddy.version&amp;gt;1.10.6&amp;lt;/byte-buddy.version&amp;gt;
      ...
      &amp;lt;/properties&amp;gt;
      ...
&amp;lt;/project&amp;gt;        &lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;라이브러리 버전 Override&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 처럼 &lt;code&gt;properties&lt;/code&gt;에 각 라이브러리 별 버전 프로퍼티가 선언되어있고, 사용측에서 이 프로퍼티를 참조하기 때문에, 버전을 변경할 일 이 있으면 위 프로퍼티명을 참고하여 우리 프로젝트의 pom.xml에서 버전정보를 Override 할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Project의 pom.xml
&lt;pre class=&quot;brush:xml;&quot;&gt;&lt;code&gt;...
&amp;lt;parent&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;사용할 Spring Boot 버전&amp;lt;/version&amp;gt;
&amp;lt;/parent&amp;gt;
&amp;lt;properties&amp;gt;
  여기에서 프로퍼티 정보를 통해 기본 의존성의 버전을 변경할 수 있습니다.
&amp;lt;/properties&amp;gt;
&amp;lt;dependencies&amp;gt;
  사용할 spring-boot-starter 모듈들 선언....
&amp;lt;/dependencies&amp;gt;
...&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/projects/spring-boot&quot;&gt;Spring Boot 공식 홈페이지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/&quot;&gt;Spring Boot Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Back-End/Spring framework</category>
      <category>spring boot</category>
      <category>SpringFramework</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/100</guid>
      <comments>https://do-study.tistory.com/100#entry100comment</comments>
      <pubDate>Tue, 18 Feb 2020 16:59:27 +0900</pubDate>
    </item>
    <item>
      <title>Java Bean Validation 사용하기</title>
      <link>https://do-study.tistory.com/99</link>
      <description>&lt;p&gt;Java Bean Validation는 공식 사이트에서는 오브젝트 레벨의 제약 선언 및 유효성 검사 기술을 제공하는 것을 목표로 하고 있는 기술이라고 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The technical objective of this work is to provide an object level constraint declaration and validation facility for the Java application developer, as well as a constraint metadata repository and query API.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;실제로 프로그래밍을 하다보면 어떤 객체의 값이 비었는지, 공백인지, 날짜가 이전인지 등의 유효성 검사를 빈번하게 수행하게 되고 이로 인해 유효성 검사 로직들이 여러 곳에 흩어지게 되는 경우도 있는데요. Java Bean Validation을 이용하면 검사 대상 클래스에 어노테이션 기반 제약조건을 선언하여 간결하게 유효성 검사를 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Bean Validation은 자바에서 유효성 검사를 하는 방법에 대한 명세입니다. 즉, 실제로 동작하는 코드가 아니라는 것입니다.&lt;/p&gt;
&lt;p&gt;때문에 실제로 동작하려면 이 명세를 구현한 구현체가 있어야하는데 대표적인게 Hibernate Validator입니다. 그리고 Spring Boot 2.0은 Bean Validation 2.0을 사용하고 있습니다.&lt;/p&gt;
&lt;h4&gt;# Bean Validation 버전별 Hibernate Validator 호환성&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;JSR&lt;/th&gt;
&lt;th&gt;Release&lt;/th&gt;
&lt;th&gt;Compatible Implementation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Bean Validation 1.0&lt;/td&gt;
&lt;td&gt;JSR 303&lt;/td&gt;
&lt;td&gt;Java EE6, 2009&lt;/td&gt;
&lt;td&gt;Hibernate Validator 4.3.1.Final&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bean Validation 1.1&lt;/td&gt;
&lt;td&gt;JSR 349&lt;/td&gt;
&lt;td&gt;Java EE7, 2013&lt;/td&gt;
&lt;td&gt;Hibernate Validator 5.1.1.Final&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bean Validation 2.0&lt;/td&gt;
&lt;td&gt;JSR 380&lt;/td&gt;
&lt;td&gt;Java EE8, 2017&lt;/td&gt;
&lt;td&gt;Hibernate Validator 6.0.1.Final&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;그럼 Bean Validation을 실제로 사용하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;h2&gt;기본적인 사용 방법&lt;/h2&gt;
&lt;h4&gt;# 의존성 추가&lt;/h4&gt;
&lt;p&gt;Bean Validation은 &lt;code&gt;javax.validation.validation-api&lt;/code&gt;로 별도 모듈이 있지만, Hibernate Validator가 해당 모듈을 포함하고 있기 때문에 pom.xml에 아래와 같이 의존성을 추가해줍니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.hibernate&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;hibernate-validator&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;6.0.7.Final&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만약 실행환경이 SE라면 아래 의존성을 함께 추가해줘야함니다. (Tomcat 등의 EE 환경에서는 필요 없음)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;org.glassfish.javax.el&lt;/code&gt;의 경우 JSR 341에 명시된 &lt;code&gt;Unified Expression Language&lt;/code&gt;에 대한 구현체로 Hibernate Validator가 제약 조건 위반 메시지 표현 등을 처리하기 위해 필요하다고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;org.hibernate.hibernate-validator-cdi&lt;/code&gt;는 JSR 346에 명시된 &lt;code&gt;CDI (Contexts and Dependency Injection)&lt;/code&gt;의 구현체로 자세히는 모르겠지만 @Inject 등으로 Validator를 주입받고, @Valid 등으로 유효성 검사를 자동으로 수행하는 등에 사용하기 위해 필요한 의존성으로 생각됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.glassfish&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;javax.el&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;3.0.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.hibernate&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;hibernate-validator-cdi&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;6.0.7.Final&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;# 유효성 검증할 객체 생성&lt;/h4&gt;
&lt;p&gt;예시로, 아래와 같이 모델 객체를 만들어 보겠습니다.&lt;/p&gt;
&lt;p&gt;필드 위에 선언한 &lt;code&gt;@NotBlank&lt;/code&gt;, &lt;code&gt;@Positive&lt;/code&gt; 어노테이션들이 바로 제약조건들입니다.&lt;/p&gt;
&lt;p&gt;아래 예시에서는 name은 공백이지 않아야하며 age는 0보다 큰 숫자여야한다는 제약조건을 걸었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Member {
    @NotBlank
    private String name;

    @Positive
    private int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;# 유효성 검증 수행&lt;/h4&gt;
&lt;p&gt;먼저 유효성 검사를 수행하는 &lt;code&gt;Validator&lt;/code&gt;를 생성합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다음으로 Validator를 통해 Member 객체의 유효성 검사를 수행합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Validator&lt;/code&gt;의 &lt;code&gt;validate&lt;/code&gt; 메소드에 검사할 객체를 전달하고, 그 결과를 &lt;code&gt;Set&amp;lt;ConstraintViolation&amp;lt;?&amp;gt;&amp;gt;&lt;/code&gt; (제약조건 위반 Set)으로 받습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Member member = new Member(&amp;quot;&amp;quot;, -1);

Set&amp;lt;ConstraintViolation&amp;lt;Member&amp;gt;&amp;gt; violations = validator.validate(member);
violations.forEach(e -&amp;gt; {
  System.out.println(String.format(&amp;quot;검사 필드: %s, 유효하지 않은 값: [%s] 메시지: %s&amp;quot;, e.getPropertyPath(), e.getInvalidValue(), e.getMessage()));
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드를 실행하면 아래와 같은 결과를 확인할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;검사 필드: age, 유효하지 않은 값: [-1] 메시지: must be greater than 0
검사 필드: name, 유효하지 않은 값: [] 메시지: 반드시 값이 존재하고 공백 문자를 제외한 길이가 0보다 커야 합니다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;만약 메시지를 커스텀 메시지를 출력하고 싶다면 각 제약조건 어노테이션의 &lt;code&gt;message&lt;/code&gt; 속성에 설정하면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Member {
    @NotBlank(message = &amp;quot;이름이 비어있습니다.&amp;quot;)
    private String name;

    @Positive(message = &amp;quot;나이는 0보다 큰 값이어야합니다.&amp;quot;)
    private int age;

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;검사 필드: name, 유효하지 않은 값: [] 메시지: 이름이 비어있습니다.
검사 필드: age, 유효하지 않은 값: [-1] 메시지: 나이는 0보다 큰 값이어야합니다.&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Built-in Constraint Definitions&lt;/h2&gt;
&lt;p&gt;Bean Validation에는 대부분의 경우에 사용가능한 제약조건 어노테이션들이 내장(Built-in) 되어있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;javax.validation.constraints&lt;/code&gt; 패키지를 확인해보면 총 22개의 내장 제약조건이 있는것을 확인할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@AssertFalse&lt;/li&gt;
&lt;li&gt;@AssertTrue&lt;/li&gt;
&lt;li&gt;@DecimalMax&lt;/li&gt;
&lt;li&gt;@DecimalMin&lt;/li&gt;
&lt;li&gt;@Digits&lt;/li&gt;
&lt;li&gt;@Email&lt;/li&gt;
&lt;li&gt;@Future&lt;/li&gt;
&lt;li&gt;@FutureOrPresent&lt;/li&gt;
&lt;li&gt;@Max&lt;/li&gt;
&lt;li&gt;@Min&lt;/li&gt;
&lt;li&gt;@Negative&lt;/li&gt;
&lt;li&gt;@NegativeOrZero&lt;/li&gt;
&lt;li&gt;@NotBlank&lt;/li&gt;
&lt;li&gt;@NotEmpty&lt;/li&gt;
&lt;li&gt;@NotNull&lt;/li&gt;
&lt;li&gt;@Null&lt;/li&gt;
&lt;li&gt;@Past&lt;/li&gt;
&lt;li&gt;@PastOrPresent&lt;/li&gt;
&lt;li&gt;@Pattern&lt;/li&gt;
&lt;li&gt;@Positive&lt;/li&gt;
&lt;li&gt;@PositiveOrZero&lt;/li&gt;
&lt;li&gt;@Size&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;거의 모든 어노테이션들이 이름만으로 어떤 제약조건인지 쉽게 유추가 가능합니다.&lt;/p&gt;
&lt;p&gt;자세한 사항은 &lt;a href=&quot;https://beanvalidation.org/2.0/spec/#builtinconstraints&quot;&gt;공식 홈페이지&lt;/a&gt;의 &lt;code&gt;8. Built-in Constraint definitions&lt;/code&gt; 섹션을 참고하시는 것을 추천드립니다.&lt;/p&gt;
&lt;h2&gt;Bean Validation 1.1 vs 2.0&lt;/h2&gt;
&lt;p&gt;Bean Validation 2.0의 주요 변경사항은 공식 사이트에서 아래와 같이 소개하고 있습니다.&lt;br&gt;전체 변경사항은 &lt;a href=&quot;https://beanvalidation.org/2.0/spec/#changelog&quot;&gt;이 곳&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;support for validating container elements by annotating type arguments of parameterized types e.g. List&amp;lt;@Positive Integer&amp;gt; positiveNumbers. This also includes:&lt;ul&gt;
&lt;li&gt;more flexible cascaded validation of container types&lt;/li&gt;
&lt;li&gt;support for java.util.Optional&lt;/li&gt;
&lt;li&gt;support for the property types declared by JavaFX&lt;/li&gt;
&lt;li&gt;support for custom container types&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;support for the new date/time data types (JSR 310) for @Past and @Future&lt;/li&gt;
&lt;li&gt;new built-in constraints: @Email, @NotEmpty, @NotBlank, @Positive, @PositiveOrZero, @Negative, @NegativeOrZero, @PastOrPresent and @FutureOrPresent&lt;/li&gt;
&lt;li&gt;leverage the JDK 8 new features (built-in constraints are marked repeatable, parameter names are retrieved via reflection)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;p&gt;아까도 언급했지만 Spring Boot 2.0 이상 버전에서는 Bean Validation 2.0이 기본적으로 사용됩니다.&lt;br&gt;하지만 이하 버전에서는 Bean Validation 1.1 버전이 사용되는데 이런 경우 다음과 같은 방법으로 버전업을 진행하면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;properties&amp;gt;
    &amp;lt;javax-validation.version&amp;gt;2.0.1.Final&amp;lt;/javax-validation.version&amp;gt;
    &amp;lt;hibernate-validation.version&amp;gt;6.0.7.Final&amp;lt;/hibernate-validation.version&amp;gt;
&amp;lt;/properties&amp;gt;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Back-End/Spring framework</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/99</guid>
      <comments>https://do-study.tistory.com/99#entry99comment</comments>
      <pubDate>Mon, 17 Feb 2020 11:25:45 +0900</pubDate>
    </item>
    <item>
      <title>JUnit5 사용해보기</title>
      <link>https://do-study.tistory.com/98</link>
      <description>&lt;p&gt;JUnit은 Java를 위한 단위 테스트 (Unit Test) 프레임워크로 개발하는 프로그램에 대한 테스트케이스를 쉽게 작성할 수 있도록 도와줍니다.&lt;br&gt;그동안 JUnit4를 주로 사용해왔는데, 최근들어 JUnit5가 있음을 알게되고(사실 나온지는 2년이 넘은..) 어떤 차이점이 있는지 알아보고 사용해보려합니다.&lt;/p&gt;
&lt;h2&gt;JUnit4에서 JUnit5로 넘어오며 달라진 점&lt;/h2&gt;
&lt;p&gt;먼저 JUnit5를 사용하기 위해서는 아래 지원범위에 만족하는지 확인해봐야합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java8 이상 지원&lt;/li&gt;
&lt;li&gt;IntelliJ IDEA 2016.2 이후 지원&lt;/li&gt;
&lt;li&gt;Eclipse Oxygen 이후 지원&lt;/li&gt;
&lt;li&gt;Kotlin 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JUnit5 User Guide 문서 첫 단락에 보면 다음과 같은 문구가 적혀있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unlike previous versions of JUnit, JUnit5 is composed of several different modules from three different sub-projects.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;JUnit5 = JUnit Platform + JUnit Jupiter + JUnit Vintage&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이전 버전들과 달리 JUnit5 에 들어서며 여러 개의 서브 모듈로 나뉘었다는 것입니다.&lt;/p&gt;
&lt;h4&gt;# JUnit Platform&lt;/h4&gt;
&lt;p&gt;JUnit Platform은 Test를 실행하기 위한 전반적인 사항들을 제공하는 역할입니다. 테스트케이스를 발견, 실행하고 결과를 보고해주는 일종의 컨텍스트라는 생각이 듭니다.&lt;br&gt;JUnit Platform은 어떻게 테스트를 발견하고 실행하고 보고할 지에 대한 실제 동작은 &lt;code&gt;TestEngine&lt;/code&gt; 인터페이스를 구현하여 정의되며 실제 구현체들은 별도 모듈로 존재합니다.&lt;/p&gt;
&lt;h4&gt;# JUnit Jupiter&lt;/h4&gt;
&lt;p&gt;JUnit Jupiter는 &lt;code&gt;TestEngine&lt;/code&gt;을 구현한 엔진입니다. (jupiter-engine) Jupiter API는 JUnit5에 새롭게 추가된 API들을 포함하고 있습니다.&lt;/p&gt;
&lt;h4&gt;# JUnit Vintage&lt;/h4&gt;
&lt;p&gt;JUnit Vintage는 기존 JUnit4 버전으로 작성된 테스트를 실행하기 위한 엔진입니다. (vintage-engine)&lt;/p&gt;
&lt;p&gt;참고로 Spring Boot 2.2.0 버전부터 JUnit5가 기본으로 채택되었는데요.&lt;br&gt;&lt;code&gt;Spring Initializer&lt;/code&gt;를 통해 프로젝트를 만들어보면 Test 디펜던시가 다음과 같이 되어있는것을 볼 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;...
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
  &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
  &amp;lt;exclusions&amp;gt;    
    &amp;lt;exclusion&amp;gt;
      &amp;lt;groupId&amp;gt;org.junit.vintage&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;junit-vintage-engine&amp;lt;/artifactId&amp;gt;
    &amp;lt;/exclusion&amp;gt;
  &amp;lt;/exclusions&amp;gt;
&amp;lt;/dependency&amp;gt;
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;junit-vintage-engine&lt;/code&gt; 모듈을 제외하고 있습니다. &lt;code&gt;vintage-engine&lt;/code&gt; 자체가 JUnit4를 지원하기 위한 모듈이다보니 기본적으로는 제외하도록 설정되는 것입니다.&lt;/p&gt;
&lt;p&gt;만약 JUnit4 테스트케이스를 같이 관리해주어야하는 상황이라면 위 &lt;code&gt;exclusion&lt;/code&gt; 설정을 삭제하면 됩니다.&lt;/p&gt;
&lt;h2&gt;새로운 혹은 변경된 기능&lt;/h2&gt;
&lt;p&gt;아키텍쳐는 많이 바뀌었지만 실제 테스트케이스를 작성하는 부분에서는 크게 바뀐 점은 느껴지지 않습니다.&lt;/p&gt;
&lt;p&gt;주로 기존에 사용되던 어노테이션들의 네이밍이 좀 더 명확해지는등의 차이점이 눈에 띕니다.&lt;/p&gt;
&lt;h4&gt;1. @DisplayName&lt;/h4&gt;
&lt;p&gt;기존에 테스트 결과가 보고될 때 메소드명이 테스트케이스 이름으로 사용되었는데요. 때문에 테스트케이스를 잘 설명하기 위해 한글로 하고는 했습니다.&lt;/p&gt;
&lt;p&gt;JUnit5에서는 &lt;code&gt;@DisplayName&lt;/code&gt; 어노테이션을 활용해 좀 더 깔끔하게 이름을 붙혀줄 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class MemberServiceTest {

  // 기존 방식
    @Test
    public void 멤버_프로필_조회() {
        ...
    }

  // JUnit5 방식
  @Test
  @DisplayName(&amp;quot;멤버 프로필 조회&amp;quot;)
  public void getMemberProfileTest() {    
        ...    
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. @Disabled&lt;/h4&gt;
&lt;p&gt;기존 &lt;code&gt;@Ignore&lt;/code&gt; 어노테이션을 대체하는 어노테이션입니다.&lt;/p&gt;
&lt;h4&gt;3. LifeCycle Hook&lt;/h4&gt;
&lt;p&gt;JUnit 테스트케이스 작성 시 개발자가 테스트 수행 전 후 특정 시점에 실행될 동작을 정의할 수 있습니다.&lt;/p&gt;
&lt;p&gt;기존에는 &lt;code&gt;@BeforeClass&lt;/code&gt;, &lt;code&gt;@AfterClass&lt;/code&gt;, &lt;code&gt;@Before&lt;/code&gt;, &lt;code&gt;@After&lt;/code&gt; 어노테이션을 활용했었는데요.&lt;/p&gt;
&lt;p&gt;JUnit5에서는 &lt;code&gt;@BeforeAll&lt;/code&gt;, &lt;code&gt;@AfterAll&lt;/code&gt;, &lt;code&gt;@BeforeEach&lt;/code&gt;, &lt;code&gt;@AfterEach&lt;/code&gt; 로 변경되었습니다. @Before와 @After는 기존에 (테스트케이스 마다)라는 의미가 없어서 혼동의 여지가 있었는데, 변경된 네이밍은 이를 잘 표현하고 있어 마음에 듭니다.&lt;/p&gt;
&lt;h4&gt;4. assertAll with Lambda&lt;/h4&gt;
&lt;p&gt;테스트에서 assertion종류가 많고 복잡한 경우 &lt;code&gt;assertAll(Executable...executables)&lt;/code&gt;을 이용할 수 있습니다.&lt;br&gt;&lt;code&gt;Executable&lt;/code&gt;은 파라미터와 반환값이 없는 함수형 인터페이스이기 때문에 assertAll로 그룹핑 + assertion을 람다로 작성할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;assertAll(&amp;quot;assertionGroup1&amp;quot;,
  () -&amp;gt; assertEquals(expected, actual),
  ...    
)&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. @ParameterizedTest&lt;/h4&gt;
&lt;p&gt;한 메소드에서 여러 종류의 파라미터를 받아 테스트를 수행할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@DisplayName(&amp;quot;공백 문자열 테스트&amp;quot;)
@ParameterizedTest(name = &amp;quot;문자열 {0}으로 조회&amp;quot;)
@ValueSource(strings = &amp;quot; &amp;quot;, &amp;quot;&amp;quot;)
public void isBlankStringTest(String source) {
  boolean isBlank = service.isBlank(source);
  assertTrue(isBlank);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;참고로 @ParamterizedTest를 사용하려면 &lt;code&gt;junit-jupiter-params&lt;/code&gt; 모듈을 추가해야합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;maven&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.junit.jupiter&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;junit-jupiter-params&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;VERSION_TO_USE&amp;lt;/version&amp;gt;
  &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;gradle&lt;pre&gt;&lt;code class=&quot;language-gradle&quot;&gt;testCompile group: &amp;#39;org.junit.jupiter&amp;#39;, name: &amp;#39;junit-jupiter-params&amp;#39;, version: &amp;#39;VERSION_TO_USE&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이외에 더 자세히 알아보고싶으면 &lt;a href=&quot;https://junit.org/junit5/&quot;&gt;JUnit5 공식 홈페이지&lt;/a&gt;를 방문해보시는 것을 추천드립니다.&lt;/p&gt;</description>
      <category>Back-End/Java</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/98</guid>
      <comments>https://do-study.tistory.com/98#entry98comment</comments>
      <pubDate>Sat, 1 Feb 2020 19:38:37 +0900</pubDate>
    </item>
    <item>
      <title>HikariCP와 커넥션 누수(Connection Leak) 관련 트러블슈팅</title>
      <link>https://do-study.tistory.com/97</link>
      <description>&lt;h1&gt;문제 발생&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영중인 서비스에서 사용하는 DB에서 특정 테이블들을 분리하여 별도 DB로 구축하는 일이 생겼습니다. 때문에 이를 위해 Multi Datasource를 적용하였습니다.&lt;br /&gt;기존 서비스는 &lt;code&gt;Tomcat connection pool&lt;/code&gt;이 적용되어있었는데, 이번 작업을 하며 &lt;code&gt;HikariCP&lt;/code&gt;로 변경하였습니다.&lt;br /&gt;(참고로 Spring Boot 2.0 부터는 HikariCP가 기본 커넥션풀이라고 합니다.)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In Spring Boot 1.x, Tomcat connection pool was the default connection pool but in Spring Boot 2.x HikariCP is the default connection pool.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용을 완료하고 정상동작을 확인한 후 개발환경에 반영해둔 다음날..&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;서비스 API 호출 시 오류가 발생한다는 문의를 받고 로그를 확인해보니 아래와 같은 에러 발생 및 API 호출 불가한 현상이 확인되었습니다.&lt;/p&gt;
&lt;pre class=&quot;brush: plain&quot;&gt;&lt;code&gt; 
[2020-01-10 08:49:08,143 +0900] [WARN] [c.z.h.p.ProxyConnection] GAMEDB-POOL - Connection com.mysql.jdbc.JDBC4Connection@158c355 marked as broken because of 
SQLSTATE(08S01), ErrorCode(0)
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The last packet successfully received from the server was 36,305,998 milliseconds ago.  The last packet sent successfully to the server was 36,305,999 milliseconds ago. is longer than the server configured value of 'wait_timeout'. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property 'autoReconnect=true' to avoid this problem.
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
        at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
        at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989)
        at com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3746)
        at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2509)
        at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2680)
        at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2487)
        at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1858)
        at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1966)
        at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
        at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)

...        

Caused by: java.net.SocketException: 파이프가 깨어짐 (Write failed)
        at java.net.SocketOutputStream.socketWrite0(Native Method)
        at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)
        at java.net.SocketOutputStream.write(SocketOutputStream.java:155)
        at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
        at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
        at com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3728)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;원인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 서비스가 사용하는 DBMS는 MySQL입니다.&lt;br /&gt;MySQL은 기본적으로 자신에게 맺어진 커넥션 중 &lt;b&gt;일정 시간이상 사용하지 않은 커넥션을 종료&lt;/b&gt;하는 프로세스가 존재합니다. (MySQL의 &lt;code&gt;wait_timeout&lt;/code&gt;이라는 값을 통해 확인할 수 있고 default 8시간)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 기존 커넥션풀은 대부분 연결을 맺은 커넥션들이 끊기는 것을 방지하기 위해 &lt;code&gt;SELECT 1&lt;/code&gt; 등의 쿼리를 주기적으로 날려 이 문제를 회피하는 반면 HikariCP는 &lt;code&gt;maxLifetime&lt;/code&gt; 설정값에 따라 스스로 미사용된 커넥션을 제거하고 새로 생성하는 방식으로 동작한다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 문제는 맺어졌던 커넥션이 장시간 사용되지 않아 MySQL 서버에 의해 끊어졌고 커넥션 풀은 끊어진 커넥션인줄 모르고 계속 사용되어 발생하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q. 커넥션이 장시간 사용되지 않더라도 HikariCP의 &lt;code&gt;maxLifeTime&lt;/code&gt;이 MySQL의 &lt;code&gt;wait_timeout&lt;/code&gt;보다 짧으면 문제될 것이 없지 않나?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급한 것처럼 HikariCP는 &lt;code&gt;maxLifetime&lt;/code&gt;에 도달한 커넥션의 연결을 끊고 새로운 커넥션을 생성하는 방식입니다. 때문에 &lt;code&gt;maxLifetime&lt;/code&gt;이 &lt;code&gt;wait_timeout&lt;/code&gt;보다 짧으면 MySQL 서버가 끊기 전에 스스로 연결을 끊고 새로 커넥션을 맺기 때문에 이런 문제가 발생할 여지가 없습니다. (&lt;code&gt;maxLifetime&lt;/code&gt; 기본값은 30분)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그럼에도 밤새도록 종료되지 않는 커넥션이 있었던 것은 작성된 &lt;code&gt;QueryDSL&lt;/code&gt;을 적용한 Repository에서 커넥션 누수가 있었기 때문입니다.&lt;br /&gt;&lt;code&gt;maxLifetime&lt;/code&gt;은 &lt;b&gt;사용하지 않는&lt;/b&gt; 커넥션 한정으로 적용됩니다. 커넥션 누수로 인해 특정 커넥션이 close되지 않았고, HikariCP입장에서는 계속 &lt;code&gt;active&lt;/code&gt; 상태로 인지하여 &lt;code&gt;maxLifetime&lt;/code&gt; 적용되지 않았던 것입니다.&lt;/p&gt;
&lt;h1&gt;문제를 해결하기 위해 시도해본 것들&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 문제 발생 초기엔 HikariCP의 동작방식에 대한 정확한 이해가 없었고, 때문에 커넥션 누수가 있을것이라고 예상치 못했습니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;대신 설정값에 문제가 있을 것이라고 추측했고 삽질의 길이 시작되었습니다....&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0. HikariCP 디버깅 로그 출력 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 더 정확히 현상을 파악하기 위해 logback 설정에 HikariCP에서 출력하는 디버그용 로그를 보기 위해 아래 설정을 진행하였습니다.&lt;/p&gt;
&lt;pre class=&quot;brush: xml&quot;&gt;&lt;code&gt; ...
&amp;lt;logger name=&quot;com.zaxxer.hikari.HikariConfig&quot; level=&quot;DEBUG&quot;/&amp;gt;
&amp;lt;logger name=&quot;com.zaxxer.hikari&quot; level=&quot;TRACE&quot;/&amp;gt;
...&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 설정값 (수치)에 따른 문제?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 서비스의 커넥션풀을 변경하면서 풀사이즈, 최소 유휴 상태 커넥션 수 등 풀 동작과 관련된 설정값을 기존 커넥션풀에 적용되어있는 수치 그대로 적용했는데요.&lt;br /&gt;이것이 문제가 되는 것으로 알고 HikariCP를 사용하면서 문제가 없었던 타 서비스들과 동일한 설정을 사용하도록 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 변경한 이후에도 문제가 발생했는데 그 이유는 아래와 같았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타 서비스들은 주기적으로 API 클라이언트 혹은 자체 스케쥴에 의해 db 호출을 하고 있었던 상황&lt;/li&gt;
&lt;li&gt;문제가 발생한 서비스는 야간 시간대에 8시간 이상 아무런 호출이 없었음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. maxLifetime 조정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP 공식 문서에 보면 &lt;code&gt;maxLifetime&lt;/code&gt;값을 DB의 &lt;code&gt;wait_timeout&lt;/code&gt;보다 &lt;b&gt;몇 초 정도&lt;/b&gt; 짧게 설정하라고 권장하는 가이드가 있어 적용해봤습니다.&lt;/p&gt;
&lt;pre class=&quot;brush: plain&quot;&gt;&lt;code&gt;We strongly recommend setting this value, and it should be several seconds shorter than any database or infrastructure imposed connection time limit. 
A value of 0 indicates no maximum lifetime (infinite lifetime), subject of course to the idleTimeout setting. 

Default: 1800000 (30 minutes)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여전히 문제는 발생하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. autoreconnect=true&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 살펴보던 중, &lt;code&gt;No operations allowed after connection closed.&lt;/code&gt; 라는 오류 메시지를 확인하게 됐습니다.&lt;br /&gt;이를 보고 JDBC URL에 &lt;code&gt;autoreconnect&lt;/code&gt;옵션을 줬지만, 효과가 없었습니다. 심지어 HikariCP 기본 작동 방식을 봤을때 넣는게 무의미한 설정이라고 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;혼란을 주는 에러메시지
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래 에러메시지도 (HikariCP라면) 믿으면 안됩니다.&lt;/li&gt;
&lt;li&gt;에러메시지: No operations allowed after connection closed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. minimumIdle 조정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;idleTimeout&lt;/code&gt;은 생성된 커넥션이 유휴 상태로 풀에 존재할 수 있는 최대시간입니다.&lt;br /&gt;이 설정은 &lt;code&gt;minimumIdle&lt;/code&gt; &amp;lt; &lt;code&gt;maximumPoolSize&lt;/code&gt; 일 경우에만 적용되는 값으로 이를 테스트하기 위해 값을 조정봤습니다. (역시 문제는 해결되지않음)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. HikariCP와 Hibernate 의 버전 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지푸라기를 잡는 심정으로 버전을 확인해봤습니다. 기존 hikariCP, hibernate 두 버전의 차이가 4년 가량 있었습니다.&lt;br /&gt;때문에 버전 차이에서 오는 알려지지 않은 버그일까 라는 생각에 버전업을 진행했지만 마찬가지로 문제가 해결되지 않았습니다.&lt;/p&gt;
&lt;pre class=&quot;brush: plain&quot;&gt;&lt;code&gt;[Before]
hibernate.version: 5.0.12.Final
hikaricp.version: 3.1.0

[After]
hibernate.version: 5.4.10.Final
hikaricp.version: 3.4.1&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Connection 누수 현상 확인 (실제 원인)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다시 오류 메시지를 보던 중 문제가 된 Connection인 &lt;code&gt;com.mysql.jdbc.JDBC4Connection@3bfbf166&lt;/code&gt;이 커넥션 풀에 등록은 되었으나 해제되지 않은 부분을 발견했습니다.&lt;/p&gt;
&lt;pre class=&quot;brush: bash&quot;&gt;&lt;code&gt;[2020-01-14 08:21:01,071 +0900] [WARN] [c.z.h.p.ProxyConnection] GAMEDB-POOL - Connection com.mysql.jdbc.JDBC4Connection@3bfbf166 marked as broken because of SQLSTATE(08S01), ErrorCode(0)
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The last packet successfully received from the server was 52,889,749 milliseconds ago.  The last packet sent successfully to the server was 52,889,750 milliseconds ago. is longer than the server configured value of 'wait_timeout'. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property 'autoReconnect=true' to avoid this problem.
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Connection 생성 / 해제 로그를 추출해 확인해본 결과&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;code&gt;maxLifetime&lt;/code&gt;에 따라 생성, 해제를 수행한 내역에서 위 문제가 된 커넥션이 Add는 되었지만 Close가 된 기록은 확인할 수 없었습니다.&lt;/p&gt;
&lt;pre class=&quot;brush: bash&quot;&gt;&lt;code&gt;[2020-01-13 16:09:29,349 +0900] [DEBUG] [c.z.h.p.HikariPool] GAMEDB-POOL - Added connection com.mysql.jdbc.JDBC4Connection@3bfbf166
[2020-01-13 16:09:29,353 +0900] [DEBUG] [c.z.h.p.HikariPool] GAMEDB-POOL - Added connection com.mysql.jdbc.JDBC4Connection@17d7631
[2020-01-13 16:09:29,359 +0900] [DEBUG] [c.z.h.p.HikariPool] GAMEDB-POOL - Added connection com.mysql.jdbc.JDBC4Connection@161f17c2
[2020-01-13 16:34:22,082 +0900] [DEBUG] [c.z.h.p.HikariPool] GAMEDB-POOL - Added connection com.mysql.jdbc.JDBC4Connection@60cc1c45
[2020-01-13 16:34:22,089 +0900] [DEBUG] [c.z.h.p.HikariPool] GAMEDB-POOL - Added connection com.mysql.jdbc.JDBC4Connection@7e3ef6ea
[2020-01-13 16:34:22,492 +0900] [DEBUG] [c.z.h.p.HikariPool] GAMEDB-POOL - Added connection com.mysql.jdbc.JDBC4Connection@6befd804
[2020-01-13 16:35:17,046 +0900] [DEBUG] [c.z.h.p.HikariPool] GAMEDB-POOL - Added connection com.mysql.jdbc.JDBC4Connection@273141d1
[2020-01-13 16:35:17,103 +0900] [DEBUG] [c.z.h.p.HikariPool] GAMEDB-POOL - Added connection com.mysql.jdbc.JDBC4Connection@3eac312e
[2020-01-13 16:35:17,208 +0900] [DEBUG] [c.z.h.p.HikariPool] GAMEDB-POOL - Added connection com.mysql.jdbc.JDBC4Connection@66d58bc3
[2020-01-13 16:35:17,278 +0900] [DEBUG] [c.z.h.p.HikariPool] GAMEDB-POOL - Added connection com.mysql.jdbc.JDBC4Connection@7d94ee53
[2020-01-13 16:55:35,178 +0900] [DEBUG] [c.z.h.p.PoolBase] GAMEDB-POOL - Closing connection com.mysql.jdbc.JDBC4Connection@161f17c2: (connection has passed maxLifetime)
[2020-01-13 16:55:43,855 +0900] [DEBUG] [c.z.h.p.PoolBase] GAMEDB-POOL - Closing connection com.mysql.jdbc.JDBC4Connection@17d7631: (connection has passed maxLifetime)
[2020-01-13 17:20:29,614 +0900] [DEBUG] [c.z.h.p.PoolBase] GAMEDB-POOL - Closing connection com.mysql.jdbc.JDBC4Connection@60cc1c45: (connection has passed maxLifetime)
[2020-01-13 17:20:47,468 +0900] [DEBUG] [c.z.h.p.PoolBase] GAMEDB-POOL - Closing connection com.mysql.jdbc.JDBC4Connection@6befd804: (connection has passed maxLifetime)
[2020-01-13 17:20:48,773 +0900] [DEBUG] [c.z.h.p.PoolBase] GAMEDB-POOL - Closing connection com.mysql.jdbc.JDBC4Connection@7e3ef6ea: (connection has passed maxLifetime)
[2020-01-13 17:21:30,939 +0900] [DEBUG] [c.z.h.p.PoolBase] GAMEDB-POOL - Closing connection com.mysql.jdbc.JDBC4Connection@273141d1: (connection has passed maxLifetime)
[2020-01-13 17:21:47,343 +0900] [DEBUG] [c.z.h.p.PoolBase] GAMEDB-POOL - Closing connection com.mysql.jdbc.JDBC4Connection@66d58bc3: (connection has passed maxLifetime)
[2020-01-13 17:21:50,493 +0900] [DEBUG] [c.z.h.p.PoolBase] GAMEDB-POOL - Closing connection com.mysql.jdbc.JDBC4Connection@3eac312e: (connection has passed maxLifetime)
[2020-01-13 17:21:59,390 +0900] [DEBUG] [c.z.h.p.HikariPool] GAMEDB-POOL - Added connection com.mysql.jdbc.JDBC4Connection@11991843
[2020-01-13 17:21:59,394 +0900] [DEBUG] [c.z.h.p.HikariPool] GAMEDB-POOL - Added connection com.mysql.jdbc.JDBC4Connection@30201cdf
[2020-01-13 17:22:01,785 +0900] [DEBUG] [c.z.h.p.PoolBase] GAMEDB-POOL - Closing connection com.mysql.jdbc.JDBC4Connection@7d94ee53: (connection has passed maxLifetime)
[2020-01-13 17:22:29,393 +0900] [DEBUG] [c.z.h.p.HikariPool] GAMEDB-POOL - Added connection com.mysql.jdbc.JDBC4Connection@35653c28&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제서야 커넥션 누수가 있음을 알고, HikariCP에서 제공하는 &lt;code&gt;leakDetectionThreshold&lt;/code&gt; 옵션을 설정해 로컬에서 확인해본 결과 &lt;code&gt;QueryDSL&lt;/code&gt; 이 적용되어있는 특정 API 부분에서 커넥션 누수가 확인되었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;커넥션 누수가 발생한 원인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 2가지 이유가 복합적으로 연관되어 문제가 발생하였습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;EntityManager를 Bean으로 설정함
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EntityManager는 스레드간 절대 공유하면 안됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;QueryDSL 구현 Resitory에서 특정 datasource의 EntityManager를 받기 위해 setEntityManager를 재정의하지 않고 별도 setting 메소드 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JPA를 사용할 때 보통 EntityManager를 클래스 멤버변수로 두고 &lt;code&gt;@PersistenceContext&lt;/code&gt;로 주입받아 사용하는데 이것은 어떻게 안전할까요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 일반적으로 &lt;code&gt;EntityManagerFactory&lt;/code&gt;를 통해 하나하나 생성되는 EntityManager가 아닌 &lt;code&gt;SharedEntityManagerBean&lt;/code&gt; 라는 공유 가능하며 커넥션 생성 / 반환에 대한 관리를 지원하는 구현체가 주입되기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QueryDslRepositorySupport는 아래와 같이 EntityManager를 주입받는 코드가 존재합니다.&lt;br /&gt;특별한 설정이 없으면 &lt;code&gt;SharedEntityManagerBean&lt;/code&gt;이 주입되어 아무 문제 없이 작동 했을것입니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;@Repository
public abstract class QueryDslRepositorySupport {
//    ....

    /**
     * Setter to inject {@link EntityManager}.
     * 
     * @param entityManager must not be {@literal null}.
     */
    @Autowired
    public void setEntityManager(EntityManager entityManager) {

        Assert.notNull(entityManager, &quot;EntityManager must not be null!&quot;);
        this.querydsl = new Querydsl(entityManager, builder);
        this.entityManager = entityManager;
    }

//    ....
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 MultidataSource를 적용하는 과정에서 @Autowired 어노테이션만으로는 프레임워크가 어떤 데이터소스의 EntityManager를 넣어줘야할지 판단하지 못하기 때문에 오류가 발생했고, 이 오류를 해결한다고 아래와 같이 일반 EntityManager를 싱글톤 빈으로 잡는 잘못된 설정을 하였습니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef = &quot;gameEntityManagerFactory&quot;,
        transactionManagerRef = ServiceTransactionManager.GAME_DATABASE_TRANSACTION_MANAGER,
        basePackages = { &quot;com.anan.repository.game&quot; }
)
public class GameDatabaseConfiguration {
    // ...

	// 문제가 된 부분 - @Autowired 시 Persistence 기능 관련 프레임워크의 관리를 받지 못하는 EntityManager가 주입됨
    @Primary
    @Bean(name = &quot;gameEntityManager&quot;)
    public EntityManager entityManager(@Qualifier(&quot;gameEntityManagerFactory&quot;) EntityManagerFactory entityManagerFactory) {
        return entityManagerFactory.createEntityManager();
    }

	// ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;public class GreetingRepositoryImpl extends QueryDslRepositorySupport implements GreetingRepositorySupport {
    // ...

    // 문제가 된 부분 - EntityManager를 주입받기위해 setEntityManage 재정의하지 않고 별도의 메소드를 통함
    // @PersistenceContext에 의해 setTargetEntityManager(정상 EntityManager) -&amp;gt; super.setEntityManager(정상 EntityManager) 
    // 상위 메소드의 @Autowired에 의해 setEntityManager(비정상 EntityManager)로 일반 EntityManager가 사용됨
    @PersistenceContext(unitName = &quot;game&quot;)
    public void setTargetEntityManager(EntityManager entityManager) {
        super.setEntityManager(entityManager);
    }

    // ...

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 해당 커넥션을 사용 후 &lt;code&gt;Connection.close&lt;/code&gt;가 제대로 동작하지 않았습니다.&lt;/p&gt;
&lt;h1&gt;해결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결하기 위해 우선 EntityManager를 Bean으로 설정한 부분을 제거하였습니다.&lt;br /&gt;두 번째로, MultidataSource를 적용하는 과정에서 @Autowired 어노테이션만으로는 프레임워크가 어떤 데이터소스의 EntityManager를 넣어줘야할지 판단하지 못하는 오류를 해결하기 위해 @PersistenceContext + unitName attribute 지정하는 방식으로 setEntityManager를 재정의하였습니다.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&lt;code&gt;public class GreetingRepositoryImpl extends QueryDslRepositorySupport implements GreetingRepositorySupport {
    // ...

    @Override
    @PersistenceContext(unitName = &quot;game&quot;)
    public void setEntityManager(EntityManager entityManager) {
        super.setEntityManager(entityManager);
    }

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;마치며&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본이 중요하다는 것을 다시 느꼈습니다. 분명 JPA를 공부하며 책에서 EntityManager는 스레드간 공유하면 안되는 것이라는 내용을 본 적이 있음에도 불고하고&lt;br /&gt;실수를 하여 문제가 발생했고 해결하기 위해 수많은 삽질을 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째는 자신이 사용하는 기술을 정확히 이해하고 사용하자는 것입니다. 이번에 겪은 이슈도 &lt;code&gt;JPA의 대한 이해&lt;/code&gt;와 &lt;code&gt;HikariCP에 대한 이해&lt;/code&gt; 부족으로 시간이 오래 걸렸습니다.&lt;br /&gt;기술에 대한 이해가 있었다면 애초에 문제가 발생하지 않았거나 훨씬 빠르게 조치를 할 수 있었을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;code&gt;퍼포먼스가 좋다&lt;/code&gt;, &lt;code&gt;가장 인기있다&lt;/code&gt;라는 것만 알고 있던 &lt;code&gt;HikariCP&lt;/code&gt;에 대해 좀 더 자세히 알게 되어서 한편으로는 좋은 경험이었다는 생각이듭니다.&lt;/p&gt;
&lt;h1&gt;참고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP 공식 문서 : &lt;a href=&quot;https://github.com/brettwooldridge/HikariCP&quot;&gt;https://github.com/brettwooldridge/HikariCP&lt;/a&gt;&lt;/p&gt;</description>
      <category>Back-End/Spring framework</category>
      <category>ConnectionPool</category>
      <category>HikariCP</category>
      <category>JPA</category>
      <category>querydsl</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/97</guid>
      <comments>https://do-study.tistory.com/97#entry97comment</comments>
      <pubDate>Fri, 31 Jan 2020 15:14:21 +0900</pubDate>
    </item>
    <item>
      <title>HikariCP Connection Pool 해제 이슈</title>
      <link>https://do-study.tistory.com/96</link>
      <description>&lt;p&gt;몇 일 전, 개발환경에 구축되어있는 백엔드 시스템의 커넥션풀을 변경해야하는 일이 생겨 기존 Tomcat 커넥션풀 (tomcat-dbcp)에서 HikariCP로 변경하였습니다. 변경하면서 기존 설정되어있던 수치들을 HikariCP에서 제공하는 옵션에 맞추어 마이그레이션 했는데요. 완료 후 특별한 문제점은 보이지 않았습니다. 하지만 다음 날 출근 후&amp;hellip;. 해당 시스템의 API를 호출하자 갑자기 아래와 같은 오류 메시지가 나오며 시스템이 정상적으로 동작하지 않는 상태인 것이 확인되었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578763608477&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[2020-01-10 08:49:08,143 +0900] [WARN] [c.z.h.p.ProxyConnection] GAMEDB-POOL - Connection com.mysql.jdbc.JDBC4Connection@158c355 marked as broken because of SQLSTATE(08S01), ErrorCode(0)
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The last packet successfully received from the server was 36,305,998 milliseconds ago.  The last packet sent successfully to the server was 36,305,999 milliseconds ago. is longer than the server configured value of 'wait_timeout'. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property 'autoReconnect=true' to avoid this problem.
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
        at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
        at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989)
        at com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3746)
        at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2509)
        at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2680)
        at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2487)
        at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1858)
        at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1966)
        at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
        at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)
Caused by: java.net.SocketException: 파이프가 깨어짐 (Write failed)
        at java.net.SocketOutputStream.socketWrite0(Native Method)
        at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)
        at java.net.SocketOutputStream.write(SocketOutputStream.java:155)
        at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
        at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
        at com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3728)      &lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;원인&quot;&gt;원인&lt;/h2&gt;
&lt;p&gt;리서치를 해본 결과 핵심은 기존 사용하던 tomcat-dbcp와 HikariCP의 철학이 달라서 생긴 문제였습니다. 현재 백엔드 시스템은 MySQL을 사용중인데요. MySQL은 맺어진 커넥션이 장시간 사용되지 않으면 해당 커넥션을 해제하도록 설계되어있습니다. (기본 8시간)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot; data-ke-size=&quot;size18&quot;&gt;tomcat-jdbc&lt;/p&gt;
&lt;p&gt;test-while-idle&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정이&lt;span&gt;&amp;nbsp;&lt;/span&gt;true로 되어있으면 IDLE 상태의 커넥션들을 대상으로 DB서버에 SELECT 1을 주기적으로 보냅니다. 이를 통해 커넥션이 지속적으로 갱신되고 서버로부터 disconnect되지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1578763639909&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.datasource.tomcat.test-while-idle=true
spring.datasource.tomcat.validation-query=SELECT 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot; data-ke-size=&quot;size18&quot;&gt;HikariCP&lt;/p&gt;
&lt;p&gt;공식 문서: &lt;a href=&quot;https://github.com/brettwooldridge/HikariCP&quot;&gt;https://github.com/brettwooldridge/HikariCP&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고로 Spring Boot 2.0을 기점으로 기본 DBCP가 Tomcat DBCP에서 HikariCP로 바뀌었습니다. HikariCP는 tomcat-dbcp와 달리 사용하지 않는 Connection을 회수하도록 설계되었습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;maxLifeTime으로 설정된 시간이 지나면 풀에서 제거되고, 다시 생성되는 방이며&lt;span&gt;&amp;nbsp;&lt;/span&gt;connectionTestQuery는 커넥션을 최초 맺을때, 풀에서 가져올 때 사용되는 옵션이라고 합니다. 또한 JDBC4를 지원하는 환경에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;connectionTestQuery&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 명시하지 않는것을 권고하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1578763661718&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Do not use JDBC4]
hikaricp.opts.connectionTestQuery=SELECT 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HikariCP 내부 JDBC 구현체에서는 아래와 같이 JDBC4에 대한 구현을 하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1578763736773&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PoolBase.java
  
boolean isConnectionAlive(final Connection connection) {
    ...
    if (isUseJdbc4Validation) {
        return connection.isValid(validationSeconds);
    }
    ...
}
  
/**
 * Execute isValid() or connection test query.
 *
 * @param connection a Connection to check
 */
private void checkDriverSupport(final Connection connection) throws SQLException {
    ...
    if (isUseJdbc4Validation) {
        connection.isValid(1);
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;대처&quot;&gt;대처&lt;/h2&gt;
&lt;p&gt;우선은&lt;span&gt;&amp;nbsp;&lt;/span&gt;maxLifeTime&lt;span&gt;&amp;nbsp;&lt;/span&gt;값을 설정하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;connectionTestQuery&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정을 제거한 후 모니터링을 해보고자 합니다.&lt;/p&gt;
&lt;p&gt;실제 운영환경에서는 발생하지 않았을 오류이지만, 오히려 개발환경인 덕에 발생하고, HikariCP에 대해 상세하게 리서치해 볼 수 있는 기회였습니다. 문제가 해결되면 해결된 글로 다시 작성해야겠습니다.&lt;/p&gt;</description>
      <category>IT기본</category>
      <category>HikariCP</category>
      <category>tomcat-dbcp</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/96</guid>
      <comments>https://do-study.tistory.com/96#entry96comment</comments>
      <pubDate>Wed, 15 Jan 2020 03:29:27 +0900</pubDate>
    </item>
    <item>
      <title>gRPC Reference - 예제</title>
      <link>https://do-study.tistory.com/95</link>
      <description>&lt;p&gt;전 편에서 gRPC에 대한 설명과 특징을 알아보았습니다. 이번 포스트에서는 grpc-java 모듈을 활용한 몇 가지 예제코드를 소개하고, 실제 프로젝트에 적용하는 방안에 대해 소개해보겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://do-study.tistory.com/94&quot;&gt;gRPC Reference - 개요 및 특징&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;grpc-basic&quot;&gt;gRPC Basic&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;프로젝트 생성
&lt;ul&gt;
&lt;li&gt;Java : 1.8&lt;/li&gt;
&lt;li&gt;gRPC : 1.25.0&lt;/li&gt;
&lt;li&gt;ProtoBuf : 3.11.0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;pom.xml 설정
&lt;ul&gt;
&lt;li&gt;gRPC + Protobuf 의존성 설정&lt;/li&gt;
&lt;li&gt;스텁 생성을 위해 protoc 플러그인 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;proto 컴파일 -&amp;gt; 애플리케이션 컴파일 순으로 진행되도록 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1578763117801&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;
         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;groupId&amp;gt;com.nhn.gia&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;gia-grpc&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
        &amp;lt;grpc.version&amp;gt;1.25.0&amp;lt;/grpc.version&amp;gt;
        &amp;lt;protobuf.version&amp;gt;3.11.0&amp;lt;/protobuf.version&amp;gt;
        &amp;lt;protoc.version&amp;gt;3.11.0&amp;lt;/protoc.version&amp;gt;
        &amp;lt;maven.compiler.source&amp;gt;1.7&amp;lt;/maven.compiler.source&amp;gt;
        &amp;lt;maven.compiler.target&amp;gt;1.7&amp;lt;/maven.compiler.target&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;io.grpc&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;grpc-netty-shaded&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
            &amp;lt;version&amp;gt;${grpc.version}&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;io.grpc&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;grpc-protobuf&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;${grpc.version}&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;io.grpc&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;grpc-stub&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;${grpc.version}&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;

    &amp;lt;build&amp;gt;
        &amp;lt;!-- Maven OS Extension --&amp;gt;
        &amp;lt;extensions&amp;gt;
            &amp;lt;extension&amp;gt;
                &amp;lt;groupId&amp;gt;kr.motd.maven&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;os-maven-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;1.6.2&amp;lt;/version&amp;gt;
            &amp;lt;/extension&amp;gt;
        &amp;lt;/extensions&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;!-- proto (IDL) generator plugin --&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.xolstice.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;protobuf-maven-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;0.6.1&amp;lt;/version&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;protocArtifact&amp;gt;com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}&amp;lt;/protocArtifact&amp;gt;
                    &amp;lt;pluginId&amp;gt;grpc-java&amp;lt;/pluginId&amp;gt;
                    &amp;lt;pluginArtifact&amp;gt;io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}&amp;lt;/pluginArtifact&amp;gt;
                &amp;lt;/configuration&amp;gt;
                &amp;lt;executions&amp;gt;
                    &amp;lt;execution&amp;gt;
                        &amp;lt;goals&amp;gt;
                            &amp;lt;goal&amp;gt;compile&amp;lt;/goal&amp;gt;
                            &amp;lt;goal&amp;gt;compile-custom&amp;lt;/goal&amp;gt;
                        &amp;lt;/goals&amp;gt;
                    &amp;lt;/execution&amp;gt;
                &amp;lt;/executions&amp;gt;
            &amp;lt;/plugin&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;source&amp;gt;8&amp;lt;/source&amp;gt;
                    &amp;lt;target&amp;gt;8&amp;lt;/target&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;proto 파일 작성 (서비스 정의)
&lt;ul&gt;
&lt;li&gt;proto 파일을 별도로 컴파일할 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;mvn protobuf:{goal}&lt;span&gt;&amp;nbsp;&lt;/span&gt;명령어로 컴파일 할 수 있다.&lt;/li&gt;
&lt;li&gt;컴파일된 결과물은 target(혹은 지정 빌드 디렉토리)/generated-sources/protobuf/java 이하 디렉토리에 생성된다.&lt;/li&gt;
&lt;li&gt;message&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 서버-클라이언트 간 송수신될 데이터 포맷&lt;/li&gt;
&lt;li&gt;service&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 서버, 클라이언트에서 원격 호출되는 서비스 메소드&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1578763217677&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

package proto_test;

option java_package = &quot;com.nhn.gia.grpc.proto&quot;;
option java_multiple_files = true;

service EchoService {
    rpc Echo (EchoRequest) returns (EchoResponse) {}
}

message EchoRequest {
    string msg = 1;
}

message EchoResponse {
    string msg = 1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;서버 프로그램 작성
&lt;ul&gt;
&lt;li&gt;서비스 기능 구현: proto파일에서 작성한&lt;span&gt;&amp;nbsp;&lt;/span&gt;service&lt;span&gt;&amp;nbsp;&lt;/span&gt;의 RPC 메소드들을 구현해줌&lt;/li&gt;
&lt;li&gt;아래 예시에서는 정의된&lt;span&gt;&amp;nbsp;&lt;/span&gt;rpc Echo (EchoRequest) returns (EchoResponse) {}&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1578763240527&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EchoServiceImpl extends EchoServiceGrpc.EchoServiceImplBase {
    @Override
    public void echo(EchoRequest request, StreamObserver&amp;lt;EchoResponse&amp;gt; responseObserver) {
        EchoResponse response = EchoResponse.newBuilder().setMsg(request.getMsg()).build();

        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;작성한 서비스를 사용하는 서버 프로그램 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1578763253792&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EchoServer {
  public static void main(String[] args) throws IOException, InterruptedException {
      Server server = ServerBuilder.forPort(8000)
              .addService(new EchoServiceImpl())
              .build();

      server.start();

      Runtime.getRuntime().addShutdownHook(new Thread(() -&amp;gt; {
          server.shutdown();
      }));

      server.awaitTermination();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;클라이언트 프로그램 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1578763281656&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EchoClient {
    public static void main(String[] args) {
        ManagedChannel channel = ManagedChannelBuilder
                .forAddress(&quot;localhost&quot;, 8000)
                .usePlaintext()
                .build();

        EchoServiceBlockingStub stub = EchoServiceGrpc.newBlockingStub(channel);
        EchoResponse response = stub.echo(EchoRequest.newBuilder().setMsg(&quot;Hello gRPC!!&quot;).build());
        Logger.log(&quot;Echo Client: %s&quot;, response.getMsg());
        channel.shutdown();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;grpc-with-springboot&quot;&gt;gRPC with SpringBoot&lt;/h2&gt;
&lt;p&gt;스프링 환경에서도 gRPC를 사용할 수 있다. 공식은 아니지만 몇 가지 개인 또는 그룹에서 제작해서 사용하고 있는 Spring boot starter가 있다. (&lt;a href=&quot;https://github.com/LogNet/grpc-spring-boot-starter)&quot;&gt;https://github.com/LogNet/grpc-spring-boot-starter)&lt;/a&gt; (&lt;a href=&quot;https://github.com/saturnism/spring-boot-starter-grpc)&quot;&gt;https://github.com/saturnism/spring-boot-starter-grpc)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;아래는 별도 Spring Boot Starter 없이 진행한 예제&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;프로젝트 생성, pom.xml 에 gRPC 관련 의존성 추가, proto 파일 작성은 기존과 동일
&lt;ul&gt;
&lt;li&gt;proto&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1578763439057&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

option java_package = &quot;com.nhn.gia.proto&quot;;
option java_multiple_files = true;

service UserService {
	rpc GetUser(UserSearchRequest) returns (User) {}
}

message UserSearchRequest {
	string id = 1;
}

message User {
	string _id = 1;
	int64 index = 2;
	bool isActive = 3;
	int32 age = 4;
	string name = 5;
	string gender = 6;
	string email = 7;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;컴포넌트 정의 (gRPC 서비스)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1578763511062&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    private final UserRepository userRepository;

    @Autowired
    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public void getUser(UserSearchRequest request, StreamObserver&amp;lt;User&amp;gt; responseObserver) {
        String requestId = request.getId();
        logger.info(&quot;requestId: {}&quot;, requestId);
        UserEntity entity = userRepository.findOne(requestId);

        responseObserver.onNext(buildUser(entity));
        responseObserver.onCompleted();
    }

    private User buildUser(UserEntity entity) {
        return User.newBuilder()
                .setId(entity.getId())
                .setIndex(entity.getIndex())
                .setIsActive(entity.getIsActive())
                .setAge(entity.getAge())
                .setName(entity.getName())
                .setGender(entity.getGender())
                .setEmail(entity.getEmail())
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;gRPC 서버 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1578763525717&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class GrpcServerConfiguration {

    @Value(&quot;${grpc.server.port}&quot;)
    private int grpcServerPort;

    @Bean
    public Server grpcServer(UserServiceImpl userService) {
        return ServerBuilder.forPort(grpcServerPort)
                .addService(userService)
                .build();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Application 구동 시 gRPC 서버가 실행되도록 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1578763538480&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class GrpcServerRunner implements ApplicationRunner, DisposableBean {
    private static final Logger logger = LoggerFactory.getLogger(GrpcServerRunner.class);

    @Autowired
    private Server grpcServer;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        grpcServer.start();

        logger.info(&quot;GrpcServer listening on port {}&quot;, grpcServer.getPort());

        grpcServer.awaitTermination();
    }
    
    @Override
    public void destroy() throws Exception {
        if (grpcServer != null) {
            grpcServer.shutdown();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기타</category>
      <category>gRPC</category>
      <category>grpc-java</category>
      <category>HTTP/2</category>
      <category>protobuf</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/95</guid>
      <comments>https://do-study.tistory.com/95#entry95comment</comments>
      <pubDate>Tue, 14 Jan 2020 03:25:44 +0900</pubDate>
    </item>
    <item>
      <title>gRPC Reference - 개요 및 특징</title>
      <link>https://do-study.tistory.com/94</link>
      <description>&lt;h2 id=&quot;1-grpc-란&quot;&gt;1. gRPC 란&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Protocol Buffer를 IDL (Interface Definition Language)로 사용하는 RPC 프레임워크&lt;/li&gt;
&lt;li&gt;구글에서 10년 이상동안 MSA 아키텍쳐 이하 수많은 시스템들, 데이터 센터 간 통신을 위해 사용하던 범용 RPC 프레임워크인&lt;span&gt;&amp;nbsp;&lt;/span&gt;Stubby를 오픈소스화해 공개한 것&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfFM71/btqA6oyhNnv/O4rsKz4gMK8lEKVcC9dbek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfFM71/btqA6oyhNnv/O4rsKz4gMK8lEKVcC9dbek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfFM71/btqA6oyhNnv/O4rsKz4gMK8lEKVcC9dbek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfFM71%2FbtqA6oyhNnv%2FO4rsKz4gMK8lEKVcC9dbek%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1.1. RPC란?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RPC (Remote Procedure Call)은 별도의 원격제어 없이 프로세스간 함수나 프로시저를 호출할 수 있도록 하는 통신 기술&lt;/li&gt;
&lt;li&gt;서로 통신하는 양 측의 Request, Response에 대한 인터페이스를 정의 후 양 측 프로그래밍 언어에 맞는 코드로 변환해야함.&lt;/li&gt;
&lt;li&gt;이 때 인터페이스를 정의하는 용어는 IDL(Interface Definition Language)라 함&lt;/li&gt;
&lt;li&gt;IDL이 컴파일러 등으로 특정 언어의 코드로 변환된 결과를 Skeleton (서버 측), Stub (클라이언트 측) 라 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1.2. Protocol Buffer (ProtoBuf)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;구글에서 개발한 데이터 직렬화 오픈소스로 gRPC의 IDL로 사용&lt;/li&gt;
&lt;li&gt;직렬화하려는 데이터의 구조를 프로토 파일에 정의 (.proto)&lt;/li&gt;
&lt;li&gt;ProtoBuf의 데이터는 message 들로 구성되며 각 message는 field (name, value pair) 로 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1578762979217&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;프로토 파일을 protoc(프로토콜 버퍼 컴파일러)로 원하는 언어에서 사용가능한 결과물로 컴파일하여 사용한다.&lt;/p&gt;
&lt;h2 id=&quot;2-grpc-의-특징&quot;&gt;2. gRPC 의 특징&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;높은 생산성과 효율적인 유지보수
&lt;ul&gt;
&lt;li&gt;서비스와 메시지를 정의하기 위해 오직 ProtoBuf 만을 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다양한 언어와 플랫폼 지원&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;LanguagePlatformCompiler&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;C/C++&lt;/td&gt;
&lt;td&gt;Linux/Mac&lt;/td&gt;
&lt;td&gt;GCC 4.8+, Clang 3.3+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C/C++&lt;/td&gt;
&lt;td&gt;Windows 7+&lt;/td&gt;
&lt;td&gt;Visual Studio 2015+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C#&lt;/td&gt;
&lt;td&gt;Linux/Mac&lt;/td&gt;
&lt;td&gt;.NET Core, Mono 4+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C#&lt;/td&gt;
&lt;td&gt;Windows 7+&lt;/td&gt;
&lt;td&gt;.NET Core, .NET 4.5+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dart&lt;/td&gt;
&lt;td&gt;Windows/Linux/Mac&lt;/td&gt;
&lt;td&gt;Dart 2.0+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Windows/Linux/Mac&lt;/td&gt;
&lt;td&gt;Go 1.6+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;Windows/Linux/Mac&lt;/td&gt;
&lt;td&gt;JDK 8 recommended. Gingerbread+ for Android&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;Windows/Linux/Mac&lt;/td&gt;
&lt;td&gt;Node v4+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Objective-C&lt;/td&gt;
&lt;td&gt;Mac OS X 10.11+/iOS 7.0+&lt;/td&gt;
&lt;td&gt;Xcode 7.2+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP (Beta)&lt;/td&gt;
&lt;td&gt;Linux/Mac&lt;/td&gt;
&lt;td&gt;PHP 5.5+ and PHP 7.0+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Windows/Linux/Mac&lt;/td&gt;
&lt;td&gt;Python 2.7 and Python 3.4+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ruby&lt;/td&gt;
&lt;td&gt;Windows/Linux/Mac&lt;/td&gt;
&lt;td&gt;Ruby 2.3+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;HTTP/2 기반 통신
&lt;ul&gt;
&lt;li&gt;gRPC 는 HTTP/2 기반 통신으로 기존 HTTP 과는 다르게 서버와 클라이언트가 서로 데이터를 스트리밍으로 주고 받을 수 있음
&lt;ul&gt;
&lt;li&gt;simple RPC : 클라이언트 요청에 서버가 응답하고 종료&lt;/li&gt;
&lt;li&gt;server-side streaming RPC : 클라이언트 요청에 응답을 여러번 보냄&lt;/li&gt;
&lt;li&gt;client-side streaming RPC : 클라이언트가 요청을 여러번 보내고, 요청이 끝나면 응답을 보냄&lt;/li&gt;
&lt;li&gt;bidirectional streaming RPC : 양방향 독립적으로 요청/응답을 보냄&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;기존 HTTP 보다 높은 헤더 압축률이 보장되고, ProtoBuf의 직렬화에 의해 전송되는 메시지가 획기적으로 줄어듬&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;3-grpc-vs-rest&quot;&gt;3. gRPC vs REST&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Payload 의 차이
&lt;ul&gt;
&lt;li&gt;gRPC는 Protobuf 형식 자체 직렬화된 데이터&lt;/li&gt;
&lt;li&gt;REST 는 JSON 데이터를 주고받음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HTTP 버전의 차이
&lt;ul&gt;
&lt;li&gt;gRPC는 HTTP/2 기반 통신&lt;/li&gt;
&lt;li&gt;REST는 일반적으로 HTTP/1.1 통신&lt;/li&gt;
&lt;li&gt;때문에 gRPC는 스트리밍, 헤더 압축 등 HTTP/2 의 여러 장점을 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;호출방식의 차이
&lt;ul&gt;
&lt;li&gt;gRPC는 proto파일에 정의한&lt;span&gt;&amp;nbsp;&lt;/span&gt;message,&lt;span&gt;&amp;nbsp;&lt;/span&gt;service&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 각 언어에 필요한 형태로 generate됨. 클라이언트에서 service 메소드를 호출하면 그에 해당하는 서버에서 구현한 service가 실행되고, 요청/응답 페이로드는 언어에 맞게 generate 결과를 사용&lt;/li&gt;
&lt;li&gt;REST는 endpoint 를 HTTP Method + URI 로 표현하고, 페이로드 처리는 각 서버/클라이언트 측에서 각각 부담&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;4-grpc의-활용&quot;&gt;4. gRPC의 활용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;gRPC는 단일 인스턴스로 동작하는 CRUD 웹 애플리케이션에서 부터 수십 수백개의 인스턴스가 상호 작용하는 MSA 까지 모든 구조에서 사용 가능&lt;/li&gt;
&lt;li&gt;클라이언트 측에 gRPC 스텁을 library 등으로 제공해줘야하는 부분은 존재하는등 몇 가지 한계점도 존재 -&amp;gt; 호출 인터페이스 측면에서 웹 환경에서 사용한다면 REST 방식에 비해 일부제약&lt;/li&gt;
&lt;li&gt;gRPC Gateway&lt;span&gt;&amp;nbsp;&lt;/span&gt;플러그인을 사용하면 gRPC 서비스에 REST API 인터페이스를 제공할 수 있게&lt;span&gt;&amp;nbsp;&lt;/span&gt;go&lt;span&gt;&amp;nbsp;&lt;/span&gt;런타임에서 작동하는 프록시 서버와 Swagger 문서를 generate 해준다. (go 이외 다른 언어는 미지원) 때문에 gRPC 서비스를 이용할 준비가 되지 않은 클라이언트의 경우나 웹 환경에서 사용 가능&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/wejrowski/grpc-gateway-java-gradle&quot;&gt;https://github.com/wejrowski/grpc-gateway-java-gradle&lt;/a&gt; (Spring Boot gRPC + grpc-gateway 예시)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://grpc-ecosystem.github.io/grpc-gateway/&quot;&gt;https://grpc-ecosystem.github.io/grpc-gateway/&lt;/a&gt; (grpc-gateway 공식 문서)&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eCy7Cw/btqA3fCl4SC/yuxYy1ywv8vDBhDo8dbD1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eCy7Cw/btqA3fCl4SC/yuxYy1ywv8vDBhDo8dbD1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eCy7Cw/btqA3fCl4SC/yuxYy1ywv8vDBhDo8dbD1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeCy7Cw%2FbtqA3fCl4SC%2FyuxYy1ywv8vDBhDo8dbD1k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타</category>
      <category>gRPC</category>
      <category>HTTP/2</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/94</guid>
      <comments>https://do-study.tistory.com/94#entry94comment</comments>
      <pubDate>Mon, 13 Jan 2020 03:16:44 +0900</pubDate>
    </item>
    <item>
      <title>Spring Ehcache 사용 간략한 정리</title>
      <link>https://do-study.tistory.com/93</link>
      <description>&lt;p&gt;1. Spring Cache Abstraction&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html#boot-features-caching-provider&quot;&gt;https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html#boot-features-caching-provider&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;2. 적용2-1. @EnableCaching&lt;/p&gt;
&lt;p&gt;Spring Boot Application 설정시&lt;span&gt;&amp;nbsp;&lt;/span&gt;@EnableCaching&lt;span&gt;&amp;nbsp;&lt;/span&gt;어노테이션 추가하여 Application에 캐시 기능 사용하겠다는 것을 알린다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578762627067&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootApplication
@ComponentScan(&quot;com.nhnent.gia&quot;)
@EntityScan(basePackages = {&quot;com.nhnent.gia.model&quot;}, basePackageClasses = {Application.class, Jsr310JpaConverters.class})
@EnableCaching 
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이후 추가적이 설정이 없는 경우 기본 캐시&lt;span&gt;&amp;nbsp;&lt;/span&gt;ConcurrentHasnMap&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용하고, 다른 캐시 라이브러리를 추가하면 Spring Boot의&lt;span&gt;&amp;nbsp;&lt;/span&gt;Auto Detect기능에 따라 해당 라이브러리를 자동으로 사용하게 된다.&lt;/p&gt;
&lt;p&gt;2-2. ehcache.xml 작성&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.ehcache.org/documentation/2.8/configuration/configuration.html&quot;&gt;https://www.ehcache.org/documentation/2.8/configuration/configuration.html&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1578762666530&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;ehcache xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:noNamespaceSchemaLocation=&quot;http://www.ehcache.org/ehcache.xsd&quot;
         updateCheck=&quot;true&quot;
         moritoring=&quot;autodetect&quot;
         dynamicConfig=&quot;true&quot;
         maxBytesLocalHeap=&quot;4M&quot;&amp;gt;
    
	&amp;lt;cache name=&quot;recentWinHistory&quot;
                eternal=&quot;false&quot;
                timeToIdleSeconds=&quot;0&quot;
                timeToLiveSeconds=&quot;300&quot;
                overflowToDisk=&quot;false&quot;
                diskPersistent=&quot;false&quot;
                diskExpiryThreadIntervalSeconds=&quot;120&quot;
                memoryStoreEvictionPolicy=&quot;LRU&quot;&amp;gt;
	&amp;lt;/cache&amp;gt;
&amp;lt;/ehcache&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2-3. application.properties추가&lt;/p&gt;
&lt;p&gt;캐시 설정파일&lt;span&gt;&amp;nbsp;&lt;/span&gt;ehcache.xml&lt;span&gt;&amp;nbsp;&lt;/span&gt;경로 설정&lt;/p&gt;
&lt;pre id=&quot;code_1578762689397&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.cache.ehcache.config=classpath:ehcache.xml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2-4. @Cacheable&lt;/p&gt;
&lt;p&gt;ehcache.xml에서 정의한 캐시를&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Cacheable(&quot;{name}&quot;)&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로 적용&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기본&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1578762719902&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Cacheable(&quot;recentWinHistory&quot;)
List&amp;lt;WinHistory&amp;gt; findAllWinHistory(Integer eventCode, Integer eventNo);&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;파라미터별로 캐싱하고 싶은 경우 캐시가 eventCode값 단위로 캐싱된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578762761966&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Cacheable(&quot;recentWinHistory&quot;, key=&quot;#eventCode&quot;)
List&amp;lt;WinHistory&amp;gt; findAllWinHistory(Integer eventCode, Integer eventNo);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Back-End/Spring framework</category>
      <category>EHCache</category>
      <category>Spring Framework</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/93</guid>
      <comments>https://do-study.tistory.com/93#entry93comment</comments>
      <pubDate>Sun, 12 Jan 2020 02:12:58 +0900</pubDate>
    </item>
    <item>
      <title>저자 블로그, git 정보</title>
      <link>https://do-study.tistory.com/notice/92</link>
      <description>&lt;p&gt;Github: &lt;a href=&quot;https://github.com/hyungyu-lee&quot; target=&quot;_blank&quot; class=&quot;tx-link&quot;&gt;https://github.com/hyungyu-lee&lt;/a&gt;&lt;/p&gt;&lt;p&gt;개츠비 블로그:&amp;nbsp;&lt;a href=&quot;https://deveely-log.netlify.app/&quot;&gt;https://deveely-log.netlify.app/&lt;/a&gt;&lt;/p&gt;</description>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/notice/92</guid>
      <pubDate>Tue, 24 Dec 2019 09:36:40 +0900</pubDate>
    </item>
    <item>
      <title>mac terminal 에서 특정 프로젝트를 vscode 또는 IntelliJ 실행하기</title>
      <link>https://do-study.tistory.com/91</link>
      <description>&lt;p&gt;mac 사용하다 보면 ide에서 Finder UI를 통해 특정 프로젝트를 open 하는 일이 자주 있는데요. 저의 경우 vscode와 IntelliJ에서 그런 경우가 자주 있는 편입니다.&lt;/p&gt;
&lt;h2 id=&quot;특정-프로젝트를-vscode로-실행하기&quot;&gt;특정 프로젝트를 vscode로 실행하기&lt;/h2&gt;
&lt;p&gt;vscode 의 경우 아주 간단합니다. 터미널에서 프로젝트 경로로 이동한 후&lt;span&gt;&amp;nbsp;&lt;/span&gt;code .&lt;span&gt;&amp;nbsp;&lt;/span&gt;명령어를 입력하면 해당 프로젝트 open으로 vscode가 실행됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1576915425029&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd 프로젝트경로
code .&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;특정-프로젝트를-intellij로-실행하기&quot;&gt;특정 프로젝트를 IntelliJ로 실행하기&lt;/h2&gt;
&lt;p&gt;IntelliJ의 경우도 vscode와 유사하지만 1가지 차이점이 있습니다. IntelliJ 실행 시 사용하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;idea&lt;span&gt;&amp;nbsp;&lt;/span&gt;명령어가 있는 경로가 환경변수에 등록되어 있지 않기 때문인데요. 이는 IntelliJ에서 간단히 추가가 가능합니다. IntelliJ를 실행 후&lt;span&gt;&amp;nbsp;&lt;/span&gt;Tools &amp;gt; Create Command-line Launcher...&lt;span&gt;&amp;nbsp;&lt;/span&gt;메뉴를 클릭하면 터미널에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;idea명령어를 바로 사용할 수 있게됩니다.&lt;/p&gt;
&lt;p&gt;명령어를 환경변수에 추가한 후 실행할 프로젝트로 이동해 아래 명령어를 입력하면 해당 프로젝트 open 으로 IntelliJ가 실행되게 됩니다. 아래 예시의 경우 제가 실행할 프로젝트가 maven 프로젝트여서&lt;span&gt;&amp;nbsp;&lt;/span&gt;pom.xml을 대상 파일로 지정했구요. gradle이면&lt;span&gt;&amp;nbsp;&lt;/span&gt;build.gradle&lt;span&gt;&amp;nbsp;&lt;/span&gt;등 다른 타입의 프로젝트는 해당하는 파일을 지정하면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1576915443639&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd 프로젝트경로
idea pom.xml&lt;/code&gt;&lt;/pre&gt;</description>
      <category>기타</category>
      <category>IntelliJ</category>
      <category>Mac</category>
      <category>vscode</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/91</guid>
      <comments>https://do-study.tistory.com/91#entry91comment</comments>
      <pubDate>Sat, 21 Dec 2019 17:08:07 +0900</pubDate>
    </item>
    <item>
      <title>git commit 날짜, author 변경하기</title>
      <link>https://do-study.tistory.com/90</link>
      <description>&lt;p&gt;git을 사용하면 거의 대부분 원격 저장소로 github을 많이 이용합니다. github엔 저장소에 commit 내역을 시각화하여 보여주는 기능(잔디밭..)이 있는데요. 가끔 실수로 날짜를 못지키거나, author를 잘못지정해 색칠이 안되는 경우가 있습니다. 잔디밭에 신경을 쓰시는 분이라면 빈 구멍이 굉장히 신경이 쓰이실수 있는데요. 이 때 사용할 수 있는 방법을 공유합니다.&lt;/p&gt;
&lt;h2 id=&quot;git-commit-변경&quot;&gt;git commit 변경&lt;/h2&gt;
&lt;p&gt;꼭 잔디밭이 아니더라도 commit의 날짜를 변경해야하는 경우는 종종 있을것입니다.&lt;/p&gt;
&lt;p&gt;기본적으로 commit 변경은 변경할 commit을 찾는것에서 시작합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. commit 히스토리 확인&lt;/p&gt;
&lt;pre id=&quot;code_1576915038672&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git log&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;git log&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;명령어를 실행하면 아래와 같이 commit 히스토리를 확인할 수 있습니다. 아래 내역에서 수정하고싶은 commit의 직전 commit의 해쉬값을 복사합니다. 이 예시에서 저는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;책 목차 추가&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;커밋을 수정하기 위해 그 아래 커밋인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;65068f594d469e848beab5fd475219f428339436&lt;span&gt;값을 사용하겠습니다. log 명령 결과화면에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;q&lt;span&gt;를 입력하면 화면이 종료됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1576915087109&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;commit 5b52b3874068dfd0b1f77023eac03113f3b6db9a
Author: HyunGyu-Lee &amp;lt;gusrb0808@naver.com&amp;gt;
Date:   Mon Dec 9 00:32:41 2019 +0900

    컨텐츠 추가(DDD Start!)

commit 0c6be06e2ede8cac5bc1b78b511620583e159bf5
Author: HyunGyu-Lee &amp;lt;gusrb0808@naver.com&amp;gt;
Date:   Fri Nov 8 13:35:32 2019 +0900

    책 목차 추가

commit 65068f594d469e848beab5fd475219f428339436
Author: HyunGyu-Lee &amp;lt;gusrb0808@naver.com&amp;gt;
Date:   Tue Nov 5 01:01:48 2019 +0900

    collapse 태그 인식 불가 분제 fix

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2. 날짜/author 변경&lt;/p&gt;
&lt;p&gt;해쉬값을 확인한 후&lt;span&gt;&amp;nbsp;&lt;/span&gt;git rebase&lt;span&gt;&amp;nbsp;&lt;/span&gt;명령을 입력합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1576915113697&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git rebase -i 65068f594d469e848beab5fd475219f428339436&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그럼 아래와 같은 UI가 나오는데요. 변경할 대상 커밋의 커멘드를&lt;span&gt;&amp;nbsp;&lt;/span&gt;edit으로 변경하고 저장합니다. (vi)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자세한 커멘드는 아래 주석에 적혀있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1576915212357&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;edit 0c6be06 책 목차 추가
pick 5b52b38 컨텐츠 추가(DDD Start!)

# Rebase 65068f5..d896625 onto 65068f5 (4 commands)
#
# Commands:
# p, pick &amp;lt;commit&amp;gt; = use commit
# r, reword &amp;lt;commit&amp;gt; = use commit, but edit the commit message
# e, edit &amp;lt;commit&amp;gt; = use commit, but stop for amending
# s, squash &amp;lt;commit&amp;gt; = use commit, but meld into previous commit
# f, fixup &amp;lt;commit&amp;gt; = like &quot;squash&quot;, but discard this commit's log message
# x, exec &amp;lt;command&amp;gt; = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop &amp;lt;commit&amp;gt; = remove commit
# l, label &amp;lt;label&amp;gt; = label current HEAD with a name
# t, reset &amp;lt;label&amp;gt; = reset HEAD to a label
# m, merge [-C &amp;lt;commit&amp;gt; | -c &amp;lt;commit&amp;gt;] &amp;lt;label&amp;gt; [# &amp;lt;oneline&amp;gt;]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c &amp;lt;commit&amp;gt; to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;그럼 다음과 같이 rebase 모드에 진입하게 되고 해당 커밋으로 이동하게됩니다. 수정을 하려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;git commit --amend&lt;span&gt;를 수정하지 않고 넘어가려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;git rebase --continue&lt;span&gt;를 입력하면 됩니다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1576915238724&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt; git rebase -i 65068f594d469e848beab5fd475219f428339436
Stopped at 0c6be06...  책 목차 추가
You can amend the commit now, with

  git commit --amend

Once you are satisfied with your changes, run

  git rebase --continue
&amp;gt;&amp;gt; &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;변경할 커밋에서 amend 를 통해 author 혹은 날짜를 수정하면 됩니다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1576915267909&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 날짜 변경
GIT_COMMITTER_DATE=&quot;{날짜}&quot; git commit --amend --no-edit --date &quot;{날짜}&quot;

# author 변경
git commit --amend --author &quot;username &amp;lt;email&amp;gt;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;그다음 아까 언급한것처럼 수정을 하려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;git commit --amend&lt;span&gt;를 수정하지 않고 넘어가려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;git rebase --continue&lt;span&gt;를 입력하면 되고, 더 이상 수정할 것이 없으면 rebase 모드가 종료되게 됩니다. 수정을 마치고 push 하여 마무리 해주면 github에도 반영되게 됩니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>DevOps</category>
      <category>commit</category>
      <category>GIT</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/90</guid>
      <comments>https://do-study.tistory.com/90#entry90comment</comments>
      <pubDate>Sat, 21 Dec 2019 17:01:46 +0900</pubDate>
    </item>
    <item>
      <title>NHN FORWARD 2019 후기</title>
      <link>https://do-study.tistory.com/89</link>
      <description>&lt;p&gt;NHN Forward 2019 컨퍼런스 참여 후기입니다.&lt;/p&gt;
&lt;h2 id=&quot;nhn-forward-2019&quot;&gt;NHN Forward 2019&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbLatu/btqAv0jG85r/CCxxLYlUl8QqbnrRBze6i1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbLatu/btqAv0jG85r/CCxxLYlUl8QqbnrRBze6i1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbLatu/btqAv0jG85r/CCxxLYlUl8QqbnrRBze6i1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbLatu%2FbtqAv0jG85r%2FCCxxLYlUl8QqbnrRBze6i1%2Fimg.jpg&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;지난 11월 27일&lt;span&gt;&amp;nbsp;&lt;/span&gt;NHN Forward 2019&lt;span&gt;&amp;nbsp;&lt;/span&gt;컨퍼런스에 참가했습니다. 회사가 회사다 보니 다양한 컨텐츠들이 준비되어 있었는데요. 크게 아래와 같았습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;세션
&lt;ol&gt;
&lt;li&gt;발표세션&lt;/li&gt;
&lt;li&gt;스몰 스텝&lt;/li&gt;
&lt;li&gt;프런트엔드 상담소&lt;/li&gt;
&lt;li&gt;커뮤니티 라운지&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;워크숍&lt;/li&gt;
&lt;li&gt;핸즈온 랩&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs2JsS/btqAtcToDVX/KWSsoBzX3hRfoB1ujjAsd0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs2JsS/btqAtcToDVX/KWSsoBzX3hRfoB1ujjAsd0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs2JsS/btqAtcToDVX/KWSsoBzX3hRfoB1ujjAsd0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs2JsS%2FbtqAtcToDVX%2FKWSsoBzX3hRfoB1ujjAsd0%2Fimg.jpg&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;프론트엔드 전문 개발자분들이 직접 고민을 들어주고 대화를 할 수 있는 프런트엔드 상담소나 평소에 궁금했거나 해보고 싶었던 것들을 쉽게 따라해보고 경혐해 볼 수 있었던 핸즈온 랩 등 준비를 정말 많이 했다는 느낌이 들었습니다.&lt;/p&gt;
&lt;p&gt;특히 발표세션에서는 실무에서 겪을수 있는 문제점들이나 겪었던 문제들을 사례를 많이 공유받을수 있어 유익한 시간이었습니다.&lt;/p&gt;
&lt;p&gt;발표세션은 총 7개 트랙에서 시간대별로 각각 강의가 주로 진행되었는데요. 저는 아래와 같이 세션을 참여했습니다.&lt;/p&gt;
&lt;h2 id=&quot;발표세션&quot;&gt;발표세션&lt;/h2&gt;
&lt;p&gt;1. &amp;lsquo;깃&amp;rsquo;깔나는 Git 워크 플로우 알아보기&lt;/p&gt;
&lt;p&gt;개발에 필수인 깃을 더 잘 쓰는 방법에 대해 사례를 통해 들을 수 있었습니다. 특히 전 GitFlow 이외에 깃헙 플로우, 깃랩 플로우가 있는 거는 처음알았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;깃 플로우: 메인브랜치(master / develop)와 서포팅프랜치 (feature, release, hotfix) 을 활용한 깃 워크플로&lt;/li&gt;
&lt;li&gt;깃헙 플로우: 깃 플로우가 대부분의 케이스에서 갖는 복잡함을 해소하고자 master / topic 브랜치를 활용해 간단하지만 강력한 워크플로&lt;/li&gt;
&lt;li&gt;깃랩 플로우: 깃 플로우는 너무 복잡하고, 깃헙 플로우는 너무 단순하다. 절충안을 제안&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2. DDD-Lite@Spring&lt;/p&gt;
&lt;p&gt;NHN의 서비스 중 Dooray! 의 위키 서비스를 사례로 애플리케이션의 복잡함으로 고통받는 개발자들을 위해 DDD를 소개하고, 어떻게 복잡함을 극복할 수 있는지 소개했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DDD에서 모델링 시 속성보다는 행위가 우선이다.&lt;/li&gt;
&lt;li&gt;속성은 어떤 행위를 할 때 필요하면 그 때 추가한다.&lt;/li&gt;
&lt;li&gt;JPA로 생각해보면 바로 양방향 매핑을 하지 않고 우선 단방향 매핑을 하고 필요 시 추가로 양방향 매핑을 한다.&lt;/li&gt;
&lt;li&gt;도메인 모델을 풍부하게 만들어준다. (Rich Domain Model)&lt;/li&gt;
&lt;li&gt;여러 변 연산을 적용해도 그 결과가 달라지지 않는 성질, 멱등성을 유지하도록 설계한다.&lt;/li&gt;
&lt;li&gt;헥사고날 아키텍쳐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nDSzC/btqAuMM1xLS/kDn9cY7Nv1hII9rb1eKez0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nDSzC/btqAuMM1xLS/kDn9cY7Nv1hII9rb1eKez0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nDSzC/btqAuMM1xLS/kDn9cY7Nv1hII9rb1eKez0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnDSzC%2FbtqAuMM1xLS%2FkDn9cY7Nv1hII9rb1eKez0%2Fimg.jpg&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;3. 레거시 웹 서비스 길들이기: 서버 개발자의 SPA 적용기&lt;/p&gt;
&lt;p&gt;저 또한 Spring 기반 웹 백엔드 애플리케이션을 만드는 개발자 다 보니 이 세션에서 소개해주신 사례들이 하나하나가 공감가는 사례들이었습니다. 제가 평소에 겪었던 고민들에 대한 해결책을 들을수있어 좋았습니다.&lt;/p&gt;
&lt;p&gt;적용하기전의 고민 / 적용하면서의 고민 / 현재 팀의 상황 / 효율적인 빌드를 위한 프로젝트 구성 / API 호출 방식 with Zuul /상태관리 / 다국어처리  &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHGdgs/btqAtcePiYM/QfqvV5UfhmjNGsDoAg8m80/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHGdgs/btqAtcePiYM/QfqvV5UfhmjNGsDoAg8m80/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHGdgs/btqAtcePiYM/QfqvV5UfhmjNGsDoAg8m80/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHGdgs%2FbtqAtcePiYM%2FQfqvV5UfhmjNGsDoAg8m80%2Fimg.jpg&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;4. Spring JPA의 사실과 오해&lt;/p&gt;
&lt;p&gt;Spring JPA에 평소에 잘 알려지지 않은 사실들과 오해들을 소개해주셨습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;연관관계: 대부분의 경우는 단방향 매핑이 간단, 하지만 영속성 전이를 사용하는 경우 양방향 매핑을 사용하자 (추가 update 쿼리 방지)&lt;/li&gt;
&lt;li&gt;JpaRepository에 대한 사실
&lt;ul&gt;
&lt;li&gt;JpaRepository를 상속하는 것만으로도 대부분의 왠만한 기능을 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;JpaRepository로도 JOIN이 가능하다.&lt;/li&gt;
&lt;li&gt;JpaRepository로도 다양한 DTO Projection이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buq4fM/btqAs24x8sM/AuiFiIJ8DFqcr0iYj8hRwk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buq4fM/btqAs24x8sM/AuiFiIJ8DFqcr0iYj8hRwk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buq4fM/btqAs24x8sM/AuiFiIJ8DFqcr0iYj8hRwk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbuq4fM%2FbtqAs24x8sM%2FAuiFiIJ8DFqcr0iYj8hRwk%2Fimg.jpg&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;5. Paas &amp;amp; API Experience: 좋은 API DX를 제공하기 위한 작은 걸음&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rTjH3/btqAtBFqM69/1ZLTAtUWtjAgP5yT4ZZzPk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rTjH3/btqAtBFqM69/1ZLTAtUWtjAgP5yT4ZZzPk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rTjH3/btqAtBFqM69/1ZLTAtUWtjAgP5yT4ZZzPk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrTjH3%2FbtqAtBFqM69%2F1ZLTAtUWtjAgP5yT4ZZzPk%2Fimg.jpg&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHk44t/btqAsSA44pF/Xxn0MCTvUa6XMsYr3uoFk1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHk44t/btqAsSA44pF/Xxn0MCTvUa6XMsYr3uoFk1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHk44t/btqAsSA44pF/Xxn0MCTvUa6XMsYr3uoFk1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHk44t%2FbtqAsSA44pF%2FXxn0MCTvUa6XMsYr3uoFk1%2Fimg.jpg&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jp5mm/btqAuL1BDNk/4cHk4NSEQOHd3HHXdtLiT0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jp5mm/btqAuL1BDNk/4cHk4NSEQOHd3HHXdtLiT0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jp5mm/btqAuL1BDNk/4cHk4NSEQOHd3HHXdtLiT0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJp5mm%2FbtqAuL1BDNk%2F4cHk4NSEQOHd3HHXdtLiT0%2Fimg.jpg&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;마치며&quot;&gt;마치며&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9Vccl/btqAs3PXx2p/73Rd2Pup3OvhlLBVqlzhU1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9Vccl/btqAs3PXx2p/73Rd2Pup3OvhlLBVqlzhU1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9Vccl/btqAs3PXx2p/73Rd2Pup3OvhlLBVqlzhU1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9Vccl%2FbtqAs3PXx2p%2F73Rd2Pup3OvhlLBVqlzhU1%2Fimg.jpg&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;NHN Forward 2019의 심볼인&amp;nbsp;&amp;raquo; 는 리달리디렉션 기호&amp;nbsp;&amp;raquo;를 형상화한 것으로 지식과 경험을 더하고 쌓을 수 있기를 바라는 마음을 담았다고 합니다.&lt;/p&gt;
&lt;p&gt;이런 NHN의 의지만큼 많은 것을 경험하고, 배우고, 즐거웠던 1분 1초가 아깝지 않은 시간이었습니다.&lt;/p&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>NHN</category>
      <category>NHNFORWARD2019</category>
      <author>@deveely</author>
      <guid isPermaLink="true">https://do-study.tistory.com/89</guid>
      <comments>https://do-study.tistory.com/89#entry89comment</comments>
      <pubDate>Mon, 16 Dec 2019 10:33:27 +0900</pubDate>
    </item>
  </channel>
</rss>