<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>yeseul.log</title>
    <link>https://yeseul-dev.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 27 Jun 2026 22:56:07 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor> 예슬</managingEditor>
    <item>
      <title>[spring-batch] 생에 처음으로 오픈 소스에 기여를 해보다</title>
      <link>https://yeseul-dev.tistory.com/99</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오픈 소스라는 존재에 대해서는 &quot;언제 한번쯤은 기여해보고 싶다&quot;는 마음은 항상있었지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;막연하게만 생각하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 우연히 &lt;b&gt;오픈소스 기여 모임&lt;/b&gt;이라는 곳을 알게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-03 오후 8.31.20.png&quot; data-origin-width=&quot;2580&quot; data-origin-height=&quot;1628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbQsnH/dJMcacPwCFM/1fbUq901WWNOL1146lieck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbQsnH/dJMcacPwCFM/1fbUq901WWNOL1146lieck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbQsnH/dJMcacPwCFM/1fbUq901WWNOL1146lieck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbQsnH%2FdJMcacPwCFM%2F1fbUq901WWNOL1146lieck%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;2580&quot; height=&quot;1628&quot; data-filename=&quot;스크린샷 2026-02-03 오후 8.31.20.png&quot; data-origin-width=&quot;2580&quot; data-origin-height=&quot;1628&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;figure id=&quot;og_1770118301448&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;&amp;ldquo;누구나 원하는 오픈소스에 기여를&amp;rdquo; 할 수 있게 기여를 직접 돕습니다!&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/opensource-contributors&quot; data-og-url=&quot;https://medium.com/opensource-contributors&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/AAJ4d/dJMb83koaTA/41NlEuzQGaahtu2K3ifvb0/img.png?width=600&amp;amp;height=599&amp;amp;face=0_0_600_599&quot;&gt;&lt;a href=&quot;https://medium.com/opensource-contributors&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/opensource-contributors&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/AAJ4d/dJMb83koaTA/41NlEuzQGaahtu2K3ifvb0/img.png?width=600&amp;amp;height=599&amp;amp;face=0_0_600_599');&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;&amp;ldquo;누구나 원하는 오픈소스에 기여를&amp;rdquo; 할 수 있게 기여를 직접 돕습니다!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초심자 분들께도 체계적으로 가이드를 주신다는 후기를 봤기 때문에 이번에는 꼭 오픈 소스에 기여를 해보겠다는 생각으로 일단 참가부터 했던 것 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/opensource-contributors/%EB%AA%A8%EC%A7%91%EC%A4%91-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC-%EB%AA%A8%EC%9E%84-10%EA%B8%B0-%EC%B0%B8%EA%B0%80%EC%9E%90%EB%A5%BC-%EB%AA%A8%EC%A7%91%ED%95%A9%EB%8B%88%EB%8B%A4-2026-01-%EC%A7%84%ED%96%89-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%ED%82%A4%EB%A7%81-%EA%B5%BF%EC%A6%88-%EC%84%A0%EB%AC%BC%EA%B9%8C%EC%A7%80-e95a8e528056&quot;&gt;오픈소스 기여 모임 10기 모집 글&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Batch를 선택한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기여할 프로젝트를 고를 때, 고민 없이 &lt;b&gt;spring-batch&lt;/b&gt;를 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring-batch는 저에게 꽤 특별한 프로젝트입니다. 본격적인 개발 스터디를 시작한 게 바로 spring-batch 스터디였기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 최근에 스프링 배치 관련 책을 잠시 읽으면서 더욱 흥미를 가지게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 기억은 안나지만, 책의 서문에서 &quot;백그라운드에서 항상 돌아가고 있지만 - 유저는 느끼지 못하는 서비스&quot;라는 표현 때문이었던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 표현을 보고 배치 서비스야말로 진짜 '백엔드'라는 느낌이었습니다. 그래서 백엔드 개발자로서 그런 부분을 더 깊이 이해하고 싶어 자연스럽게 spring-batch 프로젝트에 기여하고자 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그런데... &quot;기여할 수 있는 이슈가 없다&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기여 모임이 시작하기 전부터 불안함이 컸기 때문에, spring-batch에 기여하신 분들의 후기를 먼저 찾아보었습니다. 모임장이신 &lt;a href=&quot;https://medium.com/opensource-contributors/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98-pr-%EA%B8%B0%EC%97%AC-flowbuilder-next-%EC%9D%98-%EB%AC%B4%ED%95%9C%EB%A3%A8%ED%94%84-%EB%B2%84%EA%B7%B8-%EA%B3%A0%EC%B9%98%EA%B8%B0-3cac3237f44c&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;인제님께서 spring-batch 기여에 대해 직접 글을 남기신 게&lt;/a&gt; 있었고, 바로 카톡으로 조언을 구했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/befiqu/dJMcabXmDg6/RWVscZIXNKh8KdwCdvebc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/befiqu/dJMcabXmDg6/RWVscZIXNKh8KdwCdvebc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/befiqu/dJMcabXmDg6/RWVscZIXNKh8KdwCdvebc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbefiqu%2FdJMcabXmDg6%2FRWVscZIXNKh8KdwCdvebc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;320&quot; height=&quot;400&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;i&gt;정말 별 말씀 안드렸는데도 엄청난 지지를 해주신 인제님&amp;hellip; 천사이심이 틀림없으신 것같 습니다. 실제 모임에서도 500명이 넘는 인원들 전부 케어를 해주셨으니&amp;hellip; 대단하신 분이십니다. 별로 여쭤본 것도 없는데 엄청난 응원을 해주셔서 무척 감사했습니다..&lt;/i&gt;&lt;/span&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 실제로 오픈소스 모임이 시작되고, 저는 1회차 오프라인 모임에 참가했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스 기여 모임에서는 AI를 적극 활용하기를 권장하고 있습니다. 기여를 원하는 프로젝트만 선정한다면 AI가 이슈 선정부터 해결까지 가이드를 해주는 방식으로 진행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 과정인 이슈 선정하는 것도 AI와 함께 진행하게 되는데요, 기여하고 싶었던 spring-batch 프로젝트는 오래됐기 때문에 이미 성숙도가 높아 초심자가 손대기 어려운 이슈들이 대부분이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEJBGv/dJMcadt38fg/aoRwKWM9uKqcTJBP3pQGBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEJBGv/dJMcadt38fg/aoRwKWM9uKqcTJBP3pQGBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEJBGv/dJMcadt38fg/aoRwKWM9uKqcTJBP3pQGBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEJBGv%2FdJMcadt38fg%2FaoRwKWM9uKqcTJBP3pQGBk%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;556&quot; height=&quot;327&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1688&quot; data-origin-height=&quot;906&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpuWh8/dJMcadnjnde/oqoFjIcu19vrbcJ6M33QS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpuWh8/dJMcadnjnde/oqoFjIcu19vrbcJ6M33QS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpuWh8/dJMcadnjnde/oqoFjIcu19vrbcJ6M33QS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpuWh8%2FdJMcadnjnde%2FoqoFjIcu19vrbcJ6M33QS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;306&quot; data-origin-width=&quot;1688&quot; data-origin-height=&quot;906&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;적합한 이슈가 하나도 없다.&lt;/span&gt;&lt;/blockquote&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 코멘트들을 들으면서 '나는 spring-batch 프로젝트에 기여할 수 없겠구나', '나는 왜 이런 실력을 갖고있는 거지' 하는 생각에 갑자기 너무 우울해져서 혼술까지 했던 기억이 납니다. ㅋㅋㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오프라인 모임에서 인제님과 얘기를 하면서 그나마 초심자가 진행 하기에 적합한 이슈를 선정 해주셨고, 개인적으로 진행한 후에 개인 브랜치에 푸시 후 피드백을 받기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 인제님께 혹시 다른 프로젝트를 선정 해야할지 고민을 말씀드렸는데, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&quot;그래도 본인이 제일 하고 싶은 프로젝트에 기여하는 게 제일 좋죠&quot;라고 하시며 도움을 주셨고, 그때 선정한 이슈가 아래와 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;선정한 이슈&lt;/h3&gt;
&lt;figure id=&quot;og_1770119748564&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;MetaDataInstanceFactory default values cause StepContext collision in StepScopeTestUtils when @SpringBatchTest is active &amp;middot; Issu&quot; data-og-description=&quot;Bug description: There is a logical collision in StepSynchronizationManager when using StepScopeTestUtils in a test environment managed by @SpringBatchTest. StepExecution determines equality based ...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/spring-projects/spring-batch/issues/5181&quot; data-og-url=&quot;https://github.com/spring-projects/spring-batch/issues/5181&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/UzU7J/dJMb9lL6Pgh/SsSd6CE41mdkDzE4NKBqt0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dfxQ1T/dJMb9iaMnKb/5aR0wEfNL2KZKTyfQ4JJQk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-batch/issues/5181&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/spring-projects/spring-batch/issues/5181&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/UzU7J/dJMb9lL6Pgh/SsSd6CE41mdkDzE4NKBqt0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dfxQ1T/dJMb9iaMnKb/5aR0wEfNL2KZKTyfQ4JJQk/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;MetaDataInstanceFactory default values cause StepContext collision in StepScopeTestUtils when @SpringBatchTest is active &amp;middot; Issu&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Bug description: There is a logical collision in StepSynchronizationManager when using StepScopeTestUtils in a test environment managed by @SpringBatchTest. StepExecution determines equality based ...&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 style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/spring-projects/spring-batch/issues/5181&quot;&gt;MetaDataInstanceFactory default values cause StepContext collision in StepScopeTestUtils when @SpringBatchTest is active&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/a&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://github.com/spring-projects/spring-batch/issues/5181&quot;&gt;#5181&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 설명하면, @SpringBatchTest와 StepScopeTestUtils를 함께 사용했을 때, MetaDataInstanceFactory가 고정된 ID(1234L)로 StepExecution을 생성하면서 컨텍스트가 충돌하는 버그였습니다. 이로 인해 테스트에서 JobParameters가 제대로 주입되지 못하는 문제가 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈 작성자 분이 재현 코드까지 꼼꼼히 남겨 주셔서 이해하거나 해결하는 데 수월했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AI와 함께 공부하고, 이해하는 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈를 해결하려면 spring-batch의 테스트 관련 클래스들을 어느 정도는 이해해야 했었습니다. 평소에 spring-batch 를 이용한 서비스 개발을 해본 건 맞지만, 테스트 유틸리티 클래스들은 거의 사용해본 적이 없었었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 AI에게 &quot;spring-batch 테스트 클래스를 공부하려는데, 어떤 순서대로 봐야할까?&quot;라고 물어보니 학습 경로까지 정리해주었습니다. 큰 그림을 잡고, 각 클래스를 하나씩 들어가며 읽고, 이해가 안 되는 부분은 다시 AI와 얘기하며 반복하는 방식으로 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공부한 주요 클래스들은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MetaDataInstanceFactory&lt;/b&gt; &amp;mdash; 테스트용 Job/Step 객체를 생성하는 핵심 유틸리티&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ExecutionContextTestUtils&lt;/b&gt; &amp;mdash; ExecutionContext에서 값을 편리하게 꺼내는 테스트 유틸리티&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JobOperatorTestUtils&lt;/b&gt; &amp;mdash; Job 실행과 단일 Step 테스트를 위한 헬퍼 (기존 JobLauncherTestUtils의 후속)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JobRepositoryTestUtils&lt;/b&gt; &amp;mdash; 배치 메타데이터 저장소의 테스트 유틸리티&lt;/li&gt;
&lt;li&gt;&lt;b&gt;StepScopeTestExecutionListener&lt;/b&gt; &amp;mdash; @StepScope/@JobScope 빈을 단위 테스트에서 사용 가능하게 해주는 리스너&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 테스트 클래스들을 공부하고 나니, 이슈가 왜 발생한 건지 그리고 어떻게 접근할 수 있는지가 조금씩 보이기 시작했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PR과 merge &amp;mdash; 실제 기여한 내용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;접근 방향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈에서도 언급되었듯이 해결 방법은 여러 가지입니다. ID 생성 전략을 바꾸거나, 중복 등록 회피 로직을 추가하거나, 사용자에게 ID를 직접 받도록 하는 등이 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 &lt;b&gt;수정을 최소화&lt;/b&gt;하는 방향을 선택했습니다. 기존에 사용되던 상수값을 그대로 유지하되, 새로운 ID를 생성할 때마다 해당 상수에 기반하여 매번 고유한 ID를 제공하도록 했습니다.&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;AtomicLong&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 AtomigLong을 사용할 필요가 생겼는데, 사실 회사에서 서비스를 구현하면서 Long은 많이 사용하지만 AtomicXXX라는 클래스는 처음 접해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring-batch 코드를 들여다보면서 그 존재를 알게 되었고, 멀티 스레드 환경에서 long 값의 정합성을 유지하기 위해 이 클래스를 사용한다는 것을 알게 되었습니다.&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;PR 제출&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-03 오후 9.24.14.png&quot; data-origin-width=&quot;3312&quot; data-origin-height=&quot;1968&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvqvze/dJMcabpyb7W/mTcxdRiKalWaTIzHTp4rK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvqvze/dJMcabpyb7W/mTcxdRiKalWaTIzHTp4rK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvqvze/dJMcabpyb7W/mTcxdRiKalWaTIzHTp4rK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbvqvze%2FdJMcabpyb7W%2FmTcxdRiKalWaTIzHTp4rK0%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;3312&quot; height=&quot;1968&quot; data-filename=&quot;스크린샷 2026-02-03 오후 9.24.14.png&quot; data-origin-width=&quot;3312&quot; data-origin-height=&quot;1968&quot;/&gt;&lt;/span&gt;&lt;/figure&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/cAJlnc/dJMcaiB97xP/yUuGMh0SheC9VH4OjSGVyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAJlnc/dJMcaiB97xP/yUuGMh0SheC9VH4OjSGVyK/img.png&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;534&quot; data-is-animation=&quot;false&quot; style=&quot;width: 44.3192%; margin-right: 10px;&quot; data-widthpercent=&quot;44.84&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAJlnc/dJMcaiB97xP/yUuGMh0SheC9VH4OjSGVyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAJlnc%2FdJMcaiB97xP%2FyUuGMh0SheC9VH4OjSGVyK%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;1816&quot; height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/upfnT/dJMcafeqALo/glFN5KKPyGn0SMHimnowg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/upfnT/dJMcafeqALo/glFN5KKPyGn0SMHimnowg1/img.png&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;240&quot; data-is-animation=&quot;false&quot; width=&quot;626&quot; height=&quot;150&quot; style=&quot;width: 54.518%;&quot; data-widthpercent=&quot;55.16&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/upfnT/dJMcafeqALo/glFN5KKPyGn0SMHimnowg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FupfnT%2FdJMcafeqALo%2FglFN5KKPyGn0SMHimnowg1%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;1004&quot; height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-batch/pull/5208&quot;&gt;PR #5208&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770120169797&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;Fix ID collision in MetaDataInstanceFactory for test scenarios with @SpringBatchTest by HongYeseul &amp;middot; Pull Request #5208 &amp;middot; spri&quot; data-og-description=&quot;Problem Tests using StepScopeTestUtils with @SpringBatchTest failed with ClassCastException due to ID collisions. MetaDataInstanceFactory always created instances with the same default IDs (123L), ...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/spring-projects/spring-batch/pull/5208&quot; data-og-url=&quot;https://github.com/spring-projects/spring-batch/pull/5208&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/NINUk/dJMb8YXGIeS/vj8kDMdJxUeulS0A8acP61/img.png?width=1200&amp;amp;height=600&amp;amp;face=982_110_1036_170,https://scrap.kakaocdn.net/dn/bLswy2/dJMb8RjXaBb/NQexHkWXTwFQHYReGKT4E1/img.png?width=1200&amp;amp;height=600&amp;amp;face=982_110_1036_170&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-batch/pull/5208&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/spring-projects/spring-batch/pull/5208&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/NINUk/dJMb8YXGIeS/vj8kDMdJxUeulS0A8acP61/img.png?width=1200&amp;amp;height=600&amp;amp;face=982_110_1036_170,https://scrap.kakaocdn.net/dn/bLswy2/dJMb8RjXaBb/NQexHkWXTwFQHYReGKT4E1/img.png?width=1200&amp;amp;height=600&amp;amp;face=982_110_1036_170');&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;Fix ID collision in MetaDataInstanceFactory for test scenarios with @SpringBatchTest by HongYeseul &amp;middot; Pull Request #5208 &amp;middot; spri&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Problem Tests using StepScopeTestUtils with @SpringBatchTest failed with ClassCastException due to ID collisions. MetaDataInstanceFactory always created instances with the same default IDs (123L), ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;내가 이런 곳에 기여를 할 수 있는 건가?&quot;라는 생각이 많이 들었지만, 인제님의 응원 덕분에 용기를 내어 PR을 올릴 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&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;merge가 되다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR을 올린 후, 약 열흘 정도가 지나고 이메일 알림이 하나 날아왔습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-03 오후 9.04.44.png&quot; data-origin-width=&quot;2366&quot; data-origin-height=&quot;1206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2YbFn/dJMcaac58vj/u916aaPFnVXVeOKrao4R5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2YbFn/dJMcaac58vj/u916aaPFnVXVeOKrao4R5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2YbFn/dJMcaac58vj/u916aaPFnVXVeOKrao4R5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2YbFn%2FdJMcaac58vj%2Fu916aaPFnVXVeOKrao4R5K%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;2366&quot; height=&quot;1206&quot; data-filename=&quot;스크린샷 2026-02-03 오후 9.04.44.png&quot; data-origin-width=&quot;2366&quot; data-origin-height=&quot;1206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-03 오후 9.05.24.png&quot; data-origin-width=&quot;1932&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqlteP/dJMcacoq7zB/I97MJKOkLoywWpwX14Uui1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqlteP/dJMcacoq7zB/I97MJKOkLoywWpwX14Uui1/img.png&quot; data-alt=&quot;짜릿했던 메인테이너 분께 LGTM 받기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqlteP/dJMcacoq7zB/I97MJKOkLoywWpwX14Uui1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqlteP%2FdJMcacoq7zB%2FI97MJKOkLoywWpwX14Uui1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;195&quot; data-filename=&quot;스크린샷 2026-02-03 오후 9.05.24.png&quot; data-origin-width=&quot;1932&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;짜릿했던 메인테이너 분께 LGTM 받기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때, 팀원 한 분과 회의 중이었는데 너무 기뻐서 바로 자랑했던 기억이 납니다. 생에 첫 오픈 소스 기여라니.&lt;/p&gt;
&lt;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;2140&quot; data-origin-height=&quot;950&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boPyQg/dJMcahJ4ych/wBzToThVIpRo2uk4m1XC7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boPyQg/dJMcahJ4ych/wBzToThVIpRo2uk4m1XC7K/img.png&quot; data-alt=&quot;어떤 기준으로 프로필이 뜨는 건진 모르겠지만 바로 두번째 언급에서 프로필을 찾아 볼 수 있었습니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boPyQg/dJMcahJ4ych/wBzToThVIpRo2uk4m1XC7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboPyQg%2FdJMcahJ4ych%2FwBzToThVIpRo2uk4m1XC7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;738&quot; height=&quot;328&quot; data-origin-width=&quot;2140&quot; data-origin-height=&quot;950&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;어떤 기준으로 프로필이 뜨는 건진 모르겠지만 바로 두번째 언급에서 프로필을 찾아 볼 수 있었습니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-batch/releases/tag/v6.0.2&quot;&gt;spring-batch v6.0.2 릴리즈&lt;/a&gt;에서 저의 커밋 내용이 반영되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러고 그 다음 온라인 모임에 참여했을 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인제님께서 스프링 배치 기여 축하드린다면서 샤라웃 ..! 까지 해주시고 감사했습니다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OtZ2Y/dJMcabwhQf8/AlNOZgCjvFs1k7BO6WpCb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OtZ2Y/dJMcabwhQf8/AlNOZgCjvFs1k7BO6WpCb0/img.png&quot; data-alt=&quot;한국 스프링 사용자 모임에서도 릴리즈 됐을 때 언급해주신 분도 계셔서 반가웠답니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OtZ2Y/dJMcabwhQf8/AlNOZgCjvFs1k7BO6WpCb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOtZ2Y%2FdJMcabwhQf8%2FAlNOZgCjvFs1k7BO6WpCb0%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;339&quot; height=&quot;463&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;920&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;한국 스프링 사용자 모임에서도 릴리즈 됐을 때 언급해주신 분도 계셔서 반가웠답니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 오픈소스 모임이 진행될 때는 spring-batch를 기여하겠다는 글은 못 본 것 같은데도 많은 관심을 가져주셔서 감사했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 인제님께서 해당 프로젝트의 경우 메인테이너 한 분께서 전부 검토하고 계시기 때문에 머지까지 엄청난 시간이 걸리는 경우가 잦다는데 운도 엄청 좋았던 케이스라고 해주셨습니다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그리고...&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 기여로 &quot;오픈소스에 기여해보고 싶다&quot;라는 몇 년간의 숙제(??)를 하나 넘게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 기여하는 것 자체에 대한 두려움이 많이 없어졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 여러 프로젝트에 기여하면서, 코드를 많이 읽고 레거시한 프로젝트도 빠르게 파악할 수 있는 개발자가 되고 싶습니다. 그리고 기여를 통해서 운영진 역할까지 맡을 수 있는 사람이 되고 싶다는 목표도 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 이 과정 전반에서 엄청난 도움을 주신 운영진 분들, 특히 &lt;b&gt;인제님&lt;/b&gt;께 정말 큰 감사의 인사를 드리고 싶습니다. 덕분에 오픈소스 기여에 대한 자신감을 가질 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 기수에도, 그 다음 기수에도 반드시 참여하고 싶은 마음입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈 소스에 조금이라도 기여하고 싶다는 마음이 있다면 꼭 다들 참여 해보셨으면 좋겠습니다!  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어주셔서 감사합니다.&lt;/p&gt;</description>
      <category>기타/일기</category>
      <author> 예슬</author>
      <guid isPermaLink="true">https://yeseul-dev.tistory.com/99</guid>
      <comments>https://yeseul-dev.tistory.com/99#entry99comment</comments>
      <pubDate>Tue, 3 Feb 2026 21:25:13 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot에서 HTTP 요청/응답 로깅 필터 구현하기</title>
      <link>https://yeseul-dev.tistory.com/98</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 운영하면서 디버깅 시 어려움을 겪었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;b&gt;예외가 발생하지 않았지만 오류인 경우&lt;/b&gt;를 파악하기 어려웠습니다. 예를 들어, 사용자는 A 옵션을 선택했다고 생각했으나 클라이언트 앱에서 잘못 처리되어 B로 전송되는 경우가 있었습니다. 이런 상황에서는 서버 입장에서 정상적으로 처리되기 때문에 예외 로그가 남지 않지만, 실제로는 사용자 의도와 다른 결과가 발생하는 문제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;기존에는 사용자가 어떤 요청을 했는지 로그 포맷이 따로 정해져 있지 않아 필요한 시점에 개별적으로 로그를 추가하는 방식으로 작업했기 때문에 다음과 같은 문제점이 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 요청/응답 흐름을 추적하기 어려움&lt;/li&gt;
&lt;li&gt;일관되지 않은 로그 형식&lt;/li&gt;
&lt;li&gt;문제 발생 시 원인 파악에 시간이 오래 걸림&lt;/li&gt;
&lt;li&gt;새로운 로그가 필요하다면 재배포가 필요함&lt;/li&gt;
&lt;/ul&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;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP 요청의 특성과 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청의 InputStream은 &lt;b&gt;한 번만 읽을 수 있습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 필터에서 body를 읽어버리면 실제 컨트롤러에서는 해당 내용을 읽을 수 없게 됩니다.&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;ContentCachingWrapper 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 제공하는 다음 클래스들을 사용하면 이 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ContentCachingRequestWrapper
ContentCachingResponseWrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Wrapper들은 요청/응답의 내용을 &lt;b&gt;내부 버퍼에 캐싱&lt;/b&gt;하여, 여러 번 읽을 수 있도록 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LoggingFilter 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 API 요청이 LoggingFilter를 거치도록 설정하고, Wrapper를 활용하여 다음 정보들을 로그로 남겼습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP Method&lt;/li&gt;
&lt;li&gt;요청 URL&lt;/li&gt;
&lt;li&gt;요청 Body&lt;/li&gt;
&lt;li&gt;응답 Body&lt;/li&gt;
&lt;li&gt;HTTP 상태 코드&lt;/li&gt;
&lt;li&gt;API 실행 시간&lt;/li&gt;
&lt;/ul&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;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그 예시&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[INFO] [GET] /api/v1/products - Status: 200 - 45ms
Request: {&quot;language&quot;: &quot;ko&quot;}
Response: {&quot;products&quot;: [...]}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;일관된 로그 형식&lt;/b&gt;: 모든 API에 대해 동일한 형식으로 로깅&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디버깅 효율성 향상&lt;/b&gt;: 요청/응답을 한눈에 확인 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CloudWatch 활용&lt;/b&gt;: 로그 검색 및 필터링이 용이해짐&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 모니터링&lt;/b&gt;: API별 실행 시간 추적 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유저별 모니터링&lt;/b&gt;: API별 어떤 사용자가 어떤 요청을 했는지 추적 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoggingFilter만 INFO 레벨 유지하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 로거들은 WARN 또는 ERROR 레벨로 상향 조정한다면 불필요한 로그 출력 최소화로 성능 개선도 할 수 있을 것입니다.&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;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 필터 구현만으로도 디버깅과 모니터링이 훨씬 수월해졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제는 클라이언트-서버 간 데이터 불일치나 예상치 못한 동작을 로그만으로도 빠르게 파악할 수 있어&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실제 서비스 환경에서의 이슈 대응 시간이 크게 단축되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 예전에는 이슈 파악을 위해 여러 곳에 임시 로그를 추가하고 재배포해야 했다면,&amp;nbsp;&lt;br /&gt;이제는&amp;nbsp;CloudWatch에서&amp;nbsp;요청/응답을&amp;nbsp;바로&amp;nbsp;확인하며&amp;nbsp;문제를&amp;nbsp;해결할&amp;nbsp;수&amp;nbsp;있게&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;</description>
      <category>프로젝트/프로젝트 과정</category>
      <author> 예슬</author>
      <guid isPermaLink="true">https://yeseul-dev.tistory.com/98</guid>
      <comments>https://yeseul-dev.tistory.com/98#entry98comment</comments>
      <pubDate>Sat, 3 Jan 2026 17:01:06 +0900</pubDate>
    </item>
    <item>
      <title>[K-DEVCON] 시스템 디자인 스터디2 - 3주차 후기(3장 구글 맵)</title>
      <link>https://yeseul-dev.tistory.com/97</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이 글은 2025년 12월부터 K-DEVCON에서 진행된 시스템 디자인 스터디 시즌2의 내용을 정리한 글입니다.&lt;br /&gt;&lt;br /&gt;'가상 면접 사례로 배우는 대규모 시스템 설계 기초 2권'을 함께 읽고 토론하는 형식으로 진행되는 스터디로,&lt;br /&gt;책 내용을 그대로 옮긴 것이 아니라 스터디에서 논의된 내용과 제가 추가로 찾아본 내용을 함께 담았습니다.&lt;br /&gt;&lt;br /&gt;좋은 스터디를 만들어주신 K-DEVCON 운영진 분들께 감사드립니다!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 12월부터 K-DEVCON에서 진행된 시스템 디자인 스터디 시즌2에 참여하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'가상 면접 사례로 배우는 대규모 시스템 설계 기초 2권'을 함께 읽고 토론하는 스터디인데요, 이번 주 주제는 3장 - 구글 맵이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디를 마치고 궁금했던 부분들을 직접 찾아보면서 정리해보았습니다.&lt;/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;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지도는 N&amp;times;N개의 작은 타일(tile)들이 모여서 하나의 큰 지도를 만드는 구조인데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 타일들이 주로 PNG 같은 이미지 형식으로 제공된다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;게임에서는 벡터를 많이 쓰는데 왜 구글 맵은 PNG 이미지 타일을 쓸까? 라는 생각이 들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾아보니 래스터 타일과 벡터 타일은 각각 명확한 장단점이 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3360&quot; data-origin-height=&quot;1715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ef7W5D/dJMcaaDVB4T/mueKheq9tqEEgKgCpMwcq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ef7W5D/dJMcaaDVB4T/mueKheq9tqEEgKgCpMwcq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ef7W5D/dJMcaaDVB4T/mueKheq9tqEEgKgCpMwcq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fef7W5D%2FdJMcaaDVB4T%2FmueKheq9tqEEgKgCpMwcq0%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;3360&quot; height=&quot;1715&quot; data-origin-width=&quot;3360&quot; data-origin-height=&quot;1715&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;래스터 타일(PNG/JPG)의 장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 디바이스에서 렌더링이 쉽다 - 그냥 이미지니까&lt;/li&gt;
&lt;li&gt;서버에서 미리 렌더링해두기 때문에 클라이언트 부하가 적다&lt;/li&gt;
&lt;li&gt;복잡한 스타일과 디테일 표현이 자유롭다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d63njK/dJMcacaFZK8/1Mea9XQQejJbSfRFAUx1d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d63njK/dJMcacaFZK8/1Mea9XQQejJbSfRFAUx1d0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d63njK/dJMcacaFZK8/1Mea9XQQejJbSfRFAUx1d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd63njK%2FdJMcacaFZK8%2F1Mea9XQQejJbSfRFAUx1d0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;502&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;벡터 타일의 장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 크기가 20-50% 더 작다&lt;/li&gt;
&lt;li&gt;확대/축소해도 깔끔하다 (픽셀레이션 없음)&lt;/li&gt;
&lt;li&gt;스타일을 동적으로 변경할 수 있다&lt;/li&gt;
&lt;li&gt;하지만... 클라이언트에서 실시간 렌더링이 필요하다 &amp;rarr; 클라이언트의 하드웨어 성능이 좋아야 한다 &lt;s&gt;&amp;rarr; 게임을 할 때 좋은 PC를 사용하는 것도 이 경우에 포함되는 게 아닐까.......?&lt;/s&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 &lt;b&gt;서버 리소스 vs 클라이언트 리소스의 트레이드오프&lt;/b&gt;였습니다. 구글 맵처럼 수억 명이 사용하는 서비스라면, 서버에서 한 번 렌더링해서 CDN에 올려두고 클라이언트는 단순히 이미지만 받아 표시하는 게 훨씬 효율적이겠죠. 특히 저사양 디바이스까지 고려하면 래스터 타일이 합리적인 선택이라는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 경로 안내용 타일은 벡터 기반(이진 파일)으로 따로 제공하는 구조인 것 같아 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;관련 아티클:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.maptiler.com/guides/general/raster-vs-vector-map-tiles-what-is-the-difference-between-the-two-data-types/&quot;&gt;Raster vs Vector Map Tiles - MapTiler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.stadiamaps.com/guides/switching-your-maps-from-raster-to-vector-tiles/&quot;&gt;Switching from Raster to Vector Maps - Stadia Maps&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP Keep-Alive, 생각보다 중요하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서 위치 서비스가 HTTP를 keep-alive 옵션과 함께 사용하면 &lt;b&gt;효율적이다&lt;/b&gt;라고 나와서 &quot;이게 왜 효율적인거지?&quot; 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HTTP Keep-Alive&lt;/b&gt;란 한 번 연결한 TCP 커넥션을 여러 HTTP 요청/응답에 재사용하는 것인데요, 차이를 비교해보면 이렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Keep-Alive 없이 매번 새 연결을 만들면&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 1개 = TCP 연결 생성 + 요청 + 응답 + TCP 연결 종료&lt;/li&gt;
&lt;li&gt;요청 10개 = 위의 과정 &amp;times; 10&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Keep-Alive를 쓰면&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP 연결 생성 (1회) + 요청 10개 + 응답 10개 + TCP 연결 종료 (1회)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 SSL/TLS를 쓰는 HTTPS에서는 핸드셰이크 비용이 더 크기 때문에 효과가 상당하다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치 정보처럼 짧은 주기로 계속 업데이트되는 데이터에는 필수적인 기능이었던 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 HTTP/1.1부터는 기본값이 keep-alive입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.0에서는 명시적으로 &lt;code&gt;Connection: keep-alive&lt;/code&gt; 헤더를 보내야 했다고 하네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;관련 아티클:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/HTTP_persistent_connection&quot;&gt;HTTP persistent connection - Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.imperva.com/learn/performance/http-keep-alive/&quot;&gt;What is HTTP Keep-Alive? - Imperva&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면 HTTP/3는?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디 중에 HTTP/3 얘기가 나왔는데, 구글이 만든 QUIC 프로토콜 기반이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UDP 포트를 하나 더 열어야 한다는 게 인상적이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HTTP/3의 핵심:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP 대신 &lt;b&gt;UDP 기반&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Head-of-line blocking 문제 해결&lt;/li&gt;
&lt;li&gt;기본 암호화 (TLS 1.3 내장)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YouTube에서 이미 사용 중이라고 하는데, HTTP/3는 아직 제가 잘 몰라서 나중에 좀 더 공부해봐야 할 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가장 궁금했던 것: 경로 알고리즘이 과연 트래픽을 분산시킬까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 제일 궁금했던 부분입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;모두가 같은 시간에 네비게이션을 켜면 전부 A 경로로 몰리는 거 아닌가?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;찾아보니 구글 맵의 경로 선택 방식은 생각보다 훨씬 정교했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Google Maps의 경로 선택 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 과거 패턴 + 실시간 교통 상황 결합&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;과거 데이터: &quot;280번 고속도로는 오전 6-7시에 시속 65mph, 오후에는 15-20mph&quot;&lt;/li&gt;
&lt;li&gt;실시간 데이터: 지금 이 도로의 교통 상황&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 머신러닝으로 미래 교통 예측 (10-20분 후)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DeepMind와 협업한 Graph Neural Networks 활용&lt;/li&gt;
&lt;li&gt;ETA&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(도착 예정 시간 - Estimated Time of Arrival)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;정확도를 97% 이상으로 개선&lt;/li&gt;
&lt;li&gt;특히 베를린, 자카르타, 상파울루, 시드니, 도쿄, 워싱턴 DC에서 큰 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 다양한 요소 고려&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도로 품질 (포장/비포장/자갈/진흙)&lt;/li&gt;
&lt;li&gt;도로 크기와 직진성&lt;/li&gt;
&lt;li&gt;실시간 사고/공사 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심은 &quot;예측&quot;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;현재: A 경로가 가장 빠름 (10분)
30분 후 예측: A 경로에 정체 발생 예상 (25분)
            B 경로는 여전히 원활 (15분)
&amp;rarr; B 경로 추천&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 예시&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병원 예약 시간에 맞춰 출발했는데, 현재는 원활하지만 30분 후 예상되는 정체를 구글 맵이 미리 예측하고 우회 경로를 제안한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면 모든 사람이 같은 경로로 몰리는 문제는?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 여러 이유로인해 복합적으로 작용되어 자연스럽게 분산됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;시간차 출발&lt;/b&gt; - 모두가 정확히 같은 시간에 출발하지 않음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예측 기반 재분배&lt;/b&gt; - 이미 많이 선택된 경로는 &quot;곧 혼잡&quot; 예측&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개인 설정&lt;/b&gt; - 유료도로 회피, 고속도로 선호 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실시간 재계산&lt;/b&gt; - 주행 중에도 계속 경로 재평가&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이 내용 말고도 더 많은 이유들이 있겠지만...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 어려워서 완전히 이해하지는 못한 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;관련 아티클:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://blog.google/products/maps/google-maps-101-how-ai-helps-predict-traffic-and-determine-routes/&quot;&gt;Google Maps 101: How AI helps predict traffic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.altexsoft.com/blog/traffic-prediction/&quot;&gt;Traffic Prediction with Machine Learning - AltexSoft&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;지도 이미지를 CDN에 캐싱하는데, 건물이 새로 생기거나 도로가 바뀌면 어떻게 업데이트할까요?&lt;br /&gt;TTL도 너무 길면 문제가 될 것 같은데 말이죠.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TTL은 데이터 특성에 따라 다르게&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기본 지도&lt;/b&gt;: 24~48시간 (건물, 도로 등은 자주 안 바뀜)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;교통 정보&lt;/b&gt;: 5~15분 (실시간성이 중요)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;날씨 레이어&lt;/b&gt;: 1시간 정도&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;업데이트가 필요할때는?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;버전 기반 무효화&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타임스탬프 또는 해시 기반 캐시 키&lt;/li&gt;
&lt;li&gt;소스 데이터 변경 시 자동 업데이트&lt;/li&gt;
&lt;li&gt;중요 업데이트(도로 폐쇄, 건물 변경)는 즉시 무효화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL이 48시간이어도 도로가 바뀌면 바로 반영해야겠죠. 이때 사용하는 전략들이 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;버전 기반 무효화&lt;/b&gt;&lt;br /&gt;URL에 버전을 붙여서 /tiles/v2/{z}/{x}/{y}.png 이런 식으로 관리하면, 버전만 올려도 전체 캐시가 새로고침됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지역별 무효화&lt;br /&gt;&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;강남역 근처 건물 하나 바뀌었다고 서울 전체 타일을 갱신할 필요 없이, 해당 좌표 범위의 타일만 선택적으로 삭제합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-start-index=&quot;870&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Stale-While-Revalidate 전략&lt;/b&gt;으로 만료 직전 백그라운드에서 미리 새로고침해두면, 사용자는 빈 화면 없이 부드러운 경험을 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인기 있는 타일이 만료되기 전에 백그라운드에서 미리 새로고침(Background refresh)을 수행하여 캐시 히트율을 85% 이상으로 유지하면서도 데이터 정확성을 확보하는 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;관련 아티클:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.maplibrary.org/1387/comparison-of-tile-map-caching-methods/&quot;&gt;8 Tile Map Caching Methods&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.maplibrary.org/10552/how-to-enhance-map-performance-using-caching-layers/&quot;&gt;7 Ways to Enhance Map Performance Using Caching Layers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소에 매일 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;당연하게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;사용하는 지도 서비스인데, 시스템 설계 관점에서 보니 정말 많은 고민과 최적화가 숨어있는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 지도 서비스는 수학적인 개념이 많이 들어있어서 다른 주제보다 좀 더 어렵게 느껴졌는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 스터디에서는 드디어 3주간의 지도 시리즈를 마치고 다른 주제를 공부하게 되어 기대 중입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그 외 참고하면 좋은 아티클:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카카오맵이 최적 경로를 결정하는 데까지&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1767054652695&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;카카오맵에서 불편함을 느끼고, 궁금증을 해결하다.&amp;nbsp;&amp;nbsp;&amp;nbsp;안녕하세요! NewCodes입니다!!&amp;nbsp;우선 이전 포스팅에서제가 가졌던 궁금증에 대해 다시 살펴보겠습니다.&amp;nbsp;&amp;nbsp;&amp;nbsp;위 그림은 제가 사는 동네입니&quot; data-og-host=&quot;newcodes.tistory.com&quot; data-og-source-url=&quot;https://newcodes.tistory.com/entry/%EC%B9%B4%EC%B9%B4%EC%98%A4%EB%A7%B5%EC%9D%B4-%EC%B5%9C%EC%A0%81-%EA%B2%BD%EB%A1%9C%EB%A5%BC-%EA%B2%B0%EC%A0%95%ED%95%98%EB%8A%94-%EB%8D%B0%EA%B9%8C%EC%A7%80#google_vignette&quot; data-og-url=&quot;https://newcodes.tistory.com/entry/%EC%B9%B4%EC%B9%B4%EC%98%A4%EB%A7%B5%EC%9D%B4-%EC%B5%9C%EC%A0%81-%EA%B2%BD%EB%A1%9C%EB%A5%BC-%EA%B2%B0%EC%A0%95%ED%95%98%EB%8A%94-%EB%8D%B0%EA%B9%8C%EC%A7%80&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bl3jTe/hyZQWPGZus/9AumJOasY8R3XmthyGE0IK/img.jpg?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/MWFHf/hyZQrcCkG2/aqFRv0oZKNgl60LNsYHpj1/img.jpg?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/53kMr/hyZQxDTpnP/cSI5pomuNP2vxfpKhgn5qK/img.png?width=1132&amp;amp;height=1336&amp;amp;face=0_0_1132_1336&quot;&gt;&lt;a href=&quot;https://newcodes.tistory.com/entry/%EC%B9%B4%EC%B9%B4%EC%98%A4%EB%A7%B5%EC%9D%B4-%EC%B5%9C%EC%A0%81-%EA%B2%BD%EB%A1%9C%EB%A5%BC-%EA%B2%B0%EC%A0%95%ED%95%98%EB%8A%94-%EB%8D%B0%EA%B9%8C%EC%A7%80#google_vignette&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://newcodes.tistory.com/entry/%EC%B9%B4%EC%B9%B4%EC%98%A4%EB%A7%B5%EC%9D%B4-%EC%B5%9C%EC%A0%81-%EA%B2%BD%EB%A1%9C%EB%A5%BC-%EA%B2%B0%EC%A0%95%ED%95%98%EB%8A%94-%EB%8D%B0%EA%B9%8C%EC%A7%80#google_vignette&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bl3jTe/hyZQWPGZus/9AumJOasY8R3XmthyGE0IK/img.jpg?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/MWFHf/hyZQrcCkG2/aqFRv0oZKNgl60LNsYHpj1/img.jpg?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/53kMr/hyZQxDTpnP/cSI5pomuNP2vxfpKhgn5qK/img.png?width=1132&amp;amp;height=1336&amp;amp;face=0_0_1132_1336');&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;카카오맵에서 불편함을 느끼고, 궁금증을 해결하다.&amp;nbsp;&amp;nbsp;&amp;nbsp;안녕하세요! NewCodes입니다!!&amp;nbsp;우선 이전 포스팅에서제가 가졌던 궁금증에 대해 다시 살펴보겠습니다.&amp;nbsp;&amp;nbsp;&amp;nbsp;위 그림은 제가 사는 동네입니&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;newcodes.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카카오맵이 빠르게 길을 찾아주는 방법: CCH를 이용한 개편기 - kakao 기술 블로그&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1767054770115&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;카카오맵이 빠르게 길을 찾아주는 방법: CCH를 이용한 개편기 - tech.kakao.com&quot; data-og-description=&quot;카카오맵에서는 도보&amp;middot;자전거 길찾기 서비스를 제공하고 있습니다. 일반적으로 길찾기 ...&quot; data-og-host=&quot;tech.kakao.com&quot; data-og-source-url=&quot;https://tech.kakao.com/posts/436&quot; data-og-url=&quot;https://tech.kakao.com/posts/436&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lpzMi/hyZPDqEM2e/gZzQi9B4WSD7PdhTN6mZEk/img.png?width=896&amp;amp;height=372&amp;amp;face=0_0_896_372,https://scrap.kakaocdn.net/dn/BIzVu/hyZQyCLFB2/KFUCTQu9n2spAFNktCnB21/img.png?width=896&amp;amp;height=372&amp;amp;face=0_0_896_372,https://scrap.kakaocdn.net/dn/K8FFe/hyZQzuW0pK/y8ypeDZTfLfZeGwOROgKp0/img.png?width=896&amp;amp;height=372&amp;amp;face=0_0_896_372&quot;&gt;&lt;a href=&quot;https://tech.kakao.com/posts/436&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tech.kakao.com/posts/436&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lpzMi/hyZPDqEM2e/gZzQi9B4WSD7PdhTN6mZEk/img.png?width=896&amp;amp;height=372&amp;amp;face=0_0_896_372,https://scrap.kakaocdn.net/dn/BIzVu/hyZQyCLFB2/KFUCTQu9n2spAFNktCnB21/img.png?width=896&amp;amp;height=372&amp;amp;face=0_0_896_372,https://scrap.kakaocdn.net/dn/K8FFe/hyZQzuW0pK/y8ypeDZTfLfZeGwOROgKp0/img.png?width=896&amp;amp;height=372&amp;amp;face=0_0_896_372');&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;카카오맵이 빠르게 길을 찾아주는 방법: CCH를 이용한 개편기 - tech.kakao.com&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카카오맵에서는 도보&amp;middot;자전거 길찾기 서비스를 제공하고 있습니다. 일반적으로 길찾기 ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tech.kakao.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>
      <author> 예슬</author>
      <guid isPermaLink="true">https://yeseul-dev.tistory.com/97</guid>
      <comments>https://yeseul-dev.tistory.com/97#entry97comment</comments>
      <pubDate>Tue, 30 Dec 2025 09:26:13 +0900</pubDate>
    </item>
    <item>
      <title>Java/Kotlin 이미지 리사이징 라이브러리 간단 성능 비교 및 테스트</title>
      <link>https://yeseul-dev.tistory.com/96</link>
      <description>&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 개발하고 있는 서비스는 이미지를 많이 다루기 때문에 썸네일 서버가 필수적입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 줌(zum.com)의 썸네일 서버를 함께 사용했으나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 중 이슈가 발생했고 외부 의존성을 제거하기 위해 썸네일 작업을 자체적으로 처리하기로 결정했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그래서 구글링을 통해 찾을 수 있었던 대표적인 라이브러리들을 비교해서 적합한 라이브러리를 선택하기로 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&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;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Graphics2D&lt;/b&gt; - Java 기본 API&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Image.getScaledInstance()&lt;/b&gt; - Java 기본 API&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Imgscalr&lt;/b&gt; - 간단하고 효율적인 리사이징 전용 라이브러리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Thumbnailator&lt;/b&gt; - 사용하기 쉽고 기능이 풍부한 라이브러리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Marvin&lt;/b&gt; - 이미지 처리 프레임워크&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중에서 저희 서비스는 &lt;b&gt;Imgscalr&lt;/b&gt;를 사용하기로 했는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 간단하게 테스트를 거친 후 왜 적용하게 되었는지 적어보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. JDK 기본 제공 방식&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 Graphics2D&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 기본 그래픽 라이브러리인 AWT(Abstract Window Toolkit)를 사용하는 방식입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;java.awt.Graphics2D&lt;/li&gt;
&lt;li&gt;외부 라이브러리 의존성 없음&lt;/li&gt;
&lt;li&gt;세밀한 렌더링 옵션 제어 가능&lt;/li&gt;
&lt;li&gt;구현이 다소 복잡할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;import java.awt.RenderingHints
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO

fun resizeWithGraphics2D(inputFile: File, targetWidth: Int): BufferedImage {
    val originalImage = ImageIO.read(inputFile)
    val targetHeight = calculateTargetHeight(
        originalImage.width, 
        originalImage.height, 
        targetWidth
    )
    
    val resizedImage = BufferedImage(
        targetWidth, 
        targetHeight, 
        BufferedImage.TYPE_INT_RGB
    )
    
    val graphics = resizedImage.createGraphics()
    
    // 고품질 렌더링 설정
    graphics.setRenderingHint(
        RenderingHints.KEY_INTERPOLATION,
        RenderingHints.VALUE_INTERPOLATION_BILINEAR
    )
    graphics.setRenderingHint(
        RenderingHints.KEY_RENDERING,
        RenderingHints.VALUE_RENDER_QUALITY
    )
    graphics.setRenderingHint(
        RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON
    )
    
    graphics.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null)
    graphics.dispose()
    
    return resizedImage
}

private fun calculateTargetHeight(
    originalWidth: Int, 
    originalHeight: Int, 
    targetWidth: Int
): Int {
    val aspectRatio = originalHeight.toDouble() / originalWidth.toDouble()
    return (targetWidth * aspectRatio).roundToInt()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 Image.getScaledInstance()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Image 클래스의 기본 스케일링 메서드를 사용하는 방식입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;javax.imageio.ImageIO&lt;/li&gt;
&lt;li&gt;사용이 매우 간단함&lt;/li&gt;
&lt;li&gt;성능이 가장 느림&lt;/li&gt;
&lt;li&gt;이미지 품질 제어가 제한적&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;import java.awt.Image
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO

fun resizeWithScaledInstance(inputFile: File, targetWidth: Int): BufferedImage {
    val originalImage = ImageIO.read(inputFile)
    val targetHeight = calculateTargetHeight(
        originalImage.width, 
        originalImage.height, 
        targetWidth
    )
    
    // Image.SCALE_SMOOTH: 가장 부드러운 스케일링
    val scaledImage = originalImage.getScaledInstance(
        targetWidth, 
        targetHeight, 
        Image.SCALE_SMOOTH
    )
    
    // Image 객체를 BufferedImage로 변환
    val bufferedImage = BufferedImage(
        targetWidth, 
        targetHeight, 
        BufferedImage.TYPE_INT_RGB
    )
    val g2d = bufferedImage.createGraphics()
    g2d.drawImage(scaledImage, 0, 0, null)
    g2d.dispose()
    
    return bufferedImage
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 외부 라이브러리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 Thumbnailator&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하기 쉬운 Fluent API를 제공하며 다양한 기능을 지원합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌더 패턴으로 읽기 쉬운 코드&lt;/li&gt;
&lt;li&gt;워터마크, 회전, 크롭 등 추가 기능 풍부&lt;/li&gt;
&lt;li&gt;활발한 유지보수 (최근까지 업데이트)&lt;/li&gt;
&lt;li&gt;직관적인 API&lt;/li&gt;
&lt;li&gt;공식 문서
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitHub: &lt;a href=&quot;https://github.com/coobird/thumbnailator&quot;&gt;https://github.com/coobird/thumbnailator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Javadoc: &lt;a href=&quot;https://coobird.github.io/thumbnailator/javadoc/0.4.20/&quot;&gt;https://coobird.github.io/thumbnailator/javadoc/0.4.20/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Examples: &lt;a href=&quot;https://github.com/coobird/thumbnailator/wiki/Examples&quot;&gt;https://github.com/coobird/thumbnailator/wiki/Examples&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'net.coobird:thumbnailator:0.4.19'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;import net.coobird.thumbnailator.Thumbnails
import java.io.File

// 간단한 리사이징
fun resizeWithThumbnailator(inputFile: File, targetWidth: Int): File {
    val outputFile = File(&quot;output_thumbnailator.jpg&quot;)
    
    Thumbnails.of(inputFile)
        .width(targetWidth)  // 너비만 지정하면 비율 자동 유지
        .toFile(outputFile)
    
    return outputFile
}

// 고급 옵션 사용
fun resizeWithThumbnailatorAdvanced(
    inputFile: File, 
    targetWidth: Int,
    quality: Double = 0.9
): File {
    val outputFile = File(&quot;output_thumbnailator_quality.jpg&quot;)
    
    Thumbnails.of(inputFile)
        .width(targetWidth)
        .outputQuality(quality)  // JPEG 압축 품질
        .outputFormat(&quot;jpg&quot;)
        .toFile(outputFile)
    
    return outputFile
}

// BufferedImage로 반환받기
fun resizeWithThumbnailatorAsImage(
    inputFile: File, 
    targetWidth: Int
): BufferedImage {
    return Thumbnails.of(inputFile)
        .width(targetWidth)
        .outputQuality(0.85)
        .asBufferedImage()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Imgscalr&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 API로 한 줄에 리사이징이 가능한 라이브러리입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성능과 품질의 균형이 좋음&lt;/li&gt;
&lt;li&gt;비율 유지 자동 처리&lt;/li&gt;
&lt;li&gt;기능이 제한적 (리사이징, 크롭, 회전만)&lt;/li&gt;
&lt;li&gt;2013년 이후 업데이트 중단&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2013년 이후 업데이트 중단&lt;/b&gt; (마지막 릴리즈: 4.2, 2012년)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.imgscalr:imgscalr-lib:4.2'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;import org.imgscalr.Scalr
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO

// QUALITY 모드 (품질 우선)
fun resizeWithImgscalrQuality(inputFile: File, targetWidth: Int): BufferedImage {
    val originalImage = ImageIO.read(inputFile)
    
    return Scalr.resize(
        originalImage,
        Scalr.Method.QUALITY,      // 품질 우선
        Scalr.Mode.FIT_TO_WIDTH,   // 너비 기준 맞춤
        targetWidth
    )
}

// SPEED 모드 (속도 우선)
fun resizeWithImgscalrSpeed(inputFile: File, targetWidth: Int): BufferedImage {
    val originalImage = ImageIO.read(inputFile)
    
    return Scalr.resize(
        originalImage,
        Scalr.Method.SPEED,        // 속도 우선
        Scalr.Mode.FIT_TO_WIDTH,
        targetWidth
    )
}

// 정확한 크기 지정
fun resizeWithImgscalrExact(
    inputFile: File, 
    width: Int, 
    height: Int
): BufferedImage {
    val originalImage = ImageIO.read(inputFile)
    
    return Scalr.resize(
        originalImage,
        Scalr.Method.QUALITY,
        Scalr.Mode.FIT_EXACT,      // 정확한 크기로 맞춤
        width,
        height
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 Marvin&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 처리 프레임워크&lt;/li&gt;
&lt;li&gt;필터, 효과 등 다양한 이미지 처리 가능&lt;/li&gt;
&lt;li&gt;API가 다소 복잡함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'com.github.downgoon:marvin:1.5.5'
implementation 'com.github.downgoon:MarvinPlugins:1.5.5'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766048457345&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import marvin.image.MarvinImage
import org.marvinproject.image.transform.scale.Scale
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO

fun resizeWithMarvin(inputFile: File, targetWidth: Int): BufferedImage {
    val image = ImageIO.read(inputFile)
    
    val originWidth = image.width
    val originHeight = image.height
    val targetHeight = calculateTargetHeight(originWidth, originHeight, targetWidth)
    
    // 다운스케일링만 수행
    if (originWidth &amp;gt;= targetWidth || originHeight &amp;gt;= targetHeight) {
        val imageMarvin = MarvinImage(image)
        
        val scale = Scale()
        scale.load()
        scale.setAttribute(&quot;newWidth&quot;, targetWidth)
        scale.setAttribute(&quot;newHeight&quot;, targetHeight)
        scale.process(imageMarvin.clone(), imageMarvin, null, null, false)
        
        // JPEG는 알파 채널을 지원하지 않으므로 제거
        return imageMarvin.bufferedImageNoAlpha
    }
    
    return image
}&lt;/code&gt;&lt;/pre&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;/h2&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;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 시간 측정 (CPU 사용량 지표)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 시간은 CPU 사용량을 간접적으로 나타내는 지표라고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;System.nanoTime()을 사용하여 나노초 단위로 측정한 후 밀리초로 변환했습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val startTime = System.nanoTime()

// 리사이징 작업 수행
for (i in 0..&amp;lt;TEST_ITERATIONS) {
    val resizedImage = Scalr.resize(originalImage, Scalr.Method.SPEED, Scalr.Mode.FIT_TO_WIDTH, targetWidth)
}

val endTime = System.nanoTime()
val executionTime = (endTime - startTime) / 1_000_000 // 나노초 -&amp;gt; 밀리초
val averageTime = executionTime / TEST_ITERATIONS
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;측정 시 주의사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 테스트마다 10회 반복 수행하여 평균값 계산&lt;/li&gt;
&lt;li&gt;첫 실행은 JVM 워밍업 효과가 있을 수 있어 여러 번 반복 측정&lt;/li&gt;
&lt;/ul&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM의 Runtime 클래스를 사용하여 테스트 전후의 메모리 사용량을 측정했습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 메모리 측정을 위한 헬퍼 함수
private val usedMemory: Long
    get() {
        val runtime = Runtime.getRuntime()
        return runtime.totalMemory() - runtime.freeMemory()
    }

// 테스트 전 가비지 컬렉션 강제 실행
System.gc()
Thread.sleep(100) // GC 완료 대기

val startMemory = usedMemory

// 리사이징 작업 수행
for (i in 0..&amp;lt;TEST_ITERATIONS) {
    val resizedImage = Scalr.resize(originalImage, Scalr.Method.SPEED, Scalr.Mode.FIT_TO_WIDTH, targetWidth)
}

val endMemory = usedMemory
val memoryUsed = max(0, endMemory - startMemory) // 음수 방지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;측정 시 주의사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 전 System.gc()로 가비지 컬렉션을 강제 실행하여 정확도 향상&lt;/li&gt;
&lt;li&gt;JVM의 메모리 관리 특성상 정확한 측정이 어려울 수 있으나, 상대적 비교에는 유효&lt;/li&gt;
&lt;li&gt;totalMemory() - freeMemory()로 실제 사용 중인 메모리 계산&lt;/li&gt;
&lt;/ul&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 이미지 파일의 크기를 측정하고, 원본 대비 압축률을 계산했습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val outputFile = File(&quot;output.jpg&quot;)
val fileSize = outputFile.length() // 바이트 단위

// 압축률 계산
val compressionRatio = (1 - (fileSize.toDouble() / originalFileSize)) * 100

// 파일 크기를 사람이 읽기 쉬운 형식으로 변환 (B, KB, MB, GB 등)
private fun formatBytes(bytes: Long): String {
    // 1KB(1024바이트) 미만이면 바이트 단위 그대로 반환
    if (bytes &amp;lt; 1024) return &quot;$bytes B&quot;
    
    // 로그를 이용해 어떤 단위를 사용할지 결정
    // ln(bytes) / ln(1024) = log₁₀₂₄(bytes)
    // 결과: 1=KB, 2=MB, 3=GB, 4=TB, 5=PB, 6=EB
    val exp = (ln(bytes.toDouble()) / ln(1024.0)).toInt()
    
    // 지수에 해당하는 단위 접두사 선택
    // exp=1이면 K(킬로), exp=2이면 M(메가), exp=3이면 G(기가)...
    val pre = &quot;KMGTPE&quot;[exp - 1]
    
    // 원본 바이트를 해당 단위로 나눠서 소수점 2자리까지 포맷팅
    // 예: 2048 bytes &amp;rarr; 2048 / 1024&amp;sup1; = 2.00 KB
    // 예: 5242880 bytes &amp;rarr; 5242880 / 1024&amp;sup2; = 5.00 MB
    return String.format(&quot;%.2f %sB&quot;, bytes / 1024.0.pow(exp.toDouble()), pre)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 측정 방식으로 아래와 같이 비교 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 이미지&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원본 크기: 848 x 1264 pixels&lt;/li&gt;
&lt;li&gt;원본 파일 크기: 713.26 KB&lt;/li&gt;
&lt;li&gt;목표 크기: NNN x 626 (원본 비율 유지)&lt;/li&gt;
&lt;li&gt;테스트 반복 횟수: 10회&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스에서 사용될 이미지를 기준으로 테스트 해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;테스트 1: 420x626 픽셀로 축소&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 이미지 848*N 사이즈 이미지를 420*?(비율 유지)로 변경하는 경우&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1736&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BASwC/dJMcahbUrQW/Cu3FJpsTaigkmhfyBSCoy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BASwC/dJMcahbUrQW/Cu3FJpsTaigkmhfyBSCoy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BASwC/dJMcahbUrQW/Cu3FJpsTaigkmhfyBSCoy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBASwC%2FdJMcahbUrQW%2FCu3FJpsTaigkmhfyBSCoy1%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;1736&quot; height=&quot;696&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1736&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;테스트 2: 848x1264 픽셀 유지 (원본 크기)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 이미지 848*N 사이즈 이미지를 848*?(원본 크기와 동일하게)로 변경하는 경우&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1742&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q2VHu/dJMcai2UwoV/KV8aLCa48wrPfrXGXB3Kn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q2VHu/dJMcai2UwoV/KV8aLCa48wrPfrXGXB3Kn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q2VHu/dJMcai2UwoV/KV8aLCa48wrPfrXGXB3Kn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq2VHu%2FdJMcai2UwoV%2FKV8aLCa48wrPfrXGXB3Kn1%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;1742&quot; height=&quot;696&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1742&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;속도 순위 (빠른 순):&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Imgscalr (SPEED)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Imgscalr (QUALITY)&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Graphics2D (압축)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메모리 사용량 순위 (적은 순):&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Thumbnailator&amp;nbsp;(압축)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Thumbnailator(기본)&lt;/li&gt;
&lt;li&gt;Imgscalr (SPEED)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 라이브러리의 성능을 비교하기 위해 메모리 사용량과 실행 시간을 측정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 시간은 CPU 사용량을 간접적으로 나타내는 지표로 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 결과 Imgscalr와 Thumbnailator 모두 안정적인 성능을 보여주었지만, Imgscalr를 SPEED 설정과 함께 사용했을 때 메모리 사용량은 비슷하면서도 평균 연산 시간이 더 짧고 압축률도 높았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Imgscalr는 2013년 이후 업데이트가 중단된 반면, Thumbnailator는 비교적 최근까지 활발히 유지보수되고 있습니다. 또한 코드 가독성 측면에서도 Thumbnailator가 더 읽기 쉬운 것은 사실입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 제가 맡고 있는 서비스에서 현재 필요한 기능은 '이미지 용량 및 사이즈 줄이기'라는 단순한 작업이기 때문에, 많은 기능을 제공하는 라이브러리까지는 필요하지 않다고 판단했습니다. 향후 더 복잡한 이미지 처리 기능이 필요해지면 그때 다른 라이브러리로 전환하는 것도 충분히 가능할 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 두 라이브러리로 생성된 썸네일 이미지를 육안으로 비교했을 때도 품질 차이를 거의 느낄 수 없었기 때문에, Imgscalr를 선택하기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트/프로젝트 과정</category>
      <author> 예슬</author>
      <guid isPermaLink="true">https://yeseul-dev.tistory.com/96</guid>
      <comments>https://yeseul-dev.tistory.com/96#entry96comment</comments>
      <pubDate>Thu, 18 Dec 2025 18:08:18 +0900</pubDate>
    </item>
    <item>
      <title>Cloud Club 7기 - 스터디 기록</title>
      <link>https://yeseul-dev.tistory.com/95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;클라우드 클럽에서는 한 기수를 완료하기 위해 2번의 스터디에 참여해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7기 활동을 마친 지 꽤 시간이 지났지만, 기록으로 남겨본다.&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;1번째 스터디 - 시붕방: 시스템 붕괴 방지 위원회&lt;/h2&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/kNFnx/dJMcaihjD7Z/ujWIfkKsbZuQvzqRCILQhk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kNFnx/dJMcaihjD7Z/ujWIfkKsbZuQvzqRCILQhk/img.jpg&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;602&quot; data-is-animation=&quot;false&quot; style=&quot;width: 45.363%; margin-right: 10px;&quot; data-widthpercent=&quot;45.9&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kNFnx/dJMcaihjD7Z/ujWIfkKsbZuQvzqRCILQhk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkNFnx%2FdJMcaihjD7Z%2FujWIfkKsbZuQvzqRCILQhk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciw6IJ/dJMcacOV9Y3/T5xdRJDIUksYiMRwlVF3L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciw6IJ/dJMcacOV9Y3/T5xdRJDIUksYiMRwlVF3L1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;1706&quot; data-filename=&quot;스크린샷 2025-11-02 오후 8.14.41.png&quot; width=&quot;590&quot; height=&quot;658&quot; data-widthpercent=&quot;54.1&quot; style=&quot;width: 53.4743%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciw6IJ/dJMcacOV9Y3/T5xdRJDIUksYiMRwlVF3L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fciw6IJ%2FdJMcacOV9Y3%2FT5xdRJDIUksYiMRwlVF3L1%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;1530&quot; height=&quot;1706&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디 장으로 참여한 첫 번째 스터디였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재미를 더하기 위해 스터디만의 컨셉과 로고도 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-02 오후 8.59.09.png&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;1009&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mbrTX/dJMcai9snoA/JrBmKOoDaCjsx7QgbwBAY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mbrTX/dJMcai9snoA/JrBmKOoDaCjsx7QgbwBAY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mbrTX/dJMcai9snoA/JrBmKOoDaCjsx7QgbwBAY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmbrTX%2FdJMcai9snoA%2FJrBmKOoDaCjsx7QgbwBAY0%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;550&quot; height=&quot;318&quot; data-filename=&quot;스크린샷 2025-11-02 오후 8.59.09.png&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;1009&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 책 스터디를 할 때 챕터마다 내용을 꼼꼼히 정리하고 기록하는 스타일은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편하게 읽되, 동기부여를 위한 스터디를 진행하고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 자유로운 인증 방식으로 각자만의 정리를 해서 독서하는 것으로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 스터디는 '가상 면접 사례로 배우는 대규모 시스템 설계 기초' 1권과 2권을 모두 읽는 것이 목표였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1권까지는 비교적 순조로웠지만, 2권부터 난이도가 급상승하면서 결국 완독하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf7Ljz/dJMcad79eIe/RwdicI91MGxNhCknSD2aE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf7Ljz/dJMcad79eIe/RwdicI91MGxNhCknSD2aE1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;467&quot; data-filename=&quot;스크린샷 2025-11-02 오후 8.59.39.png&quot; style=&quot;width: 49.9055%; margin-right: 10px;&quot; data-widthpercent=&quot;50.49&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf7Ljz/dJMcad79eIe/RwdicI91MGxNhCknSD2aE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf7Ljz%2FdJMcad79eIe%2FRwdicI91MGxNhCknSD2aE1%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;847&quot; height=&quot;467&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfIkF6/dJMcaeTwmsG/STUcHK5L5wIk2vf8RuSKKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfIkF6/dJMcaeTwmsG/STUcHK5L5wIk2vf8RuSKKk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1853&quot; data-origin-height=&quot;1042&quot; data-filename=&quot;스크린샷 2025-11-02 오후 9.00.24.png&quot; data-widthpercent=&quot;49.51&quot; style=&quot;width: 48.9317%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfIkF6/dJMcaeTwmsG/STUcHK5L5wIk2vf8RuSKKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfIkF6%2FdJMcaeTwmsG%2FSTUcHK5L5wIk2vf8RuSKKk%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;1853&quot; height=&quot;1042&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표 자료나 PDF가 남아있지 않아 기록으로 남길 수 없는 점이 아쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디 장으로서 본보기가 되어야 했는데, 개인적으로 몰입도가 낮았던 것 같아 많이 아쉬웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책의 난이도가 생각보다 높아서 한 번으로는 소화하기 어려웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기회가 된다면 다음에 다시 도전해보고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2번째 스터디 - 클클 북클럽&lt;/h2&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/cZEzxp/dJMcad1nC2r/XJ1rmyKqKlzljdxOKrESdk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZEzxp/dJMcad1nC2r/XJ1rmyKqKlzljdxOKrESdk/img.jpg&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;588&quot; data-is-animation=&quot;false&quot; style=&quot;width: 41.825%; margin-right: 10px;&quot; data-widthpercent=&quot;42.32&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZEzxp/dJMcad1nC2r/XJ1rmyKqKlzljdxOKrESdk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZEzxp%2FdJMcad1nC2r%2FXJ1rmyKqKlzljdxOKrESdk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;588&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJd8yI/dJMcajAwJNK/qzAC9lr44Jdyy4hdGLtZE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJd8yI/dJMcajAwJNK/qzAC9lr44Jdyy4hdGLtZE0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;1490&quot; data-filename=&quot;스크린샷 2025-11-02 오후 8.19.14.png&quot; width=&quot;562&quot; height=&quot;529&quot; style=&quot;width: 57.0122%;&quot; data-widthpercent=&quot;57.68&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJd8yI/dJMcajAwJNK/qzAC9lr44Jdyy4hdGLtZE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJd8yI%2FdJMcajAwJNK%2FqzAC9lr44Jdyy4hdGLtZE0%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;1582&quot; height=&quot;1490&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 스터디와 비슷하게 책을 읽고 발표하는 방식으로 진행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자유롭게 책을 선정할 수 있어서 '주니어 백엔드 개발자가 반드시 알아야 할 실무 지식'을 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 출간된 책이었고, 주니어 개발자에게 좋은 길잡이가 될 것 같아 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 개발하면서 자주 마주치는 문제 상황에 대한 해결책을 제시하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 깊이 공부하면 좋을 부분들을 짚어주어 현업 선배의 조언을 듣는 느낌이었다.&lt;/p&gt;
&lt;p&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/cNaffy/dJMcafdO7S7/sqGwSW2bBkKEQFvxBJHQu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNaffy/dJMcafdO7S7/sqGwSW2bBkKEQFvxBJHQu0/img.png&quot; data-origin-width=&quot;1704&quot; data-origin-height=&quot;919&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2025-11-02 오후 9.15.59.png&quot; style=&quot;width: 49.9701%; margin-right: 10px;&quot; data-widthpercent=&quot;50.56&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNaffy/dJMcafdO7S7/sqGwSW2bBkKEQFvxBJHQu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNaffy%2FdJMcafdO7S7%2FsqGwSW2bBkKEQFvxBJHQu0%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;1704&quot; height=&quot;919&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRzotz/dJMcaboXsqa/jJ8lqSlz8xsREnlNlIR1S0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRzotz/dJMcaboXsqa/jJ8lqSlz8xsREnlNlIR1S0/img.png&quot; width=&quot;546&quot; height=&quot;301&quot; data-origin-width=&quot;2078&quot; data-origin-height=&quot;1146&quot; data-filename=&quot;스크린샷 2025-11-02 오후 8.43.18.png&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.8671%;&quot; data-widthpercent=&quot;49.44&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRzotz/dJMcaboXsqa/jJ8lqSlz8xsREnlNlIR1S0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRzotz%2FdJMcaboXsqa%2FjJ8lqSlz8xsREnlNlIR1S0%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;2078&quot; height=&quot;1146&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자 10분의 발표 시간이 주어졌고, 나는 책에서 기억에 남았던 3가지 주제를 발표했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bppM4I/dJMb99YYMJb/zgAqVRm9pAggmOIjHhZqA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bppM4I/dJMb99YYMJb/zgAqVRm9pAggmOIjHhZqA1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;3420&quot; data-origin-height=&quot;1924&quot; data-filename=&quot;스크린샷 2025-11-02 오후 8.44.25.png&quot; data-widthpercent=&quot;50&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bppM4I/dJMb99YYMJb/zgAqVRm9pAggmOIjHhZqA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbppM4I%2FdJMb99YYMJb%2FzgAqVRm9pAggmOIjHhZqA1%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;3420&quot; height=&quot;1924&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L2mZR/dJMcac2tjgb/1KPtaFXiR8wZ6KHob1C5j1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L2mZR/dJMcac2tjgb/1KPtaFXiR8wZ6KHob1C5j1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;3420&quot; data-origin-height=&quot;1924&quot; data-filename=&quot;스크린샷 2025-11-02 오후 8.45.23.png&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L2mZR/dJMcac2tjgb/1KPtaFXiR8wZ6KHob1C5j1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL2mZR%2FdJMcac2tjgb%2F1KPtaFXiR8wZ6KHob1C5j1%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;3420&quot; height=&quot;1924&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;DB의 단편화와 최적화&lt;/b&gt;&lt;br /&gt;B+ 트리를 사용하는 DB의 특성상 단편화가 발생하며, Optimize 명령어를 통해 최적화할 수 있다는 점을 배웠다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kafka와 RabbitMQ 간단 비교&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경량 스레드와 비동기 I/O&lt;/b&gt;&lt;br /&gt;I/O 대기와 컨텍스트 스위칭으로 인한 CPU 낭비, 요청마다 스레드를 할당하면서 발생하는 높은 메모리 사용량 문제를 다뤘다. 이를 해결하기 위해 가상 스레드나 코루틴 같은 경량 스레드, 논블로킹 또는 비동기 I/O를 활용할 수 있다는 내용이었다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표 당시에는 &quot;그렇구나&quot; 정도로만 이해했는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 비동기, 코루틴, 이벤트 기반 처리에 관심을 갖게 되면서 관련 강의를 찾아보게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그때 그 내용이 이거였구나!&quot; 하며 조금씩 이해의 폭을 넓혀가고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 당시 발표 자료를 아카이브용으로 첨부한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/dbNoZo/dJMcabJgfN2/VVC5kfvI4tK4DkGzHZ1Sc0/%E1%84%8F%E1%85%B3%E1%86%AF%E1%84%8F%E1%85%B3%E1%86%AF%E1%84%87%E1%85%AE%E1%86%A8%E1%84%8F%E1%85%B3%E1%86%AF%E1%84%85%E1%85%A5%E1%86%B8_%E1%84%87%E1%85%A1%E1%86%AF%E1%84%91%E1%85%AD%E1%84%8C%E1%85%A1%E1%84%85%E1%85%AD.pdf?attach=1&amp;amp;knm=tfile.pdf&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;클클북클럽_발표자료.pdf&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;3.15MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>기타/회고</category>
      <author> 예슬</author>
      <guid isPermaLink="true">https://yeseul-dev.tistory.com/95</guid>
      <comments>https://yeseul-dev.tistory.com/95#entry95comment</comments>
      <pubDate>Sun, 2 Nov 2025 21:13:23 +0900</pubDate>
    </item>
    <item>
      <title>10월의 이모 저모</title>
      <link>https://yeseul-dev.tistory.com/94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;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;바로 추석!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프런에서 특별한 이벤트를 진행했어요. 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.inflearn.com/challenge/x27%ED%96%A5%EB%A1%9Cx27-%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EC%B6%94%EC%84%9D-%EC%99%84/dashboard&quot;&gt;'향로'와 함께하는 추석 완강 챌린지&lt;/a&gt;였는데요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;연휴 동안 매일 1강 이상 듣고 인증을 남기면 신청비를 환급해주는 이벤트였어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1761980782609&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;40% 강의 쿠폰도 받고, 연휴 동안 미션 완주하면 신청비까지 몽땅 환급! 향로와 함께하는 밑져야 본전 그 이상인 추석 챌린지!&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/challenge/x27%ED%96%A5%EB%A1%9Cx27-%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EC%B6%94%EC%84%9D-%EC%99%84/dashboard&quot; data-og-url=&quot;https://www.inflearn.com/challenge/x27%ED%96%A5%EB%A1%9Cx27-%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EC%B6%94%EC%84%9D-%EC%99%84&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/U2ore/hyZMLWawl6/WTzZGkYMzywRKdnvggl4H0/img.png?width=403&amp;amp;height=262&amp;amp;face=0_0_403_262,https://scrap.kakaocdn.net/dn/bb9gCf/hyZMTzSr0l/GmNKC5Fm0jk10Amzd3CwFk/img.png?width=403&amp;amp;height=262&amp;amp;face=0_0_403_262,https://scrap.kakaocdn.net/dn/kvzCi/hyZMNsVlac/0kH19nT1VmJWvFQtM5j5PK/img.jpg?width=1024&amp;amp;height=1328&amp;amp;face=255_319_605_700&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/challenge/x27%ED%96%A5%EB%A1%9Cx27-%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EC%B6%94%EC%84%9D-%EC%99%84/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/challenge/x27%ED%96%A5%EB%A1%9Cx27-%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EC%B6%94%EC%84%9D-%EC%99%84/dashboard&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/U2ore/hyZMLWawl6/WTzZGkYMzywRKdnvggl4H0/img.png?width=403&amp;amp;height=262&amp;amp;face=0_0_403_262,https://scrap.kakaocdn.net/dn/bb9gCf/hyZMTzSr0l/GmNKC5Fm0jk10Amzd3CwFk/img.png?width=403&amp;amp;height=262&amp;amp;face=0_0_403_262,https://scrap.kakaocdn.net/dn/kvzCi/hyZMNsVlac/0kH19nT1VmJWvFQtM5j5PK/img.jpg?width=1024&amp;amp;height=1328&amp;amp;face=255_319_605_700');&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;40% 강의 쿠폰도 받고, 연휴 동안 미션 완주하면 신청비까지 몽땅 환급! 향로와 함께하는 밑져야 본전 그 이상인 추석 챌린지!&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 제가 당첨됐어요!  &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/maG1t/dJMcaa4En7r/T9SFoWF1swIjLSS4lvkUx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/maG1t/dJMcaa4En7r/T9SFoWF1swIjLSS4lvkUx0/img.png&quot; width=&quot;401&quot; height=&quot;394&quot; data-is-animation=&quot;false&quot; data-origin-height=&quot;944&quot; data-origin-width=&quot;960&quot; data-widthpercent=&quot;51.66&quot; style=&quot;width: 51.0606%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/maG1t/dJMcaa4En7r/T9SFoWF1swIjLSS4lvkUx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmaG1t%2FdJMcaa4En7r%2FT9SFoWF1swIjLSS4lvkUx0%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;960&quot; height=&quot;944&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z8quQ/dJMb99ScRaE/Sv6VscHhehkml4dFPK0iZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z8quQ/dJMb99ScRaE/Sv6VscHhehkml4dFPK0iZK/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1135&quot; data-is-animation=&quot;false&quot; width=&quot;510&quot; height=&quot;536&quot; style=&quot;width: 47.7766%;&quot; data-widthpercent=&quot;48.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z8quQ/dJMb99ScRaE/Sv6VscHhehkml4dFPK0iZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz8quQ%2FdJMb99ScRaE%2FSv6VscHhehkml4dFPK0iZK%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;1080&quot; height=&quot;1135&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 1600명이 넘는 분들이 참여하는 챌린지에서 딱 10명만 추첨이라 기대도 안 하고 있었거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자를 받고 진짜 깜짝 놀랐어요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-01 오후 4.04.53.png&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chOYdr/dJMcahQeU7f/8wycbSCmmC7TIKqWAE2vc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chOYdr/dJMcahQeU7f/8wycbSCmmC7TIKqWAE2vc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chOYdr/dJMcahQeU7f/8wycbSCmmC7TIKqWAE2vc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchOYdr%2FdJMcahQeU7f%2F8wycbSCmmC7TIKqWAE2vc0%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;323&quot; height=&quot;110&quot; data-filename=&quot;스크린샷 2025-11-01 오후 4.04.53.png&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 구매한 강의가 25만 7천원이었는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 전액이 다 환급될 거라고는 생각도 못 했어요. 확인하고 정말 기뻤답니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 챌린지 성공 보상으로 받은 40% 할인 쿠폰과 환급받은 25만 7천 포인트를 합치니,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무려 40만원이 넘는 금액의 강의를 추가로 구매할 수 있었어요. (완전 개이득)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEz8Ko/dJMcabJfUJK/xUK3vg49pmDmwkHABHyceK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEz8Ko/dJMcabJfUJK/xUK3vg49pmDmwkHABHyceK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1750&quot; data-filename=&quot;스크린샷 2025-11-01 오후 4.20.20.png&quot; style=&quot;width: 43.6899%; margin-right: 10px;&quot; data-widthpercent=&quot;44.2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEz8Ko/dJMcabJfUJK/xUK3vg49pmDmwkHABHyceK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEz8Ko%2FdJMcabJfUJK%2FxUK3vg49pmDmwkHABHyceK%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;2414&quot; height=&quot;1750&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLb2Vl/dJMcajtKNs3/tMuNHR7YxYY7QE5laj3M81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLb2Vl/dJMcajtKNs3/tMuNHR7YxYY7QE5laj3M81/img.png&quot; width=&quot;550&quot; height=&quot;316&quot; data-filename=&quot;스크린샷 2025-11-01 오후 4.14.11.png&quot; data-origin-height=&quot;680&quot; data-origin-width=&quot;1184&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;55.8&quot; style=&quot;width: 55.1473%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLb2Vl/dJMcajtKNs3/tMuNHR7YxYY7QE5laj3M81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLb2Vl%2FdJMcajtKNs3%2FtMuNHR7YxYY7QE5laj3M81%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;1184&quot; height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환급받은 돈으로 평소 제가 부족하다고 생각하거나 눈여겨보던 강의들로 꽉꽉 채워서 구매했어요.(무려 9개 강의..!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 하나씩 열심히 듣고 있는 중이고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 완강한 강의들도 있는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 로그 관리(개발자에게 필요한 로그 관리: &lt;a href=&quot;https://inf.run/4XD3B&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://inf.run/4XD3B&lt;/a&gt;) 관련된 강의는 회사에 바로 적용해서 디버깅할 때 쏠쏠하게 도움받고 있습니다 ☺️ &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1761987184434&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;개발자에게 필요한 로그 관리| 이준형(Foo) - 인프런 강의&quot; data-og-description=&quot;현재 평점 4.9점 수강생 1,033명인 강의를 만나보세요. 어떤 로그를 남겨야 하는지부터 시작하여 로그를 수집하고 활용하기까지 전체적인 사이클에 대해 다룹니다. 애플리케이션에서 어떤 로그를&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://inf.run/4XD3B&quot; data-og-url=&quot;https://www.inflearn.com/course/%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%97%90%EA%B2%8C-%ED%95%84%EC%9A%94%ED%95%9C-%EB%A1%9C%EA%B7%B8%EA%B4%80%EB%A6%AC&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/boA7gu/hyZMLPrffE/sKuG8ZwTk4fPykB9bY1fW1/img.jpg?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/A8Lx8/hyZMXI50TD/kDYlhTKMEmo81g6plrntxK/img.jpg?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/gSlup/hyZMOFln89/y8Ln2KC5k2mp2mFFihIxr1/img.jpg?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781&quot;&gt;&lt;a href=&quot;https://inf.run/4XD3B&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://inf.run/4XD3B&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/boA7gu/hyZMLPrffE/sKuG8ZwTk4fPykB9bY1fW1/img.jpg?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/A8Lx8/hyZMXI50TD/kDYlhTKMEmo81g6plrntxK/img.jpg?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/gSlup/hyZMOFln89/y8Ln2KC5k2mp2mFFihIxr1/img.jpg?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781');&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;개발자에게 필요한 로그 관리| 이준형(Foo) - 인프런 강의&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;현재 평점 4.9점 수강생 1,033명인 강의를 만나보세요. 어떤 로그를 남겨야 하는지부터 시작하여 로그를 수집하고 활용하기까지 전체적인 사이클에 대해 다룹니다. 애플리케이션에서 어떤 로그를&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>기타/일기</category>
      <author> 예슬</author>
      <guid isPermaLink="true">https://yeseul-dev.tistory.com/94</guid>
      <comments>https://yeseul-dev.tistory.com/94#entry94comment</comments>
      <pubDate>Sat, 1 Nov 2025 16:23:54 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 다국어 서비스 구현하기</title>
      <link>https://yeseul-dev.tistory.com/93</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;글로벌 서비스를 개발할 때 다국어 지원은 필수적인 기능입니다. HTTP의 Accept-Language 헤더를 활용하면 사용자의 언어 설정에 따라 자동으로 적절한 언어로 서비스를 제공할 수 있습니다. 이번 글에서는 HTTP 기초부터 Spring Boot에서 Accept-Language를 활용한 다국어 서비스 구현까지 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #0e0e0e;&quot; data-ke-style=&quot;style1&quot;&gt;이 글에서 사용한 예제 코드는 GitHub 레포지토리에 정리해 두었습니다.&lt;br /&gt;자세한 내용은 글 하단의 링크를 참고해주세요.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP 기초 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HTTP(HyperText Transfer Protocol)&lt;/b&gt;는 웹에서 클라이언트와 서버 간의 통신을 위한 프로토콜입니다. 요청-응답(Request-Response) 모델을 기반으로 하며, 클라이언트가 서버에 요청을 보내면 서버가 응답을 반환하는 방식으로 작동합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP 헤더의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 헤더는 요청과 응답에 대한 메타데이터를 전달합니다. 헤더는 다음과 같은 정보를 포함할 수 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨텐츠 타입 및 인코딩 정보&lt;/li&gt;
&lt;li&gt;인증 및 권한 정보&lt;/li&gt;
&lt;li&gt;캐싱 정책&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클라이언트의 언어 선호도&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Accept-Language 헤더 알아보기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Accept-Language란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Accept-Language&lt;/b&gt; 헤더는 클라이언트가 선호하는 언어를 서버에 알려주는 HTTP 요청 헤더입니다. 브라우저는 사용자의 언어 설정을 기반으로 이 헤더를 자동으로 설정합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Accept-Language 헤더 구조&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Accept-Language: ko-KR,ko;q=0.9,en;q=0.8,en-US;q=0.7&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 헤더는 다음과 같이 해석됩니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ko-KR&lt;/code&gt;: 한국어(대한민국) - 우선순위 1.0 (기본값)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ko&lt;/code&gt;: 한국어(일반) - 우선순위 0.9&lt;/li&gt;
&lt;li&gt;&lt;code&gt;en&lt;/code&gt;: 영어(일반) - 우선순위 0.8&lt;/li&gt;
&lt;li&gt;&lt;code&gt;en-US&lt;/code&gt;: 영어(미국) - 우선순위 0.7&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언어 태그 형식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언어 태그는 다음과 같은 형식을 따릅니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ko&lt;/code&gt;: 언어 코드만&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ko-KR&lt;/code&gt;: 언어-국가 코드 -&amp;gt; en-US(미국 영어)와 en-GB(영국 영어), zh-CN(중국 간체)와 zh-TW(대만 번체) 등&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zh-Hans-CN&lt;/code&gt;: 언어-문자체계-국가 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;품질 값(q-value)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;품질 값은 0.0부터 1.0까지의 값으로, 클라이언트의 언어 선호도를 나타냅니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1.0: 가장 선호 (기본값, 생략 가능)&lt;/li&gt;
&lt;li&gt;0.8: 높은 선호도&lt;/li&gt;
&lt;li&gt;0.5: 보통 선호도&lt;/li&gt;
&lt;li&gt;0.0: 허용하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Boot에서 다국어 서비스 구현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국제화 하는 두 가지의 방법&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;messages.properties&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;DB 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;구현 복잡도&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;간단(스프링 기본 기능)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;복잡(테이블 설계)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;성능&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;빠름(메모리 로드)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;느림(DB 조회 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;관리 편의성&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;파일 수정 후 재배포 해야하므로 개발자만 수정 가능&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;실시간 수정 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;버전 관리&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;쉬움&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;어려움&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;h3 data-ke-size=&quot;size23&quot;&gt;1. 국제화 설정&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class LocaleConfig {

    @Bean
    fun localeResolver(): LocaleResolver {
        val resolver = AcceptHeaderLocaleResolver().apply {
            //  ️ 보안 &amp;amp; 성능 - DB에 있는 언어만 처리
            setSupportedLocales(
                listOf(
                    Locale.ENGLISH,    // &quot;en&quot;
                    Locale.KOREAN,     // &quot;ko&quot;
                    Locale.JAPANESE,   // &quot;ja&quot;
                    Locale.CHINESE     // &quot;zh&quot;
                )
            )
            //   예측 가능한 기본값 - 매우 중요!
            setDefaultLocale(Locale.ENGLISH)
        }

        return resolver
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 메시지 파일 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;src/main/resources/&lt;/code&gt; 디렉토리에 언어별 메시지 파일을 생성합니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;messages.properties (기본)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;hello=안녕하세요
welcome=환영합니다
user.name=사용자 이름
user.email=이메일
button.save=저장
button.cancel=취소&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;messages_en.properties&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;hello=Hello
welcome=Welcome
user.name=User Name
user.email=Email
button.save=Save
button.cancel=Cancel&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;messages_ja.properties&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;hello=こんにちは
welcome=ようこそ
user.name=ユーザー名
user.email=メール
button.save=保存
button.cancel=キャンセル&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 컨트롤러 구현&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
class HelloController(
    private val messageSource: MessageSource
) {

    @GetMapping(&quot;/hello&quot;, produces = [&quot;text/plain;charset=UTF-8&quot;])
    fun hello(locale: Locale): String {
        val locale = LocaleContextHolder.getLocale()
        val text = messageSource.getMessage(&quot;hello&quot;, null, locale)
        return text
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. MessageSourceConfig&lt;/h3&gt;
&lt;pre id=&quot;code_1755168473263&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * DB에서 데이터를 가져오는 방법이 아니라면 필요
 */
@Configuration
class MessageSourceConfig {
    @Bean
    fun messageSource(): MessageSource =
        ReloadableResourceBundleMessageSource().apply {
            setBasenames(&quot;classpath:messages&quot;)
            setDefaultEncoding(&quot;UTF-8&quot;)
            setCacheSeconds(1) // 1초마다 파일 변경 확인
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;messages.properties 파일을 서버 재시작 하지 않고 1초에 한번씩 파일 변경 확인&lt;/li&gt;
&lt;li&gt;명시적인 UTF-8 인코딩으로 한글 등 다국어 문자가 깨지지 않도록 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-14 오후 7.45.52.png&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxUDD2/btsPVlFWjyA/r8Dk0GygLrkh3Ilw9a97ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxUDD2/btsPVlFWjyA/r8Dk0GygLrkh3Ilw9a97ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxUDD2/btsPVlFWjyA/r8Dk0GygLrkh3Ilw9a97ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxUDD2%2FbtsPVlFWjyA%2Fr8Dk0GygLrkh3Ilw9a97ak%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;550&quot; height=&quot;248&quot; data-filename=&quot;스크린샷 2025-08-14 오후 7.45.52.png&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 엔티티 클래스 만들기&lt;/h3&gt;
&lt;pre id=&quot;code_1755168644649&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;products&quot;)
class Product(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column(nullable = false, length = 255)
    var sku: String,             // 비즈니스용 코드(선택)

    @Column(nullable = false, length = 255)
    var defaultTitle: String = &quot;&quot;,

    @Column(columnDefinition = &quot;TEXT&quot;)
    var defaultDescription: String? = null,
) {
    // JPA용 기본 생성자
    constructor() : this(null, &quot;&quot;, &quot;&quot;, null)

}

@Entity
@Table(
    name = &quot;product_localization&quot;,
    uniqueConstraints = [UniqueConstraint(columnNames = [&quot;product_id&quot;, &quot;locale&quot;])]
)
class ProductLocalization(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = &quot;product_id&quot;, nullable = false)
    @OnDelete(action = OnDeleteAction.CASCADE)
    var product: Product? = null,

    @Column(nullable = false, length = 10)
    var locale: String,    // &quot;en&quot;, &quot;ko&quot;, &quot;ja&quot;, &quot;en-US&quot; 등

    @Column(nullable = false, length = 255)
    var title: String,

    @Column(columnDefinition = &quot;TEXT&quot;)
    var description: String? = null,
) {
    // JPA용 기본 생성자
    constructor() : this(null, null, &quot;&quot;, &quot;&quot;, null)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. DB 사용해서 다국어 처리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Locale 클래스 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/products&quot;)
class ProductController(
    private val productReadService: ProductReadService,
) {

    @GetMapping(&quot;/{id}&quot;)
    fun getProduct(
        @PathVariable id: Long,
        locale: Locale, // Spring이 자동으로 http 헤더의 locale 설정을 주입해줌
    ): ResponseEntity&amp;lt;ProductDto&amp;gt; {

        val languageCode = locale.language.ifBlank { &quot;en&quot; }
        val dto = productReadService.get(productId = id, locale = languageCode)

        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_LANGUAGE, languageCode)
            .body(dto)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. ProductService&lt;/h3&gt;
&lt;pre id=&quot;code_1755168705782&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class ProductReadService(
    private val productRepository: ProductRepository,
    private val productLocRepo: ProductLocRepository,
) {
    fun get(productId: Long, locale: String): ProductDto {
        val product = productRepository.findById(productId).orElseThrow {
            ResponseStatusException(HttpStatus.NOT_FOUND, &quot;product not found&quot;)
        }

        val loc = productLocRepo.findFirstByProduct_IdAndLocale(productId, locale)

        return if (loc != null) {
            ProductDto(product.id!!, loc.title, loc.description)
        } else {
            // 번역이 한 건도 없으면 기본값 사용
            ProductDto(product.id!!, product.defaultTitle, product.defaultDescription)
        }
    }
}

data class ProductDto(
    val id: Long,
    val title: String,
    val description: String?
)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 웹 설정&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class WebConfig : WebMvcConfigurer {
    @Bean
    fun stringHttpMessageConverter() =
        StringHttpMessageConverter(StandardCharsets.UTF_8)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. cURL을 사용한 테스트&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-14 오후 7.52.36.png&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkcTux/btsPULdQChw/TZ6HRoUgLBzDPl2S1ynwu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkcTux/btsPULdQChw/TZ6HRoUgLBzDPl2S1ynwu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkcTux/btsPULdQChw/TZ6HRoUgLBzDPl2S1ynwu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkcTux%2FbtsPULdQChw%2FTZ6HRoUgLBzDPl2S1ynwu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;644&quot; height=&quot;340&quot; data-filename=&quot;스크린샷 2025-08-14 오후 7.52.36.png&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&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/bnJPlU/btsPVEFftvB/rQMKdJksDsY14kWuLcUutK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnJPlU/btsPVEFftvB/rQMKdJksDsY14kWuLcUutK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;254&quot; data-filename=&quot;blob&quot; width=&quot;707&quot; height=&quot;338&quot; data-widthpercent=&quot;55.51&quot; style=&quot;width: 54.862%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnJPlU/btsPVEFftvB/rQMKdJksDsY14kWuLcUutK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnJPlU%2FbtsPVEFftvB%2FrQMKdJksDsY14kWuLcUutK%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;1134&quot; height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RJRW3/btsPUrmBgTC/UK2ranppJTnpI9e5Y3XJZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RJRW3/btsPUrmBgTC/UK2ranppJTnpI9e5Y3XJZ0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;318&quot; data-filename=&quot;스크린샷 2025-08-14 오후 7.53.45.png&quot; style=&quot;width: 43.9752%;&quot; data-widthpercent=&quot;44.49&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RJRW3/btsPUrmBgTC/UK2ranppJTnpI9e5Y3XJZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRJRW3%2FbtsPUrmBgTC%2FUK2ranppJTnpI9e5Y3XJZ0%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;1138&quot; height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;지원되지 않는 경우나 헤더를 사용하지 않을 경우 기본 값인 영어로 출력되는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드들을 참고 할 수 있는 레포지토리를 공유해드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/HongYeseul/spring-boot-locale-demo&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/HongYeseul/spring-boot-locale-demo&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1755176428136&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 - HongYeseul/spring-boot-locale-demo&quot; data-og-description=&quot;Contribute to HongYeseul/spring-boot-locale-demo development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/HongYeseul/spring-boot-locale-demo&quot; data-og-url=&quot;https://github.com/HongYeseul/spring-boot-locale-demo&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3THij/hyZzyWbWG6/sDfchsEwkMJTbYgrgzGV4k/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_116_1032_189,https://scrap.kakaocdn.net/dn/gGgdD/hyZuweXeWn/4piBemRFidAwrgmjTX29T1/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_116_1032_189&quot;&gt;&lt;a href=&quot;https://github.com/HongYeseul/spring-boot-locale-demo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/HongYeseul/spring-boot-locale-demo&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3THij/hyZzyWbWG6/sDfchsEwkMJTbYgrgzGV4k/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_116_1032_189,https://scrap.kakaocdn.net/dn/gGgdD/hyZuweXeWn/4piBemRFidAwrgmjTX29T1/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_116_1032_189');&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 - HongYeseul/spring-boot-locale-demo&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to HongYeseul/spring-boot-locale-demo 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>
      <author> 예슬</author>
      <guid isPermaLink="true">https://yeseul-dev.tistory.com/93</guid>
      <comments>https://yeseul-dev.tistory.com/93#entry93comment</comments>
      <pubDate>Thu, 14 Aug 2025 19:54:51 +0900</pubDate>
    </item>
    <item>
      <title>Kind를 이용하여 멀티 노드 Kubernetes 클러스터를 구성하고 삭제하는 방법</title>
      <link>https://yeseul-dev.tistory.com/92</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스를 간단히 실습할 수 있는 환경인 kind를 활용하는 방법을 소개합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;kind란?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kind (Kubernetes IN Docker)는 Docker 컨테이너 기반으로 쿠버네티스 클러스터를 구성할 수 있게 해주는 도구입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트, CI/CD, 경량 개발 환경에 적합&lt;/li&gt;
&lt;li&gt;VM 없이 컨테이너로만 클러스터 구성&lt;/li&gt;
&lt;li&gt;매우 빠르고 가벼움&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구성 예시&lt;/h2&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;kind-multinode/
├── kind-cluster.yaml        # 클러스터 노드 설정 파일
└── create-cluster.sh        # 클러스터 생성 스크립트&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;kind-cluster.yaml&lt;/h2&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설정은 컨트롤 플레인 1개 + 워커 노드 2개 구성입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;create-cluster.sh&lt;/h2&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash

CLUSTER_NAME=&quot;multi-node-cluster&quot;

echo &quot;  기존 클러스터 삭제 중...&quot;
kind delete cluster --name &quot;$CLUSTER_NAME&quot;

echo &quot;  클러스터 생성 중...&quot;
kind create cluster --name &quot;$CLUSTER_NAME&quot; --config kind-cluster.yaml

echo &quot;✅ 클러스터 생성 완료!&quot;
kubectl cluster-info --context kind-$CLUSTER_NAME

echo &quot;  노드 리스트:&quot;
kubectl get nodes&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 권한 부여: chmod +x create-cluster.sh&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;클러스터 실행&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;./create-cluster.sh&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과 예시&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;$ kubectl get nodes
NAME                             STATUS   ROLES           AGE     VERSION
multi-node-cluster-control-plane   Ready    control-plane   1m      v1.29.x
multi-node-cluster-worker          Ready    &amp;lt;none&amp;gt;          1m      v1.29.x
multi-node-cluster-worker2         Ready    &amp;lt;none&amp;gt;          1m      v1.29.x&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;클러스터 삭제&lt;/h2&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;kind delete cluster --name multi-node-cluster&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관련 Docker 컨테이너들도 자동 삭제됨&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl config&lt;/code&gt;에서도 context 사라짐&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 사항&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kind는 Docker 이미지를 자동으로 사용하므로 별도의 Dockerfile이 필요 없음&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>알아두면 좋은 개발 지식/인프라</category>
      <author> 예슬</author>
      <guid isPermaLink="true">https://yeseul-dev.tistory.com/92</guid>
      <comments>https://yeseul-dev.tistory.com/92#entry92comment</comments>
      <pubDate>Wed, 6 Aug 2025 12:52:49 +0900</pubDate>
    </item>
    <item>
      <title>[KSUG] 기초 지식 스터디 2회차 요약</title>
      <link>https://yeseul-dev.tistory.com/90</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;25년 KSUG에서 주니어 및 취준생 분들과 함께한 스터디 진행 기록입니다.&lt;br /&gt;스프링 프레임워크 혹은 데이터베이스에 대한 스터디를 진행 했고, 저는 스프링 프레임워크를 선택했습니다.&lt;br /&gt;&lt;br /&gt;*스프링 프레임워크 가이드 책: 스프링 부트3 핵심 가이드&lt;br /&gt;*데이터 베이스 가이드 책: 새로 쓴 대용량 데이터베이스 솔루션 1&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기여해주시는 멘토 분들께 항상 감사드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 시간에는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot - 제2장: 개발에 앞서 알면 좋은 지식&lt;/li&gt;
&lt;li&gt;데이터 베이스 - 1.3장: SQL의 실행 계획&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부분을 정리해오기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 스터디 시작 전에 발표를 자원해주신 분들이 계셔서, 비교적 원활하게 진행할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 특별히 게스트로 '김근수'님께서 참석해 주셨는데, 발표 내용이 특히 인상 깊었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Camp에서도 연사로 발표하셨다고 들었지만, 내가 참석했던 세션은 아니어서 직접 들을 수 없었던 점이 아쉬웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근수님은 평소 마인드맵 형식으로 모든 내용을 정리하신다고 한다. 마인드맵 프로그램을 이용하면 단순히 생각나는 대로 적어도 마우스를 이용해 자유롭게 구조를 바꿀 수 있어 생각을 체계화하는 데에 매우 유용한 툴이라고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근수님께서는 EdrawMind라는 툴을 쓰신다고 했는데, 유료 서비스라 예전부터 알고 있던 무료 툴인 XMind가 떠올랐다. 일단은 XMind로 시작해보고, 나중에 익숙해지면 블랙프라이데이 때 EdrawMind를 한번 사볼까 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-03 오후 12.41.56.png&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buKDMp/btsPELTuIS2/cYFPGP3VTwcCAqRkJAYqDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buKDMp/btsPELTuIS2/cYFPGP3VTwcCAqRkJAYqDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buKDMp/btsPELTuIS2/cYFPGP3VTwcCAqRkJAYqDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuKDMp%2FbtsPELTuIS2%2FcYFPGP3VTwcCAqRkJAYqDk%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;701&quot; height=&quot;368&quot; data-filename=&quot;스크린샷 2025-08-03 오후 12.41.56.png&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 날 바로 실무에 적용해보자는 마음으로, 현재 작업 중인 mevu 서비스 관련 내용을 XMind로 정리하고 앱팀과 회의를 진행했는데, 기획자분도 보기 좋고 회의도 수월하다고 칭찬해 주셔서 뿌듯했다. 아직은 정리 중이지만 마인드맵 가지가 점점 늘어나는 걸 보면서 잘 정리해보고 싶다는 의욕도 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 인상 깊었던 점은 근수님이 모든 책을 이북으로 보신다는 점이다. 이북은 마인드맵으로의 전환이나 복사-붙여넣기가 쉬워 정리에 더 적합해 보였다. 나는 그동안 전공 서적은 종이책으로만 봤는데, 책이 집에 쌓이면서 점점 부담이 되었던 터라 이북 활용도 진지하게 고려해봐야겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 유용한 팁을 많이 얻을 수 있었던 뜻깊은 스터디 2회차 시간이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 GPT를 이용한 키워드 중심 스터디 요약본이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  아키텍처 및 계층 구조&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;3계층 구조 (Layered Architecture)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Presentation Layer (프레젠테이션 계층)&lt;/b&gt;: 클라이언트와의 접점, 요청 및 응답 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Business Layer (비즈니스 계층)&lt;/b&gt;: 비즈니스 로직, 트랜잭션, 유효성 검사&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Persistence Layer (데이터 접근 계층)&lt;/b&gt;: DB 접근, Repository 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마이크로서비스 아키텍처(MSA)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모놀리식과 비교&lt;/li&gt;
&lt;li&gt;단순히 서비스 쪼개는 게 아닌 &quot;사상&quot;이 중요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DTO, Entity, VO 구분&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레이어 간 데이터 이동을 위한 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  디자인 패턴&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;생성 패턴&lt;/b&gt;: Abstract Factory, Builder, Factory Method, Singleton 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구조 패턴&lt;/b&gt;: Adapter, Composite, Decorator, Facade 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;행위 패턴&lt;/b&gt;: Chain of Responsibility, Command, Interpreter, Mediator 등&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  REST 아키텍처&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;REST 특성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유니폼 인터페이스&lt;/li&gt;
&lt;li&gt;무상태성 (Stateless)&lt;/li&gt;
&lt;li&gt;캐시 가능성&lt;/li&gt;
&lt;li&gt;클라이언트-서버 구조&lt;/li&gt;
&lt;li&gt;계층 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RESTful URL 설계 규칙&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt;로 끝나지 않음&lt;/li&gt;
&lt;li&gt;하이픈(-) 사용, 언더바(_) 지양&lt;/li&gt;
&lt;li&gt;명사 사용 (동사 대신 HTTP 메서드로 행위 표현)&lt;/li&gt;
&lt;li&gt;소문자 사용&lt;/li&gt;
&lt;li&gt;확장자 지양&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;URI vs URL&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;URI: 자원의 고유 식별자&lt;/li&gt;
&lt;li&gt;URL: 위치 및 접근 방식 포함한 URI&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  인증 및 인가&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Authentication&lt;/b&gt;: 사용자 판별 (로그인 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Authorization&lt;/b&gt;: 자원 접근 권한 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  HTTP 및 HTTPS&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;HTTP&lt;/b&gt;: 보안 없음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTPS&lt;/b&gt;: TLS 기반 보안 제공
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SSL (구버전), TLS (현재 주 사용)&lt;/li&gt;
&lt;li&gt;포트: HTTP(80), HTTPS(443)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  DispatcherServlet 흐름&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;요청 &amp;rarr; DispatcherServlet&lt;/li&gt;
&lt;li&gt;HandlerMapping &amp;rarr; Controller&lt;/li&gt;
&lt;li&gt;Service &amp;rarr; Repository &amp;rarr; DB&lt;/li&gt;
&lt;li&gt;Controller &amp;rarr; ViewResolver&lt;/li&gt;
&lt;li&gt;View 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  DB 저장 구조 및 최적화&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;분리형 테이블 구조&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;논리적 구조와 물리적 저장 분리&lt;/li&gt;
&lt;li&gt;RowID 활용: 인덱스와 실제 데이터 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IOT (Index Organized Table)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스와 데이터를 함께 저장 &amp;rarr; 빠른 조회&lt;/li&gt;
&lt;li&gt;수정 시 데이터 재배치 필요 &amp;rarr; 성능 저하&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클러스터링 테이블&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 물리적 정렬 (예: 저자명 기준)&lt;/li&gt;
&lt;li&gt;IO 최소화, 조회 성능 향상&lt;/li&gt;
&lt;li&gt;단점: 쓰기/수정 비용 큼&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해시 클러스터&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조회 속도 빠름 (O(1))&lt;/li&gt;
&lt;li&gt;단점: 레인지 조회 불가, 충돌 발생 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클러스터링 팩터 (Clustering Factor)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스와 실제 데이터의 물리적 거리 척도&lt;/li&gt;
&lt;li&gt;공간 지역성 높을수록 성능 우수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 2회차는 내가 많이 준비하지 못해서 아쉬움이 남는다. 열심히 공부해야지...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스터디는 내가 집중해서 공부하고 있는 분야뿐만 아니라, DB를 공부하고 계신 분들도 계셔서 완전히 알지는 못해도 자연스럽게 DB에 대한 내용도 함께 접할 수 있다는 점이 좋은 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 3회차 준비하러 가야지... 다음주도 파이팅&lt;/p&gt;</description>
      <category>알아두면 좋은 개발 지식/스터디</category>
      <author> 예슬</author>
      <guid isPermaLink="true">https://yeseul-dev.tistory.com/90</guid>
      <comments>https://yeseul-dev.tistory.com/90#entry90comment</comments>
      <pubDate>Sun, 3 Aug 2025 12:46:53 +0900</pubDate>
    </item>
    <item>
      <title>스프링 부트에 대한 간단한 고찰 - 2</title>
      <link>https://yeseul-dev.tistory.com/88</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;25년 KSUG에서 주니어 및 취준생 분들과 함께한 스터디 진행 기록입니다.&lt;br /&gt;스프링 프레임워크 혹은 데이터베이스에 대한 스터디를 진행 했고, 저는 스프링 프레임워크를 선택했습니다.&lt;br /&gt;&lt;br /&gt;기여해주시는 멘토님들께 항상 감사드립니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2회차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디에서 메인으로 사용하는 책은 '스프링 부트 3 핵심 가이드'라는 책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2회차에는 2장부터 정리를 하면 되고, 3장은 개발 환경 구성 하는 챕터여서 생략했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 이번 시간에 2장만 하면 되는 것이었는데 읽는 김에 금방 읽을 수 있을 것 같아 5장까지 읽고 정리해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0yEYC/btsPDaq9cCy/hW5SOMZv7b4Sufin4E51Q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0yEYC/btsPDaq9cCy/hW5SOMZv7b4Sufin4E51Q1/img.png&quot; data-alt=&quot;2장(개발에 앞서 알면 좋은 기초 지식)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0yEYC/btsPDaq9cCy/hW5SOMZv7b4Sufin4E51Q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0yEYC%2FbtsPDaq9cCy%2FhW5SOMZv7b4Sufin4E51Q1%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;1486&quot; height=&quot;934&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2장(개발에 앞서 알면 좋은 기초 지식)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1852&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oj1lY/btsPAzM9wzc/OJb6VeGMKNGNxwKuLOzCoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oj1lY/btsPAzM9wzc/OJb6VeGMKNGNxwKuLOzCoK/img.png&quot; data-alt=&quot;4장(스프링부트 애플리케이션 개발하기), 5장(API를 작성하는 다양한 방법)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oj1lY/btsPAzM9wzc/OJb6VeGMKNGNxwKuLOzCoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foj1lY%2FbtsPAzM9wzc%2FOJb6VeGMKNGNxwKuLOzCoK%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;1852&quot; height=&quot;934&quot; data-origin-width=&quot;1852&quot; data-origin-height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;4장(스프링부트 애플리케이션 개발하기), 5장(API를 작성하는 다양한 방법)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 책을 읽으면서 필요한 전공 지식을 질문 형태로 정리하고, 그에 대한 답을 찾아보는 방식으로 공부해보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기도님께서 참고하라고 공유해주신 리포지토리가 있었는데, 그 방식처럼 나도 질문지를 먼저 만들고 해답을 정리해보려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 새로 만든 리포지토리.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/HongYeseul/cs-for-interview&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/HongYeseul/cs-for-interview&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1753950839491&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 - HongYeseul/cs-for-interview&quot; data-og-description=&quot;Contribute to HongYeseul/cs-for-interview development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/HongYeseul/cs-for-interview&quot; data-og-url=&quot;https://github.com/HongYeseul/cs-for-interview&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jmsiD/hyZrso2Ft5/V6TNWiaaZFZfYXImF9odVK/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_116_1032_189,https://scrap.kakaocdn.net/dn/d55t3V/hyZrrDFHKr/7etJHeJc0Dy8M60ODmaKH1/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_116_1032_189&quot;&gt;&lt;a href=&quot;https://github.com/HongYeseul/cs-for-interview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/HongYeseul/cs-for-interview&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jmsiD/hyZrso2Ft5/V6TNWiaaZFZfYXImF9odVK/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_116_1032_189,https://scrap.kakaocdn.net/dn/d55t3V/hyZrrDFHKr/7etJHeJc0Dy8M60ODmaKH1/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_116_1032_189');&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 - HongYeseul/cs-for-interview&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to HongYeseul/cs-for-interview 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;해당 리포지토리에 질문들과 답변을 적어내려가려 했는데 ...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번주는 시간이 부족해서 &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;  조금밖에 채워 넣지 못했다.....&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;이번에 5장까지 정리했으니, 다음 진도보다는 빨라서 시간이 좀 더 남지않을까.....라고 생각하며&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;다음주에는 꼭 ... 많이 채워와야겠다고 다짐했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3회차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디 진도는 4장이었으나, 4장 5장을 정리했었기 때문에 xmind로 옮기는 작업을 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옮기면서 더 조사한 내용은 붙여넣기도 했다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-07 오후 12.30.47.png&quot; data-origin-width=&quot;2554&quot; data-origin-height=&quot;3484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRyTaC/btsPKG4FCZg/LUV6AzPVOxxgxtNZv65oCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRyTaC/btsPKG4FCZg/LUV6AzPVOxxgxtNZv65oCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRyTaC/btsPKG4FCZg/LUV6AzPVOxxgxtNZv65oCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRyTaC%2FbtsPKG4FCZg%2FLUV6AzPVOxxgxtNZv65oCK%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;2554&quot; height=&quot;3484&quot; data-filename=&quot;스크린샷 2025-08-07 오후 12.30.47.png&quot; data-origin-width=&quot;2554&quot; data-origin-height=&quot;3484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-07 오후 12.31.08.png&quot; data-origin-width=&quot;2240&quot; data-origin-height=&quot;2192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uZAyd/btsPJdJckIw/nPdZbGCWX7t3xbCbKGDU20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uZAyd/btsPJdJckIw/nPdZbGCWX7t3xbCbKGDU20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uZAyd/btsPJdJckIw/nPdZbGCWX7t3xbCbKGDU20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuZAyd%2FbtsPJdJckIw%2FnPdZbGCWX7t3xbCbKGDU20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2240&quot; height=&quot;2192&quot; data-filename=&quot;스크린샷 2025-08-07 오후 12.31.08.png&quot; data-origin-width=&quot;2240&quot; data-origin-height=&quot;2192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 저번주에 만들어둔 리포지토리에 하나씩 채워 넣는중...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/HongYeseul/cs-for-interview/tree/springboot/mechanism&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/HongYeseul/cs-for-interview/tree/springboot/mechanism&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1754569578112&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 - HongYeseul/cs-for-interview&quot; data-og-description=&quot;Contribute to HongYeseul/cs-for-interview development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/HongYeseul/cs-for-interview/tree/springboot/mechanism&quot; data-og-url=&quot;https://github.com/HongYeseul/cs-for-interview&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3ZYAu/hyZuDjCRpW/N6rlMRl3yOJVE4ZoJwpfGK/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_116_1032_189,https://scrap.kakaocdn.net/dn/mTxzU/hyZvoGoq8I/sU2pzVzoh3YcWLPxM2K1Ok/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_116_1032_189&quot;&gt;&lt;a href=&quot;https://github.com/HongYeseul/cs-for-interview/tree/springboot/mechanism&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/HongYeseul/cs-for-interview/tree/springboot/mechanism&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3ZYAu/hyZuDjCRpW/N6rlMRl3yOJVE4ZoJwpfGK/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_116_1032_189,https://scrap.kakaocdn.net/dn/mTxzU/hyZvoGoq8I/sU2pzVzoh3YcWLPxM2K1Ok/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_116_1032_189');&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 - HongYeseul/cs-for-interview&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to HongYeseul/cs-for-interview 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;브랜치에다 푸시 해뒀는데 마음에 드는 만큼 채우면 squash - push 하려고 대기중이다.&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;5회차&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-19 오후 8.42.17.png&quot; data-origin-width=&quot;1433&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o02Cc/btsPVIW9h8G/vayNo3EA8w1IoqqqKOSdp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o02Cc/btsPVIW9h8G/vayNo3EA8w1IoqqqKOSdp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o02Cc/btsPVIW9h8G/vayNo3EA8w1IoqqqKOSdp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo02Cc%2FbtsPVIW9h8G%2FvayNo3EA8w1IoqqqKOSdp0%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;1433&quot; height=&quot;311&quot; data-filename=&quot;스크린샷 2025-08-19 오후 8.42.17.png&quot; data-origin-width=&quot;1433&quot; data-origin-height=&quot;311&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주제는 6장 - 데이터베이스 연동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA는 한 장으로 끝나는 그런 지식이 아니기 때문에 양이 확실히 많았다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-19 오후 8.43.39.png&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;881&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2j3eK/btsPYMcS666/fnBfwJnBBWYIUPXMfD93UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2j3eK/btsPYMcS666/fnBfwJnBBWYIUPXMfD93UK/img.png&quot; data-alt=&quot;독서(공부?) 인증용으로 이정도만...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2j3eK/btsPYMcS666/fnBfwJnBBWYIUPXMfD93UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2j3eK%2FbtsPYMcS666%2FfnBfwJnBBWYIUPXMfD93UK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1270&quot; height=&quot;881&quot; data-filename=&quot;스크린샷 2025-08-19 오후 8.43.39.png&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;881&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;독서(공부?) 인증용으로 이정도만...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6회차&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-24 오후 5.50.50.png&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v62bH/btsP5ovXWMB/V3Ovp597dViTFywk9Cu961/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v62bH/btsP5ovXWMB/V3Ovp597dViTFywk9Cu961/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v62bH/btsP5ovXWMB/V3Ovp597dViTFywk9Cu961/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv62bH%2FbtsP5ovXWMB%2FV3Ovp597dViTFywk9Cu961%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;1508&quot; height=&quot;474&quot; data-filename=&quot;스크린샷 2025-08-24 오후 5.50.50.png&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6회차 주제는 7장. 테스트 코드 작성하기&lt;/p&gt;</description>
      <category>알아두면 좋은 개발 지식/스터디</category>
      <author> 예슬</author>
      <guid isPermaLink="true">https://yeseul-dev.tistory.com/88</guid>
      <comments>https://yeseul-dev.tistory.com/88#entry88comment</comments>
      <pubDate>Thu, 31 Jul 2025 17:40:49 +0900</pubDate>
    </item>
  </channel>
</rss>