<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>열정은 차갑고 잔잔하게</title>
    <link>https://ksabs.tistory.com/</link>
    <description>이 세상의 api가 되고픈 개발자</description>
    <language>ko</language>
    <pubDate>Fri, 26 Jun 2026 06:11:11 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>API_Dev</managingEditor>
    <image>
      <title>열정은 차갑고 잔잔하게</title>
      <url>https://tistory1.daumcdn.net/tistory/4411904/attach/fcf8552d59bb4758b95a7371cfeca89d</url>
      <link>https://ksabs.tistory.com</link>
    </image>
    <item>
      <title>컬리 태그 클라우드(키워드 리뷰) 서비스 도입기</title>
      <link>https://ksabs.tistory.com/270</link>
      <description>&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;현재 재직중인 회사에서 사내 서비스를 개선하는 아이디어를 만들어 볼 수 있는 좋은 기회를 얻게 되었습니다. 컬리의 리뷰 서비스를 좀 더 고도화 해보았고 돈 걱정 없이 AI 기술을 적극 활용해 볼 수 있는 아주 좋은 기회였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬리의 리뷰는 아주 많은 데이터가 있지만 원하는 리뷰를 찾기에는 아직 어렵다고 느껴졌습니다. 타 서비스에서는 리뷰를 작성할때마다 어떤 부분이 좋았는지 강제로 선택하게 하여 요약된 결과를 제공하거나, 이미 있는 리뷰의 키워드들을 카운팅해서 노출해주어 어떤 리뷰가 주로 작성되어있는지 다 읽어보지 않고도 파악할 수 있게 해줍니다.&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-06 오전 1.00.32.png&quot; data-origin-width=&quot;1469&quot; data-origin-height=&quot;739&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfzDpz/btsLDtCOYSF/4rUlRqJ03DuGPnzUYia9NK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfzDpz/btsLDtCOYSF/4rUlRqJ03DuGPnzUYia9NK/img.png&quot; data-alt=&quot;타 서비스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfzDpz/btsLDtCOYSF/4rUlRqJ03DuGPnzUYia9NK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfzDpz%2FbtsLDtCOYSF%2F4rUlRqJ03DuGPnzUYia9NK%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;1469&quot; height=&quot;739&quot; data-filename=&quot;스크린샷 2025-01-06 오전 1.00.32.png&quot; data-origin-width=&quot;1469&quot; data-origin-height=&quot;739&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;타 서비스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-06 오전 1.00.49.png&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oLUof/btsLDKRR2AS/t2n04RBxGYoC5XL3w5xQCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oLUof/btsLDKRR2AS/t2n04RBxGYoC5XL3w5xQCk/img.png&quot; data-alt=&quot;컬리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oLUof/btsLDKRR2AS/t2n04RBxGYoC5XL3w5xQCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoLUof%2FbtsLDKRR2AS%2Ft2n04RBxGYoC5XL3w5xQCk%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;403&quot; height=&quot;422&quot; data-filename=&quot;스크린샷 2025-01-06 오전 1.00.49.png&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컬리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;b&gt;사용자가&amp;nbsp;원하는 리뷰에 빠르게 도달시키자&lt;/b&gt;' 라는 목표를 세웠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-06 오전 1.04.14.png&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;817&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRo5B2/btsLFfJTftu/AbmFILrWhvmcka5ajQz7D1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRo5B2/btsLFfJTftu/AbmFILrWhvmcka5ajQz7D1/img.png&quot; data-alt=&quot;서비스 기획화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRo5B2/btsLFfJTftu/AbmFILrWhvmcka5ajQz7D1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRo5B2%2FbtsLFfJTftu%2FAbmFILrWhvmcka5ajQz7D1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1470&quot; height=&quot;817&quot; data-filename=&quot;스크린샷 2025-01-06 오전 1.04.14.png&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;817&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서비스 기획화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;태그 클라우드(키워드 리뷰) 서비스 아키텍처&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 그림은 해커톤 기간 총 2.5일간 구현한 서비스 아키텍처 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 컬리 프론트 화면을 구현하지 않고도, 실제 컬리 프론트 위에서 구동하는 것처럼 보이기 위해 클라이언트를 크롬 익스텐션을 이용해 구현하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-04 오후 4.54.47.png&quot; data-origin-width=&quot;1956&quot; data-origin-height=&quot;1098&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEaJ8e/btsLEFPzW9Y/K7HS0BQdsuov95CEi2eXM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEaJ8e/btsLEFPzW9Y/K7HS0BQdsuov95CEi2eXM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEaJ8e/btsLEFPzW9Y/K7HS0BQdsuov95CEi2eXM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEaJ8e%2FbtsLEFPzW9Y%2FK7HS0BQdsuov95CEi2eXM0%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;1956&quot; height=&quot;1098&quot; data-filename=&quot;스크린샷 2025-01-04 오후 4.54.47.png&quot; data-origin-width=&quot;1956&quot; data-origin-height=&quot;1098&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;&lt;b&gt;1. 워드 클라우드 데이터 구축&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1rb3s/btsLEerq8WX/j1e5nUpd1uj6F33fC7MHnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1rb3s/btsLEerq8WX/j1e5nUpd1uj6F33fC7MHnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1rb3s/btsLEerq8WX/j1e5nUpd1uj6F33fC7MHnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1rb3s%2FbtsLEerq8WX%2Fj1e5nUpd1uj6F33fC7MHnK%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;1281&quot; height=&quot;718&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 제품의 리뷰를 한 눈에, 더 쉽게 파악하기 위해서 다음 UI를 구성하는 것을 목표로 잡았습니다.&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;를 구축하기로 결정하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-04 오후 5.59.12.png&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4DspL/btsLDmXIsH5/aweJY6taMZrLoCgkpf5Od1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4DspL/btsLDmXIsH5/aweJY6taMZrLoCgkpf5Od1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4DspL/btsLDmXIsH5/aweJY6taMZrLoCgkpf5Od1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4DspL%2FbtsLDmXIsH5%2FaweJY6taMZrLoCgkpf5Od1%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;1270&quot; height=&quot;872&quot; data-filename=&quot;스크린샷 2025-01-04 오후 5.59.12.png&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;워드클라우드란?&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-04 오후 5.10.46.png&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7t33r/btsLDM2XABb/faMS5LDEk1Kj9kiZpHkndK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7t33r/btsLDM2XABb/faMS5LDEk1Kj9kiZpHkndK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7t33r/btsLDM2XABb/faMS5LDEk1Kj9kiZpHkndK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7t33r%2FbtsLDM2XABb%2FfaMS5LDEk1Kj9kiZpHkndK%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;1326&quot; height=&quot;288&quot; data-filename=&quot;스크린샷 2025-01-04 오후 5.10.46.png&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;size18&quot;&gt;&lt;b&gt;LLM (GPT api) 을 통한 각 리뷰 키워드 추출&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;리뷰를 키워드로 바꾸어 워드클라우드로 구축해야합니다. 실제 리뷰 내용에는 해당 상품을 특정하는 정보와 관련없는 내용(ex. 합니다, 써보다, 좋아요, 이용하다, 구입하다 등) 이 많이 포함되어있습니다. 이 내용들을 모두 포함하여 워드클라우드로 구축하면 실제 상품의 사용기를 특정하는 내용보다는 쓸모없는 내용들로 워드클라우드 데이터가 채워지게 됩니다. 양질의 워드클라우드를 구축하기 위해선 각 리뷰에서&amp;nbsp;&lt;/span&gt;&lt;b&gt;사용자가 느끼는 감정이나 특징, 혹은 제품의 특징이 담긴 키워드&lt;/b&gt;를 추출해내야 합니다. 수십만 건의 리뷰데이터에서 사람이 하나씩 추출해낼 순 없으므로 LLM을 이용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4mtlK/btsLEdyZX04/fLiHHoTAKmKF3IMptj7jdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4mtlK/btsLEdyZX04/fLiHHoTAKmKF3IMptj7jdK/img.png&quot; data-alt=&quot;리뷰 기반 키워드 추출&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4mtlK/btsLEdyZX04/fLiHHoTAKmKF3IMptj7jdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4mtlK%2FbtsLEdyZX04%2FfLiHHoTAKmKF3IMptj7jdK%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;1114&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;1114&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;리뷰 기반 키워드 추출&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리뷰 추출에 이용한 프롬프트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트의 효율과 정확성을 올리기 위하여 다음 4단계를 적용하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 역할을 부여하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 지침을 정확히 지시하며&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;4. 응답형식과 예시를 알려줍니다. (&lt;b&gt;Few Shot&lt;/b&gt; 적용)&lt;/p&gt;
&lt;pre id=&quot;code_1735978981067&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;system_msg = &quot;&quot;&quot;너는 리뷰를 읽고 키워드를 추출해주는 에이전트야.

# 지침
1. 입력으로 여러 개의 리뷰가 주어질 거야. 각 리뷰는 번호가 매겨져 있어.
2. 각 리뷰마다 키워드를 추출해줘.
3. 키워드는 한 단어로만 표현돼.

# 키워드 추출 기준
- 포함해야할 키워드:
  * 리뷰에 직접 언급된 내용 (예: 촉촉함, 발색력, 광택감)
  * 사용자가 느끼는 감정이나 특징 (예: 만족도, 가성비)
  * 제품에 대한 특징 (예: 지속력, 향기)
- 포함하지 말아야할 키워드:
  * 고유 명사 (예: 브랜드명, 제품명)
  * 제품 구성 (예: 세트, 패키지)
  * 단순히 '좋다', '별로' 같은 추상적인 키워드

# 응답 형식
- 반드시 다음과 같은 JSON 형식으로 반환해야 함:
{
  &quot;1&quot;: &quot;키워드1,키워드2,키워드3&quot;,
  &quot;2&quot;: &quot;키워드1,키워드2,키워드3&quot;
}
- 각 키워드는 쉼표(,)로 구분
- 키워드가 없는 경우 빈 문자열(&quot;&quot;) 반환
- 코드 블록(```)이나 다른 마크다운 형식을 사용하지 말 것

# 예시
입력:
1. 색상이 너무 예쁘고 발색도 좋아요
2. 배송이 빨라서 좋네요
3. 가격이 비싸서 망설였는데 품질이 좋네요

출력:
{
  &quot;1&quot;: &quot;예쁨,발색력&quot;,
  &quot;2&quot;: &quot;빠른배송&quot;,
  &quot;3&quot;: &quot;고가,품질&quot;
}&quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Few shot&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 의 응답 성능을 더 높이기 위해 Few Shot을 적용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-04 오후 5.26.47.png&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6uyXC/btsLEYH8Pza/gSxi3Zre7a0PYKHnmSQmXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6uyXC/btsLEYH8Pza/gSxi3Zre7a0PYKHnmSQmXk/img.png&quot; data-alt=&quot;출처: https://www.promptingguide.ai/kr/techniques/fewshot&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6uyXC/btsLEYH8Pza/gSxi3Zre7a0PYKHnmSQmXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6uyXC%2FbtsLEYH8Pza%2FgSxi3Zre7a0PYKHnmSQmXk%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;1730&quot; height=&quot;334&quot; data-filename=&quot;스크린샷 2025-01-04 오후 5.26.47.png&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://www.promptingguide.ai/kr/techniques/fewshot&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용된 부분&lt;/p&gt;
&lt;pre id=&quot;code_1735982656851&quot; class=&quot;lsl&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;&quot;&quot;&quot;
... (생략)

# 예시
입력:
1. 색상이 너무 예쁘고 발색도 좋아요
2. 배송이 빨라서 좋네요
3. 가격이 비싸서 망설였는데 품질이 좋네요

출력:
{
  &quot;1&quot;: &quot;예쁨,발색력&quot;,
  &quot;2&quot;: &quot;빠른배송&quot;,
  &quot;3&quot;: &quot;고가,품질&quot;
}&quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Batch Request&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 개발시 사용한 Azure OpenAI API 에는 분당 API 요청 개수가 정해져 있었습니다. 키워드를 추출해야하는 &lt;b&gt;리뷰의 수가 수십만 건&lt;/b&gt;이라면 제한되어있는 요청의 수로 키워드를 추출하기 위해 많은 시간이 소요되어야하는 상황입니다. 그래서 API 요청 한번에 1개의 리뷰를 처리하는 것이 아닌, &lt;b&gt;50개의 리뷰&lt;/b&gt;씩 리스트 형식으로 응답을 받도록 구현하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1735979726498&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;&quot;&quot;
... (생략)

# 응답 형식
- 반드시 다음과 같은 JSON 형식으로 반환해야 함:
{
  &quot;1&quot;: &quot;키워드1,키워드2,키워드3&quot;,
  &quot;2&quot;: &quot;키워드1,키워드2,키워드3&quot;
}

...

# 예시
입력:
1. 색상이 너무 예쁘고 발색도 좋아요
2. 배송이 빨라서 좋네요
3. 가격이 비싸서 망설였는데 품질이 좋네요

출력:
{
  &quot;1&quot;: &quot;예쁨,발색력&quot;,
  &quot;2&quot;: &quot;빠른배송&quot;,
  &quot;3&quot;: &quot;고가,품질&quot;
}&quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비동기 동시 요청&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분당 요청 개수가 정해져있기 때문에 최대한으로 이용하기 위하여 비동기로 동시요청을 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;워크클라우드 생성 및 LLM 을 이용한 보정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰의 키워드가 다 추출되고나서, 이제 상품별로 모든 키워드를 모아 워드클라우드를 구축하였습니다. 하지만 리뷰 추출 단계에서 LLM 추출한 키워드에서도 약간의 보정이 필요하였습니다. '가격이 싼, 가성비, 저렴' 같은 키워드는 사실 사용자가 보기엔 같은 의미이기때문에 같은 키워드로 카운팅 되어합니다. 이런 비슷한 의미가 키워드가 여러개로 분산되어있으면, 상위 5개의 키워드만 보여준다고 했을때 사용자가 볼 수 있는 키워드 종류가 적어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;ex. 상위 3개의 키워드가 (가격이싼, 가성비, 저렴) 인 것 보다는, (가성비, 촉촉한, 향기로운) 같이 좀 더 다양한 키워드가 있다면 사용자 입장에서 제품을 파악하기가 쉽습니다.&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;b&gt;프롬프트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1735980637614&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;system_prompt = &quot;&quot;&quot;
    당신은 word_cloud 데이터를 읽고 비슷한 의미를 가진 word 끼리 합치는 전문가 입니다.
    
    요청 예시) 유사한 의미를 가진 단어들을 하나로 합치고, count를 합산한 뒤 percentage를 다시 계산해주세요.
    
    입력 JSON 예시:
    {
        &quot;words&quot;: [
            {
                &quot;word&quot;: &quot;가격이 싼&quot;,
                &quot;count&quot;: 3,
                &quot;percentage&quot;: 0.2
            },
            {
                &quot;word&quot;: &quot;저렴&quot;,
                &quot;count&quot;: 3,
                &quot;percentage&quot;: 0.2
            }
        ]
    }
    
    응답은 반드시 다음 JSON 형식으로 작성해주세요:
    {
        &quot;merged_words&quot;: [
            {
                &quot;word&quot;: &quot;가성비&quot;,
                &quot;count&quot;: 6,
                &quot;percentage&quot;: 0.4
            }
        ]
    }
    
    비슷한 의미를 가진 단어들을 하나로 합치고, count는 합산하고, percentage도 다시 계산해주세요.
    &quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;워드 클라우드 구축 결과&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워드 클라우드가 구축되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;427&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceMRmU/btsLFhtU2OP/ThBpagPe8P60eyf7KvBJkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceMRmU/btsLFhtU2OP/ThBpagPe8P60eyf7KvBJkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceMRmU/btsLFhtU2OP/ThBpagPe8P60eyf7KvBJkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceMRmU%2FbtsLFhtU2OP%2FThBpagPe8P60eyf7KvBJkK%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;790&quot; height=&quot;427&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;427&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 사실은 사용자에게 제공될 정보는 워드클라우드 이미지가 아닌, 이미지를 만들기 위해 생성된 '&lt;b&gt;데이터&lt;/b&gt;' 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-04 오후 5.55.53.png&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XQtqK/btsLC2k9Afv/KfMW2U8BCmr5SSFv4ueFI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XQtqK/btsLC2k9Afv/KfMW2U8BCmr5SSFv4ueFI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XQtqK/btsLC2k9Afv/KfMW2U8BCmr5SSFv4ueFI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXQtqK%2FbtsLC2k9Afv%2FKfMW2U8BCmr5SSFv4ueFI1%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;914&quot; height=&quot;240&quot; data-filename=&quot;스크린샷 2025-01-04 오후 5.55.53.png&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 한번 더 LLM 보정 과정을 거치면,&lt;/p&gt;
&lt;pre id=&quot;code_1735981061386&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;system_prompt = &quot;&quot;&quot;
당신은 단어를 적절한 형용사로 변환하는 전문가입니다.
주어진 단어들을 해당 상품의 특성을 잘 나타내는 긍정적인 형용사로 변환해주세요.
형용사로 변환하기 어려운 경우 '변환불가'를 반환하세요. 각 단어에 대해 '원본단어:변환결과' 형식으로 응답해주세요.

'광택' 이라면 '광택이 있는' 같이 해당 단어를 포함하여 형용사로 변환해주면 됩니다.
예시 단어들: 
{
    &quot;광택&quot;: &quot;광택이 있는&quot;,
    &quot;발색&quot;: &quot;발색력이 뛰어난&quot;,
    &quot;가격&quot;: &quot;가격이 착한&quot;
}


JSON 응답 예시:
{
    &quot;원본단어1&quot;: &quot;변환결과1&quot;,
    &quot;원본단어2&quot;: &quot;변환결과2&quot;,
    &quot;원본단어3&quot;: &quot;변환결과3&quot;
}
&quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 키워드 데이터를 제공할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-04 오후 5.54.54.png&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;1048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cj2vB0/btsLC3D839N/CmHbOidjbwlxsrkgkssmQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cj2vB0/btsLC3D839N/CmHbOidjbwlxsrkgkssmQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cj2vB0/btsLC3D839N/CmHbOidjbwlxsrkgkssmQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcj2vB0%2FbtsLC3D839N%2FCmHbOidjbwlxsrkgkssmQk%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;1002&quot; height=&quot;1048&quot; data-filename=&quot;스크린샷 2025-01-04 오후 5.54.54.png&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;1048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. RAG 구축 - Search AI&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XyM4y/btsLC4JYTES/rq2PXQyZakzsk8oxYAwONk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XyM4y/btsLC4JYTES/rq2PXQyZakzsk8oxYAwONk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XyM4y/btsLC4JYTES/rq2PXQyZakzsk8oxYAwONk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXyM4y%2FbtsLC4JYTES%2Frq2PXQyZakzsk8oxYAwONk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;718&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;718&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;이제 해시 태그로 표시된 키워드를 클릭하면 관련된 리뷰만 보이도록 구현하기 위해 Search AI를 구축해야합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-04 오후 6.25.32.png&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;1232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F05a9/btsLFgV5swX/Gq13w3wermQnqCgRgmRmT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F05a9/btsLFgV5swX/Gq13w3wermQnqCgRgmRmT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F05a9/btsLFgV5swX/Gq13w3wermQnqCgRgmRmT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF05a9%2FbtsLFgV5swX%2FGq13w3wermQnqCgRgmRmT0%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;868&quot; height=&quot;1232&quot; data-filename=&quot;스크린샷 2025-01-04 오후 6.25.32.png&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;1232&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Azure AI Search&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-04 오후 6.28.15.png&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4W9XM/btsLC26yup0/sBTwm4aooxZDRj1uKX8GV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4W9XM/btsLC26yup0/sBTwm4aooxZDRj1uKX8GV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4W9XM/btsLC26yup0/sBTwm4aooxZDRj1uKX8GV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4W9XM%2FbtsLC26yup0%2FsBTwm4aooxZDRj1uKX8GV1%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;1464&quot; height=&quot;178&quot; data-filename=&quot;스크린샷 2025-01-04 오후 6.28.15.png&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/azure/search/search-what-is-azure-search&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://learn.microsoft.com/ko-kr/azure/search/search-what-is-azure-search&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1735982823504&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Azure AI 검색 소개 - Azure AI Search&quot; data-og-description=&quot;Azure AI 검색은 AI 기반 정보 검색 플랫폼으로, 개발자가 대규모 언어 모델을 엔터프라이즈 데이터와 결합하는 생성형 AI 앱과 풍부한 검색 환경을 빌드하는 데 도움이 됩니다.&quot; data-og-host=&quot;learn.microsoft.com&quot; data-og-source-url=&quot;https://learn.microsoft.com/ko-kr/azure/search/search-what-is-azure-search&quot; data-og-url=&quot;https://learn.microsoft.com/ko-kr/azure/search/search-what-is-azure-search&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bxuEWd/hyXWvm2LiJ/e5gRpz4qqLKY7KFG4mfLO1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/azure/search/search-what-is-azure-search&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://learn.microsoft.com/ko-kr/azure/search/search-what-is-azure-search&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bxuEWd/hyXWvm2LiJ/e5gRpz4qqLKY7KFG4mfLO1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Azure AI 검색 소개 - Azure AI Search&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Azure AI 검색은 AI 기반 정보 검색 플랫폼으로, 개발자가 대규모 언어 모델을 엔터프라이즈 데이터와 결합하는 생성형 AI 앱과 풍부한 검색 환경을 빌드하는 데 도움이 됩니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;learn.microsoft.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Search 는 한마디로 '&lt;b&gt;자연어로&lt;/b&gt;' '&lt;b&gt;질의&lt;/b&gt;'가 가능한 검색 서비스 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 LLM(gpt) 에게 자연어로 컬리의 리뷰 데이터를 물어본다면, 정확히 답할 수 없습니다. &lt;b&gt;GPT&lt;/b&gt;는 컬리의 &lt;b&gt;리뷰 데이터를 가지고 있지 않기 때문&lt;/b&gt;입니다. 반대로, &lt;b&gt;컬리&lt;/b&gt;의 리뷰데이터는 &lt;b&gt;'자연어로' 질의할 수 없습니다&lt;/b&gt;. DB의 데이터를 검색하기 위해선 SQL문을 작성해야합니다. AI Search 는 이 두 가지 방법을 합쳐서 '자연어로' 우리 데이터에 '질의'할 수 있게 해주는 기술 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자연어로 질의할 수 있게 된다면, 많은 가능성이 열립니다. 예를들어 &quot;가성비와 관련된 리뷰를 정성스러운 순으로 5개 뽑아줘.&quot; 같은 자연어로 질의가 가능해 집니다. 이 질문을 SQL로는 구현해낼 수 없습니다. SQL로 '정성스러운' 이라는 추상적인 기준을 정할 수 없고 '~와 관련된' 같은 의미를 담을 수 없습니다. 하지만 LLM 모델은 이런 추상적인 의미를 이해할 수 있기 때문에 가능합니다.&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;텍스트 임베딩 및 벡터화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 목적은 AI Search 에게 자연어로 묻고(&lt;code&gt;keyword: &quot;가성비가 있는&quot; 와 관련된 내용을 찾아줘.&lt;/code&gt;) &lt;b&gt;원하는 상품&lt;/b&gt;으로 데이터의&amp;nbsp;&lt;b&gt;개수&lt;/b&gt;를 지정해 질의하는 것입니다. 자연어로 질의하기 위해선 먼저, 데이터를 임베딩하여 '벡터화'를 해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트 임베딩이란,&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임베딩은 &lt;b&gt;고차원 공간 상의 특정 표현(Representation)을 의미&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;텍스트, 이미지, 오디오 등 &lt;b&gt;비정형 데이터&lt;/b&gt;를 고정 길이(예: 768, 1024 차원 등) 벡터로 변환합니다.&lt;/li&gt;
&lt;li&gt;임베딩은 &lt;b&gt;의미나 맥락이 비슷한 데이터&lt;/b&gt;가 &lt;b&gt;벡터 공간 상에서도 유사한 위치&lt;/b&gt;에 놓이도록 학습됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xiBBc/btsLEbVLWOT/J16UA4QRBVcnw6HrC55gSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xiBBc/btsLEbVLWOT/J16UA4QRBVcnw6HrC55gSK/img.png&quot; data-alt=&quot;출처: https://medium.com/@minji.sql/%EC%9E%84%EB%B2%A0%EB%94%A9-%EA%B9%8A%EC%9D%B4-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-rag%EC%9D%98-%ED%95%B5%EC%8B%AC-%EA%B8%B0%EC%88%A0-6f077b8344e7&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xiBBc/btsLEbVLWOT/J16UA4QRBVcnw6HrC55gSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxiBBc%2FbtsLEbVLWOT%2FJ16UA4QRBVcnw6HrC55gSK%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;700&quot; height=&quot;393&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://medium.com/@minji.sql/%EC%9E%84%EB%B2%A0%EB%94%A9-%EA%B9%8A%EC%9D%B4-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-rag%EC%9D%98-%ED%95%B5%EC%8B%AC-%EA%B8%B0%EC%88%A0-6f077b8344e7&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬리의 리뷰 데이터 (Raw Data)를 전처리 과정을 거치고, 텍스트 임베딩 모델을 통해 데이터의 '특징'을 추출합니다. 딥러닝 임베딩 모델이 리뷰 데이터가 담고있는 중요한 의미나 패턴, 구조를 벡터 공간에 압축하여 표현합니다. '벡터화된 숫자값'으로 표현하면 이를 활용해 &lt;b&gt;의미 기반의 검색&lt;/b&gt;이 가능해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736085803676&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;               ┌───────────────────────────┐
               │       1) Raw Data         │
               │  (텍스트, 이미지, 오디오)   │
               └───────────────────────────┘
                           │
                           │ (전처리: 토큰화, 정규화 등)
                           ▼
               ┌───────────────────────────┐
               │     2) Preprocessing      │
               │ (필요 없는 기호 제거,      │
               │  텍스트 정규화 등)         │
               └───────────────────────────┘
                           │
                           │ (임베딩 모델에 입력)
                           ▼
               ┌───────────────────────────┐
               │    3) Embedding Model     │
               │ (text-embedding-ada-003)  │
               └───────────────────────────┘
                           │
                           │ (임베딩 모델을 통해 특징 추출)
                           ▼
               ┌───────────────────────────┐
               │   4) N-Dimensional Vector │
               │  (3072 차원에 표현)        │
               └───────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 공간에 텍스트를 표현하게 되면 컴퓨터는 서로 다른 텍스트들에서 임베딩 간의 거리를 계산하여 이들간의 관계를 이해합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pGTCQ/btsLC5ISgLd/6ruATVaQsLiWZUMgrNqAd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pGTCQ/btsLC5ISgLd/6ruATVaQsLiWZUMgrNqAd0/img.png&quot; data-alt=&quot;출처: https://medium.com/@minji.sql/%EC%9E%84%EB%B2%A0%EB%94%A9-%EA%B9%8A%EC%9D%B4-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-rag%EC%9D%98-%ED%95%B5%EC%8B%AC-%EA%B8%B0%EC%88%A0-6f077b8344e7&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pGTCQ/btsLC5ISgLd/6ruATVaQsLiWZUMgrNqAd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpGTCQ%2FbtsLC5ISgLd%2F6ruATVaQsLiWZUMgrNqAd0%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;700&quot; height=&quot;393&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://medium.com/@minji.sql/%EC%9E%84%EB%B2%A0%EB%94%A9-%EA%B9%8A%EC%9D%B4-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-rag%EC%9D%98-%ED%95%B5%EC%8B%AC-%EA%B8%B0%EC%88%A0-6f077b8344e7&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터화된 공간에선 단순히 키워드 일치가 아니라 '의미 기반'으로 검색할 수 있게 됩니다. (king - queen)간의 관계가 벡터 공간 안에서 표현되는 방식은 (man - woman) 같이 서로 반대되는 의미가 벡터 공간에 표현 되는 방식은 서로 비슷합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3auVZ/btsLDxLY8Z9/EkRTQsPptOSdKYTZlLvEP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3auVZ/btsLDxLY8Z9/EkRTQsPptOSdKYTZlLvEP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3auVZ/btsLDxLY8Z9/EkRTQsPptOSdKYTZlLvEP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3auVZ%2FbtsLDxLY8Z9%2FEkRTQsPptOSdKYTZlLvEP1%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;714&quot; height=&quot;480&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬리의 리뷰데이터를 GPT의 text-embedding3 모델을 이용하여 텍스트 임베딩을 하였습니다. 다음의 절차를 거쳐 리뷰데이터를 임베딩하면, 각 row마다 n-tokens 값과 vector 값이 생성됩니다. 이렇게 구축된 벡터값은 추후에 의미 기반으로 리뷰를 검색할 때 사용됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWxVHv/btsLEp0LlLv/dyRCFkKNukxLyPPEjLzKRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWxVHv/btsLEp0LlLv/dyRCFkKNukxLyPPEjLzKRk/img.png&quot; data-alt=&quot;각 리뷰 데이터에 대한 벡터값 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWxVHv/btsLEp0LlLv/dyRCFkKNukxLyPPEjLzKRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWxVHv%2FbtsLEp0LlLv%2FdyRCFkKNukxLyPPEjLzKRk%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;647&quot; height=&quot;271&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;271&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;각 리뷰 데이터에 대한 벡터값 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Azure에서 AI Search를 구축하려면 다음의 순서를 거칩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Data Source (임베딩된 데이터) 를 업로드하고,&lt;/li&gt;
&lt;li&gt;인덱서에게 데이터소스의 어떤 필드를 어떻게 인덱스로 구성할지 설정하여 인덱스 문서를 만듭니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1736088185231&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; ┌───────────────────────────┐
 │       Data Sources        │
 │ (Blob, Cosmos DB, SQL DB) │
 └─────────────┬─────────────┘
               │ (인덱서가 주기적으로 데이터를 가져옴)
               ▼
      ┌───────────────────────┐
      │       Indexer         │
      │  ┌─────────────────┐  │
      │  │   Skillset(AI)  │  │
      │  │ (OCR, NLP 등)   │  │
      │  └─────────────────┘  │
      └─────────────┬─────────┘
                    │ (전처리 &amp;amp; AI로 분석된 결과를 인덱스에 저장)
                    ▼
      ┌───────────────────────┐
      │         Index         │
      │ (문서, 필드, 메타정보)│
      └─────────────┬─────────┘
                    │ (사용자가 검색 쿼리를 보냄)
                    ▼
      ┌───────────────────────┐
      │        Query          │
      │ (검색, 필터, 정렬 등) │
      └───────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://www.youtube.com/watch?v=MOOHK1b4Syk&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=MOOHK1b4Syk&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;이제 다음과 같이 코드를 구성하면 자연어로 구축된 AI Search 에게 질의가 가능해집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;vector_query: AI Search에게 자연어로 질의하기 위해 question(질의 내용) -&amp;gt; embedding 해서 -&amp;gt; 벡터화된 쿼리로 만듭니다.&lt;/li&gt;
&lt;li&gt;filter: 검색할 상품 기준 (실제 sql 처럼 정확히 특정 상품 안에서만으로도 질의가 가능해집니다.)&lt;/li&gt;
&lt;li&gt;select: 원하는 데이터 select&lt;/li&gt;
&lt;li&gt;top: 유사도 순으로 개수 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1736088670344&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;client = AzureOpenAI(
    azure_endpoint = azure_endpoint,
    api_key        = api_key,
    api_version    = api_version
)


def search(productNumber: str = &quot;111111&quot;, badgeKeyword: str = &quot;촉촉함&quot;):
    search_client = SearchClient(endpoint=endpoint, index_name=index_name, credential=credential, api_version=api_version)

    question = &quot;keyword: &quot; + badgeKeyword + &quot; 와 관련된 내용을 찾아줘. 키워드와 관련된 내용이 꼭 포함되어 있어야 하고 정성스러워야해.&quot;
    embedding = client.embeddings.create(input = question, model=embedding_model_name).data[0].embedding
    vector_query = VectorizedQuery(vector=embedding, k_nearest_neighbors=15, fields=&quot;content_vector&quot;, exhaustive=True)

    results = search_client.search(  
        search_text=f&quot;contents: '{badgeKeyword}'&quot;,  
        filter=f&quot;productNumber eq {productNumber}&quot;,  
        vector_queries=[vector_query],
        select=[&quot;sno&quot;, &quot;productNumber&quot;, &quot;contents&quot;],
        top=15  
    )
    
    # 결과를 리스트로 변환
    results_list = []
    for result in results:
        results_list.append({
            &quot;sno&quot;: result[&quot;sno&quot;],
            &quot;productNumber&quot;: result[&quot;productNumber&quot;],
            &quot;contents&quot;: result[&quot;contents&quot;]
        })

    return {&quot;results&quot;: results_list}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현재 response&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736089655708&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
	results: [
    	{
            &quot;sno&quot;: &quot;1111&quot;,
            &quot;productNumber&quot;: &quot;11111&quot;,
            &quot;contents&quot;: &quot;좋아요 촉촉하니 이거쓰고 건조함을 잘못느끼네요&quot;
        },
        ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 특정 키워드와 관련된 리뷰까지는 조회가 가능합니다. 이제 형광펜으로 리뷰의 &lt;b&gt;어떤 내용이 키워드의 내용을 포함해주고 있는지&lt;/b&gt;를 같이 응답해주어야 클라이언트에서 형광펜으로 &lt;b&gt;하이라이팅&lt;/b&gt;을 해줄 수 있습니다. 이를 LLM 을 이용하면 쉽게 구현이 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736089775048&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;system_msg = &quot;&quot;&quot;
        # 당신은 전문 뷰티 리뷰 분석가 입니다.
        보내주는 리뷰를 읽고 같이 보내주는 키워드와 가장 유사한 부분의 하이라이트 텍스트를 반환해 주세요.
        하이라이트 텍스트는 보내주는 리뷰에 꼭 포함되어있어야 하는 텍스트입니다.
        하이라이트 텍스트는 2~3단어 미만으로 구성해주세요.
        유사한 부분이 없다면 빈 문자열로 보내주세요.

        # 예시 요청
        {
            &quot;texts&quot;: [
                {
                    &quot;id&quot;: 0,
                    &quot;keyword&quot;: &quot;향기&quot;,
                    &quot;text&quot;: &quot;진작부터 써보고 싶었던 제품이에요. 1+1으로 구매해 기분이 좋아요. 제형은 세럼과 토너의 중간쯤인 거 같아요. 향이 좋아서 하루에도 여러 차례 뿌리게 되네요. 처음엔 조금 진한듯 하지만, 시간이 지날수록 은은하게 남은 잔향이  더욱 좋아요.&quot;
                },
                ... 20개
            ]
        }

        # 예시 JSON 응답
        {
            &quot;highlighted_texts&quot;: [
                {
                    &quot;id&quot;: 0,
                    &quot;keyword&quot;: &quot;향기&quot;,
                    &quot;highlighted_text&quot;: &quot;향이 좋아서&quot;
                },
                ... 20개
            ]
        }
    &quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-04 오후 6.25.32.png&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;1232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F05a9/btsLFgV5swX/Gq13w3wermQnqCgRgmRmT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F05a9/btsLFgV5swX/Gq13w3wermQnqCgRgmRmT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F05a9/btsLFgV5swX/Gq13w3wermQnqCgRgmRmT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF05a9%2FbtsLFgV5swX%2FGq13w3wermQnqCgRgmRmT0%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;868&quot; height=&quot;1232&quot; data-filename=&quot;스크린샷 2025-01-04 오후 6.25.32.png&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;1232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 네이버 쇼핑에서는 이미 키워드 리뷰 검색 기능을 제공하고 있습니다. 이를 &lt;a href=&quot;https://www.hani.co.kr/arti/economy/it/987162.html&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;1616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L6mix/btsLDfLB1Vh/oVbhWArKFQvLupoF5GOCf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L6mix/btsLDfLB1Vh/oVbhWArKFQvLupoF5GOCf0/img.png&quot; data-alt=&quot;네이버의 키워드 리뷰 검색&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L6mix/btsLDfLB1Vh/oVbhWArKFQvLupoF5GOCf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL6mix%2FbtsLDfLB1Vh%2FoVbhWArKFQvLupoF5GOCf0%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;946&quot; height=&quot;1616&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;1616&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;네이버의 키워드 리뷰 검색&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 컨셉 기획과 구현, 발표자료 모두 2일안에 만드느라 고생을 많이 했습니다. 결국 구현을 완료했다는 것에 성취함이 엄청 느껴졌고 팀원에게 너무 고마웠습니다. 수상은 하지 못했지만 나에게는 많은 것이 남은 해커톤이었습니다. GPT api만 사용할줄 알던 내가 RAG 까지 구축을 하는 경험을 했다는 것 자체가 너무 귀하고 감사하다는 생각입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;조금 더 생각해볼 부분들&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;안좋은 후기는 어떻게 처리할 것인가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용이 안좋은 후기에 대해서는 일부러 보여주지 않을 필요는 없다고 생각했습니다. 안좋은 내용을 확인하고 싶은 사용자도 당연히 있을 것이고, 리뷰 시스템에서 안좋은 내용의 후기를 일부러 노출하지 않을 책임은 없다고 생각합니다. 상품에 대해 평이 안좋은 것은 리뷰시스템에서 해결할 부분이 아니고 상품 품질을 관리하는 부분이 신경써야한다고 생각했습니다. 하지만 단순 부정후기가 아닌 '악성' 후기에 대해선 리뷰 시스템에서 막을 책임은 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;리뷰 개수가 적은 서비스에서는 제공하지 못하는 기능 아닌가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이디어의 시작 자체는 '리뷰 데이터가 너무 많아 원하는 리뷰를 찾아보기 힘들다' 에서 출발하였습니다. 리뷰 개수가 적다면 사용자는 오히려 리뷰를 파악하기쉬워 이 기능까지는 필요없지 않을까 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;LLM api 를 사용하는 부분에 대한 성능과 비용이슈는?&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;리뷰 -&amp;gt; 키워드 추출&lt;/li&gt;
&lt;li&gt;텍스트 임베딩&lt;/li&gt;
&lt;li&gt;키워드 -&amp;gt; 형용사로 변환&lt;/li&gt;
&lt;li&gt;AI Search 질의&lt;/li&gt;
&lt;li&gt;AI Search 결과 -&amp;gt; 하이라이팅할 텍스트 찾기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리뷰 키워드 추출 최적화가 가능하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신기능에 대해선 AI 비용이 당연히 드는 것이 사실입니다. 하지만 좀 더 효율적으로 사용하는 방법에 대해선 고민이 필요합니다. 리뷰 데이터에서 키워드로 추출하는 부분에 대해서는 각 row 하나마다 키워드를 추출하는 방식에서 같은 상품의 리뷰를 몇개 합쳐서 최대 토큰수(8196)개에 근접하게 만들면 리뷰 데이터 row수가 많이 줄어들 수 있습니다. &lt;b&gt;키워드가 필요한 부분&lt;/b&gt;은 사실 '각 리뷰'가 아닌 '&lt;b&gt;상품별&lt;/b&gt;' 이기 때문입니다. 실제로 리뷰 데이터들 중에서 짧아서 내용이 별로 없는 리뷰는 제거하고, 남은 리뷰를 몇개씩 합친뒤 키워드 추출을 해보았습니다. &lt;b&gt;row 수가 많이 줄어 임베딩하는 속도와 효율&lt;/b&gt;이 많이 증가했고, 주요 키워드의 워드클라우드 데이터는 거의 비슷하게 구축이 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI Search 결과에서 하이라이팅할 텍스트 찾기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간으로 조회된 리뷰에서 하이라이팅할 텍스트를 찾기 때문에 API call이 5초가량 소요됩니다. 조회하는 리뷰의 양이 많을 수록 API 응답속도가 길어집니다. 해커톤 당시에는 사용자가 어떤 키워드로 검색할 지 모르기 때문에 하이라이팅 텍스트를 미리 저장하지 못하는 것 아닌가? 라고 생각했습니다. 끝나고 다시 생각해보니 주요 키워드가 이미 추출된 상태라면 각 리뷰에서도 주요 키워드에 따라 어떤 부분을 하이라이팅 할지 미리 설정하는 것은 불가능하진 않아보였습니다.&lt;/p&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;b&gt;LLM api를 사용하는 곳이 많다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간응답을 빠르게 하기 위해 미리미리 LLM을 이용해 데이터를 만들어 놓는다고 하더라도, 리뷰 하나에 api가 여러번 사용됩니다. 실제 리뷰 수는 수백만 건이기 때문에 이 수백만 건마다 api 호출이 여러번이라면 비용이 많이 들 수 있습니다. 이 부분은 아직은 잘 모르겠습니다. 얼마를 투자하고 전환률을 얼마나 올리는데에 드는 비용을 잘 모르기 때문에 판단하기가 어려웠습니다. 그래도 LLM 적극적으로 이용한다면 기존에 없던 기능이 만들어지고 경쟁력을 확보하는데에 좋지 않을까 라는 생각을 하며... (이런 쪽으로 투자 많이 했으면 좋겠는 개인적인 바람)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;소감&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적인 도전으로는 요즘 푹 빠져있는 &lt;a href=&quot;https://codeium.com/windsurf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Windsurf&lt;/a&gt;를 이용해서 '한번도 써보지 않는 기술스택으로도 Windsurf와 함께라면 개발이 가능할까?' 라는 도전을 이번에 해봤습니다. 정말 신기하게도 성공했고, 파이썬 언어는 다룰줄 알지만 API를 구축해본적은 없던 상황에서 Windsurf로 Fast API로 서버를 구축하였습니다. 이로써 알게된 것은 어느정도 다룰줄 아는 언어라면 간단하게 서비스를 구현해내는 것은 정말 쉬워졌다는 것입니다.&lt;/p&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;table style=&quot;border-collapse: collapse; width: 100%; height: 74px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;언어 수준&lt;/td&gt;
&lt;td style=&quot;width: 20.1163%; height: 20px;&quot;&gt;개발 효율 증가 (with. Windsurf)&lt;/td&gt;
&lt;td style=&quot;width: 29.8837%; height: 20px;&quot;&gt;느낀점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;주로 사용함, 잘 알고 있음 (Java, Spring)&lt;/td&gt;
&lt;td style=&quot;width: 20.1163%; height: 18px;&quot;&gt;Low&lt;/td&gt;
&lt;td style=&quot;width: 29.8837%; height: 18px;&quot;&gt;귀찮은 작업을 대신 해줌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;주로 사용하진 않지만 알고 있음 (Python)&lt;/td&gt;
&lt;td style=&quot;width: 20.1163%; height: 18px;&quot;&gt;High&lt;/td&gt;
&lt;td style=&quot;width: 29.8837%; height: 18px;&quot;&gt;'주로 사용함' 레벨과 근접하게 사용하게 해줌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;잘 모름 (Typescript, Next.js)&lt;/td&gt;
&lt;td style=&quot;width: 20.1163%; height: 18px;&quot;&gt;Medium&lt;/td&gt;
&lt;td style=&quot;width: 29.8837%; height: 18px;&quot;&gt;어느정도 구현은 가능하지만 Windsurf가 잘못된 방향으로 가는 순간 망함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 전체적으로 사용하지 않는 것 보다는 '훨씬' 좋습니다. GPT에 매번 내 코드 컨텍스트를 학습시키는 비용이 들지 않고 그냥 '내 코드베이스 전체를 파악해서 답변해줘' 라는 말만 맨 앞에 넣으면 되기 떄문입니다.&lt;/p&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;앞으로는 이렇게 얻은 AI 관련 기술들을 사이드 프로젝트에서 적극적으로 활용해볼 계획입니다. 특히 Function Calling 기능도 가능성이 무궁무진해 보였습니다. 길고 두서없는 어려운 글 읽어주셔서 감사합니다. 나중에 내가 보더라도 이해할 수 있게 작성하려고 노력했으니 미래의 내가 판단해줄 것이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고/회사</category>
      <author>API_Dev</author>
      <guid isPermaLink="true">https://ksabs.tistory.com/270</guid>
      <comments>https://ksabs.tistory.com/270#entry270comment</comments>
      <pubDate>Sat, 4 Jan 2025 18:52:38 +0900</pubDate>
    </item>
    <item>
      <title>[Github Actions] 모두가 몰랐던 Github PR 워크플로우의 비밀</title>
      <link>https://ksabs.tistory.com/269</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;PR 을 open하면 돌아가는 워크플로우는 PR 브랜치 기준이 아니다 !&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;625&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EG1Lt/btsI3SMsNdN/Wch8fM9Ktei745Uaqi4tIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EG1Lt/btsI3SMsNdN/Wch8fM9Ktei745Uaqi4tIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EG1Lt/btsI3SMsNdN/Wch8fM9Ktei745Uaqi4tIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEG1Lt%2FbtsI3SMsNdN%2FWch8fM9Ktei745Uaqi4tIK%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;625&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;625&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 많은 분들이 PR을 열어놓고 커밋이 추가될때마다 자동으로 테스트를 돌리거나 Sonar 를 돌리는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 무의식중으로 아직 머지를 안했으니 내가 방금 업데이트한 브랜치 기준으로 워크플로우가 돌겠구나 생각하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 혹시 깃헙액션이 돌아갈때, PR을 날리는 브랜치 기준이 아니라 merge 된 브랜치를 가정하고 돌아가는 것을 알고 계셨나요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-15 오후 4.38.27.png&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckQkVe/btsI5MDGhD0/LNOo8Dw9swyqCKsMgx37vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckQkVe/btsI5MDGhD0/LNOo8Dw9swyqCKsMgx37vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckQkVe/btsI5MDGhD0/LNOo8Dw9swyqCKsMgx37vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckQkVe%2FbtsI5MDGhD0%2FLNOo8Dw9swyqCKsMgx37vk%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;1560&quot; height=&quot;408&quot; data-filename=&quot;스크린샷 2024-08-15 오후 4.38.27.png&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 PR을 날리고 있다고 가정하면 main + feature branch 를 &lt;b&gt;머지했다고 가정한 상태&lt;/b&gt;로 액션이 돌아갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 Checkout 액션을 돌리고,&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# &amp;lt;https://github.com/actions/checkout&amp;gt;
- name: Checkout
  uses: actions/checkout@v4
  with:
		...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃헙 액션이 돌아가는 스크립트를 살펴보면, Checkout 단계에서 &lt;code&gt;/merge&lt;/code&gt; 가 붙은 브랜치로 switching 하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUd8Ca/btsI5wODpP8/kDOZpwqRLoF0kkXBOCYphk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUd8Ca/btsI5wODpP8/kDOZpwqRLoF0kkXBOCYphk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUd8Ca/btsI5wODpP8/kDOZpwqRLoF0kkXBOCYphk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUd8Ca%2FbtsI5wODpP8%2FkDOZpwqRLoF0kkXBOCYphk%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;201&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;201&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(git switch vs git checkout 에 대한 설명은 아래 글 참고)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://graphite.dev/guides/git-checkout-vs-switch&quot;&gt;https://graphite.dev/guides/git-checkout-vs-switch&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;깃헙 문서의 워크플로우 트리거 이벤트 부분도 확인해 보면, &lt;span&gt;pull_request&lt;/span&gt; 이벤트에서는 &lt;code&gt;github_sha&lt;/code&gt;, &lt;code&gt;github_ref&lt;/code&gt; 를 기본적으로 둘다 &lt;span&gt;merge&lt;/span&gt;된 기준으로 제공하고 있습니다.&lt;/p&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FhGtK/btsI5b5nFOT/gbCV1ZONZ2YzGuz6oEtt8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FhGtK/btsI5b5nFOT/gbCV1ZONZ2YzGuz6oEtt8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FhGtK/btsI5b5nFOT/gbCV1ZONZ2YzGuz6oEtt8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFhGtK%2FbtsI5b5nFOT%2FgbCV1ZONZ2YzGuz6oEtt8k%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;414&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.github.com/ko/enterprise-cloud@latest/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request&quot;&gt;https://docs.github.com/ko/enterprise-cloud@latest/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request&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;actions 의 &lt;a href=&quot;https://github.com/actions/checkout&quot;&gt;checkout 레포의 Readme 문서&lt;/a&gt;를 확인해보겠습니다.&lt;/p&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;567&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EI6Uy/btsI5WM4C7s/wSzoC1kYQPuXbyYeulvWh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EI6Uy/btsI5WM4C7s/wSzoC1kYQPuXbyYeulvWh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EI6Uy/btsI5WM4C7s/wSzoC1kYQPuXbyYeulvWh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEI6Uy%2FbtsI5WM4C7s%2FwSzoC1kYQPuXbyYeulvWh1%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;567&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;567&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;체크아웃할 분기, 태그 또는 SHA입니다.&lt;br /&gt;저장소를 확인할 때 워크플로를 트리거한 경우 기본값은 해당 이벤트에 대한 refence 또는 SHA입니다.&lt;br /&gt;그렇지 않으면 기본 분기를 사용합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 이벤트에 대한 refence(ref) 또는 sha 를 사용한다고 되어있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 pull_request 이벤트 트리거를 사용했고, pull_request 트리거는 기본적으로 병합상태의 sha 혹은 ref를 제공하므로 병합상태를 기준으로 워크플로우가 동작하는 것을 확인하였습니다.&lt;/p&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;난 PR을 생성한 브랜치 기준으로 워크플로우를 실행하고 싶은데?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤드 브랜치에 대한 커밋을 얻으려면 &lt;code&gt;github.event.pull_request.head.sha&lt;/code&gt; 를 사용하라고 가이드합니다.&lt;/p&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSDJmb/btsI6BhnMbN/rLnn1SkRrQuuJ0dSgNwwYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSDJmb/btsI6BhnMbN/rLnn1SkRrQuuJ0dSgNwwYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSDJmb/btsI6BhnMbN/rLnn1SkRrQuuJ0dSgNwwYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSDJmb%2FbtsI6BhnMbN%2FrLnn1SkRrQuuJ0dSgNwwYk%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;177&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;177&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 이벤트의 GITHUB_SHA는 풀 요청 병합 분기의 마지막 병합 커밋입니다. 풀 요청의 헤드 브랜치에 대한 마지막 커밋의 커밋 ID를 얻으려면 대신 github.event.pull_request.head.sha를 사용하세요.&lt;/blockquote&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uKcms/btsI6kNHSUR/XFW6Kdl9fUHYkkU7XUfsT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uKcms/btsI6kNHSUR/XFW6Kdl9fUHYkkU7XUfsT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uKcms/btsI6kNHSUR/XFW6Kdl9fUHYkkU7XUfsT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuKcms%2FbtsI6kNHSUR%2FXFW6Kdl9fUHYkkU7XUfsT1%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;293&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/actions/checkout&quot;&gt;https://github.com/actions/checkout&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그런데 꼭 PR 브랜치 기준으로 워크플로우를 실행할 이유가 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 누군가가 깃헙에 이슈를 달았다.&lt;/p&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m9vDR/btsI4jQe7sX/BpBPgQgk9fHiJkSrOlIWtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m9vDR/btsI4jQe7sX/BpBPgQgk9fHiJkSrOlIWtk/img.png&quot; data-alt=&quot;싫어요가 2개나.. ㅠ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m9vDR/btsI4jQe7sX/BpBPgQgk9fHiJkSrOlIWtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm9vDR%2FbtsI4jQe7sX%2FBpBPgQgk9fHiJkSrOlIWtk%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;660&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;싫어요가 2개나.. ㅠ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;795&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DegWt/btsI3OiZmqz/0jKq1wvkrDULRTcbae5R2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DegWt/btsI3OiZmqz/0jKq1wvkrDULRTcbae5R2k/img.png&quot; data-alt=&quot;Github 개발자의 답변&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DegWt/btsI3OiZmqz/0jKq1wvkrDULRTcbae5R2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDegWt%2FbtsI3OiZmqz%2F0jKq1wvkrDULRTcbae5R2k%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;795&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;795&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Github 개발자의 답변&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/actions/checkout/issues/504&quot;&gt;https://github.com/actions/checkout/issues/504&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;i&gt;번역)&lt;/i&gt;&lt;/p&gt;
&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;i&gt;답변: 생성된 테스트 병합 커밋을 사용하면 기본 분기(마스터/메인)에 병합되는 컨텍스트에서 PR 변경 사항을 테스트하고 실제 병합 전에 해당 문제를 미리 포착할 수 있습니다.(기본 분기에 더 이상 업데이트가 없다고 가정).&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;납득.. PR을 날린다는건 병합할 예정이라는 뜻이고 병합 후의 발생할 이슈사항을 체크하는게 우선이 맞다고 생각이 든다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEY0KK/btsI5zR7KvQ/y9H0HtgpSVGE4fPGioHKRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEY0KK/btsI5zR7KvQ/y9H0HtgpSVGE4fPGioHKRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEY0KK/btsI5zR7KvQ/y9H0HtgpSVGE4fPGioHKRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEY0KK%2FbtsI5zR7KvQ%2Fy9H0HtgpSVGE4fPGioHKRk%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;626&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서 병합충돌을 먼저 해결해야 워크플로우가 실행된다고 설명합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;639&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yyyif/btsI6zYayzR/JoufUpSvybY1mFY9W5FfQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yyyif/btsI6zYayzR/JoufUpSvybY1mFY9W5FfQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yyyif/btsI6zYayzR/JoufUpSvybY1mFY9W5FfQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyyyif%2FbtsI6zYayzR%2FJoufUpSvybY1mFY9W5FfQK%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;639&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;639&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;번역) 풀 요청에 병합 충돌이 있는 경우 워크플로는 pull_request 활동에서 실행되지 않습니다. 병합 충돌을 먼저 해결해야 합니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 관련해 최근까지도 엄청 활발하게 토론중인 이슈 링크를 첨부합니다.. ㄷㄷ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;오류이므로 개선해야한다&amp;rsquo; VS &amp;lsquo;이게맞다&amp;rsquo; 로 의견이 갈리네요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/orgs/community/discussions/26304&quot;&gt;https://github.com/orgs/community/discussions/26304&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;번외로.. 해당 이슈를 만난 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 동료분이 로컬에서 테스트가 성공했지만, PR에서 돌아가는 CI는 자꾸 통과하지 않는다.. 라고 하셔서 같이 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 KST, UTC 문제거나 gradle test가 달라서 실패하는 경우가 대부분인데 이번에는 둘다 아니었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시나 해서 로컬에서 머지후에 테스트를 돌렸더니 머지하면서 import 문이 하나 빠지고, 의존성이 없어 컴파일 자체가 실패하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 jacoco 테스트 실패 리포트가 만들어져야하는데 자꾸 안나오는 이유도 &amp;lsquo;컴파일 자체가 안되어서&amp;rsquo; 였습니다.&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;어쨌든, 머지 후에 테스트가 동일하게 실패하고 혹시 PR이 머지 기준으로 돌아가는것 아닐까? 하고 로그를 살펴보며 발견하게된 이슈였습니다.&lt;/p&gt;</description>
      <category>Web/배포</category>
      <category>CI</category>
      <category>github</category>
      <category>github actions</category>
      <category>workflow</category>
      <author>API_Dev</author>
      <guid isPermaLink="true">https://ksabs.tistory.com/269</guid>
      <comments>https://ksabs.tistory.com/269#entry269comment</comments>
      <pubDate>Thu, 15 Aug 2024 16:44:55 +0900</pubDate>
    </item>
    <item>
      <title>지난 2개월간의 인생 리뷰 - 사이드프로젝트 DND</title>
      <link>https://ksabs.tistory.com/267</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;살면서 가장 주도적으로 살았던 기간이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2개월동안, 마음속에만 품어왔던 도전들을 하나씩 꺼내보았다.&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;a href=&quot;https://ksabs.tistory.com/266&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;사이드프로젝트 - DND&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8주동안 DND 에서 [쉽고 빠른 약속시간 정하기 - 모두의시간]을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.dnd.ac/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.dnd.ac/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1678074010742&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;DND&quot; data-og-description=&quot;프로젝트에 즐거움을, 모두에게 기회를&quot; data-og-host=&quot;www.dnd.ac&quot; data-og-source-url=&quot;https://www.dnd.ac/&quot; data-og-url=&quot;https://www.dnd.ac/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cmuTAH/hyRQhPCs3u/bmPAz44PTfF6tZ5yRuipT1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400&quot;&gt;&lt;a href=&quot;https://www.dnd.ac/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.dnd.ac/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cmuTAH/hyRQhPCs3u/bmPAz44PTfF6tZ5yRuipT1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;DND&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트에 즐거움을, 모두에게 기회를&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.dnd.ac&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3200&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oK7s8/btr2sXzvvpa/V34eWRlo01Kz67SUPQ9jR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oK7s8/btr2sXzvvpa/V34eWRlo01Kz67SUPQ9jR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oK7s8/btr2sXzvvpa/V34eWRlo01Kz67SUPQ9jR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoK7s8%2Fbtr2sXzvvpa%2FV34eWRlo01Kz67SUPQ9jR1%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;3200&quot; height=&quot;1800&quot; data-origin-width=&quot;3200&quot; data-origin-height=&quot;1800&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;a href=&quot;https://github.com/dnd-side-project/dnd-8th-5-backend&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;시작&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 아이디어를 정하는 과정에서 여러가지 좋은 의견들이 나왔다. 우리는 8주라는 기간동안 완성할 수 있는지 여부가 가장 중요하다고 판단했고, 개인적으로는 실제로 사용되는 서비스를 만들고 싶었다. 시간 약속을 잡는 불편함은 당장 하고 있던 회의시간을 잡는 중에도 느꼈던 불편함이었고, 살아가면서도 시간약속을 잡아야 할 일도 많았다. 모두가 불편함에 공감하고 있었고 아이디어 채택은 수월하게 진행됐다.&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-06 13.31.00.png&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;1118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oUx7q/btr2sGYPXuT/qk6aw5pyOg6d1a9KPnAsl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oUx7q/btr2sGYPXuT/qk6aw5pyOg6d1a9KPnAsl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oUx7q/btr2sGYPXuT/qk6aw5pyOg6d1a9KPnAsl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoUx7q%2Fbtr2sGYPXuT%2Fqk6aw5pyOg6d1a9KPnAsl0%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;1758&quot; height=&quot;1118&quot; data-filename=&quot;스크린샷 2023-03-06 13.31.00.png&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;1118&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;a href=&quot;https://ksabs.tistory.com/257&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;최대한 자신의 의견이 존중받는다는 것을 느껴야 더 좋은 아이디어를 낼 수 있다는 것을 알고있기에&lt;/a&gt;, 팀원들의 의견을 존중하며 디벨롭을 같이 생각해보는 방향으로 최대한 생각했었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1170&quot; data-origin-height=&quot;1817&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddNuAr/btr2CvKsXw1/qizmwl4KP3HKMj2oFKCix0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddNuAr/btr2CvKsXw1/qizmwl4KP3HKMj2oFKCix0/img.png&quot; data-alt=&quot;아침 7시 회의는 실화입니다. - 채민&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddNuAr/btr2CvKsXw1/qizmwl4KP3HKMj2oFKCix0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddNuAr%2Fbtr2CvKsXw1%2Fqizmwl4KP3HKMj2oFKCix0%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;400&quot; height=&quot;621&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1817&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아침 7시 회의는 실화입니다. - 채민&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;리서치 및 기획&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리서치기간에서는 앞으로 진행할 프로젝트의 기획을 구체화 하기 위해 필요한 리서치들을 했다. &lt;span&gt;기존에 해왔던 프로젝트는 개발자들끼리만 기획하고 구현했었다. 이 서비스가 사용되기 위한 고민을 개발의 관점에서만 고민했었다. 이번에는 디자이너분들과 함께 제대로된 사용자 모델링을 위해 시장조사, 사용자 설문조사 그리고 이 리서치 정보를 토대로 우리 서비스가 어떤 핵심가치를 지녀야할지 뽑아냈다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BKpMr/btr2xSTxtqf/hNXLXYf2uEf3hoE2AmVXvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BKpMr/btr2xSTxtqf/hNXLXYf2uEf3hoE2AmVXvK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;1294&quot; data-filename=&quot;스크린샷 2023-03-07 17.28.10.png&quot; data-widthpercent=&quot;45.52&quot; style=&quot;width: 44.9905%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BKpMr/btr2xSTxtqf/hNXLXYf2uEf3hoE2AmVXvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBKpMr%2Fbtr2xSTxtqf%2FhNXLXYf2uEf3hoE2AmVXvK%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;2042&quot; height=&quot;1294&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bK0f7u/btr2u0xQQze/jIxTVFioE4fFdFrFIpDNT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK0f7u/btr2u0xQQze/jIxTVFioE4fFdFrFIpDNT0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2070&quot; data-origin-height=&quot;1096&quot; data-filename=&quot;스크린샷 2023-03-07 17.29.12.png&quot; style=&quot;width: 53.8467%;&quot; data-widthpercent=&quot;54.48&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bK0f7u/btr2u0xQQze/jIxTVFioE4fFdFrFIpDNT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK0f7u%2Fbtr2u0xQQze%2FjIxTVFioE4fFdFrFIpDNT0%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;2070&quot; height=&quot;1096&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;인터뷰 응답을 중심으로 인사이트 뽑기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cp3dai/btr2xTSr4Ka/e6y898KcL7bW88hpqbMgaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cp3dai/btr2xTSr4Ka/e6y898KcL7bW88hpqbMgaK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;830&quot; data-filename=&quot;스크린샷 2023-03-07 17.29.32.png&quot; style=&quot;width: 47.3563%; margin-right: 10px;&quot; data-widthpercent=&quot;47.91&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cp3dai/btr2xTSr4Ka/e6y898KcL7bW88hpqbMgaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcp3dai%2Fbtr2xTSr4Ka%2Fe6y898KcL7bW88hpqbMgaK%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;1018&quot; height=&quot;830&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxZpRI/btr2tB522y8/Ky3KWXsu1DUNPKNehziFW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxZpRI/btr2tB522y8/Ky3KWXsu1DUNPKNehziFW0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;624&quot; data-filename=&quot;스크린샷 2023-03-07 17.28.40.png&quot; data-widthpercent=&quot;52.09&quot; style=&quot;width: 51.4809%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxZpRI/btr2tB522y8/Ky3KWXsu1DUNPKNehziFW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxZpRI%2Fbtr2tB522y8%2FKy3KWXsu1DUNPKNehziFW0%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;832&quot; height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;핵심 인사이트를 뽑아내고 이를 통해 서비스의 핵심가치 정하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정이 정말 오래걸리고 회의도 많이했다. 하지만 정말 사용될 서비스를 위해선 꼭 필요한 과정이라고 생각헀다. 기존에 우테코에서 프로젝트를 했을때는 개발자들끼리 이루어진 팀이었기 때문에 기획적인 부분이 부족할 수 밖에 없었다. 그런데 이번에 DND에서 디자이너분들과 함께할 수 있어서 기획적인 부분과 디자인을 체계적으로 진행할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&lt;b&gt;개발&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테코에서 진행한 프로젝트 코드는 우테코 내내 객체지향과 클린코드를 학습한 내용이 잘 적용되지 못했다. 그리고 우테코가 끝나고 추가로 학습한 도메인 중심적인 설계를 적용할 프로젝트가 필요했다. 누구와 개발을 같이 하게될지는 몰랐지만 잘 설득해서 내가 알고있는 지식들을 팀원과 함께 잘 녹여내보기로 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;도메인 용어 통일하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원플랫폼에서 근무하면서 배웠던 것들 중에서는 도메인 용어를 통일하는 것이 있었다. 디자이너, 개발자들끼리 도메인 용어를 통일하는 것은 도메인주도개발에 국한된이 아니라 그냥 일을 잘하는 방법중에 하나이다. 그래서 와이어프레임 설계단계에서부터 같은 화면이나 기능인데 서로 다르게 말하고 있는 것들을 잡아서 계속 바로잡으려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-13 17.24.44.png&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1kykY/btr3lDINUah/LXfH9xeU3NwHMbNe0MqDyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1kykY/btr3lDINUah/LXfH9xeU3NwHMbNe0MqDyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1kykY/btr3lDINUah/LXfH9xeU3NwHMbNe0MqDyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1kykY%2Fbtr3lDINUah%2FLXfH9xeU3NwHMbNe0MqDyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1334&quot; height=&quot;254&quot; data-filename=&quot;스크린샷 2023-03-13 17.24.44.png&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;254&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;이게 생각보다 쉽지는 않았다. 회의를 하는중에 &quot;이거는 이거라고 부르는게 정확하지 않을까요?&quot; 라던가, &quot;이렇게 부르는 것은 정확한 명칭이 아닌것 같아요&quot; 라고 말하는 것은 정말 어려웠다. 게다가 이렇게 태클을 걸었으면 정확한 워딩을 내가 짚어주지는 못하고 같이 고민하자고 하고 있으니 회의 시간이 지체되기도 했었다. 같은 화면, 같은 기능을 모두가 동의하는 합리적인 단어로 말하는 것이 이상적이지만 순조롭게 되지는 못했다. 마감기한이 있으니 급한 것들만 짚기는 했지만 그럼에도 남아있는 잘못된 단어들이 있었다. 지나고나서 보면 도메인 용어는 마감기간때문에 미뤄야할 요소가 아닌 것 같다. 지속되는 서비스를 위해서는 꼭 필요한 작업이라고 생각이 든다. 도메인 용어 하나가 잘못되면 개발코드의 변수명도 이상해지고, 사용자가 보는 단어도 달라지는 경우가 있었다. 처음에 잘못되면 이것들은 바꾸기 어려워진다. 그리고 소통의 문제도 생기게 된다. 다시 프로젝트가 재개될때는 도메인 용어 통일에 대해서는 타협하지 않고 같이 정립해 나가야된다고 팀원들을 설득해야겠다고 생각했다.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;처음부터 끝까지 TDD&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두의 시간 프로젝트는 처음부터 끝까지 TDD로 진행했다. TDD를 한번도 해보지 못한 백엔드 팀원과 TDD를 진행하는 것이 어려울 것이라고 생각했지만, 페어프로그래밍으로 처음부터 같이 진행하다보니까 생각보다 괜찮았다. 멘토링으로 TDD 강의도 진행한 경험도 있었지만, 팀원분이 배우려는 자세가 너무 잘되어있었고 학습속도도 스펀지처럼 빨랐기 때문인 것 같다. 프로젝트 중반에는 팀원분이 한번 테스트 코드 없이 프로덕션부터 짜보려고 시도했다가 내가 지금 뭘 해야하지 뇌정지가 와서 '아 이래서 TDD 하시는거군요' 하고 스스로 깨달은 뒤 혼자서 TDD를 진행하시는 것을 보면서 역시 야생형 학습법이 나쁘지 않구나라는 것을 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 우테코를 수료하고 실제 회사에 들어간 크루들을 보면 회사에서 TDD를 안하고 있다는 크루들을 많이 보았다. 우테코 내내 'TDD는 신이다' 라고 외치던 사람도 회사에 들어가선 'TDD는 사기다' 라고 외친다고도 한다. 그런데 나는 TDD를 몇년간 직접 해보면서 장점과 단점을 확실하게 알아가고 있다. 나는 해보지 않고 사용을 안하는 사람과 다르기 때문에, 장점이 빛날때 하면 되고 단점이 두드러질땐 하지 않으면 된다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JPA 매핑과 DB연결은 최대한 나중에, 우린 객체설계에 집중하자&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원분께 처음부터 우리는 ERD 설계부터 하는 것이 아닌 도메인 설계부터 하자고 요청했다. 이 부분도 다행히 팀원분이 거부감없이 새로운 것을 받아들일 준비가 되어있어서 수월하게 진행할 수 있었다. 나 조차도 이렇게 진행하는 것이 처음이었고 객체지향설계와 JPA의 장점과 단점을 직접 경험해보자는 생각으로 이렇게 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 테이블설계를 염두에 두지 않고 진행하다보니까 객체설계 자체에만 집중할 수 있었다. 통합테스트 부분은 메모리를 이용한 레포지토리를 임시로 만들어서 진행했다. 나중에 JPA Repository로 사용할 인터페이스의 이름과 스펙을 똑같이 맞춰놓으면 JPA를 추가할때 코드 변경을 최소화할 수 있겠다고 생각했다. 실제로 이 방법은 나중에 JPA 연결할때 예상대로 되는 모습을 보면서 둘 다 신기해했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;느낀 장점도 있지만, 단점도 느껴졌다. 단점이라기 보다는 어쩔 수 없는 문제를 해결해야 했다. 역시 객체와 테이블간의 패러다임 불일치 문제가 있었다. 객체를 먼저 설계하다보면 List를 쓸 일이 굉장히 많았다. 그런데 테이블은 리스트라는 개념이 없기 때문에 연관관계를 맺기 위해선 Many에 해당하는 쪽에 One의 FK를 걸어주어야 한다. 우리가 사용하고있는 객체를 엔티티로 사용하기 위해서는 List로 되어있는 객체를 신경써주어야 했다. 그리고 이 List를 엔티티에 매핑하려면 3가지 경우의 수를 따져야 했다. 1) OneToMany 단방향, 2) ManyToOne 단방향, 3) ManyToOne 양방향 이 세가지를 각 어떤 상황에 걸어야할지 고민했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OneToMany 단방향은 영한님의 강의에선 안티패턴이라고 설명해주셨지만, 같은 애그리거트에서 필수로 같이 조회되고 Many쪽의 데이터가 수정될 일이 없다면 충분히 사용해도 된다고 생각했다. 이미 지원플랫폼에서 사용했던 경험도 있다. OneToMany 단방향에서 추가로 신경써주어야 할 것은 불필요한 update 쿼리가 나가는 부분이다. Many부분의 엔티티가 저장되고 FK를 update하기위해 update쿼리가 추가로 나가는 문제가 있다. 하지만 이 쿼리도 없앨 수 있는 방법이 있다. 사실 이 방법이 있기 때문에 OneToMany 단방향을 고려한 것이다. JoinColumn에 nullable=false, updatable=false 을 걸어주면 Hibernate에서 Many가 save될때 FK를 같이 넣어줘서 update쿼리가 추가로 나가지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ManyToOne 단방향은 지금 코드를 다시 보니까 사용을 하나도 안했다. 아마 애그리거트를 나누다보니까 ManyToOne 단방향을 걸어야할 상황이 나오지 않은 것 같다. 객체를 List로 가지고있지 않다면 사실 연관관계가 필요하지 않는 경우가 있다. 그리고 굳이 FK로 가지는 것 보다는 그냥 id를 필드로 가지고 있어도 될 수 있다. 우리 코드에서는 ManyToOne 단방향보다는 양방향이 걸려있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ManyToOne 양방향도 안티패턴이라고는 하지만, 우리 코드에서는 쓰였다. 객체를 List로 갖고있는 상태에서 Many 쪽의 데이터를 수정해야할 일이 있고, One을 조회할때 항상 Many까지 같이 필요한 경우에 쓰인다. 이때는 연관관계 편의메서드를 신경써주어야한다. 우리 코드는 일반적인 Team, Member 예제코드 처럼 되어있진 않고 우리만의 비즈니스 코드였기 때문에 편의메서드를 다른 방식으로 구현해야했다.&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;코드가 어느정도 완성되고나서 JPA를 붙이는 것이 생각보다 어렵지 않았지만 List 매핑하는 부분은 신경을 많이 써주어야했다. 적용할 당시에는 최선의 방법이었지만 나중에 볼때는 제거하기 어려운 레거시가 되어있을 수 있겠다 라는 느낌이 들었다. 대신에 지금의 경험과 고민으로 다음에는 시간을 아낄 수 있게 되었다. N+1 문제 등 해결해야할 여러가지 문제들이 남았지만 차근차근 해결해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;도메인 중심 설계와 도메인 이벤트 (feat. 단방향 의존성 관리)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테코가 끝나고 추가로 도메인 중심적으로 사고하는 방법과 의존성을 단방향으로 관리하는 공부를 했었다. 이번 프로젝트에서도 처음부터 의존성 방향을 잘 관리하여 추후에 도메인 단위로 패키지를 구분할 생각으로 진행했다. 객체설계를 하는 단계에서부터 팀원분이랑 같이 고민하면서 설계했다. 요구사항과 도메인이 계속 추가되면서 구조와 설계도 변경이 자주 되었다. 그러면서도 어느정도 묶이는 개념들이 보였다. 이 부분들을 차근차근 애그리거트로 묶어가며 단방향 의존성을 관리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-10 15.22.20.png&quot; data-origin-width=&quot;2174&quot; data-origin-height=&quot;1304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4bR5d/btr24j4AuLN/H08v6cLvUr4BoV7uPULWZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4bR5d/btr24j4AuLN/H08v6cLvUr4BoV7uPULWZ0/img.png&quot; data-alt=&quot;계속해서 추가되는 도메인구조를 팀원분과 항상 같이 설계했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4bR5d/btr24j4AuLN/H08v6cLvUr4BoV7uPULWZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4bR5d%2Fbtr24j4AuLN%2FH08v6cLvUr4BoV7uPULWZ0%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;2174&quot; height=&quot;1304&quot; data-filename=&quot;스크린샷 2023-03-10 15.22.20.png&quot; data-origin-width=&quot;2174&quot; data-origin-height=&quot;1304&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;계속해서 추가되는 도메인구조를 팀원분과 항상 같이 설계했다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참여자가 본인의 일정을 수정하면, 그에 따라 &lt;b&gt;좌)일정 등록 현황&lt;/b&gt;과 &lt;b&gt;우)실시간 조율 현황&lt;/b&gt;이 바뀌어야 하는 요구사항이 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWyGLg/btr3a7PleyG/MxnXNVmGTkAs2wPj6kKIYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWyGLg/btr3a7PleyG/MxnXNVmGTkAs2wPj6kKIYk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;1070&quot; data-filename=&quot;스크린샷 2023-03-10 15.24.44.png&quot; style=&quot;width: 45.6688%; margin-right: 10px;&quot; data-widthpercent=&quot;46.21&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWyGLg/btr3a7PleyG/MxnXNVmGTkAs2wPj6kKIYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWyGLg%2Fbtr3a7PleyG%2FMxnXNVmGTkAs2wPj6kKIYk%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;612&quot; height=&quot;1070&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5mrUK/btr24jXSGXg/khqvKIH6GWdkEzFaGFaTGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5mrUK/btr24jXSGXg/khqvKIH6GWdkEzFaGFaTGK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;856&quot; data-filename=&quot;스크린샷 2023-03-10 15.25.00.png&quot; style=&quot;width: 53.1684%;&quot; data-widthpercent=&quot;53.79&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5mrUK/btr24jXSGXg/khqvKIH6GWdkEzFaGFaTGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5mrUK%2Fbtr24jXSGXg%2FkhqvKIH6GWdkEzFaGFaTGK%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;570&quot; height=&quot;856&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;좌) 일정 등록 현황 우) 조율 현황&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 일정 수정에 따라 진행하는 추가적인 로직들을 수행하려면 양방향을 의존해야 하는 상황이었다. 그래서 일정이 수정되면, 일정이 수정되었다는 도메인 이벤트를 발행하고 일정을 알고있는 다른 도메인들이 이벤트를 리슨해 자신의 로직을 수행하도록 만들었다. 이렇게 의존성을 단방향으로 관리했다. 이렇게만 사용한다면 문제가 없었을텐데 굳이 이벤트를 사용하지 않아야할 때도 이벤트를 사용한 부분이 있었다. 이미 의존을 하고 있어서 굳이 이벤트를 사용하지 않아도 되었는데도 이벤트를 사용해서 여러가지 문제가 발생했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이벤트를 사용함으로써 발생하는 추가적인 코드 (RegisterEvent, Listner코드, 이벤트 테스트 등)&lt;/li&gt;
&lt;li&gt;이벤트 객체를 이벤트를 발생한 도메인에 두지 못하고 Listner 쪽에 두어야 하는 점&lt;/li&gt;
&lt;li&gt;코드를 추적하기 어려움&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;인터페이스를 이용해 의존성 역전하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단방향 의존성을 위해서는 이벤트 뿐만아니라 인터페이스를 이용한 의존성 역전을 할 필요가 있었다. 현재 의존성 방향이 Room(방) -&amp;gt; TimeBlock으로 되어있다. 이 말의 뜻은 Room은 TimeBlock을 알고있어도 되지만, TimeBlock은 Room을 알 수 없다는 뜻이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-13 16.12.42.png&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhnEQx/btr3HrmkwMj/fWwb83zoqvygx9pJjkw3Xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhnEQx/btr3HrmkwMj/fWwb83zoqvygx9pJjkw3Xk/img.png&quot; data-alt=&quot;Room(방) -&amp;amp;gt; TimeBlock 의존성 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhnEQx/btr3HrmkwMj/fWwb83zoqvygx9pJjkw3Xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhnEQx%2Fbtr3HrmkwMj%2FfWwb83zoqvygx9pJjkw3Xk%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;400&quot; height=&quot;163&quot; data-filename=&quot;스크린샷 2023-03-13 16.12.42.png&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Room(방) -&amp;gt; TimeBlock 의존성 흐름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 TimeBlock이 수정될때 (참여자가 자신이 등록한 시간을 수정할 때) 방의 정보를 확인해 검증을 해야할 필요가 있었다. 방을 생성할때 지정한 마감기한이 지나지 않았는지, 방이 가지고 있는 날짜들과 시간들 안에 포함되는 시간인지 등을 검증해야했다. TimeBlock에서 Room을 직접 불러 호출하게되면 Room &amp;lt;-&amp;gt; TimeBlock의 양방향 의존성이 생기게 되었다. 이를 해결하기 위해서 인터페이스를 통한 의존성 역전을 이용했다. 또, TimeBlock이 Room을 구분하기 위한 RoomUuid를 불변값으로 가지고있게 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SQEYM/btr3GS5v3of/uiJFWT4Le6Nhegl8XLGyL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SQEYM/btr3GS5v3of/uiJFWT4Le6Nhegl8XLGyL1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;288&quot; data-filename=&quot;스크린샷 2023-03-13 15.37.43.png&quot; style=&quot;width: 49.7815%; margin-right: 10px;&quot; data-widthpercent=&quot;50.37&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SQEYM/btr3GS5v3of/uiJFWT4Le6Nhegl8XLGyL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSQEYM%2Fbtr3GS5v3of%2FuiJFWT4Le6Nhegl8XLGyL1%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;570&quot; height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wdsOo/btr3Hr7DDEQ/BDw8kY754uTttVA2288IB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wdsOo/btr3Hr7DDEQ/BDw8kY754uTttVA2288IB1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;628&quot; data-origin-height=&quot;322&quot; data-filename=&quot;스크린샷 2023-03-13 15.37.21.png&quot; data-widthpercent=&quot;49.63&quot; style=&quot;width: 49.0557%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wdsOo/btr3Hr7DDEQ/BDw8kY754uTttVA2288IB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwdsOo%2Fbtr3Hr7DDEQ%2FBDw8kY754uTttVA2288IB1%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;628&quot; height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;좌) Room, 우) TimeBlock&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1678689489798&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class RoomTimeValidator implements TimeReplaceValidator {

    private final RoomRepository roomRepository;
    private final TimeProvider timeProvider;

    @Override
    public void validate(String roomUuid, List&amp;lt;AvailableDateTime&amp;gt; availableDateTimes) {
        Room room = getRoomByRoomUuid(roomUuid);
        validateDeadLine(room.getDeadLineOrNull());
        validateContainsAllDates(room, availableDateTimes);
        validateStartAndEndTime(room, availableDateTimes);
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TimeBlock에서는 interface에만 의존하게 한 후, Room에 구현체를 두어서 의존성 역전을 통해 단방향 의존성으로 만들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vq9te/btr3prOBjAx/nlBnmnSQzSIeIJkbNG2hAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vq9te/btr3prOBjAx/nlBnmnSQzSIeIJkbNG2hAk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1862&quot; data-origin-height=&quot;846&quot; data-filename=&quot;스크린샷 2023-03-13 15.44.07.png&quot; style=&quot;width: 47.8253%; margin-right: 10px;&quot; data-widthpercent=&quot;48.39&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vq9te/btr3prOBjAx/nlBnmnSQzSIeIJkbNG2hAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvq9te%2Fbtr3prOBjAx%2FnlBnmnSQzSIeIJkbNG2hAk%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;1862&quot; height=&quot;846&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VjEJq/btr3IEeoaYK/CuTzjvRADxbzhRlPlnyLek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VjEJq/btr3IEeoaYK/CuTzjvRADxbzhRlPlnyLek/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1756&quot; data-origin-height=&quot;748&quot; data-filename=&quot;스크린샷 2023-03-13 15.44.18.png&quot; style=&quot;width: 51.0119%;&quot; data-widthpercent=&quot;51.61&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VjEJq/btr3IEeoaYK/CuTzjvRADxbzhRlPlnyLek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVjEJq%2Fbtr3IEeoaYK%2FCuTzjvRADxbzhRlPlnyLek%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;1756&quot; height=&quot;748&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;좌) 양방향 의존, 우) 단방향 의존&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 단방향 의존성에 집착하며 개발했다. 물론 현업에서 DDD를 경험해본 것도 아니고, MSA를 경험해본 것도 아니었지만 이번 프로젝트에서 열심히 집착해보았다. 아직은 단방향 의존성 관리의 장점을 그렇게 크게 느끼지는 못했지만, 어느 도메인을 수정했을때 어느 도메인까지 변경이 전파될 수 있는지는 한 눈에 알 수 있었다. 그래서 추후에 백엔드 팀원과 작업을 나눌때도 최대한 컨플릭트가 나지 않고 변경이 적은 방향으로 업무를 나누기도 편할거라고 생각되었다.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;시간 관련 코드 가독성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 번 면담시간 예약서비스를 개발했을때 가장 개선하고 싶었던 것이 시간관련된 코드의 가독성이다. 현재 테스트를 돌리고있는 현재시간이 계속 바뀌는 상황이었고, 원하는 시간으로 테스트하고 싶을때도 자유롭게 할 수 없었다. 어느 시간을 기준으로 테스트를 실행하고 있는지 테스트 코드만으로도 파악할 수 있게 만들고 싶었다. 그래서 이번 프로젝트에서는 시간관련 테스트를 최대한 가독성있게 짜서 시간테스트를 읽는 비용을 최소화하기 위해 노력했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단은 먼저, 테스트들에서 사용되는 시간의 범위를 미리 정해놓았다. 2월 8~10일, 시간은 11~14시 안에서 테스트를 돌리기로 결정하고 사용되는 시간들의 픽스처를 하나하나 만들면서 진행했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-13 16.26.44.png&quot; data-origin-width=&quot;1956&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lMgIH/btr3Bvpl1sT/kLwhMubiuDCiBpAQYzQOmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lMgIH/btr3Bvpl1sT/kLwhMubiuDCiBpAQYzQOmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lMgIH/btr3Bvpl1sT/kLwhMubiuDCiBpAQYzQOmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlMgIH%2Fbtr3Bvpl1sT%2FkLwhMubiuDCiBpAQYzQOmk%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;1956&quot; height=&quot;784&quot; data-filename=&quot;스크린샷 2023-03-13 16.26.44.png&quot; data-origin-width=&quot;1956&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수안에 시간과 날짜를 같이 넣어놔서 해당 픽스처를 사용하는 테스트에서는 변수명만 봐도 어느 시간과 날짜로 테스트를 돌리는지 한 눈에 알 수 있게 만들었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-13 16.29.23.png&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QolHX/btr3iIXKO19/hGjtRITKmpKNjCXWMUeNxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QolHX/btr3iIXKO19/hGjtRITKmpKNjCXWMUeNxK/img.png&quot; data-alt=&quot;11~13시, 9일 10일을 가지고 있는 방&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QolHX/btr3iIXKO19/hGjtRITKmpKNjCXWMUeNxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQolHX%2Fbtr3iIXKO19%2FhGjtRITKmpKNjCXWMUeNxK%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;1518&quot; height=&quot;250&quot; data-filename=&quot;스크린샷 2023-03-13 16.29.23.png&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;11~13시, 9일 10일을 가지고 있는 방&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-13 16.29.56.png&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nhUW2/btr3Hr0YQgF/j1EikV65mDeQktJhgLKpg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nhUW2/btr3Hr0YQgF/j1EikV65mDeQktJhgLKpg0/img.png&quot; data-alt=&quot;어떤 날짜와 시간을 등록하는지 알 수 있음&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nhUW2/btr3Hr0YQgF/j1EikV65mDeQktJhgLKpg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnhUW2%2Fbtr3Hr0YQgF%2Fj1EikV65mDeQktJhgLKpg0%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;1132&quot; height=&quot;706&quot; data-filename=&quot;스크린샷 2023-03-13 16.29.56.png&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;어떤 날짜와 시간을 등록하는지 알 수 있음&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 테스트에서 읽어도 어떤 날짜와 시간으로 데이터를 만들고 있는지 한 눈에 알 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Fake객체를 통한 테스트 시간 변경&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스를 통해 원하는 시간으로 테스트하도록 만들었다. 프로덕션에서는 실제 LocalDateTime.now(), 테스트에서는 우리가 원하는 시간을 반환하도록 만들고 싶었다. 지난 번 프로젝트에서 여러가지 방법을 한번 시도해본 적이 있었다. 1) mockStatic을 이용한 방법, 2) Clock을 mocking 하는 방법, 3) Fake객체를 만드는 방법 이렇게 세가지를 시도했었다. mockStatic은 동시성테스트를 위해 멀티쓰레딩 테스트 환경을 만들때 문제가 생기기도 하고 기본적으로도 static은 mocking하는 것이 안티패턴으로 여겨지기도 했다. 2번과 3번의 방법 두가지를 고민했는데, 사실 어느 것을 선택해도 방식은 비슷했다. 그래서 멘토링때 강의했던 fake 객체 주입방식을 선택해서 진행했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxaFoC/btr3iJoSdx2/OEUKH5y0qaey6DT3AqKQJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxaFoC/btr3iJoSdx2/OEUKH5y0qaey6DT3AqKQJk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;392&quot; data-filename=&quot;스크린샷 2023-03-13 16.50.48.png&quot; style=&quot;width: 38.1241%; margin-right: 10px;&quot; data-widthpercent=&quot;38.57&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxaFoC/btr3iJoSdx2/OEUKH5y0qaey6DT3AqKQJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxaFoC%2Fbtr3iJoSdx2%2FOEUKH5y0qaey6DT3AqKQJk%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;682&quot; height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjjD0E/btr3uEAc60A/o3AasDmhcmTkk4UHk6OfuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjjD0E/btr3uEAc60A/o3AasDmhcmTkk4UHk6OfuK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;218&quot; data-filename=&quot;스크린샷 2023-03-13 16.50.59.png&quot; style=&quot;width: 60.7131%;&quot; data-widthpercent=&quot;61.43&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjjD0E/btr3uEAc60A/o3AasDmhcmTkk4UHk6OfuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjjD0E%2Fbtr3uEAc60A%2Fo3AasDmhcmTkk4UHk6OfuK%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;604&quot; height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/div&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;TimeConfiguration 클래스를 통해 해당 Configuration을 Import한 테스트에서는 Fake 시간이 주입되도록 만들었다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;NoUniqueBeanDefinitionException 트러블 슈팅&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TimeConfiguration을 만들때 겪었던 트러블슈팅이 하나 있었는데, 구현체를 반환하는 메서드명(@Bean이 붙어있는)을 실제 Bean의 이름과 똑같이 설정해주어야 한다는 것이다. 그렇지 않으면 NoUniqueBeanDefinitionException 예외가 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1096&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cT3xYp/btr3HR6hpjD/EbyZtqmih7qfow7SDmutf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cT3xYp/btr3HR6hpjD/EbyZtqmih7qfow7SDmutf1/img.png&quot; data-alt=&quot;Bean이 유니크 하지 않다는 예외&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cT3xYp/btr3HR6hpjD/EbyZtqmih7qfow7SDmutf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcT3xYp%2Fbtr3HR6hpjD%2FEbyZtqmih7qfow7SDmutf1%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;2000&quot; height=&quot;1096&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1096&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Bean이 유니크 하지 않다는 예외&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 빈이 주입될때는 이름이 같은 것이 우선으로 주입된다. 프로덕션에서는 인터페이스에만 의존하기 때문에 TimeProvider를 의존하는데, 이때 스프링은 timeProvider라는 빈을 찾아 주입하려 한다. 처음엔 TimeConfiguration에서 메서드명을 timeProvider가 아닌 fakeTimeProvider로 했었는데, 이때 Bean의 이름이 @Bean 이 달린 메서드명으로 등록된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;timeProvider의 구현체들 중 현재 프로덕션에서는 realTimeProvider가 컴포넌트 스캔에 의해 Bean으로 등록되었고, fakeTimeProvider 또한 Bean으로 등록되었다. 스프링은 timeProvider 라는 이름으로 관리되는 Bean을 우선적으로 주입하려 했지만 해당 이름이 없었고 다른 이름으로 2개의 빈이 띄워져있었다. 그래서 스프링 입장에서는 어떤 빈을 주입할지 모르는 상태였고 그대로 NoUniqueBeanDefinitionException 예외가 발생하게 되는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해서는 FakeTimeProvider를 반환하는 메서드 이름을 fakeTimeProvider가 아닌 timeProvider로 바꿔야 했다. 그러면 FakeTimeProvider 구현체는 timeProvider 라는 이름의 빈으로 등록될 것이고 스프링은 이를 우선적으로 주입하게 되는 것이다. 프로덕션 코드에서는 프로덕션에 있는 하나의 Component만 빈으로 등록되기 때문에 신경쓰지 않아도 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-13 17.13.26.png&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ep9Xpd/btr3oeopw8b/HqTc00RgmtdzsspHZBNUm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ep9Xpd/btr3oeopw8b/HqTc00RgmtdzsspHZBNUm0/img.png&quot; data-alt=&quot;timeProvider가 없어 어떤 것을 주입할 지 모름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ep9Xpd/btr3oeopw8b/HqTc00RgmtdzsspHZBNUm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fep9Xpd%2Fbtr3oeopw8b%2FHqTc00RgmtdzsspHZBNUm0%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;1214&quot; height=&quot;502&quot; data-filename=&quot;스크린샷 2023-03-13 17.13.26.png&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;timeProvider가 없어 어떤 것을 주입할 지 모름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-13 17.13.48.png&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GJMmh/btr3kxIvzRX/RKAi2HCI5uqayjMPVEjy21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GJMmh/btr3kxIvzRX/RKAi2HCI5uqayjMPVEjy21/img.png&quot; data-alt=&quot;timeProvider를 우선적으로 주입함&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GJMmh/btr3kxIvzRX/RKAi2HCI5uqayjMPVEjy21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGJMmh%2Fbtr3kxIvzRX%2FRKAi2HCI5uqayjMPVEjy21%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;1222&quot; height=&quot;520&quot; data-filename=&quot;스크린샷 2023-03-13 17.13.48.png&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;timeProvider를 우선적으로 주입함&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;아직 한참 남은 작업들&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 깔끔하게 유지하려 했지만 결국 마지막 주에는 테스트 코드 없이 배포된 코드도 있었고, 쿼리가 말도안되게 많이 나가는 요청도 있었다. 다시 프로젝트가 재개될때는 실배포를 위한 남은 기능을 완성하고, 테스트코드 없이 주석으로만 표시한 코드의 테스트 보충, 쓸데없이 많이 나가는 쿼리개선, 쿼리성능개선을 차례로 수행해야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;너무 잘만난 페어&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 팀운이 너무 좋았다. 디자이너와 프론트분들도 잘 만났지만 백엔드 팀원도 잘 만났다. 백엔드는 2명이서 진행했는데, 스프링으로 실제 프로젝트는 처음하는 분이었지만, 흡수력이 굉장히 빨랐다. 몇주동안 만들고 버릴 프로젝트라면 파트를 나눠서 개발했겠지만, 지속해서 개발하고 싶은 마음이 있었다. 그래서 설계는 무조건 같이 진행하고 개발도 대부분 페어로 진행했다. 나눠서 개발하고 코딩컨벤션을 리뷰로 맞추는 시간보다 그냥 페어로 하나씩 같이 진행하는 것이 훨씬 빠를거라고 생각했다. 최대한 개발 스타일을 맞추고 도메인지식이 최대한 나에게 몰리지 않도록 했다. 언제든지 각자 업무를 맡아서 진행할 수 있도록 도메인 지식이 서로 동기화되는 것에 집중했고 코딩컨벤션도 맞춰나갔다. 만약 파트를 나눠서 개발했다면 한명만 빠져도 해당 프로젝트는 발전이 불가능하게 된다. 하지만 우리는 설계부터 같이 진행했기 때문에 누가 빠져도 추가로 코드를 작성할 수 있는 상태이다. 습득력이 빠른 페어덕분에 우테코에서 배워온 좋은 개발문화를 같이 나눌 수 있었고, 내가 부족한 인프라나 알고리즘부분을 페어로부터 배울 수 있었다. 페어를 하면서 서로의 장점을 배우려는 자세를 배웠다. 누군가를 설득하고 설득당하는 법에 대해서 많이 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;아쉬운 것&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회고를 한번도 진행하지 못했다. 페어와도 못했고, 팀 단위로도 못했다. 다들 바쁜상태로 사이드프로젝트를 참여해서 회고하자고 따로 부르는 것이 미안했다. 그렇다고 회의 끝나고 진행하려해도 다들 너무 피곤해보였다. 그런데 이것도 다 핑계이다. 안바쁘고 안힘든 팀이 어디있을까. 지금 당장은 분위기가 괜찮은것 처럼 보여도 사람이라면 마음 한켠에 말하고 싶은 것들을 그냥 쌓아두고 있는 경우가 많다. 그리고 이런 작은 것들이 쌓여 팀이 위태해진다. 회고는 이런 팀의 지속력을 유지하고 강화시킬 수 있는 도구이다. 그걸 알면서도 안하고 있던 것이 후회된다. 프로젝트가 재개한다면, 최소 일주일에 한번은 회고할 수 있는 시간을 가져보도록 해야할 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&lt;b&gt;앞으로&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 시작단계가 끝났다. 우리는 실서비스를 출시해야하고 운좋게 좋은 팀원들을 만난만큼 이 팀을 더욱 더 지속시킬 수 있는 방법을 고민해야한다. 8주동안 정신없이 지나갔고 힘든기간이었지만 팀원들 덕분에 웃으면서 데모데이까지 갈 수 있었다. 솔직히 말하면, 연합동아리에서는 사실 팀원들이 이렇게 좋고 잘하는 사람일거라고 기대는 안했었다. 생각보다 대단하고 멋진 사람들, 열심히하고 잘하고 재밌는 사람들이 이렇게 모일거라고 생각도 못했다. 운이 좋았던 것이라고 생각하고 이 팀을 지속하기 위한 고민을 계속해서 해 나가야겠다.&lt;/p&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;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다. DND 8기 5조 여러분들 그리고 이세희 코치님도 감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <author>API_Dev</author>
      <guid isPermaLink="true">https://ksabs.tistory.com/267</guid>
      <comments>https://ksabs.tistory.com/267#entry267comment</comments>
      <pubDate>Mon, 6 Mar 2023 13:56:08 +0900</pubDate>
    </item>
    <item>
      <title>지난 2개월간의 인생 리뷰 - 멘토링</title>
      <link>https://ksabs.tistory.com/266</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;살면서 가장 주도적으로 살았던 기간이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2개월동안, 마음속에만 품어왔던 도전들을 하나씩 꺼내보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 도전들을 하나씩 리뷰해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;&lt;b&gt;멘토링&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSvGoe/btr2hJId9is/2MbbmWM6AvQfKanhfMpfS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSvGoe/btr2hJId9is/2MbbmWM6AvQfKanhfMpfS1/img.png&quot; data-origin-width=&quot;1111&quot; data-origin-height=&quot;1072&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.1638%; margin-right: 10px;&quot; data-widthpercent=&quot;49.74&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSvGoe/btr2hJId9is/2MbbmWM6AvQfKanhfMpfS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSvGoe%2Fbtr2hJId9is%2F2MbbmWM6AvQfKanhfMpfS1%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;1111&quot; height=&quot;1072&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbHmZ1/btr16kPRRY3/hKcES6xJD3m3Vvnf23cq31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbHmZ1/btr16kPRRY3/hKcES6xJD3m3Vvnf23cq31/img.png&quot; data-origin-width=&quot;1111&quot; data-origin-height=&quot;1061&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.6735%;&quot; data-widthpercent=&quot;50.26&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbHmZ1/btr16kPRRY3/hKcES6xJD3m3Vvnf23cq31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbHmZ1%2Fbtr16kPRRY3%2FhKcES6xJD3m3Vvnf23cq31%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;1111&quot; height=&quot;1061&quot;/&gt;&lt;/span&gt;&lt;/div&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;a href=&quot;https://bramble-bite-117.notion.site/62b2cb0920e94111ba4a21e8b374aaa5&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;4명의 멘티들을 선발했었고 4주동안 2개의 미션으로 강의, 코드리뷰, 멘토링 등을 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[ 자동차 미션 PR ]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/hongik-dev-mentoring/java-racingcar/pulls?q=is%3Apr+is%3Aclosed&quot;&gt;https://github.com/hongik-dev-mentoring/java-racingcar/pulls?q=is%3Apr+is%3Aclosed&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1678003950996&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - hongik-dev-mentoring/java-racingcar: 자동차 경주 게임 미션 저장소&quot; data-og-description=&quot;자동차 경주 게임 미션 저장소. Contribute to hongik-dev-mentoring/java-racingcar development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/hongik-dev-mentoring/java-racingcar/pulls?q=is%3Apr+is%3Aclosed&quot; data-og-url=&quot;https://github.com/hongik-dev-mentoring/java-racingcar&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jOS8t/hyROS36bbF/ka2CyUJ6g5uoWrCdX8zCPk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/hongik-dev-mentoring/java-racingcar/pulls?q=is%3Apr+is%3Aclosed&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/hongik-dev-mentoring/java-racingcar/pulls?q=is%3Apr+is%3Aclosed&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jOS8t/hyROS36bbF/ka2CyUJ6g5uoWrCdX8zCPk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - hongik-dev-mentoring/java-racingcar: 자동차 경주 게임 미션 저장소&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;자동차 경주 게임 미션 저장소. Contribute to hongik-dev-mentoring/java-racingcar development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[ 로또 미션 PR]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/hongik-dev-mentoring/java-lotto/pulls?q=is%3Apr+is%3Aclosed&quot;&gt;https://github.com/hongik-dev-mentoring/java-lotto/pulls?q=is%3Apr+is%3Aclosed&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1678003981066&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - hongik-dev-mentoring/java-lotto: 로또 미션 진행을 위한 저장소&quot; data-og-description=&quot;로또 미션 진행을 위한 저장소. Contribute to hongik-dev-mentoring/java-lotto development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/hongik-dev-mentoring/java-lotto/pulls?q=is%3Apr+is%3Aclosed&quot; data-og-url=&quot;https://github.com/hongik-dev-mentoring/java-lotto&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cta0aE/hyROPfdUHw/ZyqQ2UbbHKMpEKKTuOkBaK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/hongik-dev-mentoring/java-lotto/pulls?q=is%3Apr+is%3Aclosed&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/hongik-dev-mentoring/java-lotto/pulls?q=is%3Apr+is%3Aclosed&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cta0aE/hyROPfdUHw/ZyqQ2UbbHKMpEKKTuOkBaK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - hongik-dev-mentoring/java-lotto: 로또 미션 진행을 위한 저장소&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;로또 미션 진행을 위한 저장소. Contribute to hongik-dev-mentoring/java-lotto development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dongho108.notion.site/Hongik-Dev-Mentoring-851928479f47434ba4ea273b2844b7e1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;실제 멘티분들이 보셨던 멘토링 노션 페이지&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1678004084918&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Hongik-Dev-Mentoring&quot; data-og-description=&quot;A new tool for teams &amp;amp; individuals that blends everyday work apps into one.&quot; data-og-host=&quot;dongho108.notion.site&quot; data-og-source-url=&quot;https://dongho108.notion.site/Hongik-Dev-Mentoring-851928479f47434ba4ea273b2844b7e1&quot; data-og-url=&quot;https://dongho108.notion.site/851928479f47434ba4ea273b2844b7e1&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://dongho108.notion.site/Hongik-Dev-Mentoring-851928479f47434ba4ea273b2844b7e1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dongho108.notion.site/Hongik-Dev-Mentoring-851928479f47434ba4ea273b2844b7e1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Hongik-Dev-Mentoring&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A new tool for teams &amp;amp; individuals that blends everyday work apps into one.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dongho108.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 진행했던 강의들도 다 영상으로 남겨놓았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-05 17.14.36.png&quot; data-origin-width=&quot;1820&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dFnlPA/btr1T6eoZIc/gKmyNJ3iBjLTKH0nU1uJk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dFnlPA/btr1T6eoZIc/gKmyNJ3iBjLTKH0nU1uJk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dFnlPA/btr1T6eoZIc/gKmyNJ3iBjLTKH0nU1uJk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdFnlPA%2Fbtr1T6eoZIc%2FgKmyNJ3iBjLTKH0nU1uJk0%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;1820&quot; height=&quot;576&quot; data-filename=&quot;스크린샷 2023-03-05 17.14.36.png&quot; data-origin-width=&quot;1820&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[ 자동차 경주 피드백 강의 영상 ]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=XbNpNzB_j2s&quot;&gt;https://www.youtube.com/watch?v=XbNpNzB_j2s&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=XbNpNzB_j2s&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/72HWW/hyRQof2V9l/lZc6EnwxZUKNVdcusBGRaK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/XbNpNzB_j2s&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 이렇게 까지 했을까&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 3학년 까지 다닐 당시 우리 학교에는 개발을 제대로 배울 수 있는 커뮤니티가 없었다. 동아리나 학회도 알고리즘 중심이고 개발을 제대로 배울 수 있는 곳이 없었다. 그렇기 때문에 휴학을 결정하고 독학기간, 우테코 기간을 통해 개발 실력을 성장시킬 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테코를 하는 기간 내내 실시간으로 실력이 늘어가는 과정을 느꼈고 좋은 환경에서는 성장속도를 비약적으로 상승시킬 수 있다는 것을 깨달았다. 혼자 공부했을때는 물어볼 사람도 없었고, 내가 잘 하고있는지 알 수 있는 방법이 없었다. 이 과정을 후배들도 겪게하고 싶지 않았다. 그래서 우테코를 하는 기간동안 교육프로세스를 잘 체화하고 성장해서 학교에서 꼭 내 지식과 경험을 공유하고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;무엇을,어떻게?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 우테코를 직접 했으니까 그대로 돌려주는 것은 쉬울 수 있겠다라고 생각했다. 직접 멘토링을 하려다보니 생각할 것들이 너무 많았다. 정말 내가 혼자서 리뷰를 다 할 수 있을까? 리뷰는 어디까지 해야될까? 어떤 강의를 어디까지 진행해야할까? 참여하는 사람들이 있을까? 제대로 할까? 얼마나 시간을 쏟을까? 등등 수백가지의 고민을 하며 기획했다.&lt;/p&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월달 초였고, 3월에 개강한다는 것을 생각해봤을때 완벽한 기획을 하고나서 모집을 하려면 늦을 것 같았다. 그래서 일단 에타에 글부터 올렸다. 급하게 멘토링용 팀 레포지토리를 파고, 우테코 프리코스 과제를 fork하고, 최소한의 사전미션을 진행하기 위한 가이드라인정도만 수정해서 모집을 시작했다. 모집을 시작하는 동시에 제대로된 일정 기획을 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 이들에게 4주만에 전달할 수 있는 가치는 무엇일까? 우테코는 10개월 동안 진행되었기 때문에 그 안에서 내가 이렇게 성장할 수 있었다. 그런데 고작 4주동안 내가 이들을 성장시킬 수 있을까? 고민끝에 결정한 것은, 4주동안 이들이 배우는 것들을 모두 소화하기 보다는 &lt;b&gt;앞으로 어떤 공부를 어떻게 해 나가야할지 깨달을 수 있는 것&lt;/b&gt;에 집중을 해야겠다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면서도 내가 아니면 이들이 얻지 못하는 것을 얻어가면 좋겠다고 생각했다. 그렇게 뽑아낸 키워드들은 TDD, OOP, 클린코드, 페어프로그래밍이다. 단순히 강의를 듣고 프로젝트만 따라친 학생들은 이 4가지 키워드들을 제대로 학습해보지 못했을 것이다. 4주동안 이 것들이 뭔지, 어떻게 공부를 하고 앞으로 어떻게 학습해야 이 것들을 체화할 수 있을 것인지 전달하자고 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD는 당연히 강의만 듣는다고 잘 하는 것이 아니다. 스스로 왜 필요한지 깨닫고 직접 코드를 작성하면서 체화를 꾸준히 해 나가야하는 것이었다. 그래서 TDD 강의에서는 테스트가 왜 필요한지, TDD를 처음에 어떻게 시작하는지만 알려주었다. 그리고 미션을 요구사항을 TDD로 진행하면서 스스로 깨달을 수 있도록 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OOP, 클린코드는 자신이 무엇을 잘못하고 있는지 빠르게 피드백을 받는 것이 중요했다. OOP 가이드라인인 객체지향생활체조 원칙을 제공하고 이 원칙 안에서 코드리뷰를 진행했다. 그리고 리뷰에서는 왜 이렇게 작성해야 객체지향 프로그래밍에 가까워 질 수 있는지 이유를 같이 제공했다. 자신이 작성한 코드 바로 밑에 달린 리뷰를 통해 즉각적으로 어떤 코드가 어떤 코드로 바뀌어야 하는지 생각할 수 있도록 했다. 클린코드도 프로그래밍 요구사항을 제공하여 규칙 안에서 작성하도록 진행했다. 컨벤션 문서를 제공하고 메서드가 15라인을 넘지 말아야한다는 규칙과 인덴트는 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;페어프로그래밍은 처음에 첫 미션부터 페어로 진행하려고 생각했었다. 실제 우테코 때는 3주동안 프리코스에서 3개의 미션을 통해 어느정도 컨벤션과 프로그래밍 요구사항에 대한 거부감을 해소하기 때문에 페어를 진행해도 수월할 수 있었던 것이고, 내가 진행하는 멘토링에서는 사전과제를 1번만 진행했기 때문에 우테코 합격자들보다 당연히 컨벤션과 프로그래밍 요구사항에 대한 거부감이 많이 있을 수 있겠다고 생각했다. 그래서 첫 미션에서는 코드리뷰를 소화하는 것만 해도 힘들것 같다고 생각해 두번째 미션부터 페어프로그래밍을 도입했다. 첫번째 미션을 통해 컨벤션과 클린코드 작성하는 방법에 대해 어느정도 잡고나서 두번째 미션때 건강한 토론이 일어날 수 있겠다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 마지막 미션 때는 사전과제의 코드보다 훨씬 더 나아진 퀄리티의 코드를 제출하는 멘티분들을 볼 수 있었고 너무 뿌듯했다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J2uwA/btr114T7jzW/9KAmajekQsVJ1kXJVoQY4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J2uwA/btr114T7jzW/9KAmajekQsVJ1kXJVoQY4k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;606&quot; data-filename=&quot;스크린샷 2023-03-05 18.02.56.png&quot; style=&quot;width: 54.5882%; margin-right: 10px;&quot; data-widthpercent=&quot;55.23&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J2uwA/btr114T7jzW/9KAmajekQsVJ1kXJVoQY4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ2uwA%2Fbtr114T7jzW%2F9KAmajekQsVJ1kXJVoQY4k%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;1514&quot; height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coTeTG/btr1TS8NkNO/lO5Oa95gu7nuGEspABQFj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coTeTG/btr1TS8NkNO/lO5Oa95gu7nuGEspABQFj1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;636&quot; data-filename=&quot;스크린샷 2023-03-05 18.03.12.png&quot; style=&quot;width: 44.249%;&quot; data-widthpercent=&quot;44.77&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coTeTG/btr1TS8NkNO/lO5Oa95gu7nuGEspABQFj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoTeTG%2Fbtr1TS8NkNO%2FlO5Oa95gu7nuGEspABQFj1%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;1288&quot; height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;후기&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7QZCT/btr1Wx3SFaT/wb4gi1duttiqXZfvwFui30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7QZCT/btr1Wx3SFaT/wb4gi1duttiqXZfvwFui30/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1498&quot; data-origin-height=&quot;982&quot; data-filename=&quot;스크린샷 2023-03-05 18.03.59.png&quot; data-widthpercent=&quot;30.01&quot; style=&quot;width: 29.3107%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7QZCT/btr1Wx3SFaT/wb4gi1duttiqXZfvwFui30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7QZCT%2Fbtr1Wx3SFaT%2Fwb4gi1duttiqXZfvwFui30%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;1498&quot; height=&quot;982&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6cXwU/btr2hI3EbDe/J3IfI5t7MfL3KaSLPQSnC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6cXwU/btr2hI3EbDe/J3IfI5t7MfL3KaSLPQSnC1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1476&quot; data-origin-height=&quot;838&quot; data-filename=&quot;스크린샷 2023-03-05 18.04.10.png&quot; data-widthpercent=&quot;34.65&quot; style=&quot;width: 33.843%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6cXwU/btr2hI3EbDe/J3IfI5t7MfL3KaSLPQSnC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6cXwU%2Fbtr2hI3EbDe%2FJ3IfI5t7MfL3KaSLPQSnC1%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;1476&quot; height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crUCKc/btr1MkEjP34/zzORUi2dKXaKqesg9X4KMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crUCKc/btr1MkEjP34/zzORUi2dKXaKqesg9X4KMK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1484&quot; data-origin-height=&quot;826&quot; data-filename=&quot;스크린샷 2023-03-05 18.04.26.png&quot; data-widthpercent=&quot;35.34&quot; style=&quot;width: 34.5207%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crUCKc/btr1MkEjP34/zzORUi2dKXaKqesg9X4KMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrUCKc%2Fbtr1MkEjP34%2FzzORUi2dKXaKqesg9X4KMK%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;1484&quot; height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘엔 우테코의 교육방식과 비슷한 방식으로 진행하는 부트캠프나 학원들이 많아졌다. 그럼에도 우테코 수료생들이 다른 분들보다 더 나은 결과를 낼 수 있는 차이는 어떤 크루들이 모여 있는지에 달려있다고 생각한다. 주변에 잘하는 사람들이 널려있고 또 그들이 치열하게 열심히 공부하는 모습을 보는 것으로 동기부여를 받을 수 있다. 모르는 부분이나 토론할 부분이 있다면 언제든지 건강한 토론을 할 수 있는 사람들이 많았다. 멘토링을 진행하며 가장 아쉬웠던 부분은 내가 가장 크게 느꼈던 이런 토론이나 커뮤니티 부분을 경험시키지 못했다는 것이다. 비대면으로 진행하기도 했고, 이들이 알아서 잘 소통하기를 기대하고 방치했었다. 사실 비대면이기때문에 어쩔 수 없는 부분들도 있고, 4주간의 짧은 기간동안 경험하기에는 어려운 부분일 수 있다. 그래도 다음에 또 멘토링을 진행할때 이런 부분들도 제공한다면 정말 학생들에게 소중한 가치를 전달할 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-05 18.03.39.png&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuETIg/btr133glHou/CNaXzdWz5ctMbTlxJLl3oK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuETIg/btr133glHou/CNaXzdWz5ctMbTlxJLl3oK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuETIg/btr133glHou/CNaXzdWz5ctMbTlxJLl3oK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuETIg%2Fbtr133glHou%2FCNaXzdWz5ctMbTlxJLl3oK%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;600&quot; height=&quot;276&quot; data-filename=&quot;스크린샷 2023-03-05 18.03.39.png&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;674&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 노력을 알아주었는지 GSDC에서 후배들한테 동기부여를 줄 수 있는 기회를&amp;nbsp;받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rj5jz/btr1QxDnVjL/YGKZs9TzOH2HoNCXF04dKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rj5jz/btr1QxDnVjL/YGKZs9TzOH2HoNCXF04dKk/img.png&quot; width=&quot;400&quot; height=&quot;145&quot; data-origin-width=&quot;1398&quot; data-origin-height=&quot;506&quot; data-is-animation=&quot;false&quot; style=&quot;width: 66.665%; margin-right: 10px;&quot; data-widthpercent=&quot;67.45&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rj5jz/btr1QxDnVjL/YGKZs9TzOH2HoNCXF04dKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frj5jz%2Fbtr1QxDnVjL%2FYGKZs9TzOH2HoNCXF04dKk%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;1398&quot; height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/93Zsv/btr1XQhAIt5/KIfjwTPu8vakFPEL7PTZz1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/93Zsv/btr1XQhAIt5/KIfjwTPu8vakFPEL7PTZz1/img.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1080&quot; data-filename=&quot;IMG_3412.JPG&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.1722%;&quot; data-widthpercent=&quot;32.55&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/93Zsv/btr1XQhAIt5/KIfjwTPu8vakFPEL7PTZz1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F93Zsv%2Fbtr1XQhAIt5%2FKIfjwTPu8vakFPEL7PTZz1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대학생활을 하면서 세상에 존재하는 여러 불편한 점들을 외면하지 말고, 내가 가진 기술로 스스로 해결하려고 도전하라는 메세지를 전달하고 왔다. 도전을 하다보면 알아서 공부도 할거고, 실패도 할거고, 그 실패들이 경험으로 쌓일거고, 돈도 벌게 될 수 있겠다는 내용을 내 경험을 통해 전달했다.&lt;/p&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://youtu.be/d_WXNuHOq8U&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://youtu.be/d_WXNuHOq8U&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=d_WXNuHOq8U&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/oFjHx/hyRQpAPYF5/hZgtIGKBJXGC8d3aCEwwG0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/d_WXNuHOq8U&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption&gt;얼굴이 안나와서 아쉽다. 자막 키고 1.5배속으로 들으세요 ㅎㅎ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우테코에서 테코톡, 데모데이, 유스콘 등 여러 기술 발표를 했었다. 기술발표는 어떤 정보를 전달할 것인지가 명확해서 발표를 준비하기까지 수월했던 것 같다. 기술 학습하는 기간이 어려웠을 뿐이지 발표 자체는 틀린 내용만 아니라면 ppt를 통해 정보를 전달하면 되었었다. 하지만 이번 발표는 다르게 준비했다. 내 경험을 전달하기 위해 ppt는 거의 준비하지 않았다. 대신 내가 겪었던 어떤 경험을 어떻게 전달할 것인지에 집중했다. 남들 앞에서 썰을 잘푸는 성격이 아니다보니까 기술발표보다 스트레스를 더 많이 받았다. 경험 자체가 주관적이기도 하고, 사람들은 내 입과 표정, 몸짓, 말투에 더 집중을 하게 되는 발표이기 때문에 연기자가 된 듯이 발표준비를 했다. 그리고 어떤 가치를 전달하기 위해 이 경험을 전달할 건지도 정해야했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로는 연습했던것 만큼 실제로도 했던 것 같다. 초반에 사람들에게 질문도 하는 시간이 있었는데 그때 절어서 진짜 민망했다. 그래도 어차피 내가 티내지만 않으면 듣는 사람 입장에서는 크게 와닿지 않는다는 것을 알고있었기 때문에 티내지 않고 그대로 진행했다. 그리고 또 기억에 남는 아쉬웠던 점은 처음에 컴퓨터공학과에 왜 왔냐고 질문했을때 새내기가 창업하고싶다고 대답을 했었는데, 이때 대단하다는 반응을 더 해줄걸 이라는 아쉬움이다. 말을 절어서 당황하기도 했었는데, 대답을 해줬다는 것 자체도 고마웠지만 창업이라는 대단한 이유가 있었는데 이 분에게 더 특별한 연사로 남기 위해서는 대단하는 반응 정도는 해줬어야했을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 누군가에게 가치를 전달한다는 것 자체만으로도 내 인생을 보는 시야가 달라진다는 것을 깨달았다. 취준하는 기간동안 자존감도 많이 깎였고, 내가 할 수 있는 일이 아무것도 없다는 생각이 가득찼었다. 그리고나서 본가에 왔는데, 형이 그렇게 고군분투하고 치열하게 살아가고 본인의 인생을 만들어가는 것을 보면서 다시한번 내 인생에 집중해봐야겠다고 생각했다. 그리고 내가, 나만이 할 수 있는 것을 생각하고 기획했고 도전했고 나름 성공했다. 앞으로는 더 많은 도전을 해낼 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;멘토링 도와준 숟갈이도 너무너무 고마워!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <author>API_Dev</author>
      <guid isPermaLink="true">https://ksabs.tistory.com/266</guid>
      <comments>https://ksabs.tistory.com/266#entry266comment</comments>
      <pubDate>Sun, 5 Mar 2023 17:39:48 +0900</pubDate>
    </item>
    <item>
      <title>서비스 제공자의 시각으로 살아가기</title>
      <link>https://ksabs.tistory.com/263</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;시작&lt;/b&gt;&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문을 열고 나오면 부쩍 쌀쌀해진 공기가 피부로 느껴진다. 옷이 한 겹씩 늘어날 때마다 올해도 점점 끝이 다가오는 것 같다. 요즘 입을 옷을 꺼내놓으면서 올해 초와 참 닮아있다는 생각이 든다. 그때도 아직은 쌀쌀한 날씨였기에 차마 얇게는 입고 다니지 못했었다. 꿈만 같았던 합격 날의 기쁨도 아직 가시지 않았었다. 나는 그토록 이곳에 왜 오고 싶어 했을까? 다시 떠올려본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-기술이-필요했다&quot; href=&quot;https://github.com/dongho108/woowa-writing-4/blob/level4/writing/level4/level4.md#%EA%B8%B0%EC%88%A0%EC%9D%B4-%ED%95%84%EC%9A%94%ED%96%88%EB%8B%A4&quot; aria-hidden=&quot;true&quot; data-turbo-frame=&quot;repo-content-turbo-frame&quot;&gt;&lt;/a&gt;&lt;b&gt;기술이 필요했다.&lt;/b&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;&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;&lt;a id=&quot;user-content-지금도-마음은-변하지-않았다&quot; href=&quot;https://github.com/dongho108/woowa-writing-4/blob/level4/writing/level4/level4.md#%EC%A7%80%EA%B8%88%EB%8F%84-%EB%A7%88%EC%9D%8C%EC%9D%80-%EB%B3%80%ED%95%98%EC%A7%80-%EC%95%8A%EC%95%98%EB%8B%A4&quot; aria-hidden=&quot;true&quot; data-turbo-frame=&quot;repo-content-turbo-frame&quot;&gt;&lt;/a&gt;&lt;b&gt;지금도 마음은 변하지 않았다.&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언제나 개발할 때 내 모든 판단의 기준은 서비스를 사용할 사용자이다. 우아한형제들 CEO 범준 님의 말씀 중 정말 와 닿았던 말이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;고객을 위해서 내가 짠 코드를 버릴 수 있어야 한다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말은 쉽지만 개발자로서 실천하기는 참 어렵다. 물론 고객이 직접&lt;span&gt;&amp;nbsp;&lt;/span&gt;&quot;이 코드 버려!&quot;&lt;span&gt;&amp;nbsp;&lt;/span&gt;라고 이야기하지는 않을 것이다. 범준님 말씀의 진짜 뜻은&lt;span&gt;&amp;nbsp;&lt;/span&gt;우리가 무엇을, 누구를 위해 개발을 하는 가를 계속 생각하라는 의미인 것 같다. 서비스가 만들어진 계기는 분명히 이 서비스가 필요한 고객이 있기 때문일 것이다. 모든 요구사항을 기술적으로만 풀어낼 필요는 없지만, 적어도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&quot;이 요구사항은 코드가 많이 바뀔 것 같아 못할 것 같아요.&quot;&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 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;&lt;a id=&quot;user-content-가까운-곳에-있는-사용자&quot; href=&quot;https://github.com/dongho108/woowa-writing-4/blob/level4/writing/level4/level4.md#%EA%B0%80%EA%B9%8C%EC%9A%B4-%EA%B3%B3%EC%97%90-%EC%9E%88%EB%8A%94-%EC%82%AC%EC%9A%A9%EC%9E%90&quot; aria-hidden=&quot;true&quot; data-turbo-frame=&quot;repo-content-turbo-frame&quot;&gt;&lt;/a&gt;&lt;b&gt;가까운 곳에 있는 사용자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 생각해보니 내 주위에 이미 수많은 사용자가 있었다. 내 코드를 읽고 사용하게 될 동료들이 이미 주변에 있었다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&quot;누군가를 편하게 해주고 싶다.&quot;&lt;span&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-수많은-고객을-만족시키기&quot; href=&quot;https://github.com/dongho108/woowa-writing-4/blob/level4/writing/level4/level4.md#%EC%88%98%EB%A7%8E%EC%9D%80-%EA%B3%A0%EA%B0%9D%EC%9D%84-%EB%A7%8C%EC%A1%B1%EC%8B%9C%ED%82%A4%EA%B8%B0&quot; aria-hidden=&quot;true&quot; data-turbo-frame=&quot;repo-content-turbo-frame&quot;&gt;&lt;/a&gt;&lt;b&gt;수많은 고객을 만족시키기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 가장 크게 느끼는 것은 한 명의 고객을 만족시키는 것보다 수많은 고객을 만족시키는 것은 훨씬 더 힘들다는 것이다. 수천만 명의 사용자를 받는 서비스는 근본적인 설계부터 달라진다. 사용자 수가 적을 때는 고려하지 않아도 될 문제들이 사용자가 많아질수록 계속 생겨난다. 결국, 내가 원하는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;더 많은&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사람들에게 편리함을 제공하는 것이다. 수많은 고객을 만족시키기 위해 해야 할 공부들이 넘쳐난다. 내 서비스의 사용자들 모두 만족시키기 위해 수천만 명의 고객도 서비스할 수 있는 개발자가 될 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-또-시작&quot; href=&quot;https://github.com/dongho108/woowa-writing-4/blob/level4/writing/level4/level4.md#%EB%98%90-%EC%8B%9C%EC%9E%91&quot; aria-hidden=&quot;true&quot; data-turbo-frame=&quot;repo-content-turbo-frame&quot;&gt;&lt;/a&gt;&lt;b&gt;또, 시작&lt;/b&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;/p&gt;</description>
      <category>회고/우아한테크코스</category>
      <author>API_Dev</author>
      <guid isPermaLink="true">https://ksabs.tistory.com/263</guid>
      <comments>https://ksabs.tistory.com/263#entry263comment</comments>
      <pubDate>Tue, 25 Oct 2022 22:24:02 +0900</pubDate>
    </item>
    <item>
      <title>[DI 구현하기] 의존성 주입이 필요한 이유와 DI 컨테이너의 탄생</title>
      <link>https://ksabs.tistory.com/262</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링을 사용하는 핵심이유중 하나는 DI (Dependency Injection) 의존성 주입입니다.&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;간단한 UserService와 UserDao 예제로 의존성 주입이 왜 필요한 것인지 간단히 이해해보고, 나아가서 스프링이 제공하는 DI 컨테이너를 직접 만들어 보겠습니다.&lt;/p&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;&lt;b&gt;1단계 : 생성자 주입(?)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재, 사용할 &lt;b&gt;UserDao 인스턴스&lt;/b&gt;를 &lt;b&gt;생성자를 통해&lt;/b&gt; &lt;b&gt;외부에서 전달받는 UserService&lt;/b&gt;가 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserService&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1664801157003&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserService {

    private final UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public User join(User user) {
        userDao.insert(user);
        return userDao.findById(user.getId());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserDao&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1664801172999&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserDao {

    private static final Map&amp;lt;Long, User&amp;gt; users = new HashMap&amp;lt;&amp;gt;();

    private final JdbcDataSource dataSource;

    public UserDao() {
        final var jdbcDataSource = new JdbcDataSource();
        jdbcDataSource.setUrl(&quot;jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;&quot;);
        jdbcDataSource.setUser(&quot;&quot;);
        jdbcDataSource.setPassword(&quot;&quot;);

        this.dataSource = jdbcDataSource;
    }

    public void insert(User user) {
        try (final var connection = dataSource.getConnection()) {
            users.put(user.getId(), user);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public User findById(long id) {
        try (final var connection = dataSource.getConnection()) {
            return users.get(id);
        } catch (SQLException e) {
            return null;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;개발자가 위의 코드를 이용해 UserServiceTest를 작성하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, DB 없이 돌아가는 테스트를 작성하기 위해서 어떻게 해야할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDao가 Jdbc에 직접 의존하고 있기 때문에 현재 구조에서는 방법이 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 연결하지 않고 테스트를 짤 수 없습니다.&lt;/p&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;Jdbc에 의존하지 않는 DAO를 새로 생성해 UserService 코드상 에서 교체해준뒤, 외부에서도 새로운 DAO를 전달해주는 방법이 있을 수 있습니다.&lt;/p&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;UserDao의 변경을 위해 UserService의 코드를 변경&lt;/span&gt;해주어야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDao를 사용하는 클래스가 수백개 있다고 가정하면 UserDao를 교체하기 위해 수백개의 클래스를 직접 다 수정해주어야 합니다.&lt;/p&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;위와 같은 문제점을 해결하기 위해서는 객체지향의 5가지 원칙중 하나인 DIP (구현 클래스에 의존하지 않고 인터페이스에 의존해라)를 만족하게 하면 됩니다.&lt;/p&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;UserService가 UserDao 인터페이스에 의존하도록 바꿔보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2단계 : 인터페이스에 의존하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현체들이 사용할 메서드 명세만 작성해놓은 UserDao interface를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserDao interface&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1664801601958&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface UserDao {

    void insert(User user);

    User findById(long id);
}&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;UserService는 UserDao 인터페이스에만 의존하도록 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 외부에서 사용할 UserDao의 구현체를 주입받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserService&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1664801654469&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserService {

    private final UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public User join(User user) {
        userDao.insert(user);
        return userDao.findById(user.getId());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1단계와 똑같이 위의 코드로 DB 없이 돌아가는 UserServiceTest를 작성하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 UserService가 UserDao 인터페이스에만 의존하고 있기 때문에 외부에서 DB없이 돌아가는 구현체를 주입해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserServiceTest&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InMemoryUserDao() 라는 메모리 상에서 돌아가는 DB를 구현한뒤 UserService에 주입해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1664801860834&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Test
    void joinTest() {
        final var user = new User(1L, &quot;ash&quot;);

        final UserDao userDao = new InMemoryUserDao();
        final var userService = new UserService(userDao);

        final var actual = userService.join(user);

        assertThat(actual.getAccount()).isEqualTo(&quot;ash&quot;);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스에만 의존하도록 수정했더니 UserDao의 변경이 UserService에게 영향을 미치지 않게 되었습니다.&lt;/p&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;UserDao 구현체를 변경하기 위해서는 UserService를 사용하는 어딘가에서 구현체를 결정하고 주입해주어야 한다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserController&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserController에서 UserDao의 구현체를 결정해주고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1664802526347&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
public class UserController {
    private UserService userService;

    @PostMapping(&quot;/api/user&quot;)
    public String join(final Long id, final String name) {
        final var user = new User(1L, &quot;gugu&quot;);
        
        userService = new UserService(new InMemoryUserDao());
        userService.join(user);
        return &quot;index.html&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명히 인터페이스에 의존하게 변경했는데도 어딘가에서는 UserDao의 변경에 영향이 가는 곳이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이런 문제가 발생한 것일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;관계설정 책임&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserService를 사용하는 곳에서는 자신과는 상관없는 UserDao 구현체를 결정해준다는 책임이 숨어있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserController&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserController에서 UserDao의 구현체를 결정해주고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1664802629066&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
public class UserController {
    private UserService userService;

    @PostMapping(&quot;/api/user&quot;)
    public String join(final Long id, final String name) {
        final var user = new User(1L, &quot;ash&quot;);
        
        userService = new UserService(new InMemoryUserDao());
        userService.join(user);
        return &quot;index.html&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserController에게 UserDao 구현체를 결정해줘야된다는 책임이 필요할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 관계설정의 관심사를 분리하지 않으면 결국에는 UserDao가 확장 가능한 클래스라고 볼 수는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3단계 : DI Container&lt;/b&gt;&lt;/h2&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;전혀 다른 외부에서 책임을 가지도록 구현하면 됩니다. (제어의 역전. IoC)&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;그리고 이런 객체를 생성하고 연결해주는 역할을 하는 클래스를 DI Container 라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 사용할 구현체를 능동적으로 결정하는 것이 아니라 외부의 다른 객체에게 제어권한을 넘깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;DI Container가 객체를 생성하고 관계를 설정하고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserService가 DI Container에게 모든 제어 권한을 위임하도록 구현해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserService는 DIContainer로 부터 사용할 인스턴스를 제공받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인스턴스 안에는 사용할 UserDao 구현체도 결정되어있죠.&lt;/p&gt;
&lt;pre id=&quot;code_1664803349609&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Test
    void stage3() {
        final var user = new User(1L, &quot;ash&quot;);

        final var diContainer = createDIContainer();

        final var userService = diContainer.getBean(UserService.class);

        final var actual = userService.join(user);

        assertThat(actual.getAccount()).isEqualTo(&quot;gugu&quot;);
    }

    /**
     * DIContainer가 관리하는 객체는 빈(bean) 객체라고 부른다.
     */
    private static DIContainer createDIContainer() {
        var classes = new HashSet&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt;();
        classes.add(InMemoryUserDao.class);
        classes.add(UserService.class);
        return new DIContainer(classes);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DI Container&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;제가 구현한 DI Container의 원리는 간단하게 아래와 같습니다.&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;li&gt;컨테이너는 빈을 전달받을 클래스를 인스턴스화해서 가지고 있습니다.&lt;/li&gt;
&lt;li&gt;그리고 한번 인스턴스화 된 빈을 계속 사용합니다.&lt;/li&gt;
&lt;li&gt;빈으로 등록될 &lt;b&gt;객체가 들고있는 필드&lt;/b&gt;가 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;빈으로 등록되어있으면 걔네들도 주입&lt;/b&gt;&lt;/span&gt;합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1664803446796&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 스프링의 BeanFactory, ApplicationContext에 해당되는 클래스
 */
class DIContainer {

    private final Set&amp;lt;Object&amp;gt; beans = new HashSet&amp;lt;&amp;gt;();

    // TODO: 같은 클래스로 여러 빈 등록 불가하게 수정해야한다.
    public DIContainer(final Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; classes) {
        // 클래스를 인스턴스화 해서 Set에 저장
        initBean(classes);
        // 빈의 필드에 빈으로 등록할 수 있는 필드 등록
        beans.forEach(this::setFields);
    }

    private void initBean(final Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; classes) {
        try {
            for (Class&amp;lt;?&amp;gt; aClass : classes) {
                addBeans(aClass);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void setFields(final Object bean) {
        // 필드가 빈으로 등록되어있으면 주입한다.
        final List&amp;lt;Field&amp;gt; fields = List.of(bean.getClass().getDeclaredFields());
        fields.forEach(field -&amp;gt; setField(bean, field));
    }

    private void setField(final Object obj, final Field field) {
        field.setAccessible(true);
        beans.forEach(bean -&amp;gt; setFieldValue(obj, field, bean));
    }

    private void setFieldValue(final Object obj, final Field field, final Object bean) {
        final Class&amp;lt;?&amp;gt; fieldType = field.getType();
        if (fieldType.isInstance(bean)) {
            try {
                field.set(obj, bean);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void addBeans(final Class&amp;lt;?&amp;gt; aClass)
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        final Constructor&amp;lt;?&amp;gt; constructor = aClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        final Object instance = constructor.newInstance();
        beans.add(instance);
    }

    @SuppressWarnings(&quot;unchecked&quot;)
    public &amp;lt;T&amp;gt; T getBean(final Class&amp;lt;T&amp;gt; aClass) {
        return (T) beans.stream()
                .filter(aClass::isInstance)
                .findFirst()
                .orElseThrow(() -&amp;gt; new NoSuchElementException(&quot;등록된 빈이 아닙니다.&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본생성자로 리플렉션을 이용해 인스턴스를 생성하는 부분,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드들을 읽어서 해당필드의 타입이 빈으로 등록된 인스턴스중 같은 타입이면 필드에도 주입해주는 부분&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;들이 이해하기 어려울 수 있지만 여러번 읽어보면 또 쉽게 이해할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;&lt;b&gt;4단계 : 어노테이션 기반 DI 컨테이너&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금도 DI 컨테이너는 자신의 책임에 맞게 잘 동작하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 개발자는 매번 빈을 직접 등록해주는 일도 번거롭습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 어떤 클래스가 빈으로 등록되는 클래스인지 알기 위해서는 DI컨테이너까지 가봐야 알 수 있습니다.&lt;/p&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-filename=&quot;스크린샷 2022-10-04 09.46.45.png&quot; data-origin-width=&quot;602&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAZnJI/btrNB57XxmX/zXQpoRHfmO3En4OR8aume0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAZnJI/btrNB57XxmX/zXQpoRHfmO3En4OR8aume0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAZnJI/btrNB57XxmX/zXQpoRHfmO3En4OR8aume0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAZnJI%2FbtrNB57XxmX%2FzXQpoRHfmO3En4OR8aume0%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;602&quot; height=&quot;218&quot; data-filename=&quot;스크린샷 2022-10-04 09.46.45.png&quot; data-origin-width=&quot;602&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&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;그리고 이것은 InterviewService의 @Service에 해당하는 부분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-04 09.47.26.png&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rWWcl/btrNGvZeDCp/3YxgNjpieVheNrTWdpR5L0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rWWcl/btrNGvZeDCp/3YxgNjpieVheNrTWdpR5L0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rWWcl/btrNGvZeDCp/3YxgNjpieVheNrTWdpR5L0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrWWcl%2FbtrNGvZeDCp%2F3YxgNjpieVheNrTWdpR5L0%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;608&quot; height=&quot;258&quot; data-filename=&quot;스크린샷 2022-10-04 09.47.26.png&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service 어노테이션 클래스 안에는 @Component 가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 @Component 가 빈으로 등록된다는 것을 명시하는 어노테이션 입니다.&lt;/p&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;이처럼 어노테이션 기반의 DI컨테이너를 만들면 개발자는 어떤 설정파일에 직접 빈을 등록할 필요 없이 클래스 위에 어노테이션을 붙이는 것 만으로도 객체를 빈으로 등록해 관리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어노테이션 기반의 DI 컨테이너를 직접 구현해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 간단하게 @Service, @Repository 어노테이션이 붙은 클래스, 더해서 @Inject 어노테이션이 붙은 필드의 의존성을 자동으로 주입해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(@Inject 는 @Autowired에 해당하는 어노테이션 이라고 봐도 무방합니다.)&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;b&gt;DIContainer&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;createContainerForPakage 에서는 전달받은 rootPakageName을 루트로 가지는 모든 클래스들 가져오고&lt;/li&gt;
&lt;li&gt;이 클래스들 중 @Service, @Repository 어노테이션이 붙은 클래스를 추출하고,&lt;/li&gt;
&lt;li&gt;추출된 클래스들을 빈으로 등록합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1664844782238&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    ...
    public static DIContainer createContainerForPackage(final String rootPackageName) {
        // @Service 가 붙은 클래스들의 인스턴스를 bean으로 등록하자.
        final Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; allClasses = ClassPathScanner.getAllClassesInPackage(rootPackageName);
        final Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; injectClasses = allClasses.stream()
                .filter(DIContainer::isComponent)
                .collect(Collectors.toSet());
        return new DIContainer(injectClasses);
    }

    private static boolean isComponent(final Class&amp;lt;?&amp;gt; aClass) {
        return aClass.isAnnotationPresent(Service.class)
                || aClass.isAnnotationPresent(Repository.class);
    }
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setField 에서는 모든 필드의 의존성을 주입하는 것이 아닌, @Inject 가 붙어있는 필드에만 의존성을 주입합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1664844898530&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
// @Inject 붙은 필드의 의존성을 주입해준다.
    private void setFields(final Object bean) {
        final List&amp;lt;Field&amp;gt; fields = List.of(bean.getClass().getDeclaredFields());
        final List&amp;lt;Field&amp;gt; injectFields = fields.stream()
                .filter(field -&amp;gt; field.isAnnotationPresent(Inject.class))
                .collect(Collectors.toList());
        injectFields.forEach(field -&amp;gt; setField(bean, field));
    }
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserService&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDao 인터페이스를 필드로 가지고 있지만 구현체를 할당하는 코드가 존재하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 DI 컨테이너에서 @Inject 가 붙은 필드에 의존성을 주입해주기 때문에 런타임에는 구현체를 가지고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1666672381814&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class UserService {

    @Inject
    private UserDao userDao;

    public User join(final User user) {
        userDao.insert(user);
        return userDao.findById(user.getId());
    }

    private UserService() {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CePLf/btrPynrbbyU/BQkAZkhAhIAXSdu3WNv5XK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CePLf/btrPynrbbyU/BQkAZkhAhIAXSdu3WNv5XK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CePLf/btrPynrbbyU/BQkAZkhAhIAXSdu3WNv5XK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCePLf%2FbtrPynrbbyU%2FBQkAZkhAhIAXSdu3WNv5XK%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;1098&quot; height=&quot;618&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토비의 스프링 1권 p.378~379&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 적절하게 책임과 관심이 다른 코드를 분리하고, 서로 영향을 주지 않도록 다양한 추상화 기법을 도입하고, 애플리케이션 로직과 기술/환경을 분리하는 등의 작업은 갈수록 복잡해지는 엔터프라이즈 애플리케이션에는 반드시 필요하다. 이를 위한 핵심적인 도구가 바로 스프링이 제공하는 DI다. 스프링의 DI가 없었다면 인터페이스를 도입해서 나름 추상화를 했더라도 적지 않은 코드 사이의 결합이 남아있게 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;스프링의 의존관계 주입 기술인 DI는 모든 스프링 기술의 기반이 되는 핵심 엔진이자 원리이며, 스프링이 지지하고 지원하는, 좋은 설계와 코드를 만드는 모든 과정에서 사용되는 가장 중요한 도구다. 스프링을 DI 프레임워크라고 부르는 이유는 외부 설정정보를 통한 런타임 오브젝트 DI라는 단순한 기능을 제공하기 때문이 아니다. 오히려 스프링이 DI에 담긴 원칙과 이를 응용하는 프로그래밍 모델을 자바 엔터프라이즈 기술의 많은 문제를 해결하는 데 적극적으로 활용하고 있기 때문이다. 또, 스프링과 마찬가지로 스프링을 사용하는 개발자가 만드는 애플리케이션 코드 또한 이런 DI를 활용해서 깔끔하고 유연한 코드와 설계를 만들어낼 수 있도록 지원하고 지지해주기 때문이다.&lt;/p&gt;
&lt;/blockquote&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;스프링 프레임워크의 핵심 기술인 어노테이션 기반의 DI 컨테이너를 직접 구현하면서 스프링 DI에 대한 이해도를 높여보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희가 스프링을 사용하지 않았다면 결국엔 직접 구현체를 넣어주거나 DI 컨테이너를 따로 구현해주어야 했을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이 DI를 해주는 덕분에 저희는 애플리케이션 코드에만 집중할 수 있고 더 깔끔하고 유연한 설계를 할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고마워 최고스프링아&lt;/p&gt;</description>
      <category>Web/Spring</category>
      <author>API_Dev</author>
      <guid isPermaLink="true">https://ksabs.tistory.com/262</guid>
      <comments>https://ksabs.tistory.com/262#entry262comment</comments>
      <pubDate>Mon, 3 Oct 2022 22:12:08 +0900</pubDate>
    </item>
    <item>
      <title>[Servlet 구현하기] Controller Scanner와 JSON View</title>
      <link>https://ksabs.tistory.com/261</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 시간에 &lt;a href=&quot;https://ksabs.tistory.com/260&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;어노테이션 기반의 MVC 프레임워크&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;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컨트롤러 어노테이션 스캔 역할 분리&lt;/li&gt;
&lt;li&gt;JSON view 지원&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&lt;b&gt;프레임워크에서 기본으로 어노테이션 핸들러 지원하기&lt;/b&gt;&lt;/h2&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-29 20.42.56.png&quot; data-origin-width=&quot;2774&quot; data-origin-height=&quot;1096&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FAOpS/btrNqXA6Lbe/Brksq7xcfqmNHmmkENxaZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FAOpS/btrNqXA6Lbe/Brksq7xcfqmNHmmkENxaZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FAOpS/btrNqXA6Lbe/Brksq7xcfqmNHmmkENxaZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFAOpS%2FbtrNqXA6Lbe%2FBrksq7xcfqmNHmmkENxaZK%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;2774&quot; height=&quot;1096&quot; data-filename=&quot;스크린샷 2022-09-29 20.42.56.png&quot; data-origin-width=&quot;2774&quot; data-origin-height=&quot;1096&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;MVC 프레임워크 패키지가 아닌 APP (어플리케이션) 패키지에서 지원하는 핸들러와 어댑터를 추가해주고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희가 스프링 MVC를 사용할때 어노테이션 매핑을 따로 추가해주지 않아도 기본으로 지원하는 것 처럼, 개발자는 어노테이션 매핑에 대한 추가설정 없이 기본적으로 제공받도록 구현하고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 MVC 프레임워크 패키지에 DefaultApplicationInitializer를 만들어주었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-29 20.45.52.png&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zt7jZ/btrNomWpFTt/NYRP9Vv09fcnJsgp2Fkll1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zt7jZ/btrNomWpFTt/NYRP9Vv09fcnJsgp2Fkll1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zt7jZ/btrNomWpFTt/NYRP9Vv09fcnJsgp2Fkll1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZt7jZ%2FbtrNomWpFTt%2FNYRP9Vv09fcnJsgp2Fkll1%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;856&quot; height=&quot;362&quot; data-filename=&quot;스크린샷 2022-09-29 20.45.52.png&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1664451944172&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package nextstep.mvc.config;

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import nextstep.mvc.AnnotationHandlerAdapter;
import nextstep.mvc.DispatcherServlet;
import nextstep.mvc.controller.tobe.AnnotationHandlerMapping;
import nextstep.web.WebApplicationInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DefaultAppWebApplicationInitializer implements WebApplicationInitializer {

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

    @Override
    public void onStartup(final ServletContext servletContext) throws ServletException {
        final var dispatcherServlet = DispatcherServlet.getInstance();
        dispatcherServlet.addHandlerMapping(new AnnotationHandlerMapping());
        dispatcherServlet.addHandlerAdapter(new AnnotationHandlerAdapter());

        final var dispatcher = servletContext.addServlet(&quot;dispatcher&quot;, dispatcherServlet);
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping(&quot;/&quot;);

        log.info(&quot;Start DefaultAppWebApplication Initializer&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 영역에서는 기본으로 제공되는 어노테이션 뿐만 아니라 추가로 사용하고 싶은 핸들러와 어댑터를 DispatcherServlet에 넣어주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1664452028810&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.techcourse.config;

import jakarta.servlet.ServletContext;
import nextstep.mvc.DispatcherServlet;
import nextstep.web.WebApplicationInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AppWebApplicationInitializer implements WebApplicationInitializer {

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

    @Override
    public void onStartup(final ServletContext servletContext) {
        final var dispatcherServlet = DispatcherServlet.getInstance();
        dispatcherServlet.addHandlerMapping(new ManualHandlerMapping());
        dispatcherServlet.addHandlerAdapter(new ManualHandlerAdapter());
        log.info(&quot;Start AppWebApplication Initializer&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 바뀐점이 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatcherServlet을 싱글톤으로 받고있는 부분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App 패키지에서 추가하는 핸들러와 어댑터들도 같은 DispatcherServlet 에 등록이 되도록 싱글톤으로 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1664454221446&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class DispatcherServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class);

    private final List&amp;lt;HandlerMapping&amp;gt; handlerMappings;
    private final List&amp;lt;HandlerAdapter&amp;gt; handlerAdapters;

    private DispatcherServlet() {
        this.handlerMappings = new ArrayList&amp;lt;&amp;gt;();
        this.handlerAdapters = new ArrayList&amp;lt;&amp;gt;();
    }

    public static class DispatcherServletGenerator {
        private static final DispatcherServlet INSTANCE = new DispatcherServlet();
    }
    public static DispatcherServlet getInstance() {
        return DispatcherServletGenerator.INSTANCE;
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서주의해야 할 점은 실제로 서블릿 컨테이너가 서블릿에 대해 &lt;b&gt;싱글톤을 보장하지는 않는다&lt;/b&gt;는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 기본 서블릿의 경우 서블릿 컨테이너가 서블릿 선언당 인스턴스를 1개만 사용하기는 하지만, SingleThreadModel 인터페이스를 구현하는 서블릿의 경우 많은 요청을 처리하기 위해 여러 인스턴스를 만들어 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;정리하면, 기본적으로 동일한 웹 앱에서 서블릿 선언당 인스턴스를 1개만 사용하기는 하지만, 여러 개의 인스턴스화를 막고있진 않기 때문에 서블릿은 싱글톤이 아닙니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 구현 편의상 싱글톤으로 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&lt;b&gt;Controller Scanner&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 리플렉션으로 컨트롤러를 읽어오는 코드가 AnnotationHandlerMapping에 존재하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 적지 않은 코드가 발생하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1664455269363&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AnnotationHandlerMapping implements HandlerMapping {

    private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class);
    private static final Class&amp;lt;RequestMapping&amp;gt; REQUEST_MAPPING_ANNOTATION_CLASS = RequestMapping.class;
    private static final Class&amp;lt;Controller&amp;gt; CONTROLLER_ANNOTATION_CLASS = Controller.class;

    private final Object[] basePackage;
    private final Map&amp;lt;HandlerKey, HandlerExecution&amp;gt; handlerExecutions;

    public AnnotationHandlerMapping(final Object... basePackage) {
        this.basePackage = basePackage;
        this.handlerExecutions = new HashMap&amp;lt;&amp;gt;();
    }

    public void initialize() {
        final Reflections reflections = new Reflections(basePackage);
        final Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; classes = reflections.getTypesAnnotatedWith(CONTROLLER_ANNOTATION_CLASS);

        for (Class&amp;lt;?&amp;gt; clazz : classes) {
            Object instance = getInstance(clazz);
            final List&amp;lt;Method&amp;gt; methods = getRequestMappingMethods(clazz);
            methods.forEach(method -&amp;gt; putHandlerExecutionByRequestMapping(instance, method));
        }
        log.info(&quot;Initialized AnnotationHandlerMapping!&quot;);
    }

    private Object getInstance(final Class&amp;lt;?&amp;gt; clazz) {
        try {
            final Constructor&amp;lt;?&amp;gt; constructor = clazz.getConstructor();
            return constructor.newInstance();
        } catch (NoSuchMethodException exception) {
            log.error(exception.getMessage());
            throw new IllegalArgumentException(&quot;생성자를 가져올 수 없습니다. &quot; + clazz.getName());
        } catch (InstantiationException
                | IllegalAccessException
                | InvocationTargetException exception){
            log.error(exception.getMessage());
            throw new IllegalArgumentException(&quot;인스턴스화할 수 없습니다. &quot; + clazz.getName());
        }
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ControllerScanner에게&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;li&gt;인스턴스화해서 Map으로 반환하는&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할을 부여하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1664455758047&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package nextstep.mvc.controller.annotation;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import nextstep.web.annotation.Controller;
import org.reflections.Reflections;

public class ControllerScanner {

    private static final Class&amp;lt;Controller&amp;gt; CONTROLLER_ANNOTATION_CLASS = Controller.class;

    private final Reflections reflections;

    public ControllerScanner(final Object[] basePackage) {
        this.reflections = new Reflections(basePackage);
    }

    public Map&amp;lt;Class&amp;lt;?&amp;gt;, Object&amp;gt; getControllers() {
        final Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; classes = reflections.getTypesAnnotatedWith(CONTROLLER_ANNOTATION_CLASS);
        return instantiateControllers(classes);
    }

    private Map&amp;lt;Class&amp;lt;?&amp;gt;, Object&amp;gt; instantiateControllers(final Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; classes) {
        final Map&amp;lt;Class&amp;lt;?&amp;gt;, Object&amp;gt; controllers = new HashMap&amp;lt;&amp;gt;();
        for (Class&amp;lt;?&amp;gt; controller : classes) {
            controllers.put(controller, getInstance(controller));
        }
        return controllers;
    }

    private Object getInstance(final Class&amp;lt;?&amp;gt; clazz) {
        try {
            final Constructor&amp;lt;?&amp;gt; constructor = clazz.getConstructor();
            return constructor.newInstance();
        } catch (NoSuchMethodException exception) {
            throw new IllegalArgumentException(&quot;생성자를 가져올 수 없습니다. &quot; + clazz.getName());
        } catch (InstantiationException
                | IllegalAccessException
                | InvocationTargetException exception){
            throw new IllegalArgumentException(&quot;인스턴스화할 수 없습니다. &quot; + clazz.getName());
        }
    }
}&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;AnnotationHandlerMapping은 ControllerScanner로 부터 컨트롤러 인스턴스를 받고 각 인스턴스로부터 메서드를 추출해 HandlerExecutions에 추가해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1664456344899&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AnnotationHandlerMapping implements HandlerMapping {

    private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class);
    private static final Class&amp;lt;RequestMapping&amp;gt; REQUEST_MAPPING_ANNOTATION_CLASS = RequestMapping.class;

    private final Object[] basePackage;
    private final Map&amp;lt;HandlerKey, HandlerExecution&amp;gt; handlerExecutions;

    public AnnotationHandlerMapping(final Object... basePackage) {
        this.basePackage = basePackage;
        this.handlerExecutions = new HashMap&amp;lt;&amp;gt;();
    }

    public void initialize() {
        final ControllerScanner controllerScanner = new ControllerScanner(basePackage);
        final Map&amp;lt;Class&amp;lt;?&amp;gt;, Object&amp;gt; controllers = controllerScanner.getControllers();
        for (Class&amp;lt;?&amp;gt; aClass : controllers.keySet()) {
            final List&amp;lt;Method&amp;gt; methods = getRequestMappingMethods(aClass);
            methods.forEach(method -&amp;gt; putHandlerExecutionByRequestMapping(controllers.get(aClass), method));
        }
        log.info(&quot;Initialized AnnotationHandlerMapping!&quot;);
    }

    private List&amp;lt;Method&amp;gt; getRequestMappingMethods(final Class&amp;lt;?&amp;gt; clazz) {
        return Arrays.stream(clazz.getDeclaredMethods())
                .filter(method -&amp;gt; method.isAnnotationPresent(REQUEST_MAPPING_ANNOTATION_CLASS))
                .collect(Collectors.toList());
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;JSON View&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jsp view만 지원하는 것이 아닌 Json view로도 반환하고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View 인터페이스를 구현하여 render에서 response body에 model 데이터를 json 형식으로 변환해 넣어주도록 구현했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1664457022796&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class JsonView implements View {

    @Override
    public void render(final Map&amp;lt;String, ?&amp;gt; model, final HttpServletRequest request, HttpServletResponse response)
            throws Exception {

        final ObjectMapper objectMapper = new ObjectMapper();
        final String body = objectMapper.writeValueAsString(model);

        response.setContentType(APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().write(body);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용할 때는&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ModelAndView를 생성할때 어떤 view를 사용할 것인지 넣어주고&lt;/li&gt;
&lt;li&gt;ModelAndView의 Object에 user 데이터를 넣어주면&lt;/li&gt;
&lt;li&gt;DispatcherServlet이 render를 호출합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1664457114367&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
public class UserController {

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

    @RequestMapping(value = &quot;/api/user&quot;, method = GET)
    public ModelAndView show(final HttpServletRequest request, final HttpServletResponse response) {
        log.info(&quot;test controller api&quot;);
        final String account = request.getParameter(&quot;account&quot;);
        log.debug(&quot;user id : {}&quot;, account);

        final ModelAndView modelAndView = new ModelAndView(new JsonView());
        final User user = InMemoryUserRepository.findByAccount(account)
                .orElseThrow();

        modelAndView.addObject(&quot;user&quot;, user);
        return modelAndView;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DispatcherServlet&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1664457218964&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   ...
   @Override
    protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException {
        log.debug(&quot;Method : {}, Request URI : {}&quot;, request.getMethod(), request.getRequestURI());

        try {
            final var handler = handlerMappingRegistry.getHandler(request);
            final HandlerAdapter handlerAdapter = handlerAdapterRegistry.getHandlerAdapter(handler);
            final ModelAndView modelAndView = handlerAdapter.handle(request, response, handler);
            render(modelAndView, request, response);
        } catch (Throwable e) {
            log.error(&quot;Exception : {}&quot;, e.getMessage(), e);
            throw new ServletException(e.getMessage());
        }
    }

    private void render(final ModelAndView modelAndView, final HttpServletRequest request, final HttpServletResponse response)
            throws Exception {
        modelAndView.render(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;&lt;b&gt;요청 처리 확인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL : /api/user&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Method : GET&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-29 22.15.33.png&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1jqdp/btrNosbfrGM/wIhILKdc4THa0uNFEiBphk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1jqdp/btrNosbfrGM/wIhILKdc4THa0uNFEiBphk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1jqdp/btrNosbfrGM/wIhILKdc4THa0uNFEiBphk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1jqdp%2FbtrNosbfrGM%2FwIhILKdc4THa0uNFEiBphk%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;832&quot; height=&quot;417&quot; data-filename=&quot;스크린샷 2022-09-29 22.15.33.png&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-29 22.15.07.png&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;113&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxIjth/btrNqcrYInT/keoZe2yOv39iwi4LfyGYGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxIjth/btrNqcrYInT/keoZe2yOv39iwi4LfyGYGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxIjth/btrNqcrYInT/keoZe2yOv39iwi4LfyGYGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxIjth%2FbtrNqcrYInT%2FkeoZe2yOv39iwi4LfyGYGk%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;422&quot; height=&quot;113&quot; data-filename=&quot;스크린샷 2022-09-29 22.15.07.png&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;113&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바의 서블릿을 구현해보았고, 실제로 스프링 MVC에서는 자바의 서블릿을 DispatcherServlet으로 사용하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 어노테이션 기반의 MVC 프레임워크를 만들어 컨트롤러를 스캔해보고 jsp가 아닌 다른형식의 view도 지원하도록 만들어 보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MVC&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-29 22.18.20.png&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQGgVJ/btrNnrYe6lw/tZItJECAdcZRlT2WYmqSI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQGgVJ/btrNnrYe6lw/tZItJECAdcZRlT2WYmqSI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQGgVJ/btrNnrYe6lw/tZItJECAdcZRlT2WYmqSI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQGgVJ%2FbtrNnrYe6lw%2FtZItJECAdcZRlT2WYmqSI1%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;443&quot; height=&quot;228&quot; data-filename=&quot;스크린샷 2022-09-29 22.18.20.png&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;228&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;Model, View, Controller 를 나누는 이유에 대해서 더 명확하게 이해를 할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이들이 해주는 행위에 대해서도 직접 구현해보면서 확실히 알게되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음목표는..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 핵심원리 중 하나인 DI(Dependency Injection)를 직접 구현해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dispatcher servlet : &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-servlet&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-servlet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Is Servlet the singleton? : &lt;a href=&quot;https://stackoverflow.com/questions/11820840/is-servlet-the-singleton&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://stackoverflow.com/questions/11820840/is-servlet-the-singleton&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;https://jcp.org/en/jsr/detail?id=315&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web/MVC</category>
      <author>API_Dev</author>
      <guid isPermaLink="true">https://ksabs.tistory.com/261</guid>
      <comments>https://ksabs.tistory.com/261#entry261comment</comments>
      <pubDate>Thu, 29 Sep 2022 22:23:04 +0900</pubDate>
    </item>
    <item>
      <title>[Servlet 구현하기] 어노테이션 기반 MVC 프레임워크 구현</title>
      <link>https://ksabs.tistory.com/260</link>
      <description>&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://github.com/dongho108/jwp-dashboard-http/tree/step234&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/dongho108/jwp-dashboard-http/tree/step234&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;어노테이션기반 MVC 프레임워크 코드 저장소 :&amp;nbsp;&lt;a href=&quot;https://github.com/dongho108/jwp-dashboard-mvc/tree/step1&quot;&gt;https://github.com/dongho108/jwp-dashboard-mvc/tree/step1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;요청당 쓰레드 생성 (쓰레드관리)&lt;/li&gt;
&lt;li&gt;커넥션관리&lt;/li&gt;
&lt;li&gt;서버소켓 생성&lt;/li&gt;
&lt;li&gt;HttpRequest, HttpResponse 변환&lt;/li&gt;
&lt;li&gt;컨트롤러 찾기&lt;/li&gt;
&lt;li&gt;컨트롤러 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 4~6번은 아래와 같은 구조가 만들어졌습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;884&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOAkDM/btrMrRWvFrv/MwQHCq0IKV8kWmB5BuVv61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOAkDM/btrMrRWvFrv/MwQHCq0IKV8kWmB5BuVv61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOAkDM/btrMrRWvFrv/MwQHCq0IKV8kWmB5BuVv61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOAkDM%2FbtrMrRWvFrv%2FMwQHCq0IKV8kWmB5BuVv61%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;1520&quot; height=&quot;884&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;884&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Request에 맞는 Controller를 찾아주는 메서드와 클래스가 &lt;span style=&quot;color: #ee2323;&quot;&gt;전혀 다른 곳에 있는 패키지를 의존&lt;/span&gt;하고 있습니다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 비즈니스 로직에 컨트롤러가 추가된다고 스프링 코드가 변화하지는 않죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크는 비즈니스 로직과 완전 분리되어야 한다는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 어떻게 스프링은 계속 변화하는 비즈니스 코드를 알고 Controller를 매핑시켜주는 것일까요?&lt;/p&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;&lt;b&gt;어노테이션 기반 MVC 프레임워크&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 코드의 컨트롤러를 비즈니스로직에서 작성하고, HandlerMapping에서 자동으로 읽어올 수 있게 만들어 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663553654751&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
public class TestController {

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

    @RequestMapping(value = &quot;/get-test&quot;, method = RequestMethod.GET)
    public ModelAndView findUserId(final HttpServletRequest request, final HttpServletResponse response) {
        log.info(&quot;test controller get method&quot;);
        final var modelAndView = new ModelAndView(new JspView(&quot;&quot;));
        modelAndView.addObject(&quot;id&quot;, request.getAttribute(&quot;id&quot;));
        return modelAndView;
    }

    @RequestMapping(value = &quot;/post-test&quot;, method = RequestMethod.POST)
    public ModelAndView save(final HttpServletRequest request, final HttpServletResponse response) {
        log.info(&quot;test controller post method&quot;);
        final var modelAndView = new ModelAndView(new JspView(&quot;&quot;));
        modelAndView.addObject(&quot;id&quot;, request.getAttribute(&quot;id&quot;));
        return modelAndView;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;interface HandlerMapping&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1663554053845&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface HandlerMapping {

    void initialize();

    Object getHandler(HttpServletRequest request);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;class AnnotationHandlerMapping&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;initialize()에서는 리플렉션으로 @Controller 어노테이션이 붙어있는 클래스들을 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스의 각 메서드들을 HandlerExcecution 클래스로 만들어 Map에 넣어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getHandler()에서는 request에서 url, method를 key로 작업을 수행할 HandlerExcecution을 가져올 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663554100685&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AnnotationHandlerMapping implements HandlerMapping {

    private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class);
    private static final Class&amp;lt;RequestMapping&amp;gt; REQUEST_MAPPING_ANNOTATION_CLASS = RequestMapping.class;
    private static final Class&amp;lt;Controller&amp;gt; CONTROLLER_ANNOTATION_CLASS = Controller.class;

    private final Object[] basePackage;
    private final Map&amp;lt;HandlerKey, HandlerExecution&amp;gt; handlerExecutions;

    public AnnotationHandlerMapping(final Object... basePackage) {
        this.basePackage = basePackage;
        this.handlerExecutions = new HashMap&amp;lt;&amp;gt;();
    }

    public void initialize() {
        final Reflections reflections = new Reflections(basePackage);
        final Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; classes = reflections.getTypesAnnotatedWith(CONTROLLER_ANNOTATION_CLASS);

        for (Class&amp;lt;?&amp;gt; clazz : classes) {
            Object instance = getInstance(clazz);
            final List&amp;lt;Method&amp;gt; methods = getRequestMappingMethods(clazz);
            methods.forEach(method -&amp;gt; putHandlerExecutionByRequestMapping(instance, method));
        }
        log.info(&quot;Initialized AnnotationHandlerMapping!&quot;);
    }
    
    public Object getHandler(final HttpServletRequest request) {
        final String requestURI = request.getRequestURI();
        final String method = request.getMethod();
        final RequestMethod requestMethod = RequestMethod.valueOf(method);
        final HandlerKey handlerKey = new HandlerKey(requestURI, requestMethod);

        if (handlerExecutions.containsKey(handlerKey)) {
            return handlerExecutions.get(handlerKey);
        }

        throw new NotFoundHandlerException(requestURI, method);
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HandlerKey&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HandlerExecution을 가져오기위해 url, method를 묶은 VO입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663554719255&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class HandlerKey {

    private final String url;
    private final RequestMethod requestMethod;

    public HandlerKey(final String url, final RequestMethod requestMethod) {
        this.url = url;
        this.requestMethod = requestMethod;
    }

    @Override
    public String toString() {
        return &quot;HandlerKey{&quot; +
                &quot;url='&quot; + url + '\'' +
                &quot;, requestMethod=&quot; + requestMethod +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof HandlerKey)) return false;
        HandlerKey that = (HandlerKey) o;
        return Objects.equals(url, that.url) &amp;amp;&amp;amp; requestMethod == that.requestMethod;
    }

    @Override
    public int hashCode() {
        return Objects.hash(url, requestMethod);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HandlerExecution&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러의 메서드를 대신 실행시켜주는 녀석입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;현재 제가 만들고 있는 어노테이션 기반의 컨트롤러는 HandlerExecution을 이용해야만 실행시킬 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(이 말을 잘 기억해주세요.)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1663554745671&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class HandlerExecution {

    private final Object object;
    private final Method method;

    public HandlerExecution(final Object object, final Method method) {
        this.object = object;
        this.method = method;
    }

    public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
        return (ModelAndView) method.invoke(object, 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Test&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 만든 어노테이션기반의 핸들러매핑이 잘 동작하는지 확인해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 정의한 &lt;b&gt;TestController&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1663555206334&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
public class TestController {

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

    @RequestMapping(value = &quot;/get-test&quot;, method = RequestMethod.GET)
    public ModelAndView findUserId(final HttpServletRequest request, final HttpServletResponse response) {
        log.info(&quot;test controller get method&quot;);
        final var modelAndView = new ModelAndView(new JspView(&quot;&quot;));
        modelAndView.addObject(&quot;id&quot;, request.getAttribute(&quot;id&quot;));
        return modelAndView;
    }

    @RequestMapping(value = &quot;/post-test&quot;, method = RequestMethod.POST)
    public ModelAndView save(final HttpServletRequest request, final HttpServletResponse response) {
        log.info(&quot;test controller post method&quot;);
        final var modelAndView = new ModelAndView(new JspView(&quot;&quot;));
        modelAndView.addObject(&quot;id&quot;, request.getAttribute(&quot;id&quot;));
        return modelAndView;
    }
}&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;TestController가 속한 pakage 이름을 AnnotationHandlerMapping 생성자의 매개변수로 넣어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 초기화를 시켜줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663555141419&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class AnnotationHandlerMappingTest {

    private AnnotationHandlerMapping handlerMapping;

    @BeforeEach
    void setUp() {
        handlerMapping = new AnnotationHandlerMapping(&quot;samples&quot;);
        handlerMapping.initialize();
    }

    @Test
    void get() throws Exception {
        final var request = mock(HttpServletRequest.class);
        final var response = mock(HttpServletResponse.class);

        when(request.getAttribute(&quot;id&quot;)).thenReturn(&quot;gugu&quot;);
        when(request.getRequestURI()).thenReturn(&quot;/get-test&quot;);
        when(request.getMethod()).thenReturn(&quot;GET&quot;);

        final var handlerExecution = (HandlerExecution) handlerMapping.getHandler(request);
        final var modelAndView = handlerExecution.handle(request, response);

        assertThat(modelAndView.getObject(&quot;id&quot;)).isEqualTo(&quot;gugu&quot;);
    }

    @Test
    void post() throws Exception {
        final var request = mock(HttpServletRequest.class);
        final var response = mock(HttpServletResponse.class);

        when(request.getAttribute(&quot;id&quot;)).thenReturn(&quot;gugu&quot;);
        when(request.getRequestURI()).thenReturn(&quot;/post-test&quot;);
        when(request.getMethod()).thenReturn(&quot;POST&quot;);

        final var handlerExecution = (HandlerExecution) handlerMapping.getHandler(request);
        final var modelAndView = handlerExecution.handle(request, response);

        assertThat(modelAndView.getObject(&quot;id&quot;)).isEqualTo(&quot;gugu&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-19 11.41.10.png&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UI44o/btrMtHlSn44/lH5aWMUCEdyInrBuFlvpb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UI44o/btrMtHlSn44/lH5aWMUCEdyInrBuFlvpb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UI44o/btrMtHlSn44/lH5aWMUCEdyInrBuFlvpb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUI44o%2FbtrMtHlSn44%2FlH5aWMUCEdyInrBuFlvpb0%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;646&quot; height=&quot;164&quot; data-filename=&quot;스크린샷 2022-09-19 11.41.10.png&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;b&gt;이제 @Controller 어노테이션과 @RequestMapping 어노테이션을 달아주는 것 만으로도 &lt;span style=&quot;color: #ee2323;&quot;&gt;프레임워크의 코드를 수정하지 않고&lt;/span&gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;컨트롤러를 추가&lt;/span&gt;할 수 있게 되었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;기존의 상속기반 컨트롤러는 어떻게 되는 것일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존 컨트롤러 ForwardController (톰캣 구현하기의 컨트롤러와 조금 달라졌습니다.)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 컨트롤러는 메서드명도 execute입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663555466584&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ForwardController implements Controller {

    private final String path;

    public ForwardController(final String path) {
        this.path = Objects.requireNonNull(path);
    }

    @Override
    public String execute(final HttpServletRequest request, final HttpServletResponse response) {
        return path;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 핸들러 매핑 방식도 다릅니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663555564244&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ManualHandlerMapping implements HandlerMapping {

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

    private static final Map&amp;lt;String, Controller&amp;gt; controllers = new HashMap&amp;lt;&amp;gt;();

    @Override
    public void initialize() {
        controllers.put(&quot;/&quot;, new ForwardController(&quot;/index.jsp&quot;));
        controllers.put(&quot;/login&quot;, new LoginController());
        controllers.put(&quot;/login/view&quot;, new LoginViewController());
        controllers.put(&quot;/logout&quot;, new LogoutController());
        controllers.put(&quot;/register/view&quot;, new RegisterViewController());
        controllers.put(&quot;/register&quot;, new RegisterController());

        log.info(&quot;Initialized Handler Mapping!&quot;);
        controllers.keySet()
                .forEach(path -&amp;gt; log.info(&quot;Path : {}, Controller : {}&quot;, path, controllers.get(path).getClass()));
    }

    @Override
    public Controller getHandler(HttpServletRequest request) {
        final String requestURI = request.getRequestURI();
        log.debug(&quot;Request Mapping Uri : {}&quot;, requestURI);
        return controllers.get(requestURI);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 컨트롤러와 어노테이션 기반 컨트롤러의 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;실행방법에는 차이가 존재&lt;/b&gt;&lt;/span&gt;합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;기존의 컨트롤러는 상속한 클래스의 excecute를 실행하는 방식이고&lt;/li&gt;
&lt;li&gt;어노테이션 기반의 컨트롤러는 HandlerExecution의 handle을 실행시키는 방식입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Handler Adapter (핸들러 어댑터)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 컨트롤러를 새로운 어노테이션 기반의 컨트롤러로 코드를 수정하는 방법도 있습니다만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 기존의 컨트롤러가 수백개 수천개라면 어떻게 될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 컨트롤러를 어노테이션 기반으로 변경할때까지 서비스를 중단해야할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 좋은 방법은 기존 컨트롤러와 어노테이션 기반의 컨트롤러를 둘 다 지원하게 만드는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차근차근 전환을 하면 되죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 실제 프레임워크를 만든다고 생각해도 갑자기 기존 코드를 지원중단할 수도 없겠죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 필요한 것이 핸들러 어댑터입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Spring MVC 에서 핸들러 어댑터를 사용해 여러가지 방식의 컨트롤러를 지원하도록 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pf6CT/btrMt4OSaN1/RTPYhQcAWqe8JQfvi58440/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pf6CT/btrMt4OSaN1/RTPYhQcAWqe8JQfvi58440/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pf6CT/btrMt4OSaN1/RTPYhQcAWqe8JQfvi58440/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpf6CT%2FbtrMt4OSaN1%2FRTPYhQcAWqe8JQfvi58440%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;906&quot; height=&quot;414&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금부터 핸들러 어댑터를 자세히 알아보고 기존의 컨트롤러와 새로운 어노테이션 기반의 컨트롤러를 둘 다 지원하도록 구현해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Dispatch Servlet&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 잠시,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에 갑자기 등장한 &lt;b&gt;DispatcherServlet&lt;/b&gt;에 대해 잠깐 언급하고 넘어가겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구조는 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣이 서블릿을 직접 생성해서 넘겨주는 상황입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-19 14.57.48.png&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;866&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lTjpT/btrMwYuBjpa/P6kIy9r3KRz9xym9t8h1Fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lTjpT/btrMwYuBjpa/P6kIy9r3KRz9xym9t8h1Fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lTjpT/btrMwYuBjpa/P6kIy9r3KRz9xym9t8h1Fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlTjpT%2FbtrMwYuBjpa%2FP6kIy9r3KRz9xym9t8h1Fk%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;1718&quot; height=&quot;866&quot; data-filename=&quot;스크린샷 2022-09-19 14.57.48.png&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;866&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 저희는 서블릿에게 HttpRequest를 넘겨주어도 서블릿에서는 이 요청이 어떤 컨트롤러에 의해 처리되어야 하는지 찾아야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(핸들러매핑)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 이 핸들러가 어떻게 구현했는지에 상관없이 실행시켜야 하기 때문에 컨트롤러에 맞는 핸들러어댑터도 찾아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(핸들러어댑터 조회)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 작업이 수행되고나서 ModelAndView를 렌더링해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 요청마다 아래의 과정이 반복됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;핸들러매핑&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 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;1, 2, 4 부분은 항상 같은 로직이 반복됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 등장한 것이 DispatchServlet (front controller 패턴) 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스로직마다 달라지는 코드 빼고 중복되는 처리는 front controller 인 &lt;b&gt;DispatcherServlet에서 담당&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Spring MVC에서는 아래와 같은 그림이 나오게되는 것이죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UXLRN/btrMxTmc4cX/dKXbjs0ezWF8YWFxwr6cU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UXLRN/btrMxTmc4cX/dKXbjs0ezWF8YWFxwr6cU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UXLRN/btrMxTmc4cX/dKXbjs0ezWF8YWFxwr6cU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUXLRN%2FbtrMxTmc4cX%2FdKXbjs0ezWF8YWFxwr6cU1%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;906&quot; height=&quot;414&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;interface HandlerAdapter&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핸들러 어댑터는 아래와 같은 인터페이스를 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;supports() 는 자신이 핸들링 할 수 있는 핸들러인지 판단하는 메서드입니다. 외부에서 supports를 이용해 핸들러 어댑터를 찾을 수 있게하는 메서드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;handle() 은 매개변수로 받는 Object handler를 자신이 핸들링할 수 있는 객체로 캐스팅해 메서드를 실행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663573301003&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface HandlerAdapter {
    boolean supports(Object handler);

    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;AnnotationHandlerAdapter&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;supports() 에서는 매개변수로 들어온 handler가 HandlerExecution의 인스턴스인지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;handle() 에서는 handlerExcecution의 handle을 실행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663573999130&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AnnotationHandlerAdapter implements HandlerAdapter {
    @Override
    public boolean supports(final Object handler) {
        return handler instanceof HandlerExecution;
    }

    @Override
    public ModelAndView handle(final HttpServletRequest request,
                               final HttpServletResponse response,
                               final Object handler) throws Exception {
        final HandlerExecution handlerExecution = (HandlerExecution) handler;
        return handlerExecution.handle(request, response);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ManualHandlerAdapter&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;support()에서는 매개변수로 들어온 handler가 Controller를 상속받는 클래스의 인스턴스인지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;handle()은 해당 controller의 메서드를 실행시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 controller는 String viewName을 반환하기 때문에 인터페이스의 반환타입을 맞춰주려면 ModelAndView로 변환해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 일단 View 템플릿은 JSP만 있다고 가정하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663574240986&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ManualHandlerAdapter implements HandlerAdapter {

    @Override
    public boolean supports(final Object handler) {
        return handler instanceof Controller;
    }

    @Override
    public ModelAndView handle(final HttpServletRequest request,
                               final HttpServletResponse response,
                               final Object handler) throws Exception {
        final Controller controller = (Controller) handler;
        final String viewName = controller.execute(request, response);
        return new ModelAndView(new JspView(viewName));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HandlerAdapter 덕분에 과거에 지원하던 핸들러도 실행시킬 수 있게 되었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663574380624&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Override
    protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException {
        log.debug(&quot;Method : {}, Request URI : {}&quot;, request.getMethod(), request.getRequestURI());

        try {
            final var handler = getHandler(request); // 핸들러 매핑
            final HandlerAdapter handlerAdapter = getHandlerAdapter(handler); // 핸들러어댑터 조회
            final ModelAndView modelAndView = handlerAdapter.handle(request, response, handler); // handle
            modelAndView.render(request, response); // 렌더링
        } catch (Throwable e) {
            log.error(&quot;Exception : {}&quot;, e.getMessage(), e);
            throw new ServletException(e.getMessage());
        }
    }

    private Object getHandler(final HttpServletRequest request) {
        return handlerMappings.stream()
                .map(handlerMapping -&amp;gt; handlerMapping.getHandler(request))
                .filter(Objects::nonNull)
                .findFirst()
                .orElseThrow(() -&amp;gt; new NoSuchElementException(&quot;핸들러를 찾을 수 없습니다.&quot;));
    }

    private HandlerAdapter getHandlerAdapter(final Object handler) {
        return handlerAdapters.stream()
                .filter(it -&amp;gt; it.supports(handler))
                .findFirst()
                .orElseThrow(() -&amp;gt; new NotFoundHandlerAdapterException(&quot;핸들러 어댑터를 찾을 수 없습니다.&quot;));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 스프링에는 SimpleControllerHandlerAdapter 가 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller 인터페이스를 구현하는 방식의 컨트롤러를 지원하기 위한 것이죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c047jb/btrMw6fN0AF/DZMp3LM3HfMXgfBRoR0PPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c047jb/btrMw6fN0AF/DZMp3LM3HfMXgfBRoR0PPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c047jb/btrMw6fN0AF/DZMp3LM3HfMXgfBRoR0PPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc047jb%2FbtrMw6fN0AF%2FDZMp3LM3HfMXgfBRoR0PPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;407&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;지금까지 간단한 DispatcherServlet과 핸들러매핑, 핸들러 어댑터를 구현해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 개선할 점이 보이는데요.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컨트롤러 어노테이션 스캔 역할 분리&lt;/li&gt;
&lt;li&gt;JSON view 지원&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음시간에서 차근차근 개선해봅시다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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://github.com/dongho108/jwp-dashboard-mvc/tree/step1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/dongho108/jwp-dashboard-mvc/tree/step1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web/MVC</category>
      <author>API_Dev</author>
      <guid isPermaLink="true">https://ksabs.tistory.com/260</guid>
      <comments>https://ksabs.tistory.com/260#entry260comment</comments>
      <pubDate>Mon, 19 Sep 2022 11:57:26 +0900</pubDate>
    </item>
    <item>
      <title>[Tomcat 구현하기] 2. HttpRequest, HttpResponse, RequestMapper, Controller</title>
      <link>https://ksabs.tistory.com/259</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 지난번에 구현한 톰캣을 이용해 요구사항을 구현해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 기능 요구사항이 주어졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기능요구사항&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GET /login 요청에 로그인 페이지를 보여준다.&lt;/li&gt;
&lt;li&gt;GET /register 요청에 회원가입 페이지를 보여준다.&lt;/li&gt;
&lt;li&gt;POST /register , body를 포함한 요청에 회원가입을 시키고 login 페이지로 redirect 시킨다.&lt;/li&gt;
&lt;li&gt;POST /login 요청에 로그인 처리를 한다.&lt;br /&gt;- 서버에서 세션을 생성해 로그인 정보를 저장한다.&lt;br /&gt;- 쿠키에 JSESSION 아이디를 담아서 로그인을 유지시킨다.&lt;/li&gt;
&lt;li&gt;로그인 처리가 된 사용자에게는 index.html 페이지를 보여준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;HttpRequest 구현&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;socket 에 쓰여진 inputStream을 사용하기 편하도록 HttpRequest로 변환해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ymL4u/btrL9q0l5Qg/NXGp1pMMM62oROuN3F93Pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ymL4u/btrL9q0l5Qg/NXGp1pMMM62oROuN3F93Pk/img.png&quot; data-alt=&quot;HttpReqeust&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ymL4u/btrL9q0l5Qg/NXGp1pMMM62oROuN3F93Pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FymL4u%2FbtrL9q0l5Qg%2FNXGp1pMMM62oROuN3F93Pk%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;693&quot; height=&quot;336&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HttpReqeust&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpRequest가 가지는 요소들을 포함한 클래스를 만들어 줍니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HttpStartLine : Method, Path, Version&lt;/li&gt;
&lt;li&gt;HttpHeaders&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HttpReqeust&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 socket에서 BufferReader로 읽어들이기 때문에 정적팩터리메서드로 BufferReader를 통해 HttpRequest를 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663216907356&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.apache.coyote.http11.http.request;

import static org.apache.catalina.webutils.IOUtils.readData;
import static org.apache.coyote.http11.header.HttpHeaderType.CONTENT_LENGTH;

import java.io.BufferedReader;
import java.io.IOException;
import org.apache.coyote.http11.header.HttpHeader;
import org.apache.coyote.http11.http.HttpHeaders;

public class HttpRequest {

    private final HttpRequestStartLine startLine;
    private final HttpHeaders headers;
    private final String body;

    private HttpRequest(final HttpRequestStartLine startLine, final HttpHeaders headers, final String body) {
        this.startLine = startLine;
        this.headers = headers;
        this.body = body;
    }

    public static HttpRequest from(final BufferedReader bufferedReader) throws IOException {
        final String startLine = bufferedReader.readLine();
        if (startLine == null) {
            throw new IllegalArgumentException(&quot;request가 비어있습니다.&quot;);
        }

        final HttpRequestStartLine httpRequestStartLine = HttpRequestStartLine.from(startLine);
        final HttpHeaders headers = HttpHeaders.from(bufferedReader);
        final String body = readBody(bufferedReader, headers);

        return new HttpRequest(httpRequestStartLine, headers, body);
    }

    private static String readBody(final BufferedReader bufferedReader,
                                   final HttpHeaders headers) throws IOException {

        if (!headers.contains(CONTENT_LENGTH.getValue())) {
            return &quot;&quot;;
        }

        final int contentLength = convertIntFromContentLength(headers.get(CONTENT_LENGTH.getValue()));
        return readData(bufferedReader, contentLength);
    }

    private static int convertIntFromContentLength(final HttpHeader contentLength) {
        return Integer.parseInt(String.join(&quot;&quot;, contentLength.getValues()));
    }

    public HttpRequestStartLine getStartLine() {
        return startLine;
    }

    public HttpHeaders getHeaders() {
        return headers;
    }

    public String getBody() {
        return body;
    }

    public String getUrl() {
        return startLine.getPath();
    }

    public boolean isGetMethod() {
        return startLine.getHttpMethod().isGet();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HttpStartLine&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpMethod, path, HttpVersion을 포함하는 라인을 표현하는 클래스입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663216970865&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.apache.coyote.http11.http.request;

import java.util.List;
import org.apache.coyote.http11.http.HttpVersion;

public class HttpRequestStartLine {

    private static final int START_LINE_MIN_LENGTH = 3;
    private static final String BLANK_LETTER = &quot; &quot;;

    private final HttpMethod httpMethod;
    private final String path;
    private final HttpVersion httpVersion;

    private HttpRequestStartLine(final HttpMethod httpMethod, final String path, final HttpVersion httpVersion) {
        this.httpMethod = httpMethod;
        this.path = path;
        this.httpVersion = httpVersion;
    }

    public static HttpRequestStartLine from(final String startLine) {
        return parseStartLine(startLine);
    }

    private static HttpRequestStartLine parseStartLine(final String startLine) {
        final List&amp;lt;String&amp;gt; startLineInfos = parseStartLineInfos(startLine);
        final HttpMethod method = HttpMethod.from(startLineInfos.get(0));
        final String path = startLineInfos.get(1);
        final HttpVersion version = HttpVersion.from(startLineInfos.get(2));

        return new HttpRequestStartLine(method, path, version);
    }

    private static List&amp;lt;String&amp;gt; parseStartLineInfos(final String startLine) {
        final List&amp;lt;String&amp;gt; startLineInfos = List.of(startLine.split(BLANK_LETTER));
        validateStartLineLength(startLineInfos);
        return startLineInfos;
    }

    private static void validateStartLineLength(final List&amp;lt;String&amp;gt; startLineInfos) {
        if (startLineInfos.size() &amp;lt; START_LINE_MIN_LENGTH) {
            throw new IllegalArgumentException(&quot;요청 정보가 잘못되었습니다.&quot;);
        }
    }

    public HttpVersion getHttpVersion() {
        return httpVersion;
    }

    public HttpMethod getHttpMethod() {
        return httpMethod;
    }

    public String getPath() {
        return path;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HttpHeaders&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Map (key: String, value: HtepHeader) 을 가지는 HttpHeaders.&lt;/p&gt;
&lt;pre id=&quot;code_1663216994937&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.apache.coyote.http11.http;

import static org.apache.catalina.webutils.Parser.removeBlank;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.coyote.http11.header.HttpHeader;
import org.apache.coyote.http11.header.HttpHeaderType;

public class HttpHeaders {
    private Map&amp;lt;String, HttpHeader&amp;gt; headers = new LinkedHashMap&amp;lt;&amp;gt;();

    private static final String COLON_LETTER = &quot;:&quot;;

    public HttpHeaders() {
    }

    private HttpHeaders(final Map&amp;lt;String, HttpHeader&amp;gt; headers) {
        this.headers = headers;
    }

    public static HttpHeaders from(final BufferedReader bufferedReader) throws IOException {
        return new HttpHeaders(readAllHeaders(bufferedReader));
    }

    public static HttpHeaders of(final HttpHeader... httpHeaders) {
        final Map&amp;lt;String, HttpHeader&amp;gt; headers = Arrays.stream(httpHeaders)
                .collect(Collectors.toMap(HttpHeader::getHttpHeaderType,
                        httpHeader -&amp;gt; httpHeader,
                        (key, value) -&amp;gt; value,
                        LinkedHashMap::new));
        return new HttpHeaders(headers);
    }

    private static Map&amp;lt;String, HttpHeader&amp;gt; readAllHeaders(final BufferedReader bufferedReader) throws IOException {
        final Map&amp;lt;String, HttpHeader&amp;gt; headers = new LinkedHashMap&amp;lt;&amp;gt;();

        while (true) {
            final String line = bufferedReader.readLine();
            if (line.equals(&quot;&quot;)) {
                break;
            }
            final List&amp;lt;String&amp;gt; header = parseHeader(line);
            final String headerType = removeBlank(header.get(0));
            final String headerValue = removeBlank(header.get(1));
            final String httpHeaderType = HttpHeaderType.of(headerType);
            headers.put(httpHeaderType, HttpHeader.of(httpHeaderType, headerValue));
        }

        return headers;
    }

    private static List&amp;lt;String&amp;gt; parseHeader(final String line) {
        final List&amp;lt;String&amp;gt; header = List.of(line.split(COLON_LETTER));
        validateHeader(header);
        return header;
    }

    private static void validateHeader(final List&amp;lt;String&amp;gt; header) {
        if (header.size() &amp;lt; 2) {
            throw new IllegalArgumentException(&quot;요청 정보가 잘못되었습니다.&quot;);
        }
    }

    public boolean contains(final String httpHeaderType) {
        return headers.containsKey(httpHeaderType);
    }

    public HttpHeader get(final String httpHeaderType) {
        return headers.get(httpHeaderType);
    }

    public Set&amp;lt;String&amp;gt; keySet() {
        return headers.keySet();
    }

    public void put(final String httpHeaderType, final HttpHeader httpHeader) {
        headers.put(httpHeaderType, httpHeader);
    }

    public void add(final HttpHeader httpHeader) {
        headers.put(httpHeader.getHttpHeaderType(), httpHeader);
    }
}&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;HttpRequest 구현은 끝났습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BufferReader를 통해 HttpRequst를 생성하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663217700628&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Override
    public void process(final Socket connection) {
        try (final var inputStream = connection.getInputStream();
             final var outputStream = connection.getOutputStream()) {

            final InputStreamReader inputStreamReader = new InputStreamReader(inputStream, UTF_8);
            final BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

            final HttpRequest httpRequest = HttpRequest.from(bufferedReader);
            ...

            outputStream.write(response.getBytes());
            outputStream.flush();
        } catch (IOException | UncheckedServletException e) {
            log.error(e.getMessage(), e);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HttpResponse&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package org.apache.coyote.response;

import static org.apache.coyote.header.HttpHeaderType.CONTENT_LENGTH;
import static org.apache.coyote.response.HttpStatus.OK;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.apache.coyote.HttpVersion;
import org.apache.coyote.header.HttpHeader;
import org.apache.coyote.header.HttpHeaders;

public class HttpResponse {

    private static final String NEW_LINE_LETTER = &quot;\r\n&quot;;
    private static final String EMPTY_LETTER = &quot;&quot;;
    private static final String BLANK_LETTER = &quot; &quot;;
    private static final String COLON_LETTER = &quot;:&quot;;
    private static final String SEMI_COLON_LETTER = &quot;;&quot;;
    private static final HttpVersion DEFAULT_HTTP_VERSION = HttpVersion.HTTP11;

    private HttpVersion httpVersion = DEFAULT_HTTP_VERSION;
    private HttpStatus httpStatus = OK;
    private HttpHeaders headers = new HttpHeaders();
    private String body = &quot;&quot;;

    public String getBody() {
        return body;
    }

    public void setHttpStatus(final HttpStatus httpStatus) {
        this.httpStatus = httpStatus;
    }

    public void addHeader(final String name, final String value) {
        headers.add(HttpHeader.of(name, value));
    }

    public void setBody(final String body) {
        final int length = body.getBytes(StandardCharsets.UTF_8).length;
        addHeader(CONTENT_LENGTH.getValue(), String.valueOf(length));
        this.body = body;
    }

    public String generateResponse() {
        final String statusLine = generateStatusLine();
        final String headerLine = generateHeaderLine();
        return String.join(NEW_LINE_LETTER, statusLine, headerLine, EMPTY_LETTER, body);
    }

    private String generateStatusLine() {
        return String.join(BLANK_LETTER,
                httpVersion.getVersion(),
                String.valueOf(httpStatus.getCode()),
                httpStatus.getMessage(),
                EMPTY_LETTER);
    }

    private String generateHeaderLine() {
        final List&amp;lt;String&amp;gt; headers = new ArrayList&amp;lt;&amp;gt;();
        for (String httpHeaderType : this.headers.keySet()) {
            final String header = String.join(COLON_LETTER + BLANK_LETTER, httpHeaderType,
                    String.join(SEMI_COLON_LETTER, this.headers.get(httpHeaderType).getValues()));
            headers.add(header + BLANK_LETTER);
        }
        return String.join(NEW_LINE_LETTER, headers);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h3 id=&quot;--%--GET%--%-Flogin%--%EC%-A%--%EC%B-%AD%EC%--%--%--%EB%A-%-C%EA%B-%B-%EC%-D%B-%--%ED%-E%--%EC%-D%B-%EC%A-%--%EB%A-%BC%--%EB%B-%B-%EC%--%AC%EC%A-%--%EB%-B%A--&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;각 method, url 요청에 컨트롤러 매핑을 해주겠습니다.&lt;/b&gt;&lt;/h3&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;socket에 들어오는 inputStream을 사용하게 편하게 HttpRequest로 변환하기&lt;/li&gt;
&lt;li&gt;RequestMapper에 GET /login 요청을 매핑해 컨트롤러 반환하기&lt;/li&gt;
&lt;li&gt;Controller에 HttpReqeust, HttpResponse를 같이 넣어주며 service 실행하기&lt;/li&gt;
&lt;li&gt;HttpResponse를 socket의 outputStream에 담기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 요청에 따라 다른 처리의 분기는 2, 3번만 코드를 추가해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Http11Processor&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1663210411976&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.apache.coyote.http11;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import nextstep.jwp.exception.UncheckedServletException;
import org.apache.coyote.Processor;
import org.apache.coyote.http11.http.RequestMapper;
import org.apache.coyote.http11.http.request.HttpRequest;
import org.apache.coyote.http11.http.response.HttpResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Http11Processor implements Runnable, Processor {

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

    private final Socket connection;

    public Http11Processor(final Socket connection) {
        this.connection = connection;
    }

    @Override
    public void run() {
        try {
            log.info(&quot;connect host: {}, port: {}&quot;, connection.getInetAddress(), connection.getPort());
            process(connection);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            e.printStackTrace();
        }
    }

    @Override
    public void process(final Socket connection) {
        try (final var inputStream = connection.getInputStream();
             final var outputStream = connection.getOutputStream()) {

            final InputStreamReader inputStreamReader = new InputStreamReader(inputStream, UTF_8);
            final BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

            final HttpRequest httpRequest = HttpRequest.from(bufferedReader);
            final Controller controller = RequestMapper.getControllerFrom(httpRequest);
            final HttpResponse httpResponse = new HttpResponse();
            controller.service(httpRequest, httpResponse);
            final String response = httpResponse.generateResponse();

            outputStream.write(response.getBytes());
            outputStream.flush();
        } catch (IOException | UncheckedServletException e) {
            log.error(e.getMessage(), e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RequestMapper&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 일단 RequestMapper를 enum으로 관리하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스가 로딩되는 시점에 인스턴스가 생성되기 때문에 Controller를 싱글톤처럼 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RequestMapper로부터 반환되는 Controller들은 Controller 인터페이스와 AbstractController 추상클래스를 상속하고 있기 때문에 각 Controller의 service 메서드를 호출하는 것으로 전략패턴을 구현할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663210858805&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.apache.coyote.http11.http;

import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.regex.Pattern;
import org.apache.coyote.http11.Controller;
import nextstep.jwp.controller.HomeController;
import nextstep.jwp.controller.LoginController;
import nextstep.jwp.controller.IndexController;
import nextstep.jwp.controller.RegisterController;
import nextstep.jwp.controller.ResourceController;
import org.apache.coyote.http11.http.request.HttpRequest;

public enum RequestMapper {

    HOME(Constants.HOME_URL_REGEX, new HomeController()),
    INDEX(Constants.INDEX_PAGE_REGEX, new IndexController()),
    LOGIN(Constants.LOGIN_REGEX, new LoginController()),
    REGISTER(Constants.REGISTER_REGEX, new RegisterController()),
    RESOURCE(Constants.RESOURCE_URL_REGEX, new ResourceController());

    private final Pattern urlRegex;
    private final Controller controller;

    RequestMapper(final Pattern urlRegex, final Controller controller) {
        this.urlRegex = urlRegex;
        this.controller = controller;
    }

    public static Controller getControllerFrom(final HttpRequest httpRequest) {
        final String url = httpRequest.getUrl();
        return Arrays.stream(RequestMapper.values())
                .filter(it -&amp;gt; matchUrl(url, it))
                .findFirst()
                .orElseThrow(() -&amp;gt; new NoSuchElementException(&quot;해당하는 handler가 없습니다. &quot; + url))
                .controller;
    }

    private static class Constants {
        private static final Pattern HOME_URL_REGEX = Pattern.compile(&quot;^/$&quot;);
        private static final Pattern RESOURCE_URL_REGEX = Pattern.compile(&quot;^(/[a-z|A-Z|가-힣|ㄱ-ㅎ|_|0-9|\\-]*)+(\\.[a-z]*)$&quot;);
        private static final Pattern LOGIN_REGEX = Pattern.compile(&quot;^(/login)(\\?([^#\\s]*))?&quot;);
        private static final Pattern REGISTER_REGEX = Pattern.compile(&quot;^(/register)(\\?([^#\\s]*))?&quot;);
        private static final Pattern INDEX_PAGE_REGEX = Pattern.compile(&quot;^(/index\\.html)&quot;);
    }

    private static boolean matchUrl(final String url, final RequestMapper requestMapper) {
        return requestMapper.urlRegex.matcher(url).find();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;로그인기능&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LoginController&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;SessionAuthorizeService 와 UserService를 싱글톤 객체로 받고있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GET 요청에는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인됨 -&amp;gt; index.html redirect&lt;/li&gt;
&lt;li&gt;로그인안됨 -&amp;gt; login.html 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST 요청에는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UserService를 이용해 로그인 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package nextstep.jwp.controller;

import static nextstep.jwp.controller.ResourceUrls.INDEX_HTML;
import static nextstep.jwp.controller.ResourceUrls.LOGIN_HTML;
import static nextstep.jwp.controller.ResourceUrls.UNAUTHORIZED_HTML;

import java.util.Map;
import java.util.NoSuchElementException;
import nextstep.jwp.application.SessionAuthorizeService;
import nextstep.jwp.application.UserService;
import nextstep.jwp.dto.UserLoginRequest;
import org.apache.catalina.session.SessionManager;
import org.apache.catalina.webutils.Parser;
import org.apache.coyote.header.HttpCookie;
import org.apache.coyote.request.HttpRequest;
import org.apache.coyote.response.HttpResponse;

public class LoginController extends ResourceController {

    private final SessionAuthorizeService sessionAuthorizeService = SessionAuthorizeService.getInstance();
    private final UserService userService = UserService.getInstance();

    @Override
    public void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) {
        if (sessionAuthorizeService.isAuthorized(httpRequest)) {
            setRedirectHeader(httpResponse, INDEX_HTML);
            return;
        }
        setResource(LOGIN_HTML.getValue(), httpResponse);
    }

    @Override
    public void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) {
        final String body = httpRequest.getBody();
        login(body, httpResponse);
    }

    private void login(final String body, final HttpResponse httpResponse) {
        final Map&amp;lt;String, String&amp;gt; queryParams = Parser.parseQueryParams(body);
        try {
            final UserLoginRequest userLoginRequest = getUserLoginRequest(queryParams);
            userService.login(userLoginRequest);
            final HttpCookie cookie = SessionManager.createCookie();
            setRedirectHeader(httpResponse, INDEX_HTML);
            httpResponse.addHeader(&quot;Set-Cookie&quot;, cookie.toHeaderValue());
        } catch (IllegalArgumentException exception) {
            setRedirectHeader(httpResponse, UNAUTHORIZED_HTML);
        } catch (NoSuchElementException exception) {
            setRedirectHeader(httpResponse, LOGIN_HTML);
        }
    }

    private UserLoginRequest getUserLoginRequest(final Map&amp;lt;String, String&amp;gt; queryParams) {
        validateLoginParams(queryParams);
        return new UserLoginRequest(queryParams.get(&quot;account&quot;),
                queryParams.get(&quot;password&quot;));
    }

    private void validateLoginParams(final Map&amp;lt;String, String&amp;gt; queryParams) {
        if (!queryParams.containsKey(&quot;account&quot;) || !queryParams.containsKey(&quot;password&quot;)) {
            throw new IllegalArgumentException(&quot;account와 password 정보가 입력되지 않았습니다.&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Catalina vs Coyote&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tomcat에는 catalina와 coyote라는 패키지가 있습니다.&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-filename=&quot;스크린샷 2022-09-14 21.28.35.png&quot; data-origin-width=&quot;351&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cK2KA4/btrMbxKftU0/kvdvkN6Gr9L7ocX5PwZ8a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cK2KA4/btrMbxKftU0/kvdvkN6Gr9L7ocX5PwZ8a0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cK2KA4/btrMbxKftU0/kvdvkN6Gr9L7ocX5PwZ8a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcK2KA4%2FbtrMbxKftU0%2FkvdvkN6Gr9L7ocX5PwZ8a0%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;351&quot; height=&quot;222&quot; data-filename=&quot;스크린샷 2022-09-14 21.28.35.png&quot; data-origin-width=&quot;351&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Catalina&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tomcat의 서블릿 컨테이너입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tomcat은 실제로 tomcat JSP 엔진과 다양한 커넥터를 비롯한 여러 구성요소로 구성되어 있지만 핵심 구성 요소는 Catalina라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Catalina는 tomcat의 실제 서블릿&amp;nbsp;specification의&amp;nbsp;구현을 제공합니다. tomcat서버를 시작할 때 실제로는 Catalina를 시작하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connector, manager, mapper, servlets, session 등이 있네요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWQPQy/btrMeFh4hcG/3MQPk0JJhjaKvW66ModLKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWQPQy/btrMeFh4hcG/3MQPk0JJhjaKvW66ModLKk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;1506&quot; data-filename=&quot;스크린샷 2022-09-16 08.54.38.png&quot; data-widthpercent=&quot;49.68&quot; style=&quot;width: 49.101%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWQPQy/btrMeFh4hcG/3MQPk0JJhjaKvW66ModLKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWQPQy%2FbtrMeFh4hcG%2F3MQPk0JJhjaKvW66ModLKk%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;644&quot; height=&quot;1506&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blwsvF/btrMd3cD8Dt/gUlhg86PjnPTfLGT6fCo7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blwsvF/btrMd3cD8Dt/gUlhg86PjnPTfLGT6fCo7k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;1496&quot; data-filename=&quot;스크린샷 2022-09-16 08.54.56.png&quot; style=&quot;width: 49.7362%;&quot; data-widthpercent=&quot;50.32&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blwsvF/btrMd3cD8Dt/gUlhg86PjnPTfLGT6fCo7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblwsvF%2FbtrMd3cD8Dt%2FgUlhg86PjnPTfLGT6fCo7k%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;648&quot; height=&quot;1496&quot;/&gt;&lt;/span&gt;&lt;/div&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-filename=&quot;스크린샷 2022-09-16 11.11.41.png&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOBFW3/btrMe8ElCd5/M8aZVKbRHDEAWEN5JQEg71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOBFW3/btrMe8ElCd5/M8aZVKbRHDEAWEN5JQEg71/img.png&quot; data-alt=&quot;my apche catalina&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOBFW3/btrMe8ElCd5/M8aZVKbRHDEAWEN5JQEg71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOBFW3%2FbtrMe8ElCd5%2FM8aZVKbRHDEAWEN5JQEg71%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;330&quot; height=&quot;181&quot; data-filename=&quot;스크린샷 2022-09-16 11.11.41.png&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;181&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;my apche catalina&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Coyote&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP1.1, 2 프로토콜을 웹 서버로 지원하는 tomcat용 커넥터 구성요소 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 Java 서블릿 또는 Catalina가 로컬 파일을 HTTP 문서로 제공하는 일반 웹 서버로도 작동할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Coyote는 특정 TCP 포트에서 서버로 들어오는 연결을 수신하고 Tomcat 엔진에 요청을 전달하여 요청을 처리하고 클라이언트에게 응답을 보냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http11, http2, Processor, Request, Response 등이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-16 11.14.48.png&quot; data-origin-width=&quot;468&quot; data-origin-height=&quot;673&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qopgg/btrMePZfJV0/qwGLti4ve8Qx2CPScnQTw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qopgg/btrMePZfJV0/qwGLti4ve8Qx2CPScnQTw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qopgg/btrMePZfJV0/qwGLti4ve8Qx2CPScnQTw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqopgg%2FbtrMePZfJV0%2FqwGLti4ve8Qx2CPScnQTw0%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;468&quot; height=&quot;673&quot; data-filename=&quot;스크린샷 2022-09-16 11.14.48.png&quot; data-origin-width=&quot;468&quot; data-origin-height=&quot;673&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 data-ke-size=&quot;size16&quot;&gt;Header는 사실 RequestHeader, ResponseHeader로 나뉠 수 있는데 저는 공통으로 구성해서 밖으로 뺐습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-16 11.15.29.png&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UwnLL/btrMecHekxF/hhsimFM6ab0gehE2KLwds0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UwnLL/btrMecHekxF/hhsimFM6ab0gehE2KLwds0/img.png&quot; data-alt=&quot;my apache coyote&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UwnLL/btrMecHekxF/hhsimFM6ab0gehE2KLwds0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUwnLL%2FbtrMecHekxF%2FhhsimFM6ab0gehE2KLwds0%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;390&quot; height=&quot;202&quot; data-filename=&quot;스크린샷 2022-09-16 11.15.29.png&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;202&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;my apache coyote&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat은 Servlet Container의 역할을 하는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connector를 생성하고, Socket을 통해 클라이언트로부터 데이터를 받고 Controller를 매핑해 데이터를 가공해주고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 부분들을 직접 구현해보며 어떤 역할을 하는지도 더 이해할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;다음에는?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 지금의 구조는 약간의 문제가 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller가 추가될 때마다 RequestMapper에도 url을 추가해주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 우리는 Spring을 사용할때 Controller가 추가된다고 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;다음에는 Spring MVC가 어떻게 이런 불편함들을 해결해주는지 직접 Spring MVC를 구현하며 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고자료&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tecoble &lt;a href=&quot;https://tecoble.techcourse.co.kr/post/2021-05-24-apache-tomcat/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://tecoble.techcourse.co.kr/post/2021-05-24-apache-tomcat/&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;&lt;b&gt;코드 저장소&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/dongho108/jwp-dashboard-http/tree/step234&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/dongho108/jwp-dashboard-http/tree/step234&lt;/a&gt;&lt;/p&gt;</description>
      <category>Web/Spring</category>
      <author>API_Dev</author>
      <guid isPermaLink="true">https://ksabs.tistory.com/259</guid>
      <comments>https://ksabs.tistory.com/259#entry259comment</comments>
      <pubDate>Thu, 15 Sep 2022 01:32:23 +0900</pubDate>
    </item>
    <item>
      <title>[Tomcat 구현하기] 1. Tomcat, Connector, Socket</title>
      <link>https://ksabs.tistory.com/258</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot를 사용해 클라이언트-서버 통신을 하게되면 내장 된 톰캣을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat에 대한 이해가 없더라도 우리는 어렵지 않게 외부와 통신을 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 정말 Tomcat을 모르고 웹 어플리케이션 서버를 만들어도 되는 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 서버에 요청이 많아져 부하가 생긴다면 Tomcat 설정을 바꿔야할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 Tomcat에 대한 이해가 있다면 어느부분에서 문제가 생겼는지, 파악이 가능하고 튜닝까지 가능할 것입니다.&lt;/p&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;이번 시간에는 Tomcat을 직접 구현해보며 Tomcat을 알아가보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 나아가 서블릿을 직접 구현하며 웹서버 통신의 흐름을 다뤄봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 기능 요구사항이 주어졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기능요구사항&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GET /login 요청에 로그인 페이지를 보여준다.&lt;/li&gt;
&lt;li&gt;GET /register 요청에 회원가입 페이지를 보여준다.&lt;/li&gt;
&lt;li&gt;POST /register , body를 포함한 요청에 회원가입을 시키고 login 페이지로 redirect 시킨다.&lt;/li&gt;
&lt;li&gt;POST /login 요청에 로그인 처리를 한다.&lt;br /&gt;- 서버에서 세션을 생성해 로그인 정보를 저장한다.&lt;br /&gt;- 쿠키에 JSESSION 아이디를 담아서 로그인을 유지시킨다.&lt;/li&gt;
&lt;li&gt;로그인 처리가 된 사용자에게는 index.html 페이지를 보여준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Tomcat을 직접 구현하며 해결해보겠습니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;0. 클라이언트와 서버가 통신하기 위해 Connection을 연결하고 Socket을 통해 데이터를 읽고 쓴다.&lt;/b&gt;&lt;/h3&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-15 00.59.08.png&quot; data-origin-width=&quot;1669&quot; data-origin-height=&quot;749&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c06tYw/btrL5yxP6eq/feTXczJ6ukQsGbeQ9sMSEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c06tYw/btrL5yxP6eq/feTXczJ6ukQsGbeQ9sMSEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c06tYw/btrL5yxP6eq/feTXczJ6ukQsGbeQ9sMSEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc06tYw%2FbtrL5yxP6eq%2FfeTXczJ6ukQsGbeQ9sMSEk%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;1669&quot; height=&quot;749&quot; data-filename=&quot;스크린샷 2022-09-15 00.59.08.png&quot; data-origin-width=&quot;1669&quot; data-origin-height=&quot;749&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드로 구현할 내용들&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Application이 실행되고 Tomcat이 생성되고 실행됩니다.&lt;/li&gt;
&lt;li&gt;Tomcat이 실행되면 Connector가 생성되고 실행됩니다.&lt;/li&gt;
&lt;li&gt;Connector가 생성될때 ThreadPool과 ServerSocket을 생성합니다.&lt;/li&gt;
&lt;li&gt;제일 앞단에서 Socket이 클라이언트의 요청을 기다리다가 요청이 들어오고 수락되면, ServerSocket에 요청데이터를 넘깁니다.&lt;/li&gt;
&lt;li&gt;Connector는 요청 1개당 ThreadPool에서 Thread 1개를 사용해 요청을 처리합니다.&lt;/li&gt;
&lt;li&gt;클라이언트의 요청을 읽을때는 ServerSocket의 inputStream을 읽습니다. &lt;br /&gt;(이와같이 Socket에 데이터를 읽고쓰며 클라이언트와 데이터를 주고받을 수 있습니다.)&lt;/li&gt;
&lt;li&gt;요청이 처리되면 다시 ServerSocket을 통해 outputStream을 쓰고 클라이언트에게 데이터를 전달합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Socket (oracle docs 번역)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 서버는 특정 포트가 바인딩된 소켓을 가지고 특정 컴퓨터 위에서 돌아갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 서버는 클라이언트의 연결 요청을 소켓을 통해 리스닝 하면서 기다립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 서버가 떠 있는 머신의 호스트네임과 서버가 리스닝하고 있는 포트 번호를 통해 연결을 시도합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 연결을 수락하면 서버는 동일한 로컬 포트에 바인딩된 새로운 소켓을 생성하며 클라이언트의 주소와 포트로 세팅된 엔드포인트를 가지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트와 서버는 이제 소켓에 데이터를 쓰거나 읽음으로써 통신할 수가 있게됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Connector (tomcat docs 번역)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버로 들어오는 각 요청에는 요청 기간 동안 쓰레드가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 사용 가능한 요청 처리 쓰레드에서 처리할 수 있는 것보다 많은 동시 요청이 수신되면 구성된 최대값(max-Threads)값까지 추가 쓰레드가 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 많은 동시 요청이 수신되면 Connector에 의해 생성된 ServerSocket 내부에 구성된 최대값 (accepCount)값까지 누적됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 이상의 추가 동시 요청은 리소스를 처리할 수 있을 때까지 &quot;connection refused&quot; 오류를 수신합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;그럼 이제 구현 해보겠습니다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Application&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1663172447725&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.apache.catalina.startup.Tomcat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Application {

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

    public static void main(String[] args) {
        log.info(&quot;web server start.&quot;);
        final var tomcat = new Tomcat();
        tomcat.start();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Tomcat&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 max-connections 에 따라 connection 수를 늘릴 수 있지만 현재 구현에선 Connection을 1개로 고정하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663172488802&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.apache.catalina.startup;

import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

public class Tomcat {

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

    public void start() {
        final Connector connector = new Connector(1, 10);
        connector.start();

        try {
            // make the application wait until we press any key.
            System.in.read();
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        } finally {
            log.info(&quot;web server stop.&quot;);
            connector.stop();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Connector&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;maxThread에 따라 newFixedThreadPool 를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;acceptCount에 따라 ServerSocket을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connector가 생성될때 ServerSocket을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 1개당 1개의 쓰레드에 요청을 처리합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663172505952&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.apache.catalina.connector;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.coyote.http11.Http11Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.ServerSocket;
import java.net.Socket;

public class Connector implements Runnable {

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

    private static final int DEFAULT_PORT = 8080;
    private static final int DEFAULT_ACCEPT_COUNT = 100;

    private final ExecutorService executorService;
    private final ServerSocket serverSocket;
    private boolean stopped;

    public Connector(final int port, final int acceptCount, final ExecutorService executorService) {
        this.executorService = executorService;
        this.serverSocket = createServerSocket(port, acceptCount);
        this.stopped = false;
    }

    public Connector(final int acceptCount, final int maxThreads) {
        this(DEFAULT_PORT, acceptCount, Executors.newFixedThreadPool(maxThreads));
    }

    public Connector() {
        this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, Executors.newCachedThreadPool());
    }

    private ServerSocket createServerSocket(final int port, final int acceptCount) {
        try {
            final int checkedPort = checkPort(port);
            final int checkedAcceptCount = checkAcceptCount(acceptCount);
            return new ServerSocket(checkedPort, checkedAcceptCount);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public void start() {
        var thread = new Thread(this);
        thread.setDaemon(true);
        thread.start();
        stopped = false;
    }

    @Override
    public void run() {
        while (!stopped) {
            connect();
        }
    }

    private void connect() {
        try {
            process(serverSocket.accept());
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }

    private void process(final Socket connection) {
        if (connection == null) {
            return;
        }
        log.info(&quot;connect host: {}, port: {}&quot;, connection.getInetAddress(), connection.getPort());
        var processor = new Http11Processor(connection);
        executorService.execute(processor);
    }

    public void stop() {
        stopped = true;
        try {
            serverSocket.close();
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }

    private int checkPort(final int port) {
        final var MIN_PORT = 1;
        final var MAX_PORT = 65535;

        if (port &amp;lt; MIN_PORT || MAX_PORT &amp;lt; port) {
            return DEFAULT_PORT;
        }
        return port;
    }

    private int checkAcceptCount(final int acceptCount) {
        return Math.max(acceptCount, DEFAULT_ACCEPT_COUNT);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Http11Processor&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connector로부터 받은 Socket의 InputStream을 읽고 데이터를 처리한 후 OutputStream에 데이터를 담아 클라이언트에게 전달합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1663172846461&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.apache.coyote.http11;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import nextstep.jwp.exception.UncheckedServletException;
import org.apache.coyote.Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Http11Processor implements Runnable, Processor {

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

    private final Socket connection;

    public Http11Processor(final Socket connection) {
        this.connection = connection;
    }

    @Override
    public void run() {
        try {
            log.info(&quot;connect host: {}, port: {}&quot;, connection.getInetAddress(), connection.getPort());
            process(connection);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            e.printStackTrace();
        }
    }

    @Override
    public void process(final Socket connection) {
        try (final var inputStream = connection.getInputStream();
             final var outputStream = connection.getOutputStream()) {

            final InputStreamReader inputStreamReader = new InputStreamReader(inputStream, UTF_8);
            final BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

			...

            outputStream.write(response.getBytes());
            outputStream.flush();
        } catch (IOException | UncheckedServletException e) {
            log.error(e.getMessage(), e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. GET /login 요청에 로그인 페이지를 보여준다.&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 다음 시간에..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ksabs.tistory.com/259&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.09.15 - [Web/Spring] - [Tomcat 구현하기] 2. HttpRequest, HttpResponse, RequestMapper, Controller&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고자료&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;What is Socket? &lt;a href=&quot;https://docs.oracle.com/javase/tutorial/networking/sockets/readingWriting.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.oracle.com/javase/tutorial/networking/sockets/readingWriting.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;mulesoft &lt;a href=&quot;https://www.mulesoft.com/tcat/tomcat-catalina&quot;&gt;https://www.mulesoft.com/tcat/tomcat-catalina&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;wikipedia &lt;a href=&quot;https://en.wikipedia.org/wiki/Apache_Tomcat#Catalina&quot;&gt;https://en.wikipedia.org/wiki/Apache_Tomcat#Catalina&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;tomcat-docs-excutor &lt;a href=&quot;https://tomcat.apache.org/tomcat-8.5-doc/config/executor.html&quot;&gt;https://tomcat.apache.org/tomcat-8.5-doc/config/executor.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;tomcat-docs-http &lt;a href=&quot;https://tomcat.apache.org/tomcat-8.0-doc/config/http.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://tomcat.apache.org/tomcat-8.0-doc/config/http.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Spring</category>
      <author>API_Dev</author>
      <guid isPermaLink="true">https://ksabs.tistory.com/258</guid>
      <comments>https://ksabs.tistory.com/258#entry258comment</comments>
      <pubDate>Wed, 14 Sep 2022 11:58:22 +0900</pubDate>
    </item>
  </channel>
</rss>