<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>끄적이는 개발이야기</title>
    <link>https://hanrabong.tistory.com/</link>
    <description>서버 개발에 관심이 많은 개발자</description>
    <language>ko</language>
    <pubDate>Thu, 18 Jun 2026 02:43:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>hanrabong</managingEditor>
    <image>
      <title>끄적이는 개발이야기</title>
      <url>https://tistory1.daumcdn.net/tistory/4712952/attach/8b28a9ffd0f1424eb6e82e523f03c532</url>
      <link>https://hanrabong.tistory.com</link>
    </image>
    <item>
      <title>[APM]  v2.contextRequestURL.Headers: invalid input for HTTPHeader 에러</title>
      <link>https://hanrabong.tistory.com/entry/APM-v2contextRequestURLHeaders-invalid-input-for-HTTPHeader-%EC%97%90%EB%9F%AC</link>
      <description>&lt;h4 style=&quot;border-left: 5px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 0px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;운영 환경에서 에러가 발생하여 opensearch로 로그를 확인해야 했다. 에러가 발생하였기에 error 문구가 들어있는 로그들을 확인하려고 보았는데, APM 관련된 에러 로그들이 많이 쌓여있는 것을 확인했다. 당연히 APM 관련 로그로 인해 원하는 로그 보기가 불편하였다. 왜 해당 에러 로그들이 쌓이고 있는지 그리고 어떻게 하면 해결할 수 있을지에 대해 고민하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;padding: 0.5em; margin: 0.5em 0em; color: #4891cf; border-left: 10px solid #4891cf; border-bottom: 2px solid #4891cf; text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;APM&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;간단하게 말하면, APM은 Application Performance Monitoring으로 애플리케이션의 성능을 실시간으로 수집하고 분석하여 대시보드 형태로 시각화해 주는 도구이다.&amp;nbsp; API 호출 시, 백엔드에서 처리하는 전체 흐름을 추적 및 기록하여, 어디서 시간이 오래 걸렸는지, 어떤 부분에서 병목이 있는지를 쉽게 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;에러 및 해결&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;로그 상으로 다음과 같은 에러가 발생하였다. 매 요청마다 apm 서버로 로그를 쌓고 있어, 해당 에러가 거의 도배되고 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1751291399069&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ERROR elasticapm.transport [-] Failed to submit message: 'HTTP 400: {&quot;accepted&quot;:32,&quot;errors&quot;:[{&quot;message&quot;:&quot;decode error: data read error: v2.transactionRoot.Transaction: v2.transaction.Context: v2.context.Request: v2.contextRequest.URL: v2.contextRequestURL.Headers: invalid input for HTTPHeader:&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;에러를 보면 HTTPHeader에 invalid input 값이 있어서 decoding하는데 실패했다는 에러이다. 또한 APM 서버에서 400 에러를 발생시켜서 정확한 정보가 저장이 안 되고 있을 가능성이 크다.(얼른 고쳐야 했다....)&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;해당 에러가 발생한 이유를 확인해보았는데, request나 response header 값에&amp;nbsp; 공백, 특수문자, null 등이 있는 경우 발생한다고 한다. 나의 경우는 헤더 특정 속성 값에 null이 있어서 발생하고 있었다. 근본적으로 해결하기 위해서는 header 속성에 null 값이 들어가지 않도록 처리를 해줘야 한다. 해당 서비스 구조를 보면 f/e의 요청이 바로 b/e로 오는 것이 아닌 중간 wrapper 등을 거치고 있는데 해당 wrapper에서 header에 특정 속상을 추가할 때 null값이 들어가는 경우가 있었다. 해당 구조 자체를 변경할 수가 없어 apm의 속성을 이용하여 해결하려고 하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;APM의 속성 중 `ELASTIC_APM_CAPTURE_HEADERS` 라는 속성이 있다. 해당 속성은 request, response header에 대한 정보를 수집해서 APM 서버에 보낼지 말지 결정하는 설정이다. 해당 설정은 default 값이 true여서 해당 값을 false로 수정하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006dd7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px; text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;마치며...&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;개발/검증 환경도 APM error가 동일하게 발생하고 있어 먼저 적용하여 확인해 보니 더 이상 APM HTTPHeader error가 발생하지 않는 것을 확인할 수 있었다. 운영 쪽도 로그를 좀 더 수월하게 볼 수 있게 다음 소스 반영 할 때 얼른 적용하고 싶다.&amp;nbsp;&lt;br /&gt;해당 에러가 발생하였을 때 설정 문제인 것으로 확인할 수 있었지만, k8s의 deployment yaml에 env 값을 찾아서 수정하는 데 꽤 시간이 걸리곤 했다. 서비스 구조가 좀 더 커지고 세분화됨에 따라서 요청 하나하나를 처리할 때 주의해야 하는 점과 전반적인 구조를 이해하고 있어야 에러가 발생했을 때도 원인을 빠르게 찾을 수 있다는 것을 깨달았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;참고자료&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/elastic/apm-agent-python/issues/2134&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/elastic/apm-agent-python/issues/2134&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751291986739&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;Flask 400 decode error with HTTPHeader &amp;lt;nil&amp;gt; value &amp;middot; Issue #2134 &amp;middot; elastic/apm-agent-python&quot; data-og-description=&quot;Describe the bug: Flask transaction data are not gathered due to 400 decode error with HTTPHeader value Dependency and metric data are gathered and shown on dashboard but transaction data are rejec...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/elastic/apm-agent-python/issues/2134&quot; data-og-url=&quot;https://github.com/elastic/apm-agent-python/issues/2134&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cQMkbI/hyZcdfvQi7/qlLtBKK5psxiXvv6cuMdn0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cC0gVT/hyZfxja4HL/QVx3FGxFKdjNiIOFMBINbK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/elastic/apm-agent-python/issues/2134&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/elastic/apm-agent-python/issues/2134&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cQMkbI/hyZcdfvQi7/qlLtBKK5psxiXvv6cuMdn0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cC0gVT/hyZfxja4HL/QVx3FGxFKdjNiIOFMBINbK/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;Flask 400 decode error with HTTPHeader &amp;lt;nil&amp;gt; value &amp;middot; Issue #2134 &amp;middot; elastic/apm-agent-python&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Describe the bug: Flask transaction data are not gathered due to 400 decode error with HTTPHeader value Dependency and metric data are gathered and shown on dashboard but transaction data are rejec...&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;&lt;a href=&quot;https://www.elastic.co/docs/reference/apm/agents/python/configuration#config-capture-headers&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.elastic.co/docs/reference/apm/agents/python/configuration#config-capture-headers&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751292114373&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;Configuration | APM Python agent&quot; data-og-description=&quot;To adapt the Elastic APM agent to your needs, configure it using environment variables or framework-specific configuration. You can either configure the...&quot; data-og-host=&quot;www.elastic.co&quot; data-og-source-url=&quot;https://www.elastic.co/docs/reference/apm/agents/python/configuration#config-capture-headers&quot; data-og-url=&quot;https://www.elastic.co/docs/reference/apm/agents/python/configuration&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Jb07M/hyZcrdN3Cj/zLYfuEZXq2KYbjkQI1fekk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://www.elastic.co/docs/reference/apm/agents/python/configuration#config-capture-headers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.elastic.co/docs/reference/apm/agents/python/configuration#config-capture-headers&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Jb07M/hyZcrdN3Cj/zLYfuEZXq2KYbjkQI1fekk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&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;Configuration | APM Python agent&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;To adapt the Elastic APM agent to your needs, configure it using environment variables or framework-specific configuration. You can either configure the...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.elastic.co&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>개발 이슈</category>
      <category>apm</category>
      <category>error</category>
      <category>FLASK</category>
      <author>hanrabong</author>
      <guid isPermaLink="true">https://hanrabong.tistory.com/78</guid>
      <comments>https://hanrabong.tistory.com/entry/APM-v2contextRequestURLHeaders-invalid-input-for-HTTPHeader-%EC%97%90%EB%9F%AC#entry78comment</comments>
      <pubDate>Mon, 30 Jun 2025 23:15:51 +0900</pubDate>
    </item>
    <item>
      <title>Promtail + loki + grafana를 이용한 로그 구축</title>
      <link>https://hanrabong.tistory.com/entry/movie-preview-log</link>
      <description>&lt;h4 style=&quot;border-left: 5px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 0px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;영화 무대인사 알림 프로젝트 개발을 하고 나서, 무대인사 및 시사회 를 잘 다니고 있었다. 그러던 중 며칠동안 무대인사 알림이 이메일로 전송이 안되고 있는 상황이 있었다. 새로운 시사회나 무대인사가 없을 때는 따로 이메일로 정보가 안오기에 새로운 정보가 없구나라고 생각을 하였다. 그런데 며칠이 지나도 정보가 안 오길래, 영화 사이트에 접속하여 시사회 정보를 살펴 보았는데 새롭게 개봉된 영화 정보를 확인할 수 있었다. 내 코드에 문제가 있었다는 것을 깨닫고, 크롤링 관련 컨테이너 로그들을 살펴보았다. 당연히 에러가 있었고, 크롤링하는 url이 변경이 되어서 특정 사이트에서 크롤링을 못해오는 문제가 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;해당 서비스를 완벽하게 자동화하기 위해서, 에러 발생 시 알림을 주도록 로그 시스템을 구축하면 편할 것 같다고 생각하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;해당 프로젝트 개발에 대한 내용은 아래 글에서 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hanrabong.tistory.com/entry/movie-preview&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://hanrabong.tistory.com/entry/movie-preview&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1746620004547&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;  영화 무대 인사(시사회) 알림 자동화 사이드 프로젝트 후기&quot; data-og-description=&quot;배경 우연히 영화 무대인사를 보러 가게되었는데, 너무 너무 재밌었다.   그 후에도 기회가 될 때 보러 가려고 하였는데, 인기 있는 무대 인사의 경우 자리가 없어 예매를 못하게 되었다.... 어&quot; data-og-host=&quot;hanrabong.com&quot; data-og-source-url=&quot;https://hanrabong.tistory.com/entry/movie-preview&quot; data-og-url=&quot;https://hanrabong.com/entry/movie-preview&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kWARd/hyYRkK89n7/ltZP3KNEcgnlK403aW5K01/img.png?width=800&amp;amp;height=382&amp;amp;face=0_0_800_382,https://scrap.kakaocdn.net/dn/ezHQy/hyYPpl8OIs/LCjjYmIdFfZmbqwdwHThgk/img.png?width=800&amp;amp;height=382&amp;amp;face=0_0_800_382,https://scrap.kakaocdn.net/dn/HqW0G/hyYL5WGAKY/p5eSQxu5GRQmEGsyeV3211/img.png?width=1190&amp;amp;height=542&amp;amp;face=0_0_1190_542&quot;&gt;&lt;a href=&quot;https://hanrabong.tistory.com/entry/movie-preview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hanrabong.tistory.com/entry/movie-preview&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kWARd/hyYRkK89n7/ltZP3KNEcgnlK403aW5K01/img.png?width=800&amp;amp;height=382&amp;amp;face=0_0_800_382,https://scrap.kakaocdn.net/dn/ezHQy/hyYPpl8OIs/LCjjYmIdFfZmbqwdwHThgk/img.png?width=800&amp;amp;height=382&amp;amp;face=0_0_800_382,https://scrap.kakaocdn.net/dn/HqW0G/hyYL5WGAKY/p5eSQxu5GRQmEGsyeV3211/img.png?width=1190&amp;amp;height=542&amp;amp;face=0_0_1190_542');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;  영화 무대 인사(시사회) 알림 자동화 사이드 프로젝트 후기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;배경 우연히 영화 무대인사를 보러 가게되었는데, 너무 너무 재밌었다.   그 후에도 기회가 될 때 보러 가려고 하였는데, 인기 있는 무대 인사의 경우 자리가 없어 예매를 못하게 되었다.... 어&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hanrabong.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;github 소스 코드의 경우 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Rabongg/movie_preview&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Rabongg/movie_preview&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1746620042600&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 - Rabongg/movie_preview: 영화 시사회 및 무대인사 알림 서비스&quot; data-og-description=&quot;영화 시사회 및 무대인사 알림 서비스. Contribute to Rabongg/movie_preview development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Rabongg/movie_preview&quot; data-og-url=&quot;https://github.com/Rabongg/movie_preview&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/mIb2E/hyYMSCNPEE/wOYrZGQZK7981q2638wm3K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/9RXiT/hyYM1meUyJ/B9kQJ7yJWdKCpi7jGJOkN0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Rabongg/movie_preview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Rabongg/movie_preview&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/mIb2E/hyYMSCNPEE/wOYrZGQZK7981q2638wm3K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/9RXiT/hyYM1meUyJ/B9kQJ7yJWdKCpi7jGJOkN0/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 - Rabongg/movie_preview: 영화 시사회 및 무대인사 알림 서비스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;영화 시사회 및 무대인사 알림 서비스. Contribute to Rabongg/movie_preview 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 style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;padding: 0.5em; margin: 0.5em 0em; color: #4891cf; border-left: 10px solid #4891cf; border-bottom: 2px solid #4891cf; text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;로그 시스템 구축&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;로그 시스템을 구축하려고 했을 때, 어떤 도구를 써야할지 고민을 하였다. 로그 시스템을 구축하는 목적을 생각해 봤을 때&amp;nbsp; '에러 발생 시 알림 가능'&amp;nbsp; 이유로 인해 사용하려고 했었다. 또한 AWS 서버 비용과 로그의 양이 많지 않다는 점에 가볍고 빠르게 확인할 수 있는 도구를 원했다. 적절한 로그 시스템을 찾던 중 promtail + loki + grafana 구조를 찾게 되었고 해당 구조가 목적에 부합하여 해당 구조로 로그 시스템을 구축하게 되었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;시스템 구조&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DpW90/btsNN6qKQKw/mPWA5vPM2aQabCUShZMO5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DpW90/btsNN6qKQKw/mPWA5vPM2aQabCUShZMO5k/img.png&quot; data-alt=&quot;로그 시스템 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DpW90/btsNN6qKQKw/mPWA5vPM2aQabCUShZMO5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDpW90%2FbtsNN6qKQKw%2FmPWA5vPM2aQabCUShZMO5k%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;607&quot; height=&quot;290&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로그 시스템 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;구조는 다음과 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;EC2 서버는 총 2개가 존재한다. 왼쪽 EC2 서버에는 실제 application이 존재하고, 어플리케이션의 container 로그와 file 로그를 수집하기 위해 promtail이 있다. 다른 EC2 서버에는 Loki, grafana가 존재한다. promtail에서 로그를 수집하고, loki로 전송하면, grafana로 로그를 대시보드 형태로 시각화해서 볼 수 있고, grafana의 alert rule에 기반해서 에러 발생 시 알림을 전송할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;해당 시스템을 구축하면서 고민했던 부분이나 어려웠던 점을 얘기해보려고 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;Promtail&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;promtail은 로그 수집을 해서 loki로 전송하는 역할을 한다. 따라서 어플리케이션이 위치한 서버에 존재하는게 보편적이다.&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;565&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YKS6j/btsNL3CxWGx/LfjtS3eobB7KxzegZY3iMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YKS6j/btsNL3CxWGx/LfjtS3eobB7KxzegZY3iMk/img.png&quot; data-alt=&quot;promtail 로그 수집&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YKS6j/btsNL3CxWGx/LfjtS3eobB7KxzegZY3iMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYKS6j%2FbtsNL3CxWGx%2FLfjtS3eobB7KxzegZY3iMk%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;414&quot; height=&quot;393&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;promtail 로그 수집&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;로그 수집을 위해 promtail의 docker-compose.yml 파일에서는 디렉토리를 마운트 해야한다. 컨테이너의 경우 보통 host 서버의&amp;nbsp; /var/lib/docker/containers 에 로그를 남기고, 해당 값을 promtail에서 사용하기 위해서는 promtail container에 마운트를 해줘야 한다. cron.log의 경우 python web crawling 서비스가 로그를 container 자체에 남기기 때문에 python(container) - host - promtail(container) 이렇게 볼륨을 마운트 해줘야 promtail에서 접근 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;promtail.config의 경우는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1746623149026&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  http_listen_port: 9080  # Promtail 내부 포트

positions:
  filename: /var/lib/promtail/positions.yaml  # 로그 위치 기억

clients:
  - url: ${LOKI_URL} # Loki로 로그 전송

scrape_configs:
  # 1️⃣ 컨테이너 로그 수집 설정
  - job_name: docker-logs
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
    relabel_configs:
    - source_labels: [ '__meta_docker_container_name' ]
      action: keep
      regex: '.*\/(mysql.*|redis.*|spring.*)'
    - source_labels: ['__meta_docker_container_name']
      target_label: 'container_name'

  # 2️⃣ 파일 로그 수집 설정
  - job_name: file-logs
    static_configs:
      - targets:
          - localhost
        labels:
          app: &quot;cron-job&quot;
          __path__: &quot;/var/log/_data/cron.log&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;간략하게 promtail.conf에 대해서 살펴보자.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;clients에는 loki가 존재하는 서버의 url을 명시해주면된다. 따로 ec2에 url을 명시하지 않았을 경우 ip와 port를 명시해주면된다. 이 때 당연히 전송을 위해 loki가 위치한 ec2의 security group 인 바운드는 해당 port로 열어줘야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;scrape_configs는 로그 수집 대상을 정의하는 부분이다. docker container log와 file-log(cron.log)를 수집하는 설정을 확인할 수 있다. container-log의 경우 mysql, redis, spring으로 시작된 컨테이너의 로그만 수집하게 설정한 것을 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;밑에서 loki에 대해서 얘기할껀데, label만 index하는 구조라서, labels를 잘 정의해줘야 한다. 해당 서비스의 경우 크롤링이고 로그가 별로 없어서 상관이 없긴하지만, container_name, app 이라는 라벨로 정의를 하였다. 흔히 아는 RDB의 index처럼 loki에서도 label을 무분별하게 남용하면 인덱스 비용과 성능에 문제가 생길 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;loki&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;loki는 promtail에서 수집해서 보낸 로그를 저장하는 역할을 한다. 다른 저장소인 Opensearch와 차이점이 있다면 라벨 정보만 인덱싱을 한다는 차이점이 있다. 라벨만 인덱싱 하기 때문에 저장 공간에 이점이 있지만, 본문 검색 속도는 느리다는 단점이 있다. 내가 모니터링 하는 서비스의 경우 로그가 많지 않기에, loki를 사용하기에 적당했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; 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;771&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsMGSs/btsNPz8CLTm/ggoQMh6xHNkx31WJy8KtnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsMGSs/btsNPz8CLTm/ggoQMh6xHNkx31WJy8KtnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsMGSs/btsNPz8CLTm/ggoQMh6xHNkx31WJy8KtnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsMGSs%2FbtsNPz8CLTm%2FggoQMh6xHNkx31WJy8KtnK%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;688&quot; height=&quot;526&quot; data-origin-width=&quot;771&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;공식 문서에 나오는 loki architecture 그림은 다음과 같다. loki 구조에 대한 자세한 설명은 다른 블로그에도 많아서 내가 작성한 loki 설정 파일과 비교하며 간략하게 적으려고 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;loki 설정 파일은 다음과 같이 작성하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1746705475792&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  http_listen_port: 3100

# distributor 설정

distributor:
  ring:
    kvstore:
      store: inmemory  # 키-값 저장소를 메모리에 유지

limits_config:
  reject_old_samples: true
  reject_old_samples_max_age: 168h
  retention_period: 744h # 31 days
  allow_structured_metadata: false

# ingester 설정
ingester:
  lifecycler:
    address: 127.0.0.1
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1
    final_sleep: 30s
  chunk_target_size: 1048576         # 압축 후 1MB 목표
  max_chunk_age: 1h                  # 최대 1시간 유지 후 flush
  max_chunk_idle: 5m                # 5분 이상 로그 없으면 flush

schema_config:
  configs:
    - from: 2025-01-01
      store: tsdb
      object_store: s3
      schema: v12
      index:
        prefix: index_
        period: 24h

# compactor 설정
compactor:
  working_directory: /var/loki/compactor
  shared_store: s3
  compaction_interval: 6h
  retention_enabled: true

# querier 설정
querier:
  query_store_max_look_back_period: 744h   # 31일 조회 허용
  engine:
    timeout: 1m

storage_config:
  aws:
    endpoint: s3.${S3_REGION}.amazonaws.com
    bucketnames: ${S3_BUCKET_NAME}
    region: ${S3_REGION}
    access_key_id: ${S3_ACCESS_KEY}
    secret_access_key: ${S3_SECRET_KEY}

common:
  path_prefix: /loki&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Distributor&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;처음 로그가 들어오면 distributor를 거쳐서 ingestor 에게 전달이 되는데, distributor는 클라이언트(promtail 등)로 부터 받은 로그를 처리하는 역할을 한다. 여기서 처리라고 하면, validation, 전처리 등을 말한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;distributor 설정을 보면 ring의 kvstore를 inmemory로 즉 로컬 메모리로 설정을 한다고 했는데, 단일 인스턴스만 사용하기에 저렇게 설정을 해두었다. 보통 사내 프로젝트에서는 로그도 분산 운영을 하기에 distributor, ingestor도 여러 개가 있을 수 있는데, 이 때는 ring kvstore를 이용해 distributor가 어느 ingestor에게 로그를 분산하면 되는지 판단한다. 이 때는 kvstore 값이 inmemory가 아닌 memberlist 등으로 분산되어 있는 ingestor를 명시해줘야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;limits_config 하위의 reject_old_samples, reject_old_samples_max_age 설정을 이용해서 168h(7일)이 넘은 로그는 수신 거부하게 설정을 하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Ingestor&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;ingestor는 distributor로부터 로그를 받으면 저장하는 역할을 한다. 처음 distributor로 로그 데이터를 전송받으면 메모리에 label과 본문(line, body)을 나눠서 저장을 한다. 이 후에 설정에 의해서 flush를 하게 되는데, 이 때 label은 인덱싱을 하여 chunk(본문)와 같이 설정한 저장소(fs, S3 등)에 저장한다. flush를 유발하는 설정을 보면 다음과 같다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;chunk_target_size: 1048576         # 압축 후 1MB 목표
max_chunk_age: 1h                  # 최대 1시간 유지 후 flush
max_chunk_idle: 5m                # 5분 이상 로그 없으면 flush&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;여기서도 chunk라는 단어가 나오는데, ingestor가 로그를 받으면 메모리에 [ 시리즈(label) - chunk(본문) ] 형태로 저장을 한다. 같은 label의 경우 같은 chunk에 저장이 되고 위 설정에 따르면 chunk가 1MB가 되면 flush가 일어난다. 또한 chunk가 1MB가 되지 않았더라도 1시간 동안 로그를 받으면 flush가 일어나게 된다. 마지막 조건은 5분 이상 로그가 없으면 flush가 일어나게 된다. 내 서비스에서 유효한 설정은 마지막 설정밖에 없다. 오전 9시 오후 4시에 크롤링이 돌고, 로그 양이 작기 때문에 1MB에 도달할 일도 없고, 1시간동안 로그가 발생하는 경우도 없다. 5m 이라는 idle 시간은 보통 서비스의 경우 이렇게 설정을 하곤하는데, 아직까지 문제가 없어 해당 설정을 유지하려고 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;schema_config에서는 Loki 데이터 저장 구조를 정의하고 있다. store는 tsdb로 설정을 하였는데, promethus와 비슷한 tsdb 형식으로 저장하게 설정했다. 이 방식말고 boltdb-shipper 방식도 존재하는데 해당 방식은 구버전에서만 사용하고 최신 버전에서는 tsdb 형식으로 저장하는게 일반적이다. object_store의 경우 chunk나 label이 저장되는 스토리지를 명시하면 된다. aws 에서 서버를 배포하고 있기에 s3에 저장하는 것으로 설정했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Compactor&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Loki에서 tsdb를 사용하는 경우 꼭 필요한 구성 요소이다. 말 그대로 compact하는 역할을 하는데, chunk와 index 블록을 병합을 해준다. 병합을 하게 되면 block수가 감소하기에 S3에서 구조도 더 깔끔하고 쿼리 성능과 저장비용에서도 이점이 존재한다. 병합을 어느 주기로 할지가 가장 관건인데, compaction_interval을 6h으로 설정해주었다. 보통 10m 단위로 설정을 한다고 하는데, 서비스 특성 상 하루 2번, 오전 9시, 16시에 로그가 찍히기 때문에 10m은 너무 과도하다고 생각을 했다. 10m 간격으로 설정을하면 S3에 API를 10m 간격으로 요청을 하게되는데 요청량에 따라 S3 비용도 증가하기 때문에 6h으로 설정을 했다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;retention_enabled: true 값의 경우 limit_config 하위의 retention_period를 가능하게 해준다. retention_period 주기로 s3에서 로그를 삭제하게 되는데, 해당 서비스에서는 한 달 전의 로그들은 삭제하게 설정해두었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Querier&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위에 distributor, ingestor, compactor 는 로그 저장을 담당했다면, querier는 로그 조회할 때 사용하는 컴포넌트이다. 사용자가 보낸 LogQL을 분석해서 index 를 조회하여 chunk 위치를 찾아 사용자에게 결과를 반환하는 역할을 한다. chunk가 메모리에 없고 S3에 있을 때는 S3에서 가져오기도 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;query_store_max_loop_back_period 설정은 말 그대로 며칠 전까지 조회를 하는지 설정하는 옵션이다. 설정을 안할 경우 무제한으로 확인이 가능하다. 로그의 경우 장기보관할 필요가 없어서 retention도 744h로 맞춰두어서, 해당 옵션의 값도 744h로 맞췄다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;grafana&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;grafana를 통해서 loki에 저장되어 있는 로그를 대시보드 형태로 시각화해서 볼 수 있다. 또한 모니터링 시스템을 구축한 가장 큰 이유인 알림을 보낼 수가 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;대시보드는 이렇게 만들어 둔 상태이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2528&quot; data-origin-height=&quot;623&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0N46Z/btsNOjSMogk/j5l2kl0ETnFxO2S7oKmaj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0N46Z/btsNOjSMogk/j5l2kl0ETnFxO2S7oKmaj1/img.png&quot; data-alt=&quot;grafana dashboard&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0N46Z/btsNOjSMogk/j5l2kl0ETnFxO2S7oKmaj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0N46Z%2FbtsNOjSMogk%2Fj5l2kl0ETnFxO2S7oKmaj1%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;2528&quot; height=&quot;623&quot; data-origin-width=&quot;2528&quot; data-origin-height=&quot;623&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;grafana dashboard&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;가장 첫 번째 대시보드는 정상 로그가 남는지 확인하는 대시보드이다. 두 번째 대시보드는 에러 로그를 보여주고, 세 번째 대시보드는 에러 로그 내용을 보여준다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;알림의 경우는 이메일로 알림 전송이 오게 했다. 이러한 형식으로 알림이 오고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;939&quot; data-origin-height=&quot;1042&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddEM7D/btsNP8WWedb/a7Zy3IS1NcVIIh4aoh1rE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddEM7D/btsNP8WWedb/a7Zy3IS1NcVIIh4aoh1rE1/img.png&quot; data-alt=&quot;에러 발생 시 grafana 알림&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddEM7D/btsNP8WWedb/a7Zy3IS1NcVIIh4aoh1rE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddEM7D%2FbtsNP8WWedb%2Fa7Zy3IS1NcVIIh4aoh1rE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;572&quot; height=&quot;635&quot; data-origin-width=&quot;939&quot; data-origin-height=&quot;1042&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;에러 발생 시 grafana 알림&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;alert rule을 설정할 때 주의해야할 점을 몇 가지 얘기해 보겠다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;LogQL을 만들었을 때, data가 없어서 grafana에서 알림이 오는 경우도 있다. 에러 로그에 대한 알림일 경우 에러 로그가 발생하지 않는 경우가 더 많기에 해당 LogQL로 조회되는 데이터가 없는 경우가 많다. 이 때 알림이 오지 않게 하려면&amp;nbsp; &quot;Alert state if no data or all values are null&quot; 해당 값을 Normal로 줘야 한다. 처음에 alert rule을 만들었을 때 며칠 동안 계속 알림이 와서... 당황했던 적이 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;alert rule을 설정할 때 evaluated time과 pending period 시간과 alert 조건의 시간의 관계를 잘 생각해야한다. 로그 초기에 alert 조건은 2h으로 설정을 하고 evaluated time은 1h, pending period는 1h 30m으로 설정을 한 적이 있었다. 분명 에러로 인해서 에러 로그가 찍혔는데, pending period를 기다리다보니 (alarm을 보내야 하는 시간 -&amp;nbsp; 2h) 전에는 에러가 없다고 인식하여 에러가 안 오는 경우가 있었다. 이러한 점을 고려하여 evaluated time과 pending period 시간을 설정했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;마치며...&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;로그 시스템 구축 후에 좀 더 서비스를 안정적으로 운영할 수 있었다. 에러가 발생하면 grafana를 통해서 메일로 알림을 받을 수 있어 매번 서비스가 잘 동작하는지 확인하지 않아도 된다는 점이 편했다. 또한 에러 발생 시 즉각 메일이 오다보니, 어떤 코드에서 문제가 있다는 점을 빠르게 파악하고 수정할 수 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;로그 시스템 구축 전에 어플리케이션 레벨에서 log level에 맞게 로그를 남겨야만 해당 시스템의 효과가 더 좋은 것 같다. 보통 운영 단에서 log level은 팀마다 규칙이 다르지만, 확인을 잘 못한다고 해서 info나 debug 레벨의 로그를 error로 두게되면 의미가 없어진다. 또한 내 서비스에서는 에러 로그로 찍히면 알림이 오기 때문에 무분별한 알림이 되어서 의미가 없어진다는 것을 깨달았다. 알림이 오지 않더라도 적당한 로그 레벨로 로그를 남겨야만 검색 시에도 편하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;에러가 발생하지 않는 100% 완벽한 소프트웨어는 없기때문에 에러를 쉽게 확인할 수 있는 로그 시스템 구축이 중요한 것 같다. 추후에 다른 서비스를 개발하면서 더 다양한 로그 시스템을 개발하려고 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;참고자료&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://grafana.com/docs/loki/latest/get-started/architecture/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://grafana.com/docs/loki/latest/get-started/architecture/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1746704229140&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;Loki architecture |  Grafana Loki documentation&quot; data-og-description=&quot;Loki architecture Grafana Loki has a microservices-based architecture and is designed to run as a horizontally scalable, distributed system. The system has multiple components that can run separately and in parallel. The Grafana Loki design compiles the co&quot; data-og-host=&quot;grafana.com&quot; data-og-source-url=&quot;https://grafana.com/docs/loki/latest/get-started/architecture/&quot; data-og-url=&quot;https://grafana.com/docs/loki/latest/get-started/architecture/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bLUo7K/hyYPgweenc/ZApYb2M6TNUTK28p76BBkk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cXLjPY/hyYRomEPnh/3n3SKodp1lSOINuj7GS5h0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://grafana.com/docs/loki/latest/get-started/architecture/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://grafana.com/docs/loki/latest/get-started/architecture/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bLUo7K/hyYPgweenc/ZApYb2M6TNUTK28p76BBkk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cXLjPY/hyYRomEPnh/3n3SKodp1lSOINuj7GS5h0/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;Loki architecture | Grafana Loki documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Loki architecture Grafana Loki has a microservices-based architecture and is designed to run as a horizontally scalable, distributed system. The system has multiple components that can run separately and in parallel. The Grafana Loki design compiles the co&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;grafana.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://grafana.com/blog/2018/12/12/loki-prometheus-inspired-open-source-logging-for-cloud-natives/#loki&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://grafana.com/blog/2018/12/12/loki-prometheus-inspired-open-source-logging-for-cloud-natives/#loki&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1746703971528&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;Loki: Prometheus-inspired, open source logging for cloud natives | Grafana Labs&quot; data-og-description=&quot;Loki: Prometheus-inspired, open source logging for cloud natives Introduction This blog post is a companion piece for my talk at https://devopsdaysindia.org. I will discuss the motivations, architecture, and the future of logging in Grafana! Let&amp;rsquo;s get ri&quot; data-og-host=&quot;grafana.com&quot; data-og-source-url=&quot;https://grafana.com/blog/2018/12/12/loki-prometheus-inspired-open-source-logging-for-cloud-natives/#loki&quot; data-og-url=&quot;https://grafana.com/blog/2018/12/12/loki-prometheus-inspired-open-source-logging-for-cloud-natives/#loki&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bCiEns/hyYRv0oLug/pinpYQiK83ycSKNfI4sFw1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/mjqGs/hyYPdlZYZx/D3guSLA4KnxA5e2dXQEBaK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://grafana.com/blog/2018/12/12/loki-prometheus-inspired-open-source-logging-for-cloud-natives/#loki&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://grafana.com/blog/2018/12/12/loki-prometheus-inspired-open-source-logging-for-cloud-natives/#loki&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bCiEns/hyYRv0oLug/pinpYQiK83ycSKNfI4sFw1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/mjqGs/hyYPdlZYZx/D3guSLA4KnxA5e2dXQEBaK/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;Loki: Prometheus-inspired, open source logging for cloud natives | Grafana Labs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Loki: Prometheus-inspired, open source logging for cloud natives Introduction This blog post is a companion piece for my talk at https://devopsdaysindia.org. I will discuss the motivations, architecture, and the future of logging in Grafana! Let&amp;rsquo;s get ri&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;grafana.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>프로젝트</category>
      <category>Alert</category>
      <category>alert rule</category>
      <category>grafana</category>
      <category>Log</category>
      <category>Loki</category>
      <category>promtail</category>
      <author>hanrabong</author>
      <guid isPermaLink="true">https://hanrabong.tistory.com/77</guid>
      <comments>https://hanrabong.tistory.com/entry/movie-preview-log#entry77comment</comments>
      <pubDate>Mon, 17 Mar 2025 22:00:35 +0900</pubDate>
    </item>
    <item>
      <title>  영화 무대 인사(시사회) 알림 자동화 사이드 프로젝트 후기</title>
      <link>https://hanrabong.tistory.com/entry/movie-preview</link>
      <description>&lt;h4 style=&quot;border-left: 5px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 0px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;우연히 영화 무대인사를 보러 가게되었는데, 너무 너무 재밌었다.   그 후에도 기회가 될 때 보러 가려고 하였는데, 인기 있는 무대 인사의 경우 자리가 없어 예매를 못하게 되었다.... 어떻게 많은 사람들이 무대인사 예약을 빨리 할까하고 찾아보았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;무대인사나 시사회만 모아줘서 보여주는 사이트도 존재하였고, 지인들에게 물어보니 요새는 SNS에 다 올라오기도 하고 팬 카페에 공지가 되어서 사람들이 빠르게 정보를 접할 수 있다고 하였다. SNS를 하지도 않고, 팬 카페 활동도 안하는 내가 어떻게하면 원하는 영화 무대인사를 갈 수 있을까 고민을 하다가 영화 무대인사 정보를 자동으로 알려주는 서비스를 만들어 보자고 생각을 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;개발한 code의 경우 github에서 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Rabongg/movie_preview&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Rabongg/movie_preview&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740980636618&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 - Rabongg/movie_preview&quot; data-og-description=&quot;Contribute to Rabongg/movie_preview development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Rabongg/movie_preview&quot; data-og-url=&quot;https://github.com/Rabongg/movie_preview&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/rMMcv/hyYjE6gVvb/GJda7XgOkzUNU9Fm68JJk0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/KevWG/hyYjDGeaTo/peq4k3Bl50XwTID7RcPgSK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Rabongg/movie_preview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Rabongg/movie_preview&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/rMMcv/hyYjE6gVvb/GJda7XgOkzUNU9Fm68JJk0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/KevWG/hyYjDGeaTo/peq4k3Bl50XwTID7RcPgSK/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 - Rabongg/movie_preview&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to Rabongg/movie_preview 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 style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;padding: 0.5em; margin: 0.5em 0em; color: #4891cf; border-left: 10px solid #4891cf; border-bottom: 2px solid #4891cf; text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;Version 0(Beta)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v0의 경우 빠르게 기능을 개발하는 것에만 초점을 맞추어서 개발을 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;사용 기술&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Python 3.11&lt;/li&gt;
&lt;li&gt;Mysql 8.x&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;기능&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;version 0의 경우 python으로 개발을 하였다. Python을 선택한 이유는 여러가지가 있었는데, &lt;b&gt;&lt;u&gt;빠르게 개발&lt;/u&gt;&lt;/b&gt;을 하고 싶었고, &lt;u&gt;&lt;b&gt;웹 크롤링&lt;/b&gt;&lt;/u&gt; 기능을 사용하기에 선택하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; 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;스크린샷 2025-03-02 오후 4.46.14.png&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;1044&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s0cJL/btsMBf9TtPC/uSYM4hGRagnwWw0fQXKyh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s0cJL/btsMBf9TtPC/uSYM4hGRagnwWw0fQXKyh0/img.png&quot; data-alt=&quot;v0&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s0cJL/btsMBf9TtPC/uSYM4hGRagnwWw0fQXKyh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs0cJL%2FbtsMBf9TtPC%2FuSYM4hGRagnwWw0fQXKyh0%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;559&quot; height=&quot;517&quot; data-filename=&quot;스크린샷 2025-03-02 오후 4.46.14.png&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;1044&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;v0&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Python을 이용하여 웹 크롤링을 진행한다.&lt;/li&gt;
&lt;li&gt;디비에 데이터를 저장하였다.&lt;/li&gt;
&lt;li&gt;사용자에게 메일 전송&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;좀 더 자세하게 얘기하자면, Python을 이용하여 웹 크롤링을 진행하였다. 웹 크롤링을 한 사이트는 대중적인 영화 사이트인 CGV, LOTTE, MEGABOX 3개의 사이트에서 무대인사 부분의 데이터를 갖고왔다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 갖고 온 후에, 가공하여 데이터베이스에 저장하였다. 매번 같은 데이터가 아닌, DB에 없는 데이터인지 비교 후에 없는 데이터들만 저장을 하였고, 새로운 데이터들만 사용자에게 메일로 보내였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;사용자에게 알림을 보내는 기능을 개발할 때 어떤 알림을 이용하는게 가장 적합한지 고민을 하게 되었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;당연히, 문자(SMS)나 메신져(카카오톡)가 편하고 확인하기 수월하다. 그러나 해당 기능의 경우 발송 건당 추가적으로 돈이 들어가서 1인 서비스로 사용하기에는 과하다는 생각이 들었다. 푸시 알림의 경우 웹 서비스나 모바일 앱이 있어야 가능했기에, 배보다 배꼽이 더 큰 것 같아서 사용하지 않았다. 그러하여, 이메일 서비스를 이용하여 사용자에게 알림을 보내기로 결정하였다.&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;후기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;결론을 말하자면, 해당 서비스를 이용해서 원하는 영화 무대인사를 여러 번 다녀올 수 있었다.  &lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;서비스 자체도 혼자 사용하고자 만들었기에 문제가 없었다. 그러면서도, 코드를 좀 더 깔끔하게 리팩토링하고 싶은 욕심도 생기게 되었다. 무엇보다, 해당 서비스의 완벽한 자동화를 꿈꾸게 되었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;현재 내가 개발한 서비스의 경우, 새로운 정보가 없으면 이메일을 안 보내는 구조이다. 데이터가 실제로 없어서 안 보내는지, 아니면 서비스에 에러가 생겨서 안 보내는지에 대해 알 수 가 없었다... 매일 로그를 직접 보면 확인이 가능하지만, 신경을 안써도 자동으로 동작하는 서비스를 원했기에 서비스를 리팩토링 하기로 결정하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;padding: 0.5em; margin: 0.5em 0em; color: #4891cf; border-left: 10px solid #4891cf; border-bottom: 2px solid #4891cf; text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;Version 2&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v2의 경우 v0와 다르게 빠르게 기능만 구현하려고 하지 않았고, 유지보수성이나 확장성 등에도 초점을 맞추어서 개발을 진행하였다.&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;사용 기술&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Python 3.11&lt;/li&gt;
&lt;li&gt;Mysql 8.x&lt;/li&gt;
&lt;li&gt;Redis 7.4&lt;/li&gt;
&lt;li&gt;Java 17이상&lt;/li&gt;
&lt;li&gt;spring boot 3.4.x&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;기능&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;version 2의 경우 version 0과 다르게 Python으로만 개발하지 않았다. 분리한 이유는 크게 3가지가 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;첫 번째는 역할을 분리하여 유지보수 및 확장성을 용이하게 하고 싶었다. version 0에서는 Python 프로그램이 전부 담당하고 있어서 만약 확장을 하려고 해도 해당 process 전부를 여러 개 띄워야 한다. 분리를 하면, 웹 크롤링만 확장하고 싶으면 Python 의 프로세스만 여러 개 띄워서 처리할 수 있다. 또한, 역할을 분리함으로써 유지보수도 쉬워진다. 추후 에러 발생에 대한 alert가 왔을 때, 어떤 기능에서 에러가 발생했는지 확인이 빠르게 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;두 번째는, Spring boot를 사용하면 데이터 관리에 용이성이 있다. Python에서도 sqlalchemy 등 라이브러리를 사용하여 트랜잭션을 지원하게 할 수 있다. 하지만 Spring boot에서는 JPA의 트랜잭션 관리 기능을 내장하고 있어서 따로 개발자가 신경 쓸 일이 거의 없다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;세 번째로 pub/sub 기능을 사용하고 싶었다. 비즈니스 서비스가 아니라, 개인적인 프로젝트라서 pub/sub 기능을 사용하고 싶기도 했다. Python, Spring boot로 분리를 하는 순간, 2개의 서비스 사이에서 데이터를 주고 받기 위해 pub/sub 구조를 사용해야하기에 분리한 이유도 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;기능을 간단하게 도식화 하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KwDBG/btsMBz1vyP4/mKONapDD8fAaAbrJVEux2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KwDBG/btsMBz1vyP4/mKONapDD8fAaAbrJVEux2k/img.png&quot; data-alt=&quot;v2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KwDBG/btsMBz1vyP4/mKONapDD8fAaAbrJVEux2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKwDBG%2FbtsMBz1vyP4%2FmKONapDD8fAaAbrJVEux2k%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;1190&quot; height=&quot;542&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;542&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;v2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Python을 이용하여 웹 크롤링을 진행&lt;/li&gt;
&lt;li&gt;데이터를 가공하여 Redis로 전달(publish)&lt;/li&gt;
&lt;li&gt;Redis Pub/Sub 구조에서 Spring으로 데이터를 전송&lt;/li&gt;
&lt;li&gt;Spring에서 사용자에게 메일을 전송&lt;/li&gt;
&lt;li&gt;전송한 데이터를 데이터베이스에 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;각 단계를 자세히 설명해보겠다. 첫 번째로 Python에서 웹 크롤링(cron 매일 오전 9시, 오후 4시)을 진행한다. 웹 크롤링 진행 후, 데이터만 가공해서 Redis로 데이터를 보낸다. Redis에서 해당 데이터를 받고 Spring boot로 데이터를 보낸다. 데이터를 받은 Spring 서비스에서는 DB에 존재하는 데이터인지 확인을 하고, 새로운 데이터들만 사용자에게 이메일로 전송한다. 전송이 성공적으로 되었으면, 데이터를 DB에 저장을한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;v2 버전을 개발하면서, 고민했던 부분들이나 겪었던 어려웠던 사항에 대해서는 아래에서 좀 더 자세하게 적어보겠다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;Pub/Sub 구조&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Pub/Sub 구조를 지원하는 시스템에는 크게 Redis, Kafka, RabbitMQ 등이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 중에서 Redis를 사용하였는데, 다른 시스템과 비교하였을 때 간단하고 빨라서 사용하였다. 발행한 메세지를 따로 저장하지 않는다는 단점이 있지만, 해당 서비스에서 발행한 메세지의 정확한 전달 보장(?)이 크게 중요하지 않았다. Python 프로그램이 매일 오전 9시, 오후 4시마다 데이터를 갖고 오기에, 오전 9시의 데이터가 유실되도 오후 4시에 포함되기 때문에 유실되도 문제가 없었다. 또한 영화 데이터이고 민감한 데이터가 아니였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;또한 실시간으로 웹 크롤링 된 데이터를 전달하고 받기만 하면 되어서 적합하였다. 메세지 순서 보장도 필요가 없었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Redis의 경우, 현재 사용자가 많지 않기에, 단일 구조로 배포를 해둔 상태이다. 데이터도 매일 딱 2번 정도만 받기에, 굳이 cluster나 sentinel을 사용하지 않아도 된다고 생각하였다. (추후, 해야한다면 변경할 예정이다)&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;이메일 화면&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;알림의 경우 v2에서도 v0와 마찬가지로, 이메일 알림을 사용하였다. v0 버전은 빠르게 만들기 위해 이메일 디자인에도 신경을 안쓰고 정보 전달에 초점을 맞췄다. 다른 사람이 보면, 이게 뭐지 할 정도긴하다....&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;v2에서는 이메일도 사용자가 봤을 때 한 눈에 알아볼 수 있게 수정하였다. 정보를 받아보고 싶다고 하는 사람이 생겨서... 이쁘게 만들기도 하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;935&quot; data-origin-height=&quot;617&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kCnMA/btsMBG7msQk/K25ttIcevwbaImEzGkt0bK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kCnMA/btsMBG7msQk/K25ttIcevwbaImEzGkt0bK/img.png&quot; data-alt=&quot;이메일 알림 화면(v0(좌), v2(우))&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kCnMA/btsMBG7msQk/K25ttIcevwbaImEzGkt0bK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkCnMA%2FbtsMBG7msQk%2FK25ttIcevwbaImEzGkt0bK%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;690&quot; height=&quot;455&quot; data-origin-width=&quot;935&quot; data-origin-height=&quot;617&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이메일 알림 화면(v0(좌), v2(우))&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이메일 전송을 하고나서, 특이한 점 몇가지가 있었다. 이메일마다 지원하는 코드(?)가 조금씩 달랐다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;크게 2가지가 있었는데, &lt;b&gt;style tag&lt;/b&gt;와&amp;nbsp;&amp;nbsp; &lt;b&gt;&amp;lt; &amp;gt;&lt;/b&gt; 이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;style tag의 경우 gmail에서는 정상적으로 동작을 하였는데, naver 메일에서는 정상적으로 동작을 하지 않았다. 원하는 대로 동작 시키기 위해 &amp;lt;style&amp;gt; &amp;lt;/style&amp;gt; 태그 대신 각 tag안에 inline style 형식으로 스타일을 추가를 해줘야 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt; &amp;gt;&lt;/b&gt;의 경우, 위의 사진에서 보이다시피 제목에 해당 기호가 포함되어 있는 경우가 있다. naver 메일 에서는 상관이 없었지만, gmail에서는 tag로 인식하여 제목이 안나오는 경우가 발생하였다. 이 경우에는 &amp;lt;는 `&amp;amp;lt`, &amp;gt;는 `&amp;amp;gt`로 변환해주는 StringEscapeUtils를 지원해주는 자바 라이브러리를 사용하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;배포 환경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;v0의 경우 배포를 window 로컬 컴퓨터에 하였는데, v2는 우분투 서버(aws, gcp 등)에 배포를 하려고 하였다. 서버에 코드 전체를 옮기고 배포할 수도 있지만, docker를 사용하면 수월하기에 docker를 사용하여 배포를 하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;docker 컨테이너 내부에서 웹 크롤링을 하려면 브라우저가 필요했다. 정적 페이지를 크롤링할 때는 requests를 이용하여 조회 후에 데이터를 가공하면 되었지만, 영화 시사회 사이트의 경우 동적 데이터가 있어서 브라우저를 이용하여 크롤링을 해야했다. 로컬에서 프로그램을 돌렸을 때는 chrome이 기본적으로 설치가 되어 있어서, 상관이 없었지만, docker container 내부에는 직접 설치를 해야했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;당연히, 각 서비스 코드에서도 각 container와 통신하기 위해, DB나 redis 설정하는 부분을 수정해야했다. localhost가 아닌 container 이름으로 전부 수정해야했고, volume도 mount를 해야 컨테이너가 죽었을 때 데이터가 날아가지 않기에 설정도 해주었다. docker 설정의 경우도 위의 github의 docker-compose.yaml로 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;로그&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;v2 기능 개발을 할 때 제일 신경 쓴 부분이 로그였다. 애초에 v0에서 v2로 리팩토링한 이유도 에러 발생 시 grafana에서 이메일로 알림을 줄 수 있게 완벽한 자동화를 만들기 위함이였다. 고로, log를 잘 남겨야 grafana에서도 의미있는 알림을 줄 수 있을 것이라고 생각했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;흔히들 잘 아는 log level을 기준으로, info 및 error 로그를 남겼다. 보통, 다른 API 호출이나 자신의 서비스의 에러가 아닐 때 warn level로 로그를 설정한다고 많이들 한다. 그러나 해당 서비스의 경우 실시간 성 알림이 중요하기에, 다른 API 호출 실패일 때도 빠르게 수정을 해야해서, error 레벨로 설정하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;로그 수집과 알림 에 대한 이야기는 다음 블로그에 자세히 적을 예정이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;후기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;리팩토링 후에도 여전히 무대인사나 시사회 관련 알림은 잘 오고 있다!!!&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;사실 에러가 발생할 확률이 거의 없긴한데, 알림 덕분에 크롤링하는 URL이 변경이 되었거나 하는 정보들을 빠르게 알 수 있었다. 또한, 사용 안해봤던 pub/sub나 loki, grafana 등을 사용할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;일상에서 필요성에 의해서 만든 프로젝트라서 개발하는 내내 재밌었다. 현재는 해당 서비스를 만족하면서 사용하고 있다. 추가적으로 기능이 필요하다고 생각이 되거나, 재밌겠다고 생각이 들면 그 때 좀 더 서비스를 발전시키려고 한다.&lt;/p&gt;</description>
      <category>프로젝트</category>
      <category>cronjob</category>
      <category>docker</category>
      <category>Log</category>
      <category>Python</category>
      <category>redis</category>
      <category>spring</category>
      <category>영화 무대인사</category>
      <category>자동화</category>
      <author>hanrabong</author>
      <guid isPermaLink="true">https://hanrabong.tistory.com/75</guid>
      <comments>https://hanrabong.tistory.com/entry/movie-preview#entry75comment</comments>
      <pubDate>Fri, 7 Mar 2025 00:02:31 +0900</pubDate>
    </item>
    <item>
      <title>[Python] WSGI 그리고 WebOb</title>
      <link>https://hanrabong.tistory.com/entry/Python-WSGI-%EA%B7%B8%EB%A6%AC%EA%B3%A0-WebOb</link>
      <description>&lt;h4 style=&quot;border-left: 5px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 0px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;사내에서 개발 완료 후, 테스트를 진행하게 되었습니다. 제가 테스트를 할 때는 잘되었는데, 다른 개발자가 테스트를 할 때 정상적으로 동작하지 않았습니다.&amp;nbsp;화면에서 서버에 데이터를 넘기는데, body의 특정 속성이 0 ~ 10000 사이즈 까지 넘길 수 있었습니다. 해당 데이터에 500정도 사이즈를 넘겼을 때는 정상적으로 동작을 했지만, 최대값에 근접한 사이즈로 데이터를 넘길 때면 에러가 발생하곤 했습니다. 제가 테스트 할 때는 사이즈를 크게 안 넣었고, 다른 개발자가 테스트 할 때는 최대 사이즈로 값을 넣어서 에러가 발생한 것이었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Spring 프레임워크를 사용한 서비스에서는 해당 사이즈에 대한 에러가 발생하지 않았지만, python 프레임워크로 개발한 서비스에서는 발생한 점이 신기했습니다. 그래서 왜 Python 프레임 워크로 개발한 서비스만 발생하였는지 확인해보니, WebOb이라는 라이브러리와 관련이 있다는 것을 알게되었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;왜 Python 프레임워크에서 발생했는지 간략하게 얘기해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;padding: 0.5em; margin: 0.5em 0em; color: #4891cf; border-left: 10px solid #4891cf; border-bottom: 2px solid #4891cf; text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;요청 및 응답&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;대부분 사람들이 알고 있듯이, 보편적인 Spring 프레임 워크의 요청 및 응답 구조를 간략하게 보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnzJFA/btsK4uhzvzg/pl1y6fPalTNb12wROpWFek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnzJFA/btsK4uhzvzg/pl1y6fPalTNb12wROpWFek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnzJFA/btsK4uhzvzg/pl1y6fPalTNb12wROpWFek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnzJFA%2FbtsK4uhzvzg%2Fpl1y6fPalTNb12wROpWFek%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;1159&quot; height=&quot;284&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;284&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트의 요청이 apache/nginx 웹 서버로 요청이 들어옵니다. 정적 파일 요청의 경우 직접 처리가 가능하고, 동적 처리 요청의 경우 tomcat으로 전달합니다. tomcat에서 받은 요청은 Spring application으로 전달을 하고 로직 처리 후에 다시 응답을 하는 구조 입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;padding: 0.5em; margin: 0.5em 0em; color: #4891cf; border-left: 10px solid #4891cf; border-bottom: 2px solid #4891cf; text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Python Web application&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;요청 및 응답&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1131&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciIilg/btsK3jubGuK/w802dDiEGKl1AZWFvQgZbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciIilg/btsK3jubGuK/w802dDiEGKl1AZWFvQgZbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciIilg/btsK3jubGuK/w802dDiEGKl1AZWFvQgZbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciIilg%2FbtsK3jubGuK%2Fw802dDiEGKl1AZWFvQgZbK%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;1131&quot; height=&quot;251&quot; data-origin-width=&quot;1131&quot; data-origin-height=&quot;251&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Python 프레임 워크(Django, Flask, FastAPI 등)의 경우 다음과 같은 요청 및 응답 구조를 갖고 있습니다. 위와 동일하게 정적 파일 요청의 경우 직접 처리가 가능하고, 동적 처리 요청의 경우 WSGI 서버로 전달합니다. WSGI는 요청을 Python 애플리케이션(Django, Flask 등)으로 전달합니다. 전달받은 요청을 애플리케이션이 처리하고 응답하는 구조입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;※ WSGI란?&lt;br /&gt;Web Server Gateway Interface 약자로, 말 그대로 Web Server와 Python 웹 어플리케이션의 통신을 위한 표준 인터페이스입니다. 다양한 웹 서버와 Pytohn 웹 어플리케이션을 통신해 주기 위해 나오게 되었습니다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;span style=&quot;font-size: 21px;&quot;&gt;&lt;b&gt;WebOb&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;WebOb은 WSGI 기반 어플리케이션에서 HTTP 요청과 응답을 처리하기 위한 라이브러리로, Python application에서도 사용 가능합니다. 위 그림으로 보면&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;WSGI &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;rarr; Python application 이 통신을 할 때 사용하게 됩니다. 표준으로 정해진 것이 아니여서 필수적으로 사용할 필요는 없습니다. 제 프로젝트의 경우에는 WSGI와 Python application 사이에 있는 WSGI middleware에서 WebOb을 통해서 응답과 요청을 주고 받고 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 WebOb의 어떤 코드가 에러와 관련이 있었을까요?&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;코드는 여기서 확인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Pylons/webob/blob/main/src/webob/request.py#L959&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Pylons/webob/blob/main/src/webob/request.py#L959&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1733144689854&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;webob/src/webob/request.py at main &amp;middot; Pylons/webob&quot; data-og-description=&quot;WSGI request and response objects. Contribute to Pylons/webob development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Pylons/webob/blob/main/src/webob/request.py#L959&quot; data-og-url=&quot;https://github.com/Pylons/webob/blob/main/src/webob/request.py&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/biwCGx/hyXGOApJEF/28u4m3Sd2KWLI4dOC79WT1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bw9Q6g/hyXGER6ehk/q9RBykjV88LkgMtaW4GZu0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Pylons/webob/blob/main/src/webob/request.py#L959&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Pylons/webob/blob/main/src/webob/request.py#L959&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/biwCGx/hyXGOApJEF/28u4m3Sd2KWLI4dOC79WT1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bw9Q6g/hyXGER6ehk/q9RBykjV88LkgMtaW4GZu0/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;webob/src/webob/request.py at main &amp;middot; Pylons/webob&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;WSGI request and response objects. Contribute to Pylons/webob 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;WebOb의 코드 중 다음과 같은 코드를 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1733144653161&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def copy_body(self):
...

    tempfile_limit = self.request_body_tempfile_limit
    todo = self.content_length if self.content_length is not None else 65535

    newbody = b&quot;&quot;
    fileobj = None
    input = self.body_file

    while todo &amp;gt; 0:
        data = input.read(min(todo, 65535))

        ...
        if fileobj:
            fileobj.write(data)
        else:
            newbody += data

            # When we have enough data that we need a tempfile, let's
            # create one, then clear the temporary variable we were
            # using

            if len(newbody) &amp;gt; tempfile_limit:
                fileobj = self.make_tempfile()
                fileobj.write(newbody)
                newbody = b&quot;&quot;
...&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;위 코드를 간략하게 보면, content_length가 &lt;b&gt;65535&lt;/b&gt;보다 크면 새로운 tempfile 을 만든다는 코드입니다. Unix 계열의 서버는 /tmp 디렉토리를 사용하고, Windows에서는 C:\Users\&amp;lt;사용자&amp;gt;\AppData\Local\Temp 를 기본적으로 사용한다고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;span style=&quot;font-size: 21px;&quot;&gt;&lt;b&gt;왜 이런 제한이 있을까?&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;왜 WebOb에서는 64KB로 제한을 하였을까요?&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;서버의 메모리 부담을 줄이고 안정성을 높이기 위해서 제한을 하였는데요. 64KB가 넘어가면 메모리가 아닌 디스크에 임시 파일로 저장을 하기에 메모리 부담을 줄일 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 spring에서는 제한이 없는지 궁금할 수 있는데요. 보통 spring과 같이 사용하는 was 인 tomcat에도 2MB로 기본 제한이 있습니다. 또한 spring 설정을 통해 자체적으로 제한을 할 수도 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;여기서의 size 제한은 web server인 apache/nginx에서 요청 크기 제한이랑은 다르다는 점을 알아야합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;(apache/nginx의 경우 요청 크기 제한을 걸고 크기가 초과하면 413 ERROR 발생, webob이나 tomcat은 임시 파일에 저장)&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006dd7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px; text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;마치며...&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;webob, tomcat 다른 was에도 요청 크기 제한이 있는데, 크기를 초과하면 임시파일에 저장해서 사용하는 것 뿐인데, 이게 왜 에러를 발생하였을까요?&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;보통 서버를 구동할 때 k8s를 사용하여 구동을 하는데, 해당 pod 에 /tmp 임시 디렉토리에 대한 권한이 없어 64KB 크기를 초과한 요청에 대해서 임시 파일로 저장을 못하여 발생한 문제였습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이번 에러를 해결하면서, 서버마다 크기 제한을 넘으면 임시 파일에 저장하고 처리를 한다는 것을 알게되었습니다. 또한 알고리즘 문제를 풀 때마다, 범위가 주어지면 최소값, 최대값을 넣어서 확인을 했었는데... 기능 테스트할 때도 다양하게 테스트를 하자고 다짐하였습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&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;&lt;a href=&quot;https://github.com/Pylons/webob/blob/main/src/webob/request.py&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Pylons/webob/blob/main/src/webob/request.py&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1733144362368&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;webob/src/webob/request.py at main &amp;middot; Pylons/webob&quot; data-og-description=&quot;WSGI request and response objects. Contribute to Pylons/webob development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Pylons/webob/blob/main/src/webob/request.py&quot; data-og-url=&quot;https://github.com/Pylons/webob/blob/main/src/webob/request.py&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cv31oJ/hyXGBVnXKP/pSWtD1ePJT9Jy3PocNixdK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cxAK7n/hyXGIGZNHo/gT6K5BOVmIung0JgIrWYl1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Pylons/webob/blob/main/src/webob/request.py&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Pylons/webob/blob/main/src/webob/request.py&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cv31oJ/hyXGBVnXKP/pSWtD1ePJT9Jy3PocNixdK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cxAK7n/hyXGIGZNHo/gT6K5BOVmIung0JgIrWYl1/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;webob/src/webob/request.py at main &amp;middot; Pylons/webob&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;WSGI request and response objects. Contribute to Pylons/webob 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;</description>
      <category>개발 이슈</category>
      <category>Python</category>
      <category>spring</category>
      <category>tomcat</category>
      <category>webob</category>
      <category>WSGI</category>
      <author>hanrabong</author>
      <guid isPermaLink="true">https://hanrabong.tistory.com/76</guid>
      <comments>https://hanrabong.tistory.com/entry/Python-WSGI-%EA%B7%B8%EB%A6%AC%EA%B3%A0-WebOb#entry76comment</comments>
      <pubDate>Tue, 3 Dec 2024 00:29:05 +0900</pubDate>
    </item>
    <item>
      <title>[Test] ArgumentCaptor 란?</title>
      <link>https://hanrabong.tistory.com/entry/Test-ArgumentCaptor%EB%9E%80</link>
      <description>&lt;h4 style=&quot;border-left: 5px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 0px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;테스트 코드를 짜면서 Stubbing을 통해서 제가 원하는 대로 코드를 조작할 수 있었습니다. 또한 해당 메서드의 Stubbing이 잘 동작하는지 &lt;i&gt;Mockito.verify&lt;/i&gt;, &lt;i&gt;BDDMockito.then&lt;/i&gt; 을 이용하여 해당 메서드가 몇 번 호출이 되었고 어떤 인자를 갖고 호출이 되었는지 확인할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;점차 다양한 테스트 코드를 짜면서 해당 코드의 인자의 값이 로직에서 의도한 대로 세팅이 되었는지 확인을 필수로 해야하는 경우가 생겼습니다. 이 때 자주 사용했던 ArgumentCaptor 라는 클래스에 대해 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서 사용한 코드는 해당 github에 저장되어 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Rabongg/Test-Junit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;테스트 코드&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1702889760011&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 - Rabongg/Test-Junit: Junit을 이용한 test 예제&quot; data-og-description=&quot;Junit을 이용한 test 예제. Contribute to Rabongg/Test-Junit development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Rabongg/Test-Junit&quot; data-og-url=&quot;https://github.com/Rabongg/Test-Junit&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/deWLo8/hyUPJBlPZK/btLlmxaJsXDTCgpcSZNWnK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Rabongg/Test-Junit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Rabongg/Test-Junit&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/deWLo8/hyUPJBlPZK/btLlmxaJsXDTCgpcSZNWnK/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 - Rabongg/Test-Junit: Junit을 이용한 test 예제&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Junit을 이용한 test 예제. Contribute to Rabongg/Test-Junit 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 style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;padding: 0.5em; margin: 0.5em 0em; color: #4891cf; border-left: 10px solid #4891cf; border-bottom: 2px #4891cf solid;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ArgumentCaptor&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;ArgumentCaptor를 보면 Argument + Captor 라고 생각이 들 것입니다. Argument는 &lt;b&gt;인수&lt;/b&gt;라고 하며 우리가 함수를 실행할 때 넘기는 값들을 말합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 Captor의 뜻은 무엇일까요. Captor의 사전적 의미를 보면 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;border-radius: 20px;&quot; data-ke-style=&quot;style3&quot;&gt;Captor&lt;br /&gt;명사) 포획자&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이 뜻으로 유추를 해보면 &lt;i&gt;인수를 포획한다 &lt;/i&gt;라고 대충 이해가 될 것입니다. 실제로 ArgumentCaptor를 이용하여 내가 원하는 대로 값이 넘어가는지 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;사용법&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;선언하고 사용하는 법은 다음과 같습니다. &lt;span style=&quot;color: #006dd7; background-color: #dddddd;&quot;&gt;@Captor&lt;/span&gt; 를 붙여서 인스턴스 변수처럼 사용할 수도 있고 로컬 변수처럼 하나의 메서드 안에서만 사용할 수도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1702277310953&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Class Test {
    @Captor
    ArgumentCaptor&amp;lt;User&amp;gt; userArgumentCaptor;   // 인스턴스 변수
    
    
    public void test() {
        // 로컬 변수
        ArgumentCaptor&amp;lt;User&amp;gt; userArgumentCaptor1 = ArgumentCaptor.forClass(User.class);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;※ 참고 ※&lt;br /&gt;&lt;i&gt;Captor&lt;/i&gt;, &lt;i&gt;ArgumentCaptor&lt;/i&gt; 전부 mockito 라이브러리에 포함이 되어있기에 mockito 라이브러리를 의존성으로 추가를 했으면 따로 설정을 해줄 필요가 없습니다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;ArgumentCator 클래스 내부는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1702284349845&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@CheckReturnValue
public class ArgumentCaptor&amp;lt;T&amp;gt; {

    private final CapturingMatcher&amp;lt;T&amp;gt; capturingMatcher = new CapturingMatcher&amp;lt;T&amp;gt;();
    private final Class&amp;lt;? extends T&amp;gt; clazz;

    private ArgumentCaptor(Class&amp;lt;? extends T&amp;gt; clazz) {
        this.clazz = clazz;
    }

    public T capture() {
        T ignored = Mockito.argThat(capturingMatcher);
        return defaultValue(clazz);
    }

    public T getValue() {
        return this.capturingMatcher.getLastValue();
    }

    public List&amp;lt;T&amp;gt; getAllValues() {
        return this.capturingMatcher.getAllValues();
    }

    public static &amp;lt;U, S extends U&amp;gt; ArgumentCaptor&amp;lt;U&amp;gt; forClass(Class&amp;lt;S&amp;gt; clazz) {
        return new ArgumentCaptor&amp;lt;U&amp;gt;(clazz);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;IDE에서 ArgumentCaptor 내부를 보면 각 메서드마다 설명도 잘 적혀 있고 예시 코드도 잘 적혀 있으니 꼭 참고 하시기 바랍니다. 간단히 메서드에 대해 설명하겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;ArgumentCaptor 내부 메서드&lt;br /&gt;&lt;br /&gt;&lt;i&gt;capture()&lt;/i&gt;: 말 그대로 해당 인자를 낚아채는 메서드입니다.&lt;br /&gt;&lt;i&gt;getValue()&lt;/i&gt;: 낚아챈 인자의 값을 불러올 때 사용합니다. 만약 해당 메서드가 여러번 호출이 되었을 경우 가장 마지막으로 호출된 메서드의 인자를 갖고옵니다.&lt;br /&gt;&lt;i&gt;getAllValues()&lt;/i&gt;: 낚아챈 인자 전부를 &lt;i&gt;List&lt;/i&gt; 형태로 받아옵니다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이제 사용하는 방법을 알았으니 코드를 통해 어떻게 보통 사용하는지 확인해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;코드 예시&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702278078791&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// User.java
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
@AllArgsConstructor
public class User {

    private String name;

    private String state;
}

// UserService.java
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserDao userDao;

    public void deleteUser(User user) {
        user.setState(&quot;DELETED&quot;);
        userDao.saveUser(user);
    }

    public void createUser(User user) {
        user.setState(&quot;CREATED&quot;);
        userDao.saveUser(user);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같은 코드가 있다고 가정해보겠습니다. 코드에 대해서 짤막하게 설명을 하자면, User라는 객체를 저장하고 삭제하는 로직입니다. 실제 저장, 삭제를 하는 것이 아닌 저장을 할 때는 상태를 &quot;CREATED&quot;로 삭제를 할 때는 상태를 &quot;DELETED&quot; 상태로 저장을 하게됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;deleteUser, createUser&lt;/i&gt; 에 대한 테스트 코드를 작성해 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1702278765638&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserDao userDao;

    @InjectMocks
    private UserService subject;

    @Captor
    ArgumentCaptor&amp;lt;User&amp;gt; userArgumentCaptor;
    
    @Test
    void testDeleteUser() {
        User user = new User(&quot;rabong&quot;, &quot;CREATED&quot;);

        subject.deleteUser(user);

        verify(userDao, times(1)).saveUser(userArgumentCaptor.capture());

        User value = userArgumentCaptor.getValue();
        assertEquals(&quot;DELETED&quot;, value.getState());
    }

    @Test
    void testCreateUser() {
        User user = new User(&quot;rabong&quot;, &quot;CREATING&quot;);

        subject.createUser(user);

        ArgumentCaptor&amp;lt;User&amp;gt; userArgumentCaptor1 = ArgumentCaptor.forClass(User.class);
        verify(userDao, times(1)).saveUser(userArgumentCaptor1.capture());
        User value = userArgumentCaptor1.getValue();
        assertEquals(&quot;CREATED&quot;, value.getState());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;다음과 같이 테스트 코드를 작성할 수 있습니다. 먼저 테스트 코드를 짜기 전에 항상&lt;b&gt; '어떤 기능에 대한 테스트를 해야할까'&lt;/b&gt; 에 대해 생각을 해야합니다. deleteUser, createUser를 보면 s&lt;i&gt;tate&lt;/i&gt; 값만 다르다는 것을 알 수 있습니다. 따라서 해당 테스트에서는 setState()에 값이 원하는 대로 들어가는지를 필수적으로 확인을 해야합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;saveUser 메서드가 실행될 때 User를 인자로 넘기기에 이 때 User 인자를 ArgumentCapture를 이용해서 낚아챘습니다. 그리고 해당 인자의 &lt;i&gt;state&lt;/i&gt; 값이 무엇인지 assertEquals 메서드를 이용해서 비교를 했습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;마치며...&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이번 글에서 ArgumentCaptor를 이용해서 mock 객체 메서드에 어떤 인자가 넘어가는지 확인해보았습니다. 항상 테스트 코드를 작성할 때, 단지 라인 커버리지를 채우자는 생각보다는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;해당 메서드에서 어떤 기능을 확인해야하고 어떤 것을 검증해야하는지&lt;/b&gt;&lt;/span&gt; 생각하며 작성하고 있습니다. &lt;br /&gt;&lt;i&gt;&amp;nbsp;Mockito.verify&lt;/i&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;i&gt;BDDMockito.then&lt;/i&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 이용하여 몇 번 호출이 되었는지 어떤 인자가 넘어가는지 검증을 할 수 있지만, &lt;i&gt;ArgumentCaptor&lt;/i&gt; 를 이용하면 인자를 캡쳐해서 정확히 의도대로 인자가 넘어가는지 자세하게 확인을 할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;다음 글에서도, 테스트 코드를 짤 때 사용하면 좋은 기능들에 대해서 적어보겠습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Spring/테스트 코드</category>
      <category>ArgumentCaptor</category>
      <category>JUnit</category>
      <category>test</category>
      <author>hanrabong</author>
      <guid isPermaLink="true">https://hanrabong.tistory.com/72</guid>
      <comments>https://hanrabong.tistory.com/entry/Test-ArgumentCaptor%EB%9E%80#entry72comment</comments>
      <pubDate>Mon, 18 Dec 2023 18:03:40 +0900</pubDate>
    </item>
    <item>
      <title>[Test] Mock, Spy</title>
      <link>https://hanrabong.tistory.com/entry/Test-Mock-Spy</link>
      <description>&lt;h4 style=&quot;border-left: 5px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 0px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;테스트 코드를 작성할 때 &lt;b&gt;Mock&lt;/b&gt;, &lt;b&gt;Spy&lt;/b&gt;, &lt;b&gt;Stubbing&lt;/b&gt;이라는 단어는 빼 놓을 수 없습니다. 테스트 코드를 처음 접했을 때 해당 용어들이 많이 헷갈렸습니다. 테스트 코드를 계속 작성을 해나가면서 어떤 상황에서는 &lt;b&gt;Spy&lt;/b&gt;를 사용하고 어떤 상황에서는 &lt;b&gt;Mock&lt;/b&gt;을 사용하고 이런 것들을 stubbing이라고 말하는 구나를 깨우칠 수 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 Test의 기본이 되는 Mock, Spy에 대해서 얘기해보려고 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;해당 글에서 사용한 코드는 밑의 github에 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Rabongg/Test-Junit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;테스트 소스 코드&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1702886030103&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 - Rabongg/Test-Junit: Junit을 이용한 test 예제&quot; data-og-description=&quot;Junit을 이용한 test 예제. Contribute to Rabongg/Test-Junit development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Rabongg/Test-Junit&quot; data-og-url=&quot;https://github.com/Rabongg/Test-Junit&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/deWLo8/hyUPJBlPZK/btLlmxaJsXDTCgpcSZNWnK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Rabongg/Test-Junit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Rabongg/Test-Junit&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/deWLo8/hyUPJBlPZK/btLlmxaJsXDTCgpcSZNWnK/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 - Rabongg/Test-Junit: Junit을 이용한 test 예제&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Junit을 이용한 test 예제. Contribute to Rabongg/Test-Junit 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 style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;padding: 0.5em; margin: 0.5em 0em; color: #4891cf; border-left: 10px solid #4891cf; border-bottom: 2px #4891cf solid;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Mock&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;사전에 정의된 Mock의 뜻을 보면 다음과 같습니다.&lt;/p&gt;
&lt;blockquote style=&quot;border-radius: 20px;&quot; data-ke-style=&quot;style3&quot;&gt;동사&lt;br /&gt;(특히 흉내를 내며) 놀리다[조롱하다]&lt;br /&gt;&lt;br /&gt;형용사&lt;br /&gt;거짓된, 가짜의, 모의의&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 코드에서 말하는 Mock도 비슷합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Test 작성 시 Mock을 이용하여 &lt;b&gt;모의 객체&lt;/b&gt;를 만듭니다. 모의 객체이기 때문에 우리가 원하는 대로 동작을 할 수 있게 만들 수 있습니다. 이렇게 모의 객체, 즉 Mock 객체를 원하는 대로 동작을 할 수 있게 조작하는 것을 &lt;span style=&quot;border-bottom: 8px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Stubbing &lt;/span&gt;이라고 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;말로만 하면 잘 이해가 안 가기에 코드를 통해 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701174104832&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Person.java
public class Person {

    private int age;

    private String name;

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public Person() {
        this.age = 11;
        this.name = &quot;rabong&quot;;
    }
}

// Test.java
public class SimpleMockTest {

    @Test
    public void mockTest() {
        Person mockPerson = mock(Person.class);
        Person person = new Person();

        System.out.println(mockPerson.getAge());
        System.out.println(mockPerson.getName());
        System.out.println(person.getAge());
        System.out.println(person.getName());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;위의 예시 코드를 실행시키면 어떤 값이 나오게 될까요?&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1701174258768&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;0
null
11
rabong&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같이 나오게됩니다. 왜 이렇게 값이 나오는지 살펴보겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;mockPerson&lt;/span&gt;이라는 객체는 mock 객체이고 &lt;span style=&quot;color: #006dd7; background-color: #dddddd;&quot;&gt;person&lt;/span&gt;이라는 객체는 실제 생성자를 이용해서 생성한 객체입니다. 따라서 mock 객체인 mockPerson의 메서드를 실행시키면 기본 값인 int면 0, String이면 null이 나오는 것입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 여기서 mockPerson의 나이를 불러올 때 20이라는 값을 나오게 하려면 어떻게 해야할까요? 이 때 바로 &lt;b&gt;stubbing&lt;/b&gt;을 하면됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701174764174&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
public void mockTest() {
	Person mockPerson = mock(Person.class);
	Person person = new Person();
	when(mockPerson.getAge()).thenReturn(20);  // mockPerson mock 객체 stubbing

	System.out.println(mockPerson.getAge());  
	System.out.println(mockPerson.getName());
	System.out.println(person.getAge());
	System.out.println(person.getName());
}

20
null
11
rabong&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 stubbing을 하면 원하는 값을 나오게 할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;※ 참고 ※&lt;br /&gt;mock을 하기 위해 &lt;i&gt;mockito-junit-jupiter&lt;/i&gt;, &lt;i&gt;mockito-core&lt;/i&gt; 의 2개 라이브러리가 필요합니다. 보통 gradle로 프로젝트를 설정할 때 &lt;i&gt;spring-boot-starter-test&lt;/i&gt;를 의존성에 포함시켜주는데 해당 라이브러리에 기본적으로 포함이 되어 있습니다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지 Mock 객체가 무엇인지 Stubbing이 무엇인지에 대해 알아보았습니다. 솔직하게 예시 코드를 보면 어떻게 동작을 하는지는 알겠지만, 언제 써야하고 왜 필요한지에 대해서는 이해가 가지 않습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;제가 테스트 코드를 짜면서 언제 Mock 객체를 쓰면 좋은지에 대해 설명하려고 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;Spring boot에서 Unit Test를 작성할 때&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Spring boot에서 service 로직에 대하여 단위 테스트(Unit Test)를 짤 때 Mock 객체를 가장 많이 사용합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같은 코드가 있다고 생각을 해봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1701354442766&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UserService.java
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserDao userDao;

    public String[] getAllUser() {
        String[] users = userDao.findAllUser();
        for (int i = 0; i &amp;lt; users.length; i++) {
            users[i] = users[i].toUpperCase();
        }
        return users;
    }
}

// UserDao.java
@Service
public class UserDao {

    public String[] findAllUser() {

        return new String[]{&quot;rabong&quot;, &quot;orange&quot;};
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;위 코드를 간략하게 설명하면 &lt;span style=&quot;color: #006dd7; background-color: #dddddd;&quot;&gt;UserService&lt;/span&gt;에서 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;UserDao&lt;/span&gt;를 주입받고 있고 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;UserDao&lt;/span&gt;에서 받아온 User들의 이름을 대문자로 바꿔서 반환하는 로직입니다. 가상의 시나리오여서 User 값을 직접 설정을 해주었지만, 보통 프로젝트에서는 데이터 베이스에서 해당 값을 추출하여 반환합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;위의 코드에 대한 테스트 코드를 어떻게 짜야하는지 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701355131428&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserDao userDao;

    @InjectMocks
    private UserService subject;  // 테스트 할 객체

    @Test
    void getAllUserTest() {
        String[] users = {&quot;rabong&quot;, &quot;apple&quot;};

        when(userDao.findAllUser()).thenReturn(users);  // userDao stubbing

        String[] result = subject.getAllUser();  // 테스트 할 메서드

        Assertions.assertEquals(2, result.length);
        Assertions.assertEquals(&quot;RABONG&quot;, result[0]);
        Assertions.assertEquals(&quot;APPLE&quot;, result[1]);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같이 코드를 작성할 수 있습니다. 갑자기 못보던 어노테이션이 많아져서 헷갈릴 수도 있습니다. 찾아보면 자세히 설명해주는 글들이 많아서 간단히 말하고 넘어가겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;※ 참고 ※&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;@Mock&lt;/span&gt; 은 Mock 객체를 만들어주는 어노테이션입니다.&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;@InjectMocks&lt;/span&gt; 의 경우 해당 Mock 객체를 주입하여 해당 객체를 만들어 주는 어노테이션입니다.&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;@Extendwith(MockitoExtension.class)&lt;/span&gt;의 경우 앞서 설명한 어노테이션을 해당 테스트 클래스에서 사용하기위해 필요한 어노테이션입니다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;getAllUser&lt;/span&gt; 메서드를 Test하기 위해서는 해당 객체가 의존하는 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;UserDao&lt;/span&gt;의 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;findAllUser&lt;/span&gt; 메서드를 호출을 해야합니다. 그렇지 않으면 로직 상으로 값을 제대로 받아와서 처리하는지 확인하기 어렵습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;위 코드에서는 단순하게 String 배열을 반환하지만, 실제 로직에서는 &lt;b&gt;데이터베이스에 저장&lt;/b&gt;되어있는 값들을 읽어오곤 합니다. 만약, &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;UserDao&lt;/span&gt;를 Mocking하지 않고 실제 객체를 주입하고 호출을 한다면 데이터베이스에서 값을 정확하게 읽어와야만 해당 코드가 정상적으로 동작함을 보장할 수 있습니다. 이럴 경우 단위테스트의 목적에 부합하지 않게 됩니다. 단위테스트는 말 그대로 해당 테스트 자체가 잘 동작하는지 여부를 확인하는 것입니다. 흔히 단위 테스트 원칙이라고 말하는 &lt;b&gt;FIRST &lt;/b&gt;의 F(Fast), I(Independent)에 어긋나게 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;※ 참고 ※&lt;br /&gt;First란&lt;br /&gt;F(fast): 단위 테스트는 빠르게 실행되고 빠르게 결과값을 알아야한다.&lt;br /&gt;I(Independent): 단위 테스트는 다른 테스트에 의존하지 않고 그 자체만으로 실행되어야 한다.&lt;br /&gt;R(Repeatable): 단위 테스트는 몇 번을 진행하든 똑같은 결과가 나올 수 있게 반복가능해야한다.&lt;br /&gt;S(Self-validating): 단위 테스트는 테스트 자체로 통과/실패 결과로 자체 검증이 가능해야한다.&lt;br /&gt;T(Timly): 단위 테스트는&amp;nbsp; 철저하고 적절하게 작성이 되어야한다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;Interface를 테스트 해야할 때&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Interface를 인자로 받거나 인자로 넘길 때 mock을 주로 사용합니다. 말로 설명을 하면 이해가 잘 가기 때문에 바로 코드로 설명을 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같은 코드가 있다고 가정을 해보겠습니다. (테스트를 위한 코드이지 보통 이렇게 코드를 짜진 않습니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1701421102538&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UserService.java
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserDao userDao;

    public String[] getUsers() {
        Page users = userDao.findUsers();

        String[] userList = users.getContent();

        for (int i = 0; i &amp;lt; userList.length; i++) {
            userList[i] = userList[i].toUpperCase();
        }
        return userList;
    }
}

// Page.java(interface)
public interface Page {

    int getTotalCount();

    String[] getContent();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;코드에 대해 간략히 설명하면 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;userDao&lt;/span&gt;에서 값을 받아오는데 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;Page&lt;/span&gt;라는 interface로 값을 받아와서 대문자로 변경하여 반환하는 코드입니다. 당연히 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;userDao.findUsers()&lt;/span&gt; 메서드에서는 Page라는 interface를 구현한 객체를 반환할 것입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;userService&lt;/span&gt;에서 구현된 객체를 알 수 없다고 했을 때 어떻게 테스트 코드를 짜야할까요?&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Mock 없이는 다음과 같이 테스트 코드를 짤 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701421666136&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserDao userDao;

    @InjectMocks
    private UserService subject;

    @Test
    void getUsersTest() {
        when(userDao.findUsers()).thenReturn(new Page() {
            @Override
            public int getTotalCount() {
                return 2;
            }

            @Override
            public String[] getContent() {
                return new String[]{&quot;rabong&quot;, &quot;apple&quot;};
            }
        });

        String[] result = subject.getUsers();

        Assertions.assertEquals(2, result.length);
        Assertions.assertEquals(&quot;RABONG&quot;, result[0]);
        Assertions.assertEquals(&quot;APPLE&quot;, result[1]);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Page가 interface이기 때문에 &lt;b&gt;익명 클래스&lt;/b&gt;를 사용해서 구현을 해야합니다. 위의 코드의 경우 메서드가 2개이기 때문에 간단하게 익명 클래스로 구현을 하고 원하는 값을 반환하게 설정할 수 있습니다. 하지만 해당 인터페이스에 메서드가 20개가 있고 내가 테스트에서 필요한 메서드는 2개뿐인데 테스트할 때마다 항상 익명 클래스를 만든다고 생각을 해보면 가독성도 떨어지고 테스트 코드 작성 시에 많은 시간이 소요됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Mock을 사용하면 다음과 같이 간단하게 테스트를 짤 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701422184569&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void getUsersTest() {

	Page users = mock(Page.class); // interface를 mocking
	when(users.getContent()).thenReturn(new String[]{&quot;rabong&quot;, &quot;apple&quot;}); // mock한 interface stubbing
	when(userDao.findUsers()).thenReturn(users);

	String[] result = subject.getUsers();

	Assertions.assertEquals(2, result.length);
	Assertions.assertEquals(&quot;RABONG&quot;, result[0]);
	Assertions.assertEquals(&quot;APPLE&quot;, result[1]);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;훨씬 코드가 더 간결해집니다. interface를 mock하고 필요한 메서드만 stubbing을 해서 사용하면 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Interface를 Mocking한다는 말이 잘 안 와닿을 수도 있습니다. Interface를 인자로 전달하거나 받으려면 항상 구현한 객체가 있을텐데 그럼 구현한 객체를 쓰면되지 왜 mock을 해야할까라고 의문이 들 수도 있습니다. 하지만 Interface로 인자를 받을 때 구현한 객체를 모르는 경우도 많습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, jpa에서 Pageable로 데이터를 pagination하여 값을 받을 때도 인터페이스로 데이터를 받습니다. 또한 큰 프로젝트의 경우는 보통 서비스를 하나로 크게 배포하지 않고 MSA 구조로 도메인 별로 나눠서 서버에 서비스를 배포하게 됩니다. 이럴 때 다른 서비스에 요청을 하여 데이터를 받을 때도 인터페이스로 데이터를 받는 경우도 종종 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;padding: 0.5em; margin: 0.5em 0em; color: #4891cf; border-left: 10px solid #4891cf; border-bottom: 2px #4891cf solid;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spy&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;사전에 정의된 Spy의 뜻을 보면 다음과 같습니다.&lt;/p&gt;
&lt;blockquote style=&quot;border-radius: 20px;&quot; data-ke-style=&quot;style3&quot;&gt;명사&lt;br /&gt;스파이, 정보원, 첩자&lt;br /&gt;&lt;br /&gt;동사&lt;br /&gt;정보[스파이] 활동을 하다&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 코드에서도 Spy 는 비슷합니다. 영화에서 보면 Spy들은 잘 행동하는 것처럼 하면서 정보를 빼돌리는 행위를 합니다. Spy 객체 또한 &lt;b&gt;실제 객체의 행동을 하면서 우리가 조작한 행동(stubbing)은 우리가 설정한대로 동작&lt;/b&gt;을 하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Mock과 차이점을 생각하면 Mock 객체는 애초에 가짜 객체로 모든 메서드를 전부 stubbing해서 사용을 해야합니다. 반면, Spy 객체는 조작하고 싶은 행동만 stubbing으로 조작하고나머지 메서드들은 기존 메서드처럼 실행이됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;코드를 통해서 알아보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701423247716&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SimpleSpyTest {

    @Test
    public void spyTest() {
        Person spyPerson = spy(Person.class);
        Person person = new Person();

        System.out.println(spyPerson.getAge());
        System.out.println(spyPerson.getName());
        System.out.println(person.getAge());
        System.out.println(person.getName());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;위의 SimpleMockTest 와 비슷한 코드입니다. 다른 점이 있다면 mock 객체 대신 spy 객체를 만들었다는 점입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;결과는 어떻게 나올까요?&lt;/p&gt;
&lt;pre id=&quot;code_1701423318308&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;11
rabong
11
rabong&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 말했듯이 Spy 객체는 stubbing을 해주지 않는 이상 원래 객체의 메서드가 실행되기에 다음과 같은 값이 나오게 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;stubbing을 하는 방법은 mock에서 했던 방법과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701423550408&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
public void spyTest() {
    Person spyPerson = spy(Person.class);
    Person person = new Person();
    
    when(spyPerson.getAge()).thenReturn(20);
    
    System.out.println(spyPerson.getAge());    // 20
    System.out.println(spyPerson.getName());   // rabong
    System.out.println(person.getAge());       // 11
    System.out.println(person.getName());      // rabong
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 stubbing을 하면 우리가 조작한 값이 반환이 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 이제, Mock 과 Spy에 대해서 알아보았는데 Mock 객체를 쓰는 경우에 대해서도 알아보았습니다. 그럼 Spy 객체는 언제 써야할까요?&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;둘의 차이를 생각해보면 Spy는 &lt;b&gt;실제 객체의 동작을 유지&lt;/b&gt;하기 때문에, &lt;b&gt;실제 객체의 동작을 유지해야할 때 써야하는 것&lt;/b&gt;을 알 수 있습니다. 말로하면 이해가 되기는 하는데 실제 테스트 코드를 작성할 때 언제 작성을 해야할지 감이 안 잡힐 수도 있습니다. 예시를 통해서 언제 Spy를 쓰면 되는지에 대해 알아보겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;Unit Test시 내부에서 호출하는 메서드를 Stubbing하기 위해&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;위에 Mock을 설명할 때 사용한 코드를 다음과 같이 수정해 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701668500370&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class UserService {

    private final UserDao userDao;

    public String[] getAllUser() {
        String[] users = userDao.findAllUser();
        setUserUpper(users);

        return users;
    }

    public String[] getUsers() {
        Page users = userDao.findUsers();

        String[] userList = users.getContent();
        setUserUpper(userList);

        return userList;
    }
    
    public void setUserUpper(String[] users) {
        for (int i = 0; i &amp;lt; users.length; i++) {
            users[i] = users[i].toUpperCase();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;변경된 점이 있다면 users를 대문자로 바꾸는 코드만 따로 메서드로 뺐습니다. 원래는 내부에서만 사용하기에 private으로 접근 제어자를 설정해야하지만, Spy 예시를 들기 위해 public으로 설정해두었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;보통 단위 테스트를 작성할 때, 라인 커버리지 100%를 기준으로 삼는 경우도 있고 모든 메서드에 대해 테스트를 전부 작성하는 것을 목표로 삼는 경우도 있습니다. 저의 경우 메서드에 대한 테스트 코드는 전부 다 작성을 하는 편이고 라인 커버리지의 경우 100%를 목표로 잡고 테스트 코드를 작성합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 이제 테스트 코드를 작성해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701669487878&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void setUserUpperTest() {
    String[] users = {&quot;rabong&quot;, &quot;apple&quot;};
	
    subject.setUserUpper(users);

    Assertions.assertEquals(&quot;RABONG&quot;, users[0]);
    Assertions.assertEquals(&quot;APPLE&quot;, users[1]);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;setUserUpper&lt;/span&gt; 에 대한 테스트 코드는 다음과 같이 작성할 수 있습니다. 여기서 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;setUserUpper&lt;/span&gt;를 보면 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;getAllUser&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;getUsers &lt;/span&gt;메서드에서도 호출이 되고 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;예시에서 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;setUserUpper &lt;/span&gt;코드 자체가 짧기도 하고 따로 mock객체를 만들다든가 복잡하지가 않아서 테스트 코드를 그대로 놔둬도 테스트 코드가 잘 돌아갑니다. 하지만 만약 메서드 내부에서 복잡한 코드를 호출하는 경우가 많을 때 호출한 메서드내부 코드에 대한 stubbing 등 테스트 코드 작성을 해줘야합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이럴 때 사용할 수 있는게 Spy 입니다.&amp;nbsp;Spy를 이용하여 해당 메서드를 원하는대로 Stubbing을 해주면 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Spy를 사용해서 어떻게 코드를 수정할 수 있을지 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701670263708&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserDao userDao;

    @Spy
    @InjectMocks
    private UserService subject;

    @Test
    void getAllUserTest() {
        String[] users = {&quot;rabong&quot;, &quot;apple&quot;};

        when(userDao.findAllUser()).thenReturn(users);  // userDao stubbing

        String[] result = subject.getAllUser();

        Assertions.assertEquals(2, result.length);
        Assertions.assertEquals(&quot;RABONG&quot;, result[0]);
        Assertions.assertEquals(&quot;APPLE&quot;, result[1]);
    }
    @Test
    void getUsersTest() {

        Page mockUsers = mock(Page.class); // interface를 mocking
        String[] users = {&quot;rabong&quot;, &quot;apple&quot;};
        when(userDao.findUsers()).thenReturn(mockUsers);
        when(mockUsers.getContent()).thenReturn(users); // mock한 interface stubbing
        doNothing().when(subject).setUserUpper(users);  // 내부 메서드 stubbing

        String[] result = subject.getUsers();

        Assertions.assertEquals(2, result.length);
        Assertions.assertEquals(&quot;rabong&quot;, result[0]);
        Assertions.assertEquals(&quot;apple&quot;, result[1]);
    }

    @Test
    void setUserUpperTest() {
        String[] users = {&quot;rabong&quot;, &quot;apple&quot;};

        subject.setUserUpper(users);

        Assertions.assertEquals(&quot;RABONG&quot;, users[0]);
        Assertions.assertEquals(&quot;APPLE&quot;, users[1]);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;getAllUserTest&lt;/span&gt;의 경우는 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;setUserUpperTest&lt;/span&gt;를 Stubbing을 하지 않았고 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;getUsersTest&lt;/span&gt;의 경우는 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;setUserUpper&lt;/span&gt;를 stubbing했습니다. &lt;span style=&quot;background-color: #dddddd; color: #006dd7; text-align: justify;&quot;&gt;getUsersTest&lt;/span&gt; 에서 &lt;span style=&quot;background-color: #dddddd; color: #006dd7; text-align: justify;&quot;&gt;setUserUpper&lt;/span&gt; 가 아무 동작도 하지않게 stubbing을 하였기에 결과 값이 기존과 똑같이 나오게됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;위의 코드를 보면 &lt;span style=&quot;background-color: #dddddd; color: #006dd7;&quot;&gt;setUserUpper&lt;/span&gt;에 대해서만 stubbing을 하고 싶을 때만 하고 나머지 코드는 제대로 동작을 하게 만들어야 하기에 Mock을 사용하면 안되고 Spy를 사용해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;※ 참고 ※&lt;br /&gt;mock을 주입한 해당 객체를 Spy할 때 @Spy를 붙여주면 Spy 객체가 만들어집니다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;마무리...&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이번 글에서는 테스트에 기본이 되는 Mock, Spy에 대해 알아보았습니다. 처음 테스트 코드를 작성하였을 때 해당 개념이 너무 헷갈렸습니다. 점차 테스트 코드를 많이 작성을 하면서 언제 mock이 필요하고 Spy가 필요한지에 대해 알게 되었습니다. Mock 객체를 사용하면 단위 테스트 코드를 작성할 때 정말 편하게 코드를 작성할 수 있습니다. 심지어 Static 메서드에 대해서도 Stubbing을 할 수도 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;앞으로도 테스트 코드를 짜면서 사용한 꿀팁과 사용하면 좋은 기능들에 대해서 적어보겠습니다.  &lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/테스트 코드</category>
      <category>mock</category>
      <category>Spy</category>
      <category>Stubbing</category>
      <category>unit test</category>
      <author>hanrabong</author>
      <guid isPermaLink="true">https://hanrabong.tistory.com/71</guid>
      <comments>https://hanrabong.tistory.com/entry/Test-Mock-Spy#entry71comment</comments>
      <pubDate>Mon, 4 Dec 2023 15:50:25 +0900</pubDate>
    </item>
    <item>
      <title>[Test] 테스트 코드를 왜 작성해야할까?</title>
      <link>https://hanrabong.tistory.com/entry/Test-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%99%9C-%EC%9E%91%EC%84%B1%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C</link>
      <description>&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-left: 5px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 0px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;회사에서 테스트 코드를 작성하면서 문득 테스트 코드를 왜 작성해야하는가에 대한 생각을 하게되었습니다. 많은 블로그에서 테스트 코드를 잘 작성하면 로직이 잘 동작하는지 빠르게 확인할 수 있고, 여러 예외 케이스들도 확인할 수 있다 등 여러 장점에 대해서 설명을 하고 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;오늘은 제가 테스트 코드를 작성하면서 개인적으로 테스트 코드를 작성하면 어떤 이점이 있는지 얘기해보려고 합니다.&amp;nbsp; 제가 여기서 다루는 테스트 코드는 &lt;span style=&quot;border-bottom: 8px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;Unite Test &lt;/span&gt;를 의미합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;해당 기능 빠르게 확인&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;테스트 코드를 통해서 기능을 쉽게 확인할 수 있습니다. 기능이라면 해당 로직에 어떤 값이 들어왔을 때 어떻게 동작하는지를 파악할 수 있다는 뜻입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;java spring boot를 기준으로 보겠습니다. 보통 spring boot로 프로젝트를 하면 다음과 같이 계층 구조로 분리하여 코드를 개발합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1366&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/25z5t/btsziOFRfVY/UjLRQtUvVlYfkeR3eDvBOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/25z5t/btsziOFRfVY/UjLRQtUvVlYfkeR3eDvBOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/25z5t/btsziOFRfVY/UjLRQtUvVlYfkeR3eDvBOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F25z5t%2FbtsziOFRfVY%2FUjLRQtUvVlYfkeR3eDvBOK%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;1366&quot; height=&quot;670&quot; data-origin-width=&quot;1366&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Test code 없이 Service Layer, 서비스 로직을 확인하려면 Presentation layer(controller)에서 값을 넘겨주었을 때 제대로 동작하는지 확인을 해야합니다. 로직이 복잡해서 해당 로직에 값이 잘 들어가는지 확인을 세세히 하려고 하면 debug 모드로 값들을 하나씩 확인해봐야 합니다. Service Layer만 해도 controller에 요청을 보내야하는데 Data Access layer의 로직을 확인하려고 하면 두 번의 layer를 통과해야합니다. 또한 중간에 에러가 발생하면 Data access layer만 테스트하려고 하는 목적을 달성하기 어렵습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Controller를 테스트 코드 없이 직접 테스트 하려고 하는 경우에 실제 Client에서 요청을 날려보거나, api 호출을 할 수 있는 Postman으로 직접 요청을 보내거나 swagger를 이용해서 요청을 보낼 수 있습니다. 그러나 해당 api 요청을 매번 호출하여 테스트 하는 것은 번거롭습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Unit test를 통해서 해당 Layer만 테스트해 볼 수 있어 빠르게 해당 로직이 원하는대로 작동하는지 확인할 수 있습니다. 다른 Layer의 로직을 실행시켜야 할 경우 Mock을 이용해서 원하는대로 Stubbing할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Test code를 이용해서 해당 기능을 확인하기 위해 테스트 코드 작성 시 성공하는 케이스와 실패하는 케이스를 예외 없이 전부 다 작성하는 편입니다. 실패 케이스가 10개가 넘더라도 해당 케이스를 다 작성해주어 예외 케이스가 발생하지 않게 하고 있습니다. 작성을 하다보면 사소한 파라미터의 차이로 테스트 메서드를 또 작성해야하는게 여간 귀찮은 일이 아닙니다. 이런 경우에는 @Parameterized Test를 사용하면 됩니다. 해당 기능에 관련해서는 추후에 다시 정리하겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-size: 21px;&quot;&gt;&lt;b&gt;코드 품질 개선&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;테스트 코드를 작성하다 보면 코드가 중복으로 들어가 있거나 해당 로직이 필요없을 수 있다고 느끼는 경우가 종종 있습니다. 처음 코드를 작성할 때 코드를 완벽하게 작성할 수 있으면 당연히 좋지만 그럴 수 없는 상황이 존재하고 완벽하게 코드를 짜는 경우가 드뭅니다. 혼자하는 프로젝트가 아닌 회사의 업무는 코드를 자그만치 몇 백줄을 작성해야하는 경우도 있고 개발기한이 촉박한 경우가 많습니다. 이럴 경우 테스트 코드를 통해서 코드 품질을 개선할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;중복이나 필요없는 로직뿐만 아니라 테스트 코드를 작성하면서 모든 성공 케이스 및 예외 케이스를 작성하면서 해당 로직의 필요성(내가 의도하는대로 동작하는지) 및 어떻게 개선을 하면 되는지도 알 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;리팩토링 용이&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;코드를 처음부터 클린하게 작성을 하면 리팩토링이 필요가 없습니다. 하지만 로직 등 변경 사항이 많아 개발 시간이 촉박한 경우가 많습니다. 이러한 경우 에자일적으로 &lt;span style=&quot;border-bottom: 8px solid #dcf1fb; padding: 0 0 0 0.2em; font-weight: bold;&quot;&gt;동작하는 소프트웨어 위주&lt;/span&gt;로 생각을 하여 어떻게든 동작하게만 코드를 작성하곤 합니다. 동작하는 소프트웨어를 만들고 추후에 리팩토링을 진행합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;리팩토링에 용이하다는 장점은 테스트 코드를 엄청 꼼꼼하게 작성했을 때 해당이 된다고 생각합니다. 현재 코드에서는 해당 로직의 테스트 코드가 항상 성공하고 있다고 가정했을 때 리팩토링 시에도 테스트 코드가 항상 성공해야합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;코드를 리팩토링할 때 로직이 실제로 예전과 같이 동작을 할까라는 걱정을 많이 하곤 했습니다. 꼼꼼하게 작성된 테스트코드가 있으면 리팩토링을 부분부분 할 때마다 테스트 코드를 실행시켜보고 잘 동작하는지 확인만 하면됩니다. 이러한 이점으로 인해 Spring boot 코드를 리팩토링 전에 테스트 코드를 꼼꼼하게 작성을 하고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;코드 이해도 증가&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;회사에서 내가 직접 작성한 코드가 아닌데 테스트 코드를 작성해야하는 경우가 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;처음에는 다른 사람이 짠 로직의 테스트 코드를 짜는게 나한테 무슨 도움이 있을까 의문이 들었습니다. 로직을 충분히 이해하고 테스트 코드를 작성해야 했기에 당연히 테스트 코드 작성 속도가 느렸습니다. 테스트 코드를 다 작성하고 난 후에 로직에 대한 이해도가 증가하였습니다. 테스트 코드 없이도 원래 코드만 보고 이해할 수도 있지만 테스트 코드를 작성하면서 더 세세하게 이해할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;다른 사람이 짠 로직의 테스트 코드를 짤 때 단점도 존재했습니다. 어떤 의도로 해당 코드를 작성하는지를 잘 몰랐기에 코드를 작성한 개발자 분께 여쭤봐야했고 직접 작성한 코드의 테스트 코드보다는 테스트 코드를 기계적으로 짜는 느낌이 들었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;border-bottom: 1px solid #006DD7; letter-spacing: -1px; padding: 0px 0px 10px 7px; margin: 10px 0px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-size: 21px; font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #006dd7;&quot;&gt;&lt;b&gt;마무리...&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;신입이였을 때 테스트 코드 필요성을 글로만 알고 있었지만 막상 테스트 코드를 작성할 때는 기계적으로 작성을 하였습니다. 1년이 지난 지금 테스트 코드가 왜 필요한지 어떤 장점이 있는지 생각을 하면서 전보다 꼼꼼하고 효율적으로 테스트 코드를 작성하고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이 글을 시작으로 앞으로 테스트 코드를 직접 작성할 때 어떻게 작성을 했는지 적어보려고 합니다.&lt;/p&gt;</description>
      <category>Spring/테스트 코드</category>
      <category>Test Code</category>
      <category>unit test</category>
      <author>hanrabong</author>
      <guid isPermaLink="true">https://hanrabong.tistory.com/68</guid>
      <comments>https://hanrabong.tistory.com/entry/Test-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%99%9C-%EC%9E%91%EC%84%B1%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C#entry68comment</comments>
      <pubDate>Sat, 28 Oct 2023 16:34:43 +0900</pubDate>
    </item>
    <item>
      <title>[NETWORK] ARP란?</title>
      <link>https://hanrabong.tistory.com/entry/NETWORK-ARP%EB%9E%80</link>
      <description>&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;네트워크를 공부할 때, OSI 7계층이라는 것을 배웁니다. 송신쪽에서 7계층에서 1계층으로 데이터가 캡슐화되어 전달을 하면 수신쪽에서 역캡슐화를 하면서 데이터를 주고받습니다. 그럼 어떻게 요청하는 쪽(클라이언트라고 하겠습니다)에서 수신하는 쪽(서버)의 위치를 알 수 있을까요?? 2계층의 물리적 주소인 MAC 주소와 3계층의 논리적 주소인 IP주소로 원하는 목적지로 데이터를 전달할 수 있는데요. IP는 우리가 어떤 네트워크에 접속되어 있는지에 따라서 바뀌게 됩니다.(예를 들어 집, 도서관, 핫스팟 등등) 반면, MAC 주소는 하드웨어를 만드는 업체가 임의로 정한 주소이고 일반적으로 ROM에 고정되어 나오기에 변경이 불가능합니다. (사실 변경을 원하면 할 수 있긴 합니다.. 몇몇 운영체제에서는 변경이 불가능하게 막아놓았습니다.)&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 MAC 주소를 알아야 원하는 목적지로 요청을 보낼 수 있는데, 어떻게 MAC 주소를 알 수 있을까요?? 바로 ARP 를 이용하여 알 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ARP&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;ARP는 Address Resolution Protocol의 약자로 한국어로 직역하면 &lt;b&gt;주소 확인 프로토콜&lt;/b&gt; 입니다. 논리적 주소인 IP를 이용해서 MAC 주소를&amp;nbsp; 찾습니다. 한 마디로, &lt;b&gt;IP 주소를 MAC 주소와 연관&lt;/b&gt; 시켜주기 위한 프로토콜이라고 생각하셔도 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;여기서 어떻게 IP를 가지고 MAC 주소를 얻어올 수 있지라고 &lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;의문이 생길 수도 있습니다.&amp;nbsp;&lt;/span&gt;쉽게 생각하면 MAC 주소를 이용해서 우리가 통신을 하려고 하기에 통신 대상은 네트워크 어딘가 IP를 갖고 있습니다. 이러한 IP를 클라이언트 쪽에서 미리 알고 있을 수도 있고 모르게 된다면 도메인(예: &lt;a href=&quot;https://naver.com&quot;&gt;https://naver.com&lt;/a&gt; )으로라도 알고 있을 것입니다. 당연히 알고 있어야 요청을 보낼 수도 있으니까요. 도메인으로부터 ip는 DNS를 이용하여 찾아낼 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;ARP&amp;nbsp; 필드는 다음과 같습니다. (실제로 차지하는 byte 크기는 다를 수 있습니다)&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 Recipient MAC Address 필드의 경우 00-00-00-00-00-00으로 채워서 네트워크에 뿌리게 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; 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;425&quot; data-origin-height=&quot;289&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ESVg7/btsftHqGJfh/ivfGZVPHvgvscZZdhobcK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ESVg7/btsftHqGJfh/ivfGZVPHvgvscZZdhobcK1/img.png&quot; data-alt=&quot;ARP field&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ESVg7/btsftHqGJfh/ivfGZVPHvgvscZZdhobcK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FESVg7%2FbtsftHqGJfh%2FivfGZVPHvgvscZZdhobcK1%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;425&quot; height=&quot;289&quot; data-origin-width=&quot;425&quot; data-origin-height=&quot;289&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ARP field&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;ARP 작동 원리를 간단하게 설명하겠습니다. 먼저, 클라이언트가 ARP broadcast를 이용해서 로컬 네트워크 전체에 상대방의 MAC 주소를 물어봅니다. 목적지 IP에 해당하는 서버가 자신의 MAC 주소를 클라이언트에게 unicast로 전달을 합니다. 서버가 클라이언트에게 다시 보낼 때는 대상자와 전송자 IP 및 MAC 주소 위치는 변경되어야 합니다. 서버가 클라이언트에게 unicast로 전달을 하는 이유는 ARP 필드를 받았을 때, 출발지 MAC주소가 이미 담겨있기에 broadcast 없이 바로 응답을 할 수 있습니다. 클라이언트가 해당 MAC 주소를 받았으면 ARP table에 저장을 해놓습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;ARP table에 저장을 하는 이유는 매번 요청을 보낼때마다 ARP broadcast를 하면 통신에 효율성이 떨어지기에 ARP table이라는 cache를 이용해서 저장을 해놓습니다. 그럼 여기서 또 의문이 생길 수 있습니다. IP라는 것은 네트워크에 따라서 달라지는데 그럼 MAC 주소와 ip주소 매핑 정보가 바뀔 수도 있지 않을까하고 생각할 수 있습니다. 일정 시간(arp timeout) 동안 통신이 없으면 해당 테이블이 사라지게 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;윈도우 에서 ARP 테이블을 직접 확인해 볼 수 있습니다. cmd 창에 다음과 같이 명령어를 치면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683964719952&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;arp -a&lt;/code&gt;&lt;/pre&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;414&quot; data-origin-height=&quot;199&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9pYFf/btsftJhKca6/SMZEsszQ8l3J1y8uZ4Rn00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9pYFf/btsftJhKca6/SMZEsszQ8l3J1y8uZ4Rn00/img.png&quot; data-alt=&quot;window ARP 테이블&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9pYFf/btsftJhKca6/SMZEsszQ8l3J1y8uZ4Rn00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9pYFf%2FbtsftJhKca6%2FSMZEsszQ8l3J1y8uZ4Rn00%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;414&quot; height=&quot;199&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;199&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;window ARP 테이블&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 이제 ARP가 어떻게 동작하는지 예시를 통해서 알아보겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ARP 동작 예시&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1) 같은 LAN 에서 요청 (A가 B에게 요청)&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1037&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G4j5v/btsfg38ZNnv/b17Bt91LguMkmq5KVzpjik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G4j5v/btsfg38ZNnv/b17Bt91LguMkmq5KVzpjik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G4j5v/btsfg38ZNnv/b17Bt91LguMkmq5KVzpjik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG4j5v%2Fbtsfg38ZNnv%2Fb17Bt91LguMkmq5KVzpjik%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;1037&quot; height=&quot;298&quot; data-origin-width=&quot;1037&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;A가 B에게 요청을 보내려고 하는데 MAC 주소를 몰라서 ARP broadcast를 요청한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; 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;1099&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LAPVo/btsfdp56r9f/bvbsm0Phi7BcVJq39ZQKJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LAPVo/btsfdp56r9f/bvbsm0Phi7BcVJq39ZQKJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LAPVo/btsfdp56r9f/bvbsm0Phi7BcVJq39ZQKJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLAPVo%2Fbtsfdp56r9f%2Fbvbsm0Phi7BcVJq39ZQKJk%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;1099&quot; height=&quot;323&quot; data-origin-width=&quot;1099&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;요청을 받은 B는 대상자 IP를 보고 자신한테 요청을 보내는 것을 알고 MAC 주소를 담아서 응답(unicast)를 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; 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;1309&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu9kyX/btsfclC4AIX/LKaxzRYyl5la5CwlVvw9l0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu9kyX/btsfclC4AIX/LKaxzRYyl5la5CwlVvw9l0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu9kyX/btsfclC4AIX/LKaxzRYyl5la5CwlVvw9l0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu9kyX%2FbtsfclC4AIX%2FLKaxzRYyl5la5CwlVvw9l0%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;1309&quot; height=&quot;274&quot; data-origin-width=&quot;1309&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;응답을 받은 A는 ARP 캐시 테이블에 저장한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2) 다른 LAN에서 요청(A가 C에게)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;밑의 그림은 완벽한 그림은 아니고 간단하게 그린 그림입니다. 원래는 L2 switch도 있어야합니다.&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;1168&quot; data-origin-height=&quot;489&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUZDjz/btsfaIZRnRd/t4kynp3i99uCgE70H8jAFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUZDjz/btsfaIZRnRd/t4kynp3i99uCgE70H8jAFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUZDjz/btsfaIZRnRd/t4kynp3i99uCgE70H8jAFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUZDjz%2FbtsfaIZRnRd%2Ft4kynp3i99uCgE70H8jAFK%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;1168&quot; height=&quot;489&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;489&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;A가 C에게 요청을 보내려고 합니다. 이 때 C의 ip는 알고 있는데 MAC 주소는 따로 알지 못합니다. 위의 1번 경우와는 다르게 이 때 A는 자신의 라우팅 테이블을 통해서 C는 다른 LAN임을 알고 라우터의 MAC주소를 사용합니다. 라우터의 MAC 주소가 없으면 &lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;ARP broadcast를 요청합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; 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;1206&quot; data-origin-height=&quot;525&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh811S/btsfaHNppo3/FcrLttFYderdFyDjSLCE80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh811S/btsfaHNppo3/FcrLttFYderdFyDjSLCE80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh811S/btsfaHNppo3/FcrLttFYderdFyDjSLCE80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh811S%2FbtsfaHNppo3%2FFcrLttFYderdFyDjSLCE80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1206&quot; height=&quot;525&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;525&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;라우터의 MAC 주소를 받고 난 후, 서버 A는 요청을 보내게 됩니다. 이 때 2계층에서 사용하는 MAC 주소는 라우터의 MAC 주소입니다. 해당 라우터에 C의 ip에 해당하는 MAC 주소가 없으면 라우터 R과 서버 C가 있는 LAN에서 다시 ARP broadcast를 진행해서 mac 주소를 알아옵니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; 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;1425&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bv83dt/btsfaHNppok/FXMSRcBksaW6MO9PxZo1rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bv83dt/btsfaHNppok/FXMSRcBksaW6MO9PxZo1rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bv83dt/btsfaHNppok/FXMSRcBksaW6MO9PxZo1rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbv83dt%2FbtsfaHNppok%2FFXMSRcBksaW6MO9PxZo1rk%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;1425&quot; height=&quot;648&quot; data-origin-width=&quot;1425&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;서버 C의 mac 주소를 알게된 후에 라우터는 패킷을 재구성하여 출발지 MAC 주소와 목적지 MAC 주소를 변경한 후 패킷을 전달합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;위의 예시를 보면 ARP broadcast는 같은 LAN에서만 이루어지는 것을 알 수 있습니다. 그래서 LAN을 ARP 패킷이 도달하는 범위의 네트워크로 정의하곤 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; 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;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;참고자료&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.guru99.com/address-resolution-protocol.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.guru99.com/address-resolution-protocol.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1683963205611&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;Address Resolution Protocol: What is ARP Header in Networking&quot; data-og-description=&quot;This tutorial covers address resolution protocol (ARP) basics, how arp works, ARP related important terms, ARP Types, ARP header, advantages, and more&quot; data-og-host=&quot;www.guru99.com&quot; data-og-source-url=&quot;https://www.guru99.com/address-resolution-protocol.html&quot; data-og-url=&quot;https://www.guru99.com/address-resolution-protocol.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/biKyge/hySBveSZ01/QHirhDAfTVILbbcfADMc4K/img.png?width=972&amp;amp;height=579&amp;amp;face=0_0_972_579,https://scrap.kakaocdn.net/dn/xu24H/hySBDYgJuw/tb6YtyURTclpQ8ZobGZKZK/img.png?width=972&amp;amp;height=579&amp;amp;face=0_0_972_579&quot;&gt;&lt;a href=&quot;https://www.guru99.com/address-resolution-protocol.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.guru99.com/address-resolution-protocol.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/biKyge/hySBveSZ01/QHirhDAfTVILbbcfADMc4K/img.png?width=972&amp;amp;height=579&amp;amp;face=0_0_972_579,https://scrap.kakaocdn.net/dn/xu24H/hySBDYgJuw/tb6YtyURTclpQ8ZobGZKZK/img.png?width=972&amp;amp;height=579&amp;amp;face=0_0_972_579');&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;Address Resolution Protocol: What is ARP Header in Networking&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This tutorial covers address resolution protocol (ARP) basics, how arp works, ARP related important terms, ARP Types, ARP header, advantages, and more&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.guru99.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://walkwidnetwork.blogspot.com/2013/04/address-resolution-protocol.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://walkwidnetwork.blogspot.com/2013/04/address-resolution-protocol.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1683963868141&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;Address Resolution Protocol&quot; data-og-description=&quot;Address Resolution Protocol (ARP) is a telecommunications protocol used for resolution of network layer addresses into link layer addresses...&quot; data-og-host=&quot;walkwidnetwork.blogspot.com&quot; data-og-source-url=&quot;http://walkwidnetwork.blogspot.com/2013/04/address-resolution-protocol.html&quot; data-og-url=&quot;http://walkwidnetwork.blogspot.com/2013/04/address-resolution-protocol.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cLzbya/hySBzn2p1Y/WlnK3N1K7mY775SfjvUhm1/img.png?width=324&amp;amp;height=170&amp;amp;face=0_0_324_170,https://scrap.kakaocdn.net/dn/by0H7M/hySBIedS1I/ikzL0MlmqoBiqPnfXB40b0/img.png?width=466&amp;amp;height=355&amp;amp;face=0_0_466_355,https://scrap.kakaocdn.net/dn/dezKHx/hySBFPk9xm/R9W6UlriRck2p3LbiyqwhK/img.png?width=324&amp;amp;height=218&amp;amp;face=0_0_324_218&quot;&gt;&lt;a href=&quot;http://walkwidnetwork.blogspot.com/2013/04/address-resolution-protocol.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;http://walkwidnetwork.blogspot.com/2013/04/address-resolution-protocol.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cLzbya/hySBzn2p1Y/WlnK3N1K7mY775SfjvUhm1/img.png?width=324&amp;amp;height=170&amp;amp;face=0_0_324_170,https://scrap.kakaocdn.net/dn/by0H7M/hySBIedS1I/ikzL0MlmqoBiqPnfXB40b0/img.png?width=466&amp;amp;height=355&amp;amp;face=0_0_466_355,https://scrap.kakaocdn.net/dn/dezKHx/hySBFPk9xm/R9W6UlriRck2p3LbiyqwhK/img.png?width=324&amp;amp;height=218&amp;amp;face=0_0_324_218');&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;Address Resolution Protocol&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Address Resolution Protocol (ARP) is a telecommunications protocol used for resolution of network layer addresses into link layer addresses...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;walkwidnetwork.blogspot.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://catsbi.oopy.io/eec728e7-0a31-4c96-9d33-20421bd5e6b3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://catsbi.oopy.io/eec728e7-0a31-4c96-9d33-20421bd5e6b3&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1683964822619&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;네트워크 통신하기&quot; data-og-description=&quot;목차&quot; data-og-host=&quot;catsbi.oopy.io&quot; data-og-source-url=&quot;https://catsbi.oopy.io/eec728e7-0a31-4c96-9d33-20421bd5e6b3&quot; data-og-url=&quot;https://catsbi.oopy.io/eec728e7-0a31-4c96-9d33-20421bd5e6b3&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c01lLQ/hySBxw1NXK/3iiHJJ1dwNlJGPRBxpuKgk/img.png?width=2000&amp;amp;height=886&amp;amp;face=0_0_2000_886,https://scrap.kakaocdn.net/dn/8Dn5G/hySBt2sFyK/Wubj0J3bGMkzCB6RTOYLTK/img.png?width=2000&amp;amp;height=886&amp;amp;face=0_0_2000_886,https://scrap.kakaocdn.net/dn/b3GGur/hySBxqfs4Y/xzUrqu0jYlmkVRp7QLMrCK/img.png?width=2000&amp;amp;height=1017&amp;amp;face=0_0_2000_1017&quot;&gt;&lt;a href=&quot;https://catsbi.oopy.io/eec728e7-0a31-4c96-9d33-20421bd5e6b3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://catsbi.oopy.io/eec728e7-0a31-4c96-9d33-20421bd5e6b3&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c01lLQ/hySBxw1NXK/3iiHJJ1dwNlJGPRBxpuKgk/img.png?width=2000&amp;amp;height=886&amp;amp;face=0_0_2000_886,https://scrap.kakaocdn.net/dn/8Dn5G/hySBt2sFyK/Wubj0J3bGMkzCB6RTOYLTK/img.png?width=2000&amp;amp;height=886&amp;amp;face=0_0_2000_886,https://scrap.kakaocdn.net/dn/b3GGur/hySBxqfs4Y/xzUrqu0jYlmkVRp7QLMrCK/img.png?width=2000&amp;amp;height=1017&amp;amp;face=0_0_2000_1017');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;네트워크 통신하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;목차&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;catsbi.oopy.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CS</category>
      <category>ARP</category>
      <category>iP</category>
      <category>LAN</category>
      <category>mac</category>
      <category>Network</category>
      <author>hanrabong</author>
      <guid isPermaLink="true">https://hanrabong.tistory.com/64</guid>
      <comments>https://hanrabong.tistory.com/entry/NETWORK-ARP%EB%9E%80#entry64comment</comments>
      <pubDate>Sat, 13 May 2023 18:34:18 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Spring Container란?(feat: DI, IOC, Singleton)</title>
      <link>https://hanrabong.tistory.com/entry/Spring-Spring-Container%EB%9E%80feat-DI-IOC-SingleTon</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;DI, Ioc, Singleton, Bean 등 Spring을 공부하는 사람이라면 한 번쯤은 들어봤을 내용입니다. 저 또한 Spring을 처음 공부할 때, 블로그나 강의에서 많이 듣곤 하였습니다. Spring boot를 사용할 때 이러한 개념들이 헷갈려서 정리하려고 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 엄청 자세하게 이야기를 하지 않으려고 합니다. 제가 공부하면서 헷갈렸던 부분이나, 연관성이 어떻게 있는지에 대해서 풀어보려고 합니다. 자세한 내용이 궁금하시면 &lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8#curriculum&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;'김영한님의 스프링 핵심원리 기본편'&lt;/a&gt;을 들으시는 것을 추천드리겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&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;Spring Container&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Spring Container는 쉽게 말해 객체를 관리해주는 컨테이너라고 생각하면 됩니다. 아무 객체나 관리해주는 것이 아니라, 당연히 개발자가 해당 객체를 관리해달라고 컨테이너에게 요청합니다. 객체를 Spring container에 등록하여 spring container가 관리하는 자바 객체, 즉 spring에 의하여 생성되고 관리되는 자바 객체를 &lt;b&gt;Spring bean&lt;/b&gt;이라고 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그럼 여기서 의문점이 생깁니다. 객체가 필요할 때 생성해서 사용하는 게 낫지.. 왜 객체의 생성부터 관리까지 Spring container에게 맡기는 것일까요?&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;제 생각에 그 이유는 Spring이 생겨난 이유와도 연관이 있다고 생각합니다. Spring이 나오기 전에도 java를 사용하여 개발을 많이 해왔지만 상당히 코드도 복잡하고 어려웠다고 합니다. Spring은 Java의 가장 큰 특징인 객체 지향 언어의 특징을 살려내는 프레임워크 입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;흔히 말하는 좋은 객체 지향의 원칙이라고 하면 SOLID가 먼저 생각납니다. SOLID 원칙은 전부 중요합니다. 5가지 원칙 중에서 Spring container와 가장 밀접한 관련이 있다고 생각하는 것은 OCP와 DIP입니다. 간략하게 알아보겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;OCP(Open Closed Principle)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;OCP는 한국어로 번역하면 '개방 폐쇄의 원칙' 입니다. 무엇에 개방되어야 하고 무엇에 폐쇄되어야 할까요? 기능을 추가하는 면에서는 개방되어야 하고 수정하는 부분에서는 폐쇄되어 있어야 합니다. 쉽게 말해 기존의 코드를 변경하지 않고 기능을 추가할 수 있도록 설계가 되어야 한다는 뜻입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;DIP(Dependency Inversion Principle)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;DIP는 '의존관계 역전 원칙'입니다. 상위 모듈은 하위모듈의 구현에 의존하면 안 되고 추상화에 의존해야 한다는 이야기입니다. Spring에서 예를 들면 controller 객체가 service의 구현체에 의존하면 안 되고 추상화에 의존해야 한다는 이야기입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;예시 코드를 통해서 좀 더 알아보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1673183061934&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// TestController.java
public class TestController {

	private TestService testService = new MysqlTestService();       // 변경 전
	private TestService testService = new PostgresqlTestService();  // 변경 후
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위의 예시 코드는 &lt;i&gt;MysqlTestService&lt;/i&gt;를 사용했다가 데이터베이스가 바뀌어 &lt;i&gt;PostgresqlTestService&lt;/i&gt;로 바꾸려고 하는 코드입니다. 이 코드는 OCP, DIP 원칙을 전부 어겼습니다. 구현하려고 하는 객체를 변경하려고 할 때, controller의 코드를 변경해야만 합니다. 또한 추상화에 의존하는 것이 아닌, 구현체에 의존하고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;스프링은 OCP, DIP를 가능하게 해 줍니다. Spring Container가 객체를 관리하고 직접 객체를 주입해주기 때문에 위의 코드처럼 직접 구현체를 주입해줄 필요가 없습니다. 이렇게 의존성(객체)을 주입해주는 것을&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; DI(Dependency Injection)&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;라고&lt;/span&gt;&lt;/span&gt; 합니다. 또한 제어 흐름을 직접 제어하는 것이 아닌 외부(Spring container)에서 관리하기에 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;IOC(Inversion Of Control)&lt;/span&gt;&lt;/b&gt;라고도 합니다. 따라서 Spring container를 DI container, IOC container라고도 부르기도 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;객체를 등록해야 Spring container가 알아서 주입도 해주고 관리를 해주기에 Spring container에 spring bean으로 등록하는 방법에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;직접 등록&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;첫 번째, 방법으로는 직접 등록하는 방법이 있습니다. 직접 등록이라고해서 복잡하게 넣어주고 그러는 것이 아니고 @Configuration과 @Bean을 이용해서 직접 등록하는 방식입니다. 이 방법이 직접 등록하는 방법이라고 하는 이유는 나중에 나오는 @Component를 사용하는 방법보다 복잡하고 말 그대로 @Bean을 이용해서 등록을 해주기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Bean annotation의 경우 다음과 같이 되어있습니다. @Target을 보면 @Bean은 method나 annotation에만 붙여줄 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1673854531714&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean { 
```

```
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이제 코드를 통해 보겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1673853905899&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// TestConfiguration.java
@Configuration
public class TestConfiguration {

    @Bean
    public Person person() {
        return new Person();
    }
}

// Person.java
public class Person {

    private String username;
    private int age;

    private void canSpeak() {
        System.out.println(&quot;can Speak&quot;);
    }

    private void canEat() {
        System.out.println(&quot;can Eat&quot;);
    }

    public Person() {}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위의 코드처럼 @Configuration 을 붙여주고 method에 @Bean을 붙여주면 해당 객체가 bean으로 등록이 됩니다. 이름의 경우 메소드 이름을 그대로 갖고 가는데 @Bean의 name 속성을 이용하여 이름을 변경해줄 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Bean으로 등록이 되었는지 두 번째 방법까지 얘기하고 한 번에 확인해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;@Component 사용&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;@Component 어노테이션을 사용해서 등록하는 방법입니다. 프로젝트 규모가 커질수록 직접 bean을 등록하기 어렵고 부담스러워집니다. 대신 Component 어노테이션을 사용하면 쉽게 등록할 수 있습니다. 그렇다고 항상 @Component만을 사용하는 것은 아니고 외부 라이브러리 등 @Component를 사용 못하는 경우에는 직접 등록해서 사용합니다. 어떻게 Component 어노테이션만 있다고 Spring이 인식을 할 수 있을까요?&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;그 이유는 @ComponentScan 어노테이션을 이용하여 Spring container가 Component 어노테이션이 붙은 객체들을 전부 Spring bean으로 등록 시키기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Spring boot 코드를 한 번 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1673185051956&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// TestApplication.java
@SpringBootApplication
public class TestApplication {
	
	public static void main(String[] args) {
		SpringApplication.run(TestApplication.class, args);
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위 코드에 있는 @SpringBootApplication 어노테이션을 들어가서 보면 @ComponentScan 어노테이션이 있는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; 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;842&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJumJl/btrVF9HbxAD/ipERPpL6zg6BRFC15yJrh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJumJl/btrVF9HbxAD/ipERPpL6zg6BRFC15yJrh0/img.png&quot; data-alt=&quot;SpringBootApplication Annotation&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJumJl/btrVF9HbxAD/ipERPpL6zg6BRFC15yJrh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJumJl%2FbtrVF9HbxAD%2FipERPpL6zg6BRFC15yJrh0%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;842&quot; height=&quot;210&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SpringBootApplication Annotation&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;또한 @Controller, @Service, @Repository, @Configuration 어노테이션 안에 @Component 어노테이션이 있기에, 따로 Component 어노테이션을 안 줘도 됩니다.&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/bwMpcA/btrVBRgbtZw/SceErPkDOR6eKouqZcEFDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwMpcA/btrVBRgbtZw/SceErPkDOR6eKouqZcEFDK/img.png&quot; data-origin-width=&quot;306&quot; data-origin-height=&quot;123&quot; data-is-animation=&quot;false&quot; style=&quot;width: 46.8229%; margin-right: 10px;&quot; data-widthpercent=&quot;47.37&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwMpcA/btrVBRgbtZw/SceErPkDOR6eKouqZcEFDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwMpcA%2FbtrVBRgbtZw%2FSceErPkDOR6eKouqZcEFDK%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;306&quot; height=&quot;123&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSmrR6/btrVDiki7YN/FnKNHOJZDXQyLFJdWwKfU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSmrR6/btrVDiki7YN/FnKNHOJZDXQyLFJdWwKfU1/img.png&quot; data-origin-width=&quot;304&quot; data-origin-height=&quot;110&quot; data-is-animation=&quot;false&quot; style=&quot;width: 52.0143%;&quot; data-widthpercent=&quot;52.63&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSmrR6/btrVDiki7YN/FnKNHOJZDXQyLFJdWwKfU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSmrR6%2FbtrVDiki7YN%2FFnKNHOJZDXQyLFJdWwKfU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;304&quot; height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rA1sl/btrVGYZU56B/64GSKL2FWUna6QW3VOQkQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rA1sl/btrVGYZU56B/64GSKL2FWUna6QW3VOQkQK/img.png&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;117&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.9206%; margin-right: 10px; margin-top: 10px;&quot; data-widthpercent=&quot;49.5&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rA1sl/btrVGYZU56B/64GSKL2FWUna6QW3VOQkQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrA1sl%2FbtrVGYZU56B%2F64GSKL2FWUna6QW3VOQkQK%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;291&quot; height=&quot;117&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Lkp3d/btrVAsVBI7t/KUZr9kkaapalZW5yQK4SjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Lkp3d/btrVAsVBI7t/KUZr9kkaapalZW5yQK4SjK/img.png&quot; data-origin-width=&quot;302&quot; data-origin-height=&quot;119&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.9166%; margin-top: 10px;&quot; data-widthpercent=&quot;50.5&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Lkp3d/btrVAsVBI7t/KUZr9kkaapalZW5yQK4SjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLkp3d%2FbtrVAsVBI7t%2FKUZr9kkaapalZW5yQK4SjK%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;302&quot; height=&quot;119&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Controller, Service, Repository, Configuration Annotation&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;예시 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1673854052683&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// TestController.java
@Controller
@RequestMapping(&quot;/test&quot;)
@RequiredArgsConstructor
public class testController {

    @GetMapping
    public void getTest() {
        System.out.println(&quot;test for get method&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 해당 bean이 잘 등록 되었는지 확인해 보겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size23&quot;&gt;Bean 객체 확인&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Bean이 잘 등록되었는지 확인을 해 보겠습니다. Spring boot를 실행할 때 Spring container가 @Componenet가 붙어 있는 class와 @Bean이 붙어 있는 method등을 Bean으로 등록을 합니다. 이렇게 바로 실행시킬 때 등록하는 방법을 eager loading이라고 하는데, 바로 등록시키지 않고 사용할 때 등록시키게끔 lazy loading으로 변경할 수 있습니다. 자세한 방법은 구글링하면 설명을 잘 해주는 글들이 많습니다!!&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1672907235893&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// TestApplication.java
@SpringBootApplication
public class TestApplication implements CommandLineRunner {

	@Autowired
	private ApplicationContext appContext;  // import org.springframework.context.ApplicationContext;

	public static void main(String[] args) {
		SpringApplication.run(TestApplication.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
		String[] beans = appContext.getBeanDefinitionNames();  // 등록된 bean 객체의 이름을 받아온다.
		for(String bean : beans) {
			Object beanDetail = appContext.getBean(bean);  // bean 이름으로 bean 객체를 받아온다.
			System.out.println(bean);
			System.out.println(beanDetail);
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Application 코드를 위와 같이 바꾸면 등록되어 있는 bean들을 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; 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;626&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJtilT/btrWq2Ar7NQ/Z58OhEFc14n2Zyx7Yei930/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJtilT/btrWq2Ar7NQ/Z58OhEFc14n2Zyx7Yei930/img.png&quot; data-alt=&quot;등록된 Bean 객체&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJtilT/btrWq2Ar7NQ/Z58OhEFc14n2Zyx7Yei930/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJtilT%2FbtrWq2Ar7NQ%2FZ58OhEFc14n2Zyx7Yei930%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;626&quot; height=&quot;142&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;등록된 Bean 객체&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;코드를 실행시키면 위와 같이 Bean들이 등록되는 것을 볼 수 있습니다. Bean을 어떻게 등록하고 관리하는지에 대해 알아 보았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이제, 앞에서 언급한 DI(의존성 주입)에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size23&quot;&gt;의존성 주입&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Spring container에 Bean 객체를 등록시키면, 알아서 관리를 해주고 객체들을 주입해준다고 하였습니다. 어떻게 주입을 시켜주는지 알아보겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;의존성을 주입 시키는 방법에는 생성자 주입, 필드 주입, Setter 주입 등이 있습니다. 주입을 할 때는 &lt;b&gt;@Autowired&lt;/b&gt; annotation을 사용하여 Spring container에게 Bean을 주입해 달라고 요청합니다. 위의 3가지 주입 방식 중에서 일반적으로 생성자 주입을 많이 사용하고 권장하고 있습니다. 각 주입의 장단점을 찾아보면 자세하게 설명한 글들이 많습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;코드로 살펴보겠습니다. TestController.java 파일을 밑의 코드처럼 변경해주겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1673857486728&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// TestController.java
@RestController
@RequestMapping(&quot;/test&quot;)
@RequiredArgsConstructor
public class testController {


    private final Person person1;

    private final Person person2;

    @GetMapping
    public void getTest() {
        System.out.println(&quot;person1 = &quot; + person1);
        System.out.println(&quot;person2 = &quot; + person2);
    }

    @PostMapping
    public void JacksonTest1(@RequestBody TestOneRequestDto testOneRequestDto) {

        System.out.println(testOneRequestDto.getTest());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같이 코드를 변경하고 Postman 또는 api tester 등을 이용해서 요청을 보내보겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; 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/bNB5MX/btrWlkCrS7h/SpDR83pA2oKka8xj2fp4F0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNB5MX/btrWlkCrS7h/SpDR83pA2oKka8xj2fp4F0/img.png&quot; data-origin-width=&quot;274&quot; data-origin-height=&quot;50&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;50.96&quot; style=&quot;width: 50.3654%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNB5MX/btrWlkCrS7h/SpDR83pA2oKka8xj2fp4F0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNB5MX%2FbtrWlkCrS7h%2FSpDR83pA2oKka8xj2fp4F0%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;274&quot; height=&quot;50&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nfndr/btrWqPVAJ4B/hzJeGNbGA33tGPozuQHebk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nfndr/btrWqPVAJ4B/hzJeGNbGA33tGPozuQHebk/img.png&quot; data-origin-width=&quot;385&quot; data-origin-height=&quot;73&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.4718%;&quot; data-widthpercent=&quot;49.04&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nfndr/btrWqPVAJ4B/hzJeGNbGA33tGPozuQHebk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnfndr%2FbtrWqPVAJ4B%2FhzJeGNbGA33tGPozuQHebk%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;385&quot; height=&quot;73&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;person Bean 객체&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;요청을 하고 터미널을 확인해보면 이와 같이 등록된 Bean 객체를 주입해주는 것을 알 수 있습니다. 위의 코드에서 왜 @Autowired를 사용하지 않았는데도 Spring container가 알아서 주입을 해주는지 궁금증이 생길 수도 있습니다. 생성자 주입의 경우 생성자가 1개만 존재할 때 @Autowired를 해주지 않아도 Spring container가 알아서 @Autowired를 해주게 됩니다. 위의 코드에서는 lombok annotation @RequiredArgsConstructor를 이용하여 생성자를 한 개만 만들어주었기에 알아서 주입이 되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;api 요청을 계속 보내보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; 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;359&quot; data-origin-height=&quot;315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yCwJC/btrWrIWxX22/RaETmT9kHRk5Egr1YdBuxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yCwJC/btrWrIWxX22/RaETmT9kHRk5Egr1YdBuxK/img.png&quot; data-alt=&quot;Bean 객체&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yCwJC/btrWrIWxX22/RaETmT9kHRk5Egr1YdBuxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyCwJC%2FbtrWrIWxX22%2FRaETmT9kHRk5Egr1YdBuxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;359&quot; height=&quot;315&quot; data-origin-width=&quot;359&quot; data-origin-height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Bean 객체&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;결과를 확인해보면 새로운 요청을 보내도 매번 똑같은 객체를 주입해 준다는 것을 알 수 있습니다. 어떻게 이런게 가능할까요?&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;그 이유는 기본적으로 Spring container는 bean 객체를 &lt;b&gt;Singleton&lt;/b&gt;으로 관리하기 때문입니다. Bean 객체의 Scope 즉 빈이 사용되는 범위는 singleton, prototype, request 등이 있지만, Spring container는 디폴트로 singleton으로 관리를 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;Singleton으로 관리를 하면 매번 요청을 할 때마다 객체를 새로 생성하지 않아도 되기에 메모리나 성능면에서 효율적입니다. 하지만 단점도 있기에 주의하여 사용해야합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Spring container, DI, IOC, Bean 등 Spring의 기본 개념에 대해서 적어보았습니다. 주입 종류, Bean 생명주기, 스코프 등 자세한 내용은 다루지 않았습니다. 궁금한 내용은 구글링을 하면 자세히 설명해주는 블로그들이 많아서 직접 찾아보시기 바랍니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; 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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;참고자료&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1673180771364&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;스프링 핵심 원리 - 기본편 - 인프런 | 강의&quot; data-og-description=&quot;스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jZ7cJ/hyRcAbzW01/NPEhzQ50eelOBrwkWkplPK/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/cKESct/hyRcK6jQr8/AsUDw63Bq2smrqCld5SeH1/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/byykct/hyRcLKYuqe/o7yJssih32LQJq7YOEkim1/img.png?width=1000&amp;amp;height=479&amp;amp;face=0_0_1000_479&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jZ7cJ/hyRcAbzW01/NPEhzQ50eelOBrwkWkplPK/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/cKESct/hyRcK6jQr8/AsUDw63Bq2smrqCld5SeH1/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/byykct/hyRcLKYuqe/o7yJssih32LQJq7YOEkim1/img.png?width=1000&amp;amp;height=479&amp;amp;face=0_0_1000_479');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;스프링 핵심 원리 - 기본편 - 인프런 | 강의&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/56214329/how-can-i-check-if-a-bean-has-been-loaded-by-springboot&quot;&gt;https://stackoverflow.com/questions/56214329/how-can-i-check-if-a-bean-has-been-loaded-by-springboot&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1672905693340&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;How can I check if a bean has been loaded by springboot&quot; data-og-description=&quot;I am trying to run some Grpc beans using Springboot and all I see confirmed is the springboot application loads. Where can I find confirmation that the beans loaded? Is there a way to start spring...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/56214329/how-can-i-check-if-a-bean-has-been-loaded-by-springboot&quot; data-og-url=&quot;https://stackoverflow.com/questions/56214329/how-can-i-check-if-a-bean-has-been-loaded-by-springboot&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/0m7Vg/hyRatiyBdM/CWn2OwDYSPuMi3S9xGeyVk/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/56214329/how-can-i-check-if-a-bean-has-been-loaded-by-springboot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/56214329/how-can-i-check-if-a-bean-has-been-loaded-by-springboot&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/0m7Vg/hyRatiyBdM/CWn2OwDYSPuMi3S9xGeyVk/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&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;How can I check if a bean has been loaded by springboot&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I am trying to run some Grpc beans using Springboot and all I see confirmed is the springboot application loads. Where can I find confirmation that the beans loaded? Is there a way to start spring...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.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;</description>
      <category>Spring</category>
      <category>di</category>
      <category>DIP</category>
      <category>IOC</category>
      <category>Java</category>
      <category>OCP</category>
      <category>singleton</category>
      <category>spring</category>
      <category>spring bean</category>
      <category>Spring Container</category>
      <author>hanrabong</author>
      <guid isPermaLink="true">https://hanrabong.tistory.com/60</guid>
      <comments>https://hanrabong.tistory.com/entry/Spring-Spring-Container%EB%9E%80feat-DI-IOC-SingleTon#entry60comment</comments>
      <pubDate>Tue, 17 Jan 2023 10:47:22 +0900</pubDate>
    </item>
    <item>
      <title>[JUnit5] test code에서 Array 비교(assertarrayEquals vs assertEquals vs assertTrue)</title>
      <link>https://hanrabong.tistory.com/entry/JUnit5-AssertarrayEquals</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;JUnit을 이용하여 test code를 짤 때 assertTrue와 assertEquals를 이용해서 원하는 값이 나오는지 테스트를 하곤 합니다. Array를 비교할 때는 어떤 method를 쓰는 것이 좋은지에 대해 비교해보려고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;assertTrue, assertArrayEquals, assertEquals 3개를 비교하려고 합니다. 저의 주관적인 생각이 들어가기에 원하는 상황에 원하는 method를 알아서 사용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&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;pre id=&quot;code_1672810008035&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ArrayTest {

    @Test
    public void compareArrayTest() {
        List&amp;lt;String&amp;gt; list1 = Arrays.asList(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;, &quot;e&quot;, &quot;f&quot;);
        List&amp;lt;String&amp;gt; list2 = Arrays.asList(&quot;a&quot;, &quot;b&quot;, &quot;e&quot;, &quot;d&quot;, &quot;e&quot;, &quot;h&quot;);

        Assertions.assertAll(
                () -&amp;gt; { Assertions.assertArrayEquals(list1.toArray(), list2.toArray());},
                () -&amp;gt; { Assertions.assertTrue(Arrays.equals(list1.toArray(), list2.toArray()));},
                () -&amp;gt; { Assertions.assertEquals(list1, list2);});
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Assertions.assertAll()&amp;nbsp;&lt;/i&gt;을 사용한 이유는 중간에 테스트가 실패해도 모든 테스트를 다 돌리기 위하여 사용하였습니다. (&lt;u&gt;한 줄씩 주석처리하고 하나씩 돌리면서 error를 찾는 건 너무 귀찮기 때문에...)&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;위의 코드가 발생시키는 error를 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&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;assertTrue&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;org.opentest4j.AssertionFailedError: expected: &amp;lt;true&amp;gt; but was: &amp;lt;false&amp;gt;&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위의 에러는 assertTrue가 발생시키는 error 입니다. 해당 error를 보면 &lt;span style=&quot;color: #006dd7;&quot;&gt;true&lt;/span&gt;로 예상했는데 &lt;span style=&quot;color: #ee2323;&quot;&gt;false&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;h3 data-ke-size=&quot;size23&quot;&gt;assertArrayEquals&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;org.opentest4j.AssertionFailedError: array contents differ at index [2], expected: &amp;lt;c&amp;gt; but was: &amp;lt;e&amp;gt;&lt;/span&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위의 에러는 assertArrayEquals가 발생시키는 error 입니다. 해당 error를 보면 2번째 index에서 &lt;b&gt;&quot;c&quot;&lt;/b&gt;를 예상했지만, &lt;b&gt;&quot;e&quot;&lt;/b&gt;가 실제로 나왔다는 정보를 알 수 있습니다. 해당 정보를 통해서 2번째 index 값이 달라서 에러가 발생했음을 알 수 있습니다. 그러나 2번째 index 뒤로는 값이 같은지를 보장할 수는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&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;assertEquals&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;org.opentest4j.AssertionFailedError: expected: &amp;lt;[a, b, c, d, e, f]&amp;gt; but was: &amp;lt;[a, b, e, d, e, h]&amp;gt;&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위의 에러는 assertEquals가 발생시키는 error 입니다. 해당 error를 보면 전반적인 array 값들을 볼 수 있고, 몇 번째 index 값들이 다른지 한 눈에 확인할 수 있습니다. array의 길이가 엄청 길면 직접 비교를 해줘야 하고, 최악의 경우에 길이가 10만이고 마지막 하나만 다를 경우 앞에서부터 비교를 해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 19%; text-align: center;&quot;&gt;에러 코드&lt;/td&gt;
&lt;td style=&quot;width: 18%; text-align: center;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;width: 18%; text-align: center;&quot;&gt;단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11%;&quot;&gt;assertTrue&lt;/td&gt;
&lt;td style=&quot;width: 19%;&quot;&gt;true or false&lt;/td&gt;
&lt;td style=&quot;width: 18%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 18%;&quot;&gt;알 수 있는 정보가 너무 적다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11%;&quot;&gt;assertArrayEquals&lt;/td&gt;
&lt;td style=&quot;width: 19%; text-align: justify;&quot;&gt;몇 번 index 값이 다른지 보여준다.&lt;/td&gt;
&lt;td style=&quot;width: 18%; text-align: justify; line-height: 35px;&quot;&gt;&amp;nbsp;몇 번째 index가 다른지 알기에 해당 index와 관련된 logic에만 집중하면 된다.&lt;br /&gt;&lt;br /&gt;&amp;nbsp;method이름만으로도 array를 비교하는구나하고 유추 가능하다.&lt;/td&gt;
&lt;td style=&quot;width: 18%; text-align: justify; line-height: 35px;&quot;&gt;error가 난 index이후의 값들이 같은지 보장하지 않기에, 계속 error나면 매번 수정해줘야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11%;&quot;&gt;assertEquals&lt;/td&gt;
&lt;td style=&quot;width: 19%; text-align: justify;&quot;&gt;전반적인 array 값을 다 보여준다.&lt;/td&gt;
&lt;td style=&quot;width: 18%; text-align: justify; line-height: 35px;&quot;&gt;&amp;nbsp;몇 번 index의 값이 다른지 다 알기에 해당 index들의 logic에 집중하면 된다.&lt;/td&gt;
&lt;td style=&quot;width: 18%; text-align: justify; line-height: 35px;&quot;&gt;array의 길이가 길면 직접 하나씩 비교하는데 비용이 든다.&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 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;3개의 method를 요약하자면 다음과 같습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify; line-height: 35px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;저는 개인적으로 array를 비교할 때, &lt;b&gt;assertArrayEquals&lt;/b&gt;를 사용할 것 같습니다. method 이름으로도 직관적으로 array를 비교하는구나 알 수 있고, 몇 번 index가 다른지 알려주기에 어떤 부분이 error를 발생시키는지 빠르게 확인할 수 있습니다.&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;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;※ 궁금한 사항이나 잘못된 점 댓글 부탁드립니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>프로그래밍 언어/Java</category>
      <category>assertArrayEquals</category>
      <category>assertEquals</category>
      <category>assertTrue</category>
      <category>junit5</category>
      <author>hanrabong</author>
      <guid isPermaLink="true">https://hanrabong.tistory.com/59</guid>
      <comments>https://hanrabong.tistory.com/entry/JUnit5-AssertarrayEquals#entry59comment</comments>
      <pubDate>Wed, 4 Jan 2023 14:53:51 +0900</pubDate>
    </item>
  </channel>
</rss>