<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>PoroGramr's Log</title>
    <link>https://jspark33.tistory.com/</link>
    <description>모두 행복하세요. (❁&amp;acute;▽`❁)</description>
    <language>ko</language>
    <pubDate>Thu, 7 May 2026 03:37:33 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>PoroGramr</managingEditor>
    <image>
      <title>PoroGramr's Log</title>
      <url>https://tistory1.daumcdn.net/tistory/3085457/attach/d9608229d68046719ebc1c665ac0d0f9</url>
      <link>https://jspark33.tistory.com</link>
    </image>
    <item>
      <title>출석관리체계에 AI 에이전트 구축하기 - 1</title>
      <link>https://jspark33.tistory.com/156</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이전에 작성했던 계획을 바탕으로 간단한 에이전트를 구현해보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-27 at 10.01.11 AM.png&quot; data-origin-width=&quot;1513&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjIxCx/dJMcag5Ga5S/rrZZvHTbhtKkFi0pI5czU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjIxCx/dJMcag5Ga5S/rrZZvHTbhtKkFi0pI5czU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjIxCx/dJMcag5Ga5S/rrZZvHTbhtKkFi0pI5czU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjIxCx%2FdJMcag5Ga5S%2FrrZZvHTbhtKkFi0pI5czU1%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;1513&quot; height=&quot;416&quot; data-filename=&quot;Screenshot 2026-02-27 at 10.01.11 AM.png&quot; data-origin-width=&quot;1513&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;전체적인 구조는 위와 같다&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0b5394;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;작동 방식 (6단계)&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자 질문 수신&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; - POST /api/ai/chat 엔드포인트로 질문 전송&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의도 파악&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; - &lt;/span&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;Gemini API로 질문 분석&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;, 6가지 의도 분류, JSON으로 파라미터 추출&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터 검색 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- Switch Expression으로 라우팅, MySQL에서 실제 출석 데이터 조회&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터 포맷팅&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; - 엔티티를 텍스트로 변환 (목록형/통계형)&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자연어 답변 생성&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; - &lt;span style=&quot;color: #ee2323;&quot;&gt;Gemini API&lt;/span&gt;로 친절한 답변 생성&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;총 2번의 GEMINI API 호출이 필요하다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 방식을 채택한 이유&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;1. 간단한 질문만 가능하게끔 하고 싶었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;2. AI가 직접적으로 쿼리에 접근하는 것은 위험하다고 판단되었다,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 결국 6가지 의도로 분류된 질문만 답변이 가능했기에&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;완전한 AI 에이전트라고 할수는 없었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;결국 내가 원하는 에이전트는&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;모든 출석 데이터 관련 모든 질문에 답변 가능한 에이전트가 필요했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 고민을 하던 중&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;DB에 직접 접근 하지않고 간접적으로 접근하는 방식을 고안해냈다&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Lang Chain을 적용하기 위해 FASTAPI 로 프레임워크를 변경하고&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;DB는 필요한 테이블만 SQLite로 추출하는 방식으로 진행하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3738&quot; data-origin-height=&quot;4920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VAuYz/dJMcah4z5QD/4oFRNuZoS2AIUvzrdB7ED0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VAuYz/dJMcah4z5QD/4oFRNuZoS2AIUvzrdB7ED0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VAuYz/dJMcah4z5QD/4oFRNuZoS2AIUvzrdB7ED0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVAuYz%2FdJMcah4z5QD%2F4oFRNuZoS2AIUvzrdB7ED0%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;485&quot; height=&quot;638&quot; data-origin-width=&quot;3738&quot; data-origin-height=&quot;4920&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;자연어 질문을 받아&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;랭체인을 통해 적절한 쿼리를 생성한뒤&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;필요한 데이터만 있는 SQLITE에서 데이터 추출 후&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;응답하는 구조이다&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;만약 랭체인을 적용하지 않았다면&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;1. Gemini에게 &quot;어떤 SQL 써야 해?&quot; 물어보기&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;2. SQL 받아서 SQLite에 실행하기&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;3. 결과를 다시 Gemini에게 넘기기&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;4. &quot;이걸로 충분해?&quot; 판단하기&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;5. 부족하면 2번으로 다시 돌아가기&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;6. 최종 답변 생성하기&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;LangChain 쓰면&lt;/p&gt;
&lt;pre id=&quot;code_1772155052223&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;python create_sql_agent(llm, db=db)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이 한 줄로 다 가능해진다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 구조로 변경 후 출석 관련 모든 질문에 대해 답변이 가능한 에이전트를 간단하게 개발하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/PoroGramr/PW3_AiAgent&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/PoroGramr/PW3_AiAgent&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772155210509&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 - PoroGramr/PW3_AiAgent&quot; data-og-description=&quot;Contribute to PoroGramr/PW3_AiAgent development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/PoroGramr/PW3_AiAgent&quot; data-og-url=&quot;https://github.com/PoroGramr/PW3_AiAgent&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/HdiR0/dJMb88F17ZY/fb0UsKM30gzjfDyqbPerDK/img.png?width=1200&amp;amp;height=600&amp;amp;face=941_151_987_201,https://scrap.kakaocdn.net/dn/yNB4X/dJMb88F17ZX/6I689MDsa7G8kVO0F9kzj0/img.png?width=1200&amp;amp;height=600&amp;amp;face=941_151_987_201&quot;&gt;&lt;a href=&quot;https://github.com/PoroGramr/PW3_AiAgent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/PoroGramr/PW3_AiAgent&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/HdiR0/dJMb88F17ZY/fb0UsKM30gzjfDyqbPerDK/img.png?width=1200&amp;amp;height=600&amp;amp;face=941_151_987_201,https://scrap.kakaocdn.net/dn/yNB4X/dJMb88F17ZX/6I689MDsa7G8kVO0F9kzj0/img.png?width=1200&amp;amp;height=600&amp;amp;face=941_151_987_201');&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 - PoroGramr/PW3_AiAgent&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to PoroGramr/PW3_AiAgent development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;프롬프트나 소스코드가 궁금한 자는 여기로..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;194&quot; data-origin-height=&quot;259&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lnDyX/dJMcacPJRXw/AKIEnwTlEGbKbtkRa95Xg1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lnDyX/dJMcacPJRXw/AKIEnwTlEGbKbtkRa95Xg1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lnDyX/dJMcacPJRXw/AKIEnwTlEGbKbtkRa95Xg1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlnDyX%2FdJMcacPJRXw%2FAKIEnwTlEGbKbtkRa95Xg1%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;194&quot; height=&quot;259&quot; data-origin-width=&quot;194&quot; data-origin-height=&quot;259&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;추후에는 MCP나 vector db를 적용해 좀 더 고도화 해보고자 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이런 AI 에이전트에 관심이 있는 분은&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=372710393&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=372710393&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772155150277&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;books.book&quot; data-og-title=&quot;요즘 당근 AI 개발 | 당근 팀&quot; data-og-description=&quot;AI로 과연 될까?라는 질문은 AI로 어떻게 하면 될까?라는 고민으로 이어졌다. 엔지니어, 프로덕트 매니저, 운영 매니저, 누구든 문제의 중심에서 AI를 도구로 삼아 새로운 길을 만들어갔다. 이 책&quot; data-og-host=&quot;www.aladin.co.kr&quot; data-og-source-url=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=372710393&quot; data-og-url=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=372710393&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pxA8x/dJMb8950PsH/vuZ4TUge1SZFKVSwZWEQ1k/img.jpg?width=500&amp;amp;height=715&amp;amp;face=0_0_500_715,https://scrap.kakaocdn.net/dn/diSMZK/dJMb85vL9yr/wTfxFInOyTKqKZA5Ke3wtk/img.jpg?width=500&amp;amp;height=715&amp;amp;face=0_0_500_715&quot;&gt;&lt;a href=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=372710393&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=372710393&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pxA8x/dJMb8950PsH/vuZ4TUge1SZFKVSwZWEQ1k/img.jpg?width=500&amp;amp;height=715&amp;amp;face=0_0_500_715,https://scrap.kakaocdn.net/dn/diSMZK/dJMb85vL9yr/wTfxFInOyTKqKZA5Ke3wtk/img.jpg?width=500&amp;amp;height=715&amp;amp;face=0_0_500_715');&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;요즘 당근 AI 개발 | 당근 팀&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AI로 과연 될까?라는 질문은 AI로 어떻게 하면 될까?라는 고민으로 이어졌다. 엔지니어, 프로덕트 매니저, 운영 매니저, 누구든 문제의 중심에서 AI를 도구로 삼아 새로운 길을 만들어갔다. 이 책&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.aladin.co.kr&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 style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이 책 추천합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;재밌어서 하루만에 다 읽음&lt;/p&gt;</description>
      <category>Spring</category>
      <author>PoroGramr</author>
      <guid isPermaLink="true">https://jspark33.tistory.com/156</guid>
      <comments>https://jspark33.tistory.com/156#entry156comment</comments>
      <pubDate>Fri, 27 Feb 2026 10:19:23 +0900</pubDate>
    </item>
    <item>
      <title>AWS에 의존했다가 2달동안 10만원 써버린 건에 대하여..</title>
      <link>https://jspark33.tistory.com/155</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;요즘 블로그에 올라오는 출석관리체계 서비스&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;원래는 홈서버에 배포해서 운영하다가&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;새해를 맞이해 클라우드로 옮겨야겠다는 생각이 들었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;왜냐?&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;작년에 아파트가 한번 점검으로 인해 정전된 적이 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;우리집의 네트워크 구조는&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xrF0c/dJMcab37CUO/K99ZuFn538KbSMAg5nruT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xrF0c/dJMcab37CUO/K99ZuFn538KbSMAg5nruT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xrF0c/dJMcab37CUO/K99ZuFn538KbSMAg5nruT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxrF0c%2FdJMcab37CUO%2FK99ZuFn538KbSMAg5nruT0%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;597&quot; height=&quot;896&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이런 구조로 구성되어 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;포트포워딩을 통해 홈서버로 요청이 들어가 응답이 오는 구조였는데&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;정전 이후 DHCP로 홈서버의 IP가 변경되면서&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;서버는 살아있지만 외부에서 접속할 수 없는 상황이 발생한것이다..&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;하필 이 당시에 나는 제주도에 있었고,,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;네트워크 설정을 건드릴 수 없는 상황에 놓인것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이 때 나중을 위해서라도 클라우드로 전환이 필요하다고 생각이 들었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 새해부터는 클라우드 비용을 지원받을 수 있게 되어서&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;aws로의 전환을 시도했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;1차 전환기&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;일단 도메인은 가비아를 통해 구매했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.14.10 PM.png&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3dbah/dJMcagK9Q4z/MzDvn1DmNQWVrN4ykcWXWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3dbah/dJMcagK9Q4z/MzDvn1DmNQWVrN4ykcWXWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3dbah/dJMcagK9Q4z/MzDvn1DmNQWVrN4ykcWXWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3dbah%2FdJMcagK9Q4z%2FMzDvn1DmNQWVrN4ykcWXWk%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;813&quot; height=&quot;83&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.14.10 PM.png&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;83&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;1년치 3,300원&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;AWS에서 사용한 서비스 들은 다음과 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;EC2 - T3 micro&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;VPC&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;RDS&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Route 53&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;ELB&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;클라우드 서버, DB, HTTPS 적용을 위한 서비스..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;들만 사용했다..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 1달 뒤..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.16.21 PM.png&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WhHXb/dJMcacaVb9v/ZeqJZy2xzBmnWowuwxerj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WhHXb/dJMcacaVb9v/ZeqJZy2xzBmnWowuwxerj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WhHXb/dJMcacaVb9v/ZeqJZy2xzBmnWowuwxerj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWhHXb%2FdJMcacaVb9v%2FZeqJZy2xzBmnWowuwxerj1%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;1252&quot; height=&quot;736&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.16.21 PM.png&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;736&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;예?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.16.48 PM.png&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;381&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2Rab6/dJMcai3cSvQ/pWYN8lWAgbCAoOyvoce8y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2Rab6/dJMcai3cSvQ/pWYN8lWAgbCAoOyvoce8y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2Rab6/dJMcai3cSvQ/pWYN8lWAgbCAoOyvoce8y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2Rab6%2FdJMcai3cSvQ%2FpWYN8lWAgbCAoOyvoce8y1%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;717&quot; height=&quot;381&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.16.48 PM.png&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;381&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;미친 환율덕에 63542원 이라는 미친 금액이 발생했다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;2차 전환기&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;아무리 금액을 지원받을 수 있다고 해도&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;매달 63,000원&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;1년에 &lt;span style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot;&gt;756,000이라는 돈을 청구할 수는 없다..&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot;&gt;그렇게&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;RDS&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Route 53&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;ELB&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;는 바로 제거했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;RDS는 cafe24 뉴아우토반 호스팅 DB로 변경&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.19.24 PM.png&quot; data-origin-width=&quot;193&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XKkoP/dJMb996kiJl/OaIyM6KSU1uMexzOI9OQW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XKkoP/dJMb996kiJl/OaIyM6KSU1uMexzOI9OQW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XKkoP/dJMb996kiJl/OaIyM6KSU1uMexzOI9OQW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXKkoP%2FdJMb996kiJl%2FOaIyM6KSU1uMexzOI9OQW0%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;193&quot; height=&quot;441&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.19.24 PM.png&quot; data-origin-width=&quot;193&quot; data-origin-height=&quot;441&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;원래는 간단한 서비스 배포용 호스팅 서비스 이지만&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;나는 DB용으로 사용한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;월 450원의 저렴한 가격...&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;또한 HTTPS는 route53과 ELB를 사용하는게 아닌&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;클라우드 서버에서 nginx + certBot을 적용해 HTTPS로 전환 작업을 진행했다..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 한달이 지나고 다음 요금을 확인해보니..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.21.05 PM.png&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;687&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvzcQ1/dJMcabC4PCC/VAA6SKFos8DiMK6vCEegp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvzcQ1/dJMcabC4PCC/VAA6SKFos8DiMK6vCEegp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvzcQ1/dJMcabC4PCC/VAA6SKFos8DiMK6vCEegp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvzcQ1%2FdJMcabC4PCC%2FVAA6SKFos8DiMK6vCEegp1%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;1263&quot; height=&quot;687&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.21.05 PM.png&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;687&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;19달러요?,,,..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;아니 그래도 T3 micro 제일 작은거 썼는데..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.21.14 PM.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v9SIq/dJMcaioFc1g/kogrkb6hWapdSahkkIZo11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v9SIq/dJMcaioFc1g/kogrkb6hWapdSahkkIZo11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v9SIq/dJMcaioFc1g/kogrkb6hWapdSahkkIZo11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv9SIq%2FdJMcaioFc1g%2Fkogrkb6hWapdSahkkIZo11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;394&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.21.14 PM.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;미친 환율&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;결국 이거도 아니라고 판단했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;너무 비싸다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;다른 방식을 찾아내야겠다고 다짐했다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;3차 전환기&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;aws를 사용하기엔 너무 비싸다 판단 다른 저렴한 클라우드를 찾아냈다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;해외 서비스가 아닌 국내서비스로..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;달러가 너무 비싸니까..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 찾아낸 저렴한 서버&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;iwinv&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.23.30 PM.png&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;577&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6om4g/dJMcac20cHe/M3waTqySkCs9mrKDkIGLsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6om4g/dJMcac20cHe/M3waTqySkCs9mrKDkIGLsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6om4g/dJMcac20cHe/M3waTqySkCs9mrKDkIGLsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6om4g%2FdJMcac20cHe%2FM3waTqySkCs9mrKDkIGLsK%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;1179&quot; height=&quot;577&quot; data-filename=&quot;Screenshot 2026-02-03 at 4.23.30 PM.png&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;577&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;일단 접속자가 많지 않기에 가장 저렴한 서버 vgna_1_n을 선택했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;무려 월 5,600원&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;저장소도 25gb이기에&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;1gb의 적은 메모리에 스왑 메모리 2gb 변경하면 꽤 쓸만할거란 생각이 들었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 aws 서비스는 모두 해지하고..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;가비아(도메인) + cafe24(db) + iwinv(서버)&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;을 통해 월 약 6,300원으로 운영비를 절약했다..&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;무려 63,000원에서 6,300원으로&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;10%로 절약했다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;다들 나처럼 폭탄맞지 말고..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;저렴한 서비스를 찾아보도록 하자..&lt;/p&gt;</description>
      <category>DevOps</category>
      <author>PoroGramr</author>
      <guid isPermaLink="true">https://jspark33.tistory.com/155</guid>
      <comments>https://jspark33.tistory.com/155#entry155comment</comments>
      <pubDate>Tue, 3 Feb 2026 16:26:28 +0900</pubDate>
    </item>
    <item>
      <title>certbot + nginx로 https 적용하기</title>
      <link>https://jspark33.tistory.com/154</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;windows-xp-bliss-wallpaper-in-8k-ai-upscaled-v0-puzkje595kf81.webp&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;868&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQfcPA/dJMcac2Zzkr/eZsKw9rj8TGwzE1Fxsefh1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQfcPA/dJMcac2Zzkr/eZsKw9rj8TGwzE1Fxsefh1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQfcPA/dJMcac2Zzkr/eZsKw9rj8TGwzE1Fxsefh1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQfcPA%2FdJMcac2Zzkr%2FeZsKw9rj8TGwzE1Fxsefh1%2Fimg.webp&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;868&quot; data-filename=&quot;windows-xp-bliss-wallpaper-in-8k-ai-upscaled-v0-puzkje595kf81.webp&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;868&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;하도 자주 반복해서 기록용으로 남겨둔다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;도메인은 구입해야한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Client&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;darr;&amp;nbsp;https&lt;br /&gt;nginx&amp;nbsp;(host,&amp;nbsp;443)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;darr;&amp;nbsp;proxy_pass&lt;br /&gt;Spring&amp;nbsp;Boot&amp;nbsp;(Docker,&amp;nbsp;8080)&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;전체적인 구조&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;1. 도메인 dns a 레코드 등록 확인&lt;/h3&gt;
&lt;pre id=&quot;code_1770007665118&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dig api.domain.xyz&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;해당 명령어를 통해 구입한 도메인에 a 레코드가 제대로 등록되었는지 확인한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;새로 구입한 도메인에 경우 운 없으면 1시간이상 걸린다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;(내가 그랬다.)&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;2. nginx 설치 ( 호스트)&lt;/h3&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1770007736645&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update sudo apt install -y nginx

상태 확인:
sudo systemctl status nginx&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;3. nginx&amp;nbsp; 기본 프록시 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1770007758417&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo vi /etc/nginx/sites-enabled/default&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 기본 설정들이 입력되있을 텐데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dd&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dG&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하면 다 지워진다.&lt;/p&gt;
&lt;pre id=&quot;code_1770007765857&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
    listen 80;
    server_name api.domain.xyz;

    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용&lt;/p&gt;
&lt;pre id=&quot;code_1770007809267&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo nginx -t
sudo systemctl reload nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트&lt;/p&gt;
&lt;pre id=&quot;code_1770007824896&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl http://localhost:8080/swagger-ui/index.html
curl http://api.domain.xyz/swagger-ui/index.html&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘 다 HTML 나오면 다음 단계로 진행&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;4. certbot 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1770007851072&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt install -y certbot python3-certbot-nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;5. HTTPS 인증서 발급&lt;/h3&gt;
&lt;pre id=&quot;code_1770007872566&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo certbot --nginx -d api.pw3hub.xyz&lt;/code&gt;&lt;/pre&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 data-end=&quot;1621&quot; data-start=&quot;1609&quot;&gt;이메일 &amp;rarr; 아무거나&lt;/li&gt;
&lt;li data-end=&quot;1630&quot; data-start=&quot;1622&quot;&gt;약관 &amp;rarr; Y&lt;/li&gt;
&lt;li data-end=&quot;1659&quot; data-start=&quot;1631&quot;&gt;HTTP &amp;rarr; HTTPS 리다이렉트 &amp;rarr; &lt;b&gt;Y&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;6. Spring Boot 설정 추가&lt;/h3&gt;
&lt;pre id=&quot;code_1770007907327&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  forward-headers-strategy: framework&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <author>PoroGramr</author>
      <guid isPermaLink="true">https://jspark33.tistory.com/154</guid>
      <comments>https://jspark33.tistory.com/154#entry154comment</comments>
      <pubDate>Mon, 2 Feb 2026 13:52:50 +0900</pubDate>
    </item>
    <item>
      <title>출석관리체계에 AI 에이전트 구축하기 - 0</title>
      <link>https://jspark33.tistory.com/153</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;교회에서 중고등부 교사를 하면서 비효율적으로 출석 체크를 하는 현상을 확인했다...&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;매주 학생 리스트에 엑셀로 1,0 을 입력하며 출석을 하고 있던 것이였다..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 간단하게 AI로 프론트 개발하고&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;백엔드는 직접 개발하며 출석관리체계를 만들어 1년간 사용했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1895&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SunDV/dJMcah36FnB/JwRuFUsZiuKqVJ3BieMxcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SunDV/dJMcah36FnB/JwRuFUsZiuKqVJ3BieMxcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SunDV/dJMcah36FnB/JwRuFUsZiuKqVJ3BieMxcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSunDV%2FdJMcah36FnB%2FJwRuFUsZiuKqVJ3BieMxcK%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;1895&quot; height=&quot;772&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1895&quot; data-origin-height=&quot;772&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;부끄러운 프론트&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그냥 간단하게 출석, 지각, 결석 여부를 체크하는 방식이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;뭐 숨겨진 여러 기능이 있긴하다만..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;2026년이 되면서 2.0으로 업그레이드 할 겸&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지 쌓은 출석 데이터를 바탕으로 AI 에이전트를 만들어보고자 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;이번 달 출석 위험 학생 알려줘&amp;rdquo;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;ldquo;지각이&amp;nbsp;잦은&amp;nbsp;순서로&amp;nbsp;10명&amp;nbsp;보여줘&amp;rdquo;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;지난 학기 대비 출석 개선된 학생은?&amp;rdquo;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;이번&amp;nbsp;달&amp;nbsp;한&amp;nbsp;번도&amp;nbsp;안&amp;nbsp;나온&amp;nbsp;아이들&amp;nbsp;알려줘&amp;rdquo;&lt;br /&gt;&lt;br /&gt;&amp;ldquo;3주&amp;nbsp;연속&amp;nbsp;결석한&amp;nbsp;학생&amp;nbsp;있어?&amp;rdquo;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이런식의 자연어 질문을 AI가 해석하고 응답하게끔 하는 것이 목표다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;먼저 우리 시스템이 어떻게 사용자의 질문을 이해하고 답변하는지 큰 그림은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1. &lt;b&gt;사용자:&lt;/b&gt; &quot;김민준 학생 결석 몇 번 했어?&quot; 라고 질문을 던지면&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2. &lt;b&gt;백엔드 서버 (우리의 Spring Boot 앱):&lt;/b&gt; 이 질문을 받아서 처리한다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3. &lt;b&gt;LLM (AI 언어 모델):&lt;/b&gt; 백엔드 서버가 자연어 질문을 이해하고 답변을 생성할 수 있도록 도와주는 두뇌 역할을 한다. (예: OpenAI의 GPT, Google의 Gemini)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;4. &lt;b&gt;데이터베이스 (우리의 MySQL DB):&lt;/b&gt; 실제 출석 데이터가 저장된 곳&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;핵심은 &quot;LLM이 우리 데이터베이스 내용을 어떻게 알까?&quot;&lt;/b&gt;&amp;nbsp;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;LLM은 세상의 일반적인 지식은 알아도, 우리가 쌓은 '출석 데이터'는 전혀 모르기 때문이다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 문제를 해결하는 기술이 바로 &lt;b&gt;RAG (Retrieval-Augmented Generation, 검색&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;증강 생성)&lt;/b&gt;&amp;nbsp;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;핵심 기술: RAG 쉽게 이해하기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;RAG는 3단계로 이루어진다. &quot;민우 학생 결석 몇 번 했어?&quot; 라는 질문을 예시로 설명하자면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1. &lt;b&gt;Retrieval (검색):&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* AI가 먼저 &quot;아, 이건 '특정 학생의 결석 횟수'를 묻는 거구나!&quot; 라고 &lt;b&gt;질문의 의도를 파악하고&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* 그 다음, 이 의도에 맞춰 우리 데이터베이스에서 실제 정보를 &lt;b&gt;검색한다&lt;/b&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;/span&gt;&lt;span&gt;SELECT COUNT(*) FROM attendance WHERE student_name = '민우' AND status = 'ABSENT';&lt;/span&gt;&lt;span&gt; 와 같은 쿼리를 실행해서 &lt;b&gt;&quot;민우, 결석, 5회&quot;&lt;/b&gt; 라는 데이터를 찾아낸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2. &lt;b&gt;Augmentation (정보 보강):&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* 이제 LLM에게 보낼 질문지(프롬프트)에, 우리가 데이터베이스에서 찾아낸 정보를 &lt;b&gt;추가(보강)한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;gt; &lt;b&gt;[LLM에게 보낼 프롬프트 예시]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;gt; 너는 친절한 출석 관리 조교야. 아래 &quot;정보&quot;를 바탕으로 &quot;질문&quot;에 대답해줘&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;gt; &lt;b&gt;# 정보:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;gt; * &lt;span&gt;&amp;nbsp; &lt;/span&gt;학생 이름: 민우&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;gt; * &lt;span&gt;&amp;nbsp; &lt;/span&gt;결석 횟수: 5회&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;gt; &lt;b&gt;# 질문:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;gt; 민우 학생 결석 몇 번 했어?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3. &lt;b&gt;Generation (답변 생성):&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* 이렇게 정보가 보강된 프롬프트를 LLM API에 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* LLM은 주어진 정보를 보고 아주 자연스러운 문장으로 답변을 &lt;b&gt;생성하고&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;&quot;네, 민우 학생은 총 5번 결석했습니다.&quot;&lt;/b&gt; 와 같은 답변이 만들어지고, 우리는 이걸 사용자에게 보여주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;AI 에이전트 구축 과정 (총 4단계)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;i&gt;1단계: 기능 정의 및 DB 쿼리 메서드 구현&amp;nbsp;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;AI가 답변할 수 있는 질문의 종류를 명확하게 정의하는 것이 가장 중요하다. 이 단계가 잘 되어야 뒤따르는 개발이 수월해진다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1766845523109&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 예시: AttendanceService.java 에 추가될 메서드들
public int getAbsenceCountForStudent(String studentName) {
    // ... JPA Repository를 호출하여 결석 횟수 조회 로직
}

public List&amp;lt;Student&amp;gt; findStudentsWithExcessiveLateness(int count, LocalDate startDate, LocalDate endDate) {
     // ... 지각 3회 이상 학생 목록 조회 로직
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI의 도움을 받아 답변을 구할 질문 5가지를 선정했다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;AI 에이전트가 답변할 핵심 질문 5가지&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;i&gt;1. 장기 결석자 찾기&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;사용자 질문:&lt;/b&gt; &quot;이번 달 한 번도 안 나온 아이들 알려줘&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;AI의 해석 (의도):&lt;/b&gt; &lt;/span&gt;&lt;span&gt;LIST_FULL_ABSENCE_STUDENTS&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;필요한 정보:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;기간:&lt;/b&gt; &quot;이번 달&quot; (예: &lt;/span&gt;&lt;span&gt;2025-12-01&lt;/span&gt;&lt;span&gt; ~ &lt;/span&gt;&lt;span&gt;2025-12-31&lt;/span&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;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;구현 방향:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1. 해당 기간의 모든 출석/지각 기록을 조회합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2. 전체 학생 명단에서, 위 출석 기록에 한 번도 등장하지 않은 학생을 찾아냅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3. 찾아낸 학생들의 명단을 반환합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;선정 이유:&lt;/b&gt; 가장 기본적이고 필수적인 기능입니다. '관리의 시작'이라고 할 수 있는 질문이라 활용도가 높습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;i&gt;2. 연속 결석 패턴 감지&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;사용자 질문:&lt;/b&gt; &quot;3주 연속 결석한 학생 있어?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;AI의 해석 (의도):&lt;/b&gt; &lt;/span&gt;&lt;span&gt;FIND_CONSECUTIVE_ABSENCE_STUDENTS&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;필요한 정보:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;주(Week) 수:&lt;/b&gt; 3&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;구현 방향:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1. 최근 3주간의 날짜 범위를 계산합니다. (예: 3주 전 일요일 ~ 지난주 토요일)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2. 각 주(Week)마다 결석한 학생들을 각각 조회합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3. 3주 모두 결석 명단에 포함된 학생을 찾아냅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;선정 이유:&lt;/b&gt; 단순 조회가 아닌 &lt;b&gt;'패턴'을 분석&lt;/b&gt;하는 질문입니다. 기술적으로 한 단계 더 깊이가 있어, 개발자의 문제 해결 능력을 보여주기 좋은 예시입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;i&gt;3. 신입생 정착 현황 파악&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;사용자 질문:&lt;/b&gt; &quot;신입인데 2주 연속 나온 학생&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;AI의 해석 (의도):&lt;/b&gt; &lt;/span&gt;&lt;span&gt;FIND_NEW_CONSECUTIVE_ATTENDEES&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;필요한 정보:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;신입생 기준:&lt;/b&gt; &quot;최근 3개월 내 등록&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;연속 출석 주(Week) 수:&lt;/b&gt; 2&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;구현 방향:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1. 먼저 '신입생' 그룹을 정의하고, 최근 3개월 내 등록한 학생 목록을 조회합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2. 이 학생들을 대상으로, '2번 질문'과 유사하게 최근 2주간 연속으로 출석했는지 패턴을 분석합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;선정 이유:&lt;/b&gt; &lt;b&gt;학생 정보(신입생 여부)와 출석 정보를 결합&lt;/b&gt;해야 하는 질문입니다. 여러 도메인의 데이터를 조합하는 능력을 보여줄 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;i&gt;4. 학년별 데이터 요약 및 비교&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;사용자 질문:&lt;/b&gt; &quot;학년별 평균 출석률 보여줘&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;AI의 해석 (의도):&lt;/b&gt; &lt;/span&gt;&lt;span&gt;GET_AVERAGE_ATTENDANCE_RATE_BY_GRADE&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;필요한 정보:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;기간:&lt;/b&gt; &quot;이번 학기&quot; 또는 &quot;이번 달&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;구현 방향:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1. 해당 기간 동안 각 학년의 전체 학생 수와 실제 출석 수를 각각 계산합니다. (데이터베이스의 &lt;/span&gt;&lt;span&gt;GROUP BY&lt;/span&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;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2. &lt;/span&gt;&lt;span&gt;(실제 출석 수 / 전체 학생 수) * 100&lt;/span&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;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;선정 이유:&lt;/b&gt; 개별 학생 조회가 아닌, &lt;b&gt;데이터를 집계하고 통계를 내는 능력&lt;/b&gt;을 보여줍니다. 백엔드 개발자의 중요한 역량 중 하나입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;i&gt;5. 복합 조건 분석 및 요약 (AI의 강점)&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;사용자 질문:&lt;/b&gt; &quot;이번 분기 관리가 필요한 학생 요약해줘&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;AI의 해석 (의도):&lt;/b&gt; &lt;/span&gt;&lt;span&gt;SUMMARIZE_STUDENTS_NEEDING_CARE&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;필요한 정보:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;'관리가 필요한'의 정의:&lt;/b&gt; &lt;b&gt;(예시) &quot;결석 2회 이상&quot; 또는 &quot;지각 3회 이상&quot;&lt;/b&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;(이 기준은 우리가 직접 정해야 합니다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;기간:&lt;/b&gt; &quot;이번 분기&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;구현 방향:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1. 해당 분기 동안, 결석 횟수가 2회 이상인 학생 목록을 조회합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2. 지각 횟수가 3회 이상인 학생 목록도 조회합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3. 두 목록을 합친 후 중복을 제거하여 최종 &quot;관리가 필요한 학생&quot; 목록을 만듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;선정 이유:&lt;/b&gt; &quot;관리가 필요하다&quot;는 다소 추상적인 질문을 &lt;b&gt;명확한 데이터 기준으로 정의하고, 여러 조건을 조합하여 해결&lt;/b&gt;하는 과정을 보여줍니다. AI 에이전트의 가장 큰 가치를 보여줄 수 있는 질문입니다.&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&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;DB 쿼리 메서드 구현:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* 위에서 정의한 각 질문 유형에 맞춰, &lt;/span&gt;&lt;span&gt;AttendanceService&lt;/span&gt;&lt;span&gt; 또는 새로 만들 &lt;/span&gt;&lt;span&gt;AIService&lt;/span&gt;&lt;span&gt;에 데이터를 조회하는 메서드를 미리 만들어둔다. JPA Repository에 필요한 쿼리 메서드를 추가해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;i&gt;2단계: LLM 연동 및 프롬프트 엔지니어링&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Java에서 LLM을 쉽게 사용하도록 도와주는 &lt;b&gt;Spring AI&lt;/b&gt; 프로젝트를 활용하는 것을 추천한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1766845574021&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   * 의존성 추가 (`build.gradle`):

// Spring AI는 아직 정식 버전이 아닐 수 있어, repository 추가가 필요할 수 있습니다.
repositories {
   maven { url 'https://repo.spring.io/milestone' }
}
dependencies {
    implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0-M1' // OpenAI 연동용
}

   * API 키 설정 (`application.yml`):
spring:
	ai:
		openai:
			api-key: ${OPENAI_API_KEY} // GitHub Secret 또는 환경 변수로 관리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;프롬프트 작성 및 API 호출:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* 1단계에서 만든 서비스 메서드를 호출하여 얻은 &lt;b&gt;데이터&lt;/b&gt;와 사용자의 &lt;b&gt;질문&lt;/b&gt;을 조합하여 LLM에게 보낼 &lt;b&gt;프롬프트&lt;/b&gt;를 만든다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1766845637605&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;     // 예시: AIService.java
     @Service
     @RequiredArgsConstructor
     public class AiChatService {
 
 		 private final OpenAiChatClient chatClient; // Spring AI가 제공
         private final AttendanceService attendanceService;
 
         public String getAttendanceAnswer(String userQuestion) {
             // TODO: 사용자의 질문 의도 파악 (초기에는 '학생 결석 횟수' 질문만 처리한다고 가정)
 
             // 1. 데이터베이스에서 정보 검색 (Retrieval)
             int absenceCount = attendanceService.getAbsenceCountForStudent(&quot;민우&quot;); // 이름은 질문에서 파싱해야 함
 
             // 2. 프롬프트 생성 (Augmentation)
             String promptTemplate = &quot;&quot;&quot;
                 너는 친절한 출석 관리 조교야.
                 주어진 정보를 바탕으로 사용자의 질문에 한국어로 대답해줘.
 
                 # 정보:
                 - 학생 이름: 민우
                 - 결석 횟수: {count}회
 
                 # 질문:
                 {question}
                 &quot;&quot;&quot;;
             Prompt prompt = new PromptTemplate(promptTemplate)
                 .create(Map.of(&quot;count&quot;, absenceCount, &quot;question&quot;, userQuestion));
 
             // 3. LLM API 호출 및 답변 생성 (Generation)
             ChatResponse response = chatClient.call(prompt);
             return response.getResult().getOutput().getContent();
         }
     }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;i&gt;3단계: API 엔드포인트 생성&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;사용자의 자연어 질문을 받고 답변을 반환하는 API를 만든다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* &lt;b&gt;컨트롤러 생성 (`AiController.java`):&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1766845675789&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;     @RestController
     @RequestMapping(&quot;/api/ai&quot;)
     @RequiredArgsConstructor
     public class AiController {
 
         private final AiChatService aiChatService;
 
         @PostMapping(&quot;/chat&quot;)
         public ResponseEntity&amp;lt;String&amp;gt; chatWithAgent(@RequestBody String question) {
             String answer = aiChatService.getAttendanceAnswer(question);
             return ResponseEntity.ok(answer);
         }
     }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;i&gt;4단계: 간단한 UI 구현&lt;/i&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;</description>
      <category>Spring</category>
      <author>PoroGramr</author>
      <guid isPermaLink="true">https://jspark33.tistory.com/153</guid>
      <comments>https://jspark33.tistory.com/153#entry153comment</comments>
      <pubDate>Sat, 27 Dec 2025 23:32:10 +0900</pubDate>
    </item>
    <item>
      <title>RPS 30.19에서 42.4로: 부하테스트를 바탕으로 한 쿼리 튜닝,인덱싱과 JVM 튜닝을 통한 추천 API 성능 개선기</title>
      <link>https://jspark33.tistory.com/152</link>
      <description>&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요약 (Summary)&lt;/b&gt;&lt;br /&gt;'동네한끼' 프로젝트의 핵심 기능인 &lt;b&gt;개인화 추천 API&lt;/b&gt;의 부하테스트를 진행하며 &lt;br /&gt;응답 속도가 평균 26초에 달하는 심각한 성능 저하를 확인했습니다.&amp;nbsp;&lt;br /&gt;이를 해결하기 위해 로직 수정, &lt;b&gt;DB 커넥션 풀 조정, 쿼리 리팩토링, 인덱스 적용, JVM 튜닝&lt;/b&gt;의 4단계를 거쳤습니다.&lt;br /&gt;그 결과, &lt;b&gt;RPS는 증가(30.19&amp;rarr; 42.4)&lt;/b&gt;하고, &lt;b&gt;응답 속도는 단축(1656ms &amp;rarr; 1100ms)&lt;/b&gt;하는 성과를 얻었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 배경 및 문제 상황 (Background &amp;amp; Problem)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 리팩토링 과정에서 메인 페이지의 &lt;b&gt;'사용자 맞춤 추천 게시글 조회 API'&lt;/b&gt;를 점검하던 중, 예상보다 훨씬 심각한 성능 지연을 발견했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 API는 로그인한 사용자의 활동(좋아요, 조회 등)을 분석해 관심사를 파악하고, 이에 맞는 게시글을 추천하는 복합적인 로직을 가지고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-10-12 at 11.51.11 PM.png&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tdzwA/btsQ6cu0uLS/ZdxJwb6EJoWYTsSfHjaReK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tdzwA/btsQ6cu0uLS/ZdxJwb6EJoWYTsSfHjaReK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tdzwA/btsQ6cu0uLS/ZdxJwb6EJoWYTsSfHjaReK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtdzwA%2FbtsQ6cu0uLS%2FZdxJwb6EJoWYTsSfHjaReK%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;1380&quot; height=&quot;284&quot; data-filename=&quot;Screenshot 2025-10-12 at 11.51.11 PM.png&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;284&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 로직 흐름&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;User Context:&lt;/b&gt; 현재 접속한 사용자가 좋아요를 누른 게시글 목록 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Tag Analysis:&lt;/b&gt; 해당 게시글들의 해시태그를 분석하여 상위 태그 추출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hybrid Recommendation:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추출된 태그 기반의 게시글 조회 (Personalized)&lt;/li&gt;
&lt;li&gt;부족한 수량만큼 인기 게시글(좋아요/최신순) 조회 (Popularity)&lt;/li&gt;
&lt;li&gt;두 리스트 병합 및 중복 제거 후 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. 성능 측정 (Baseline)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache Bench(ab)를 사용하여 부하 테스트를 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 데이터는 10,000개를 넣고 진행하였습니다.&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;Total Requests:&lt;/b&gt; 500&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Concurrency:&lt;/b&gt; 50&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;height: 70px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;Metric&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;결과값&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;&amp;nbsp;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;RPS (Throughput)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;30.19 [#/sec]&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; align=&quot;left&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;Time per request&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;1656[ms]&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 25px;&quot; align=&quot;left&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache Bench 는 부하테스트에 사용하는 간단한 툴로서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 시간 동안, 정해진 횟수/동시 접속 수로 HTTP 요청을 보내고, 응답 속도와 처리량을 측정해주는 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 작동중인 서버에 아래와 같이 요청을 보내면 부하테스트 결과를 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764511569329&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ab -n 100 -c 10 http://localhost:8080/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;-n&amp;nbsp;100&amp;nbsp;:&amp;nbsp;총&amp;nbsp;100번&amp;nbsp;요청&amp;nbsp;보내라&amp;nbsp;(requests)&lt;br /&gt;&lt;br /&gt;-c&amp;nbsp;10&amp;nbsp;:&amp;nbsp;동시에&amp;nbsp;10명씩&amp;nbsp;붙는&amp;nbsp;것처럼(concurrent)&amp;nbsp;요청&amp;nbsp;보내라&lt;br /&gt;&lt;br /&gt;뒤 URL : 벤치마크할 주소&amp;nbsp;&lt;br /&gt;&lt;br /&gt;아래는 실제 요청을 보내고 결과를 응답받은 예시입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;418&quot; data-start=&quot;371&quot;&gt;초당 처리 요청 수(Throughput, Requests per second)&lt;/li&gt;
&lt;li data-end=&quot;443&quot; data-start=&quot;421&quot;&gt;평균 응답 시간 / 최솟값 / 최댓값&lt;/li&gt;
&lt;li data-end=&quot;481&quot; data-start=&quot;446&quot;&gt;퍼센타일(50%, 90%, 95%, 99% 지점 응답 시간)&lt;/li&gt;
&lt;li data-end=&quot;510&quot; data-start=&quot;484&quot;&gt;전송된 데이터량, 에러 수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 확인 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764511434950&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; ab -n 500 -c 50 \
        -H 'accept: */*' \
        -H 'Authorization: Bearer (Access Token)' \
         'https://dh.porogramr.site/api/posts/recommendPosts?limit=10'
This is ApacheBench, Version 2.3 &amp;lt;$Revision: 1913912 $&amp;gt;
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking dh.porogramr.site (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Finished 500 requests


Server Software:        
Server Hostname:        dh.porogramr.site
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-CHACHA20-POLY1305,4096,256
Server Temp Key:        ECDH X25519 253 bits
TLS Server Name:        dh.porogramr.site

Document Path:          /api/posts/recommendPosts?limit=10
Document Length:        8636 bytes

Concurrency Level:      50
Time taken for tests:   79.725 seconds
Complete requests:      500
Failed requests:        499
   (Connect: 0, Receive: 0, Length: 499, Exceptions: 0)
Total transferred:      4523637 bytes
HTML transferred:       4394637 bytes
Requests per second:    6.27 [#/sec] (mean)
Time per request:       7972.543 [ms] (mean)
Time per request:       159.451 [ms] (mean, across all concurrent requests)
Transfer rate:          55.41 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       77  270 366.9    135    3122
Processing:   687 7319 3109.6   7163   19024
Waiting:      681 7305 3106.0   7148   18875
Total:        793 7588 3100.8   7368   20089

Percentage of the requests served within a certain time (ms)
  50%   7368
  66%   8255
  75%   9444
  80%  10052
  90%  11284
  95%  13551
  98%  16034
  99%  17632
 100%  20089 (longest request)&lt;/code&gt;&lt;/pre&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;2. 가설 및 검증 과정 (Troubleshooting)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Phase 1. DB 커넥션 풀(HikariCP)이 원인일까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 로그 분석 결과, DB 커넥션을 획득하기 위해 스레드들이 대기하는 현상이 관측되었습니다. &lt;code&gt;HikariCP&lt;/code&gt;의 기본 설정(Size=10)이 동시 요청 50개를 처리하기엔 턱없이 부족하다고 판단했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-11-29 at 9.33.02 PM.png&quot; data-origin-width=&quot;1569&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bld7zV/dJMcac9meGy/z2OiAHI692zKAWknvuBUf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bld7zV/dJMcac9meGy/z2OiAHI692zKAWknvuBUf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bld7zV/dJMcac9meGy/z2OiAHI692zKAWknvuBUf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbld7zV%2FdJMcac9meGy%2Fz2OiAHI692zKAWknvuBUf0%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;1569&quot; height=&quot;305&quot; data-filename=&quot;Screenshot 2025-11-29 at 9.33.02 PM.png&quot; data-origin-width=&quot;1569&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 설정 값인 10개의 커넥션이 모두 사용되고 있음을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Action:&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;code&gt;maximum-pool-size&lt;/code&gt;를 10개에서 &lt;b&gt;30개&lt;/b&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;1590&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KUW15/dJMcabvS9or/zcxsyyKJoli6TUWSX3eYV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KUW15/dJMcabvS9or/zcxsyyKJoli6TUWSX3eYV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KUW15/dJMcabvS9or/zcxsyyKJoli6TUWSX3eYV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKUW15%2FdJMcabvS9or%2FzcxsyyKJoli6TUWSX3eYV0%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;1590&quot; height=&quot;341&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Result:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커넥션 풀을 증가시키고 Apache Bench(ab)를 사용하여 동일한 부하테스트를 진행하였습니다.&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;Connection Time:&lt;/b&gt; 954ms &amp;rarr; &lt;b&gt;317ms&lt;/b&gt; (대기 시간 감소)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Processing Time:&lt;/b&gt; 639ms &amp;rarr; &lt;b&gt;1435ms&lt;/b&gt; (처리 시간 &lt;b&gt;2배 증가&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RPS:&lt;/b&gt; 30.19 &amp;rarr; &lt;b&gt;27.58&lt;/b&gt; (오히려 &lt;b&gt;성능 하락&lt;/b&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션 풀을 늘리자 대기하던 요청들이 한꺼번에 DB로 유입되었습니다. 하지만 DB가 이를 처리하지 못해 &lt;b&gt;Processing Time&lt;/b&gt;이 오히려 급증했습니다.&lt;br /&gt;이는 &lt;b&gt;&quot;커넥션 부족은 현상일 뿐, 근본 원인은 쿼리 처리 속도(Slow Query)에 있다&quot;&lt;/b&gt;는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병목 지점은 WAS가 아닌 Database 자체였습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Phase 2. 쿼리 분석 및 인덱스 튜닝 (Core Solution)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 처리가 느린 원인을 찾기 위해 쿼리 실행 계획(Explain)과 Hibernate 로그를 분석했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 치명적인 문제가 발견되었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. 비효율적인 정렬 로직 (Filesort)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드는 &lt;code&gt;Post&lt;/code&gt;와 &lt;code&gt;PostLike&lt;/code&gt;를 조인한 뒤, &lt;code&gt;COUNT()&lt;/code&gt;를 사용하여 실시간으로 좋아요 개수를 세고 정렬했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Before]&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;// GROUP BY와 집계 함수 사용으로 인한 성능 저하
@Query(&quot;SELECT p.postId FROM Post p LEFT JOIN p.postLikes pl GROUP BY p.postId ORDER BY COUNT(pl.id) DESC&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 매 요청마다 &lt;code&gt;Using temporary; Using filesort&lt;/code&gt;를 유발하여 디스크 I/O를 폭증시켰습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[After] 반정규화 및 쿼리 단순화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Post&lt;/code&gt; 엔티티에 &lt;code&gt;likeCount&lt;/code&gt; 컬럼을 추가하여 반정규화를 진행했습니다. 조회 시에는 조인 없이 단일 테이블 조회로 변경했습니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;// 이미 집계된 컬럼을 사용하여 정렬 단순화
@Query(&quot;SELECT p.postId FROM Post p ORDER BY p.likeCount DESC, p.createdAt DESC&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2. 인덱스(Index) 부재&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순화된 쿼리라도 데이터가 많아지면 Full Table Scan이 발생합니다. &lt;code&gt;EXPLAIN&lt;/code&gt; 확인 결과, 정렬 조건(&lt;code&gt;likeCount&lt;/code&gt;, &lt;code&gt;createdAt&lt;/code&gt;)에 대한 인덱스가 없어 비효율적인 스캔이 발생하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Action:&lt;/b&gt;&lt;br /&gt;복합 인덱스를 적용하여 정렬 연산(Sorting)을 인덱스 스캔으로 대체했습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;1.인기 게시물 조회를 위한 인덱스 (likeCount + createdAt)
CREATE INDEX idx_post_like_count ON post (like_count DESC, created_at DESC);
    
2. 최신 게시물 조회를 위한 인덱스 (createdAt)
CREATE INDEX idx_post_created_at ON post (created_at DESC);
   
3. 사용자가 좋아한 게시물 기반 추천을 위한 인덱스
CREATE INDEX idx_post_like_user_id ON post_like (user_id);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인덱싱 적용전&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-11-27 at 6.20.09 PM.png&quot; data-origin-width=&quot;971&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGpTYc/dJMcaiV1gJk/Kz9wGdce11lxIurPZ0EkPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGpTYc/dJMcaiV1gJk/Kz9wGdce11lxIurPZ0EkPk/img.png&quot; data-alt=&quot;인기 게시물 조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGpTYc/dJMcaiV1gJk/Kz9wGdce11lxIurPZ0EkPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGpTYc%2FdJMcaiV1gJk%2FKz9wGdce11lxIurPZ0EkPk%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;971&quot; height=&quot;248&quot; data-filename=&quot;Screenshot 2025-11-27 at 6.20.09 PM.png&quot; data-origin-width=&quot;971&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인기 게시물 조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-11-27 at 6.20.33 PM.png&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ybOJQ/dJMcadtEi5M/o69wNY6JXMKgNDD48Je8wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ybOJQ/dJMcadtEi5M/o69wNY6JXMKgNDD48Je8wK/img.png&quot; data-alt=&quot;최신 게시물 조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ybOJQ/dJMcadtEi5M/o69wNY6JXMKgNDD48Je8wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FybOJQ%2FdJMcadtEi5M%2Fo69wNY6JXMKgNDD48Je8wK%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;1056&quot; height=&quot;265&quot; data-filename=&quot;Screenshot 2025-11-27 at 6.20.33 PM.png&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;최신 게시물 조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-11-27 at 6.24.28 PM.png&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0lTWt/dJMcacIj8K3/1Enh2RwCRgtqU6ELnAnOD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0lTWt/dJMcacIj8K3/1Enh2RwCRgtqU6ELnAnOD1/img.png&quot; data-alt=&quot;사용자가 좋아한 게시물 조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0lTWt/dJMcacIj8K3/1Enh2RwCRgtqU6ELnAnOD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0lTWt%2FdJMcacIj8K3%2F1Enh2RwCRgtqU6ELnAnOD1%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;861&quot; height=&quot;269&quot; data-filename=&quot;Screenshot 2025-11-27 at 6.24.28 PM.png&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;269&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사용자가 좋아한 게시물 조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인덱싱 적용 후&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 다들 Type이 index로 변경된 점을 확인할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-11-27 at 6.23.26 PM.png&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwWbOk/dJMcaiBIHoN/7IfUDpoGV60mNsIJY1ribk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwWbOk/dJMcaiBIHoN/7IfUDpoGV60mNsIJY1ribk/img.png&quot; data-alt=&quot;인기 게시물 조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwWbOk/dJMcaiBIHoN/7IfUDpoGV60mNsIJY1ribk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwWbOk%2FdJMcaiBIHoN%2F7IfUDpoGV60mNsIJY1ribk%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;1000&quot; height=&quot;290&quot; data-filename=&quot;Screenshot 2025-11-27 at 6.23.26 PM.png&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인기 게시물 조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-11-27 at 6.24.04 PM.png&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/de4ChS/dJMcabvSlGS/KcbKB7OIkFgtaZhFEzJprK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/de4ChS/dJMcabvSlGS/KcbKB7OIkFgtaZhFEzJprK/img.png&quot; data-alt=&quot;최신 게시물 조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/de4ChS/dJMcabvSlGS/KcbKB7OIkFgtaZhFEzJprK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fde4ChS%2FdJMcabvSlGS%2FKcbKB7OIkFgtaZhFEzJprK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1098&quot; height=&quot;299&quot; data-filename=&quot;Screenshot 2025-11-27 at 6.24.04 PM.png&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;최신 게시물 조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-11-27 at 6.26.08 PM.png&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uSvfl/dJMcaioecmT/ovJwKrh1WzzKQkYjrLxP11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uSvfl/dJMcaioecmT/ovJwKrh1WzzKQkYjrLxP11/img.png&quot; data-alt=&quot;사용자가 좋아한 게시물 조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uSvfl/dJMcaioecmT/ovJwKrh1WzzKQkYjrLxP11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuSvfl%2FdJMcaioecmT%2FovJwKrh1WzzKQkYjrLxP11%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;818&quot; height=&quot;255&quot; data-filename=&quot;Screenshot 2025-11-27 at 6.26.08 PM.png&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;255&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사용자가 좋아한 게시물 조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- key 가 변경된 모습을 확인할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Result (Phase 2):&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;RPS:&lt;/b&gt; 27.58 &amp;rarr; &lt;b&gt;42.42&lt;/b&gt; (약 53% 향상)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Processing Time:&lt;/b&gt; 1435ms &amp;rarr; &lt;b&gt;334ms&lt;/b&gt; (약 76% 감소)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 병목이었던 DB 처리 시간이 획기적으로 줄어들었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Phase 3. JVM 및 GC 튜닝 (Optimization)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 튜닝 후 RPS는 개선되었으나, 간헐적으로 응답이 튀는 현상(Tail Latency)이 있었습니다. 프로파일링 결과 GC(Garbage Collection) 수행 시 &lt;b&gt;Stop-the-world&lt;/b&gt;가 길게 발생하는 것을 확인했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-11-29 at 9.42.04 PM.png&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;295&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yWNyh/dJMcadf7vsY/FMtKV75Kic9bzBb3OouFak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yWNyh/dJMcadf7vsY/FMtKV75Kic9bzBb3OouFak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yWNyh/dJMcadf7vsY/FMtKV75Kic9bzBb3OouFak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyWNyh%2FdJMcadf7vsY%2FFMtKV75Kic9bzBb3OouFak%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;794&quot; height=&quot;295&quot; data-filename=&quot;Screenshot 2025-11-29 at 9.42.04 PM.png&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;295&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현상:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker 컨테이너 환경에서 메모리 제한 설정 미비로 Host OS의 메모리를 잘못 인식&lt;/li&gt;
&lt;li&gt;Heap 영역 부족으로 빈번한 GC 발생 (Pause Time: ~3.47s)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Action:&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;메모리 가시성 확보:&lt;/b&gt; &lt;code&gt;-Xms1536, -Xmx1536m&lt;/code&gt;&amp;nbsp;설정으로 컨테이너 메모리 제한 인식&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GC 알고리즘 변경:&lt;/b&gt; Latency에 유리한 &lt;b&gt;ZGC&lt;/b&gt; (&lt;code&gt;-XX:+UseZGC&lt;/code&gt;) 적용 테스트&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-11-29 at 9.44.11 PM.png&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;333&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf32YK/dJMcah3WkWM/4MiCwxVl27YplB29ywGxbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf32YK/dJMcah3WkWM/4MiCwxVl27YplB29ywGxbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf32YK/dJMcah3WkWM/4MiCwxVl27YplB29ywGxbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf32YK%2FdJMcah3WkWM%2F4MiCwxVl27YplB29ywGxbk%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;828&quot; height=&quot;333&quot; data-filename=&quot;Screenshot 2025-11-29 at 9.44.11 PM.png&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;333&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DockerFile에서 메모리 옵션, GC 튜닝 옵션 추가&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-11-29 at 9.45.12 PM.png&quot; data-origin-width=&quot;421&quot; data-origin-height=&quot;95&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oOvUL/dJMcahv6tXw/DkIMJamOALWVa5NfRMz1qK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oOvUL/dJMcahv6tXw/DkIMJamOALWVa5NfRMz1qK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oOvUL/dJMcahv6tXw/DkIMJamOALWVa5NfRMz1qK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoOvUL%2FdJMcahv6tXw%2FDkIMJamOALWVa5NfRMz1qK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;421&quot; height=&quot;95&quot; data-filename=&quot;Screenshot 2025-11-29 at 9.45.12 PM.png&quot; data-origin-width=&quot;421&quot; data-origin-height=&quot;95&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GithubAction workflow 에서 메모리 제한 옵션 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Result:&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-11-29 at 9.42.40 PM.png&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;295&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRDQnS/dJMcadf7vtz/WkReSyXJyKEsd9Fgt1U5J0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRDQnS/dJMcadf7vtz/WkReSyXJyKEsd9Fgt1U5J0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRDQnS/dJMcadf7vtz/WkReSyXJyKEsd9Fgt1U5J0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRDQnS%2FdJMcadf7vtz%2FWkReSyXJyKEsd9Fgt1U5J0%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;780&quot; height=&quot;295&quot; data-filename=&quot;Screenshot 2025-11-29 at 9.42.40 PM.png&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;295&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;튜닝 이후에는 부하테스트를 진행해도 GC Stop the World가 발생하지 않음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;DB 튜닝으로 객체의 생존 주기가 짧아지면서 이미 G1GC로도 충분히 안정적인 상태가 되었지만, 메모리 설정을 명확히 함으로써 시스템 안정성을 확보했습니다. 최종적으로 GC로 인한 중단 시간은 0초에 수렴했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 최종 결과&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 단계의 튜닝을 거친 후의 성능 변화 그래프입니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;단계&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;RPS (req/sec)&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Time per request (ms)&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;Baseline&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;30.19&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;1,656&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;초기 상태&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;1. Pool 증설&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;27.58&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;1,812&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;커넥션 대기 해소, DB 부하 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;2. 쿼리/인덱스&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;b&gt;42.42&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;b&gt;1,178&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;Slow Query 해결 (Key Factor)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;Lessons Learned&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;&lt;br /&gt;단순히 Connection Timeout 로그만 보고 Pool Size를 늘리는 것은 미봉책에 불과했습니다. &lt;code&gt;Wait Time&lt;/code&gt;과 &lt;code&gt;Processing Time&lt;/code&gt;을 구분하여 분석함으로써 진짜 문제(Slow Query)를 찾을 수 있었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RDB에서 인덱스는 선택이 아닌 필수다:&lt;/b&gt;&lt;br /&gt;복잡한 비즈니스 로직을 코드로 풀기보다, DB 레벨에서 인덱스를 잘 타도록 쿼리를 설계하는 것이 성능에 훨씬 지대한 영향을 미칩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 기반의 의사결정:&lt;/b&gt;&lt;br /&gt;&lt;code&gt;ab&lt;/code&gt; 테스트와 프로파일링 도구의 수치를 근거로 튜닝 전후를 명확히 비교할 수 있었습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-11-27 at 8.24.37 PM.png&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUKaBQ/dJMcacIkavg/MAccoaUYlQhuhuaZxc5A41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUKaBQ/dJMcacIkavg/MAccoaUYlQhuhuaZxc5A41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUKaBQ/dJMcacIkavg/MAccoaUYlQhuhuaZxc5A41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUKaBQ%2FdJMcacIkavg%2FMAccoaUYlQhuhuaZxc5A41%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;1390&quot; height=&quot;176&quot; data-filename=&quot;Screenshot 2025-11-27 at 8.24.37 PM.png&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;176&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;i&gt;참고: 본 포스팅의 테스트는 로컬 Docker 환경에서 진행되었으며, 실제 운영 환경과는 차이가 있을 수 있습니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JAVA</category>
      <author>PoroGramr</author>
      <guid isPermaLink="true">https://jspark33.tistory.com/152</guid>
      <comments>https://jspark33.tistory.com/152#entry152comment</comments>
      <pubDate>Sat, 29 Nov 2025 17:23:16 +0900</pubDate>
    </item>
    <item>
      <title>Audit Log 조회 API 성능 개선기 - 3</title>
      <link>https://jspark33.tistory.com/150</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;auditLog는 기록도 기록이지만 조회가 가장 중요한 API이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;조회 성능을 개선하기 위해 어떤 방식이 있을가 고민하던 중 인덱싱을 적용하기로 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-08-08 at 6.37.49 PM.png&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7CfSF/btsPNhYaU0V/VvzrpkC47okKq6eu5uD66k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7CfSF/btsPNhYaU0V/VvzrpkC47okKq6eu5uD66k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7CfSF/btsPNhYaU0V/VvzrpkC47okKq6eu5uD66k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7CfSF%2FbtsPNhYaU0V%2FVvzrpkC47okKq6eu5uD66k%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;692&quot; height=&quot;255&quot; data-filename=&quot;Screenshot 2025-08-08 at 6.37.49 PM.png&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;인덱싱은 커서 기반 페이징에서 커서 키로 사용하는&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754645794809&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_auditlog_loggedat_id
  ON audit_log (logged_at DESC, id DESC);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;logged_at , id 에 인덱싱을 걸었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;인덱싱 적용 후 이전과 동일하게 마지막 페이지를 조회했더니&lt;/p&gt;
&lt;pre id=&quot;code_1754645786053&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-08-08 18:35:58.215 [https-jsse-nio-443-exec-9]  INFO com.welcommu.moduleapi.aop.ExecutionTimeLogger - ⏱️ LogsWithCursor com.welcommu.moduleservice.logging.AuditLogSearchServiceImpl.searchLogs(ActionType,TargetType,String,String,Long,LocalDateTime,Long,int) executed in 23 ms
2025-08-08 18:35:58.216 [https-jsse-nio-443-exec-9]  INFO com.welcommu.moduleapi.aop.ExecutionTimeLogger - ⏱️ ResponseEntity com.welcommu.moduleapi.logging.AuditLogController.search(ActionType,TargetType,String,String,Long,LocalDateTime,Long,int) executed in 24 ms&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;23ms가 나온다..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;너무 극단적이라 당황스럽다..&lt;/p&gt;</description>
      <category>Spring</category>
      <author>PoroGramr</author>
      <guid isPermaLink="true">https://jspark33.tistory.com/150</guid>
      <comments>https://jspark33.tistory.com/150#entry150comment</comments>
      <pubDate>Fri, 8 Aug 2025 18:37:57 +0900</pubDate>
    </item>
    <item>
      <title>Audit Log 조회 API 성능 개선기 - 2</title>
      <link>https://jspark33.tistory.com/149</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이전 게시글에서 AuditLog의 N + 1 문제를 해결하여 성능을 개선하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 10,000건의 데이터는 너무 적다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;여러 조언을 구해본결과 1000만건은 해야 유의미한 결과를 얻을 수 있다는 판단이 들었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 해서 데이터 1000만건 삽입!&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;audlitLog는 1000만건&lt;/p&gt;
&lt;pre id=&quot;code_1754643417547&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INSERT INTO vivim.audit_log (actor_id, target_type, target_id, action_type, logged_at)
WITH RECURSIVE numbers AS (
SELECT 1 AS n
         UNION ALL
         SELECT n + 1 FROM numbers WHERE n &amp;lt; 10000000 -- 1000만 건
     )
     SELECT
         FLOOR(1 + RAND() * 100000) AS actor_id, -- 1부터 100000 사이의 랜덤 actor_id
         ELT(FLOOR(1 + RAND() * 5), 'PROJECT', 'POST', 'COMMENT', 'USER', 'COMPANY') AS target_type, -- 예시 TargetType 값
        FLOOR(1 + RAND() * 1000000) AS target_id, -- 1부터 1000000 사이의 랜덤 target_id
        ELT(FLOOR(1 + RAND() * 3), 'CREATE', 'MODIFY', 'DELETE') AS action_type, -- 예시 ActionType 값
        FROM_UNIXTIME(UNIX_TIMESTAMP('2023-01-01 00:00:00') + FLOOR(RAND() * (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP('2023-01-01 00:00:00')))) AS logged_at -- 2023년 1월 1일부터 현재까지의 랜덤 시간
    FROM
        numbers;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;audlitLogDetail는 200만건 삽입하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1754643437574&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INSERT INTO vivim.audit_log_detail (audit_log_id, field_name, old_value, new_value)
        SELECT
            al.id,
            CONCAT('field_', FLOOR(1 + RAND() * 10)),
            CONCAT('old_value_', FLOOR(1 + RAND() * 100)),
            CONCAT('new_value_', FLOOR(1 + RAND() * 100))
        FROM
            vivim.audit_log al
        WHERE
            RAND() &amp;lt; 0.2;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;근데 문제가 발생했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;10000건일땐 아무런 문제 없던 API가&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754644375912&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-08-08 16:51:07.248 [https-jsse-nio-443-exec-9] ERROR org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception   │
│    [Request processing failed: java.lang.RuntimeException: java.lang.OutOfMemoryError: Java heap space] with root cause                                                                                                                        │
│    java.lang.OutOfMemoryError: Java heap space&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;데이터가 1000만건이 되니 out of memory가 발생하는 것이였다..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;OutOfMemoryError: Java heap space&lt;/span&gt;&lt;span&gt; 오류는 애플리케이션이 너무 많은 데이터를 한 번에 메모리에 로드하려고 할 때 발생한다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 &lt;/span&gt;&lt;span&gt;HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory&lt;/span&gt;&lt;span&gt; 경고는 &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt; 엔티티를&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;조회하면서 &lt;/span&gt;&lt;span&gt;AuditLogDetail&lt;/span&gt;&lt;span&gt;과 같은 컬렉션(&lt;/span&gt;&lt;span&gt;@OneToMany&lt;/span&gt;&lt;span&gt; 관계)을 함께 가져올 때, Hibernate가 페이징을 메모리에서 처리하기 위해 모든 관련 데이터를 먼저 로드하기 때문에 발생한다고 한다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;1000만 건의 데이터에서는 이 방식이 메모리 부족이 발생했던 것이다..&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이러한 문제를 해결하기위해 기존 커스텀쿼리에 추가한 fetch join을 제거하고&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;AuditLog.java에 @BatchSize를 추가하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt; 엔티티의 &lt;/span&gt;&lt;span&gt;details&lt;/span&gt;&lt;span&gt; 필드에 &lt;/span&gt;&lt;span&gt;@BatchSize(size = N)&lt;/span&gt;&lt;span&gt; 어노테이션을 추가하여, Hibernate가 &lt;/span&gt;&lt;span&gt;AuditLogDetail&lt;/span&gt;&lt;span&gt;을 가져올 때 N개의 &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt;에 대한 상세 정보를 한 번에 가져오도록 하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&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;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;b&gt;1. &lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;JOIN FETCH&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;와 &lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;OutOfMemoryError&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;의 원인&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;JOIN FETCH&lt;/span&gt;&lt;span&gt;는 N+1 쿼리 문제를 해결하기 위해 매우 유용한 기능입니다. &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt;와 &lt;/span&gt;&lt;span&gt;AuditLogDetail&lt;/span&gt;&lt;span&gt;의 경우, &lt;/span&gt;&lt;span&gt;JOIN FETCH a.details&lt;/span&gt;&lt;span&gt;를 사용하면 &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt;를 조회할 때 연관된 모든 &lt;/span&gt;&lt;span&gt;AuditLogDetail&lt;/span&gt;&lt;span&gt;도 한 번의 SQL 쿼리로 함께 가져옵니다.&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&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;b&gt;문제점 (페이징과 함께 사용될 때):&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;* &lt;b&gt;카테시안 곱 (Cartesian Product):&lt;/b&gt; &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt; 하나에 여러 개의 &lt;/span&gt;&lt;span&gt;AuditLogDetail&lt;/span&gt;&lt;span&gt;이 있을 수 있습니다. &lt;/span&gt;&lt;span&gt;JOIN FETCH&lt;/span&gt;&lt;span&gt;를 사용하면 데이터베이스에서 &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt; 레코드 수 * &lt;/span&gt;&lt;span&gt;AuditLogDetail&lt;/span&gt;&lt;span&gt; 레코드 수 만큼의 결과 행이 생성될 수 있습니다 (정확히는 &lt;/span&gt;&lt;span&gt;DISTINCT&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &lt;/span&gt;키워드를 사용하더라도 내부적으로는 더 많은 행이 생성될 수 있습니다).&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;* &lt;b&gt;Hibernate의 메모리 내 페이징:&lt;/b&gt; 더 중요한 문제는 Hibernate가 &lt;/span&gt;&lt;span&gt;OneToMany&lt;/span&gt;&lt;span&gt; 관계를 &lt;/span&gt;&lt;span&gt;FETCH JOIN&lt;/span&gt;&lt;span&gt;으로 가져오면서 &lt;/span&gt;&lt;span&gt;LIMIT&lt;/span&gt;&lt;span&gt; 또는 &lt;/span&gt;&lt;span&gt;OFFSET&lt;/span&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;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;* Hibernate는 &lt;/span&gt;&lt;span&gt;HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory&lt;/span&gt;&lt;span&gt; 경고를 띄우면서, &lt;b&gt;데이터베이스에서 모든 `AuditLog`와 그에 연결된 모든 `AuditLogDetail`을 먼저 가져온 다음, 애플리케이션 메모리에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;b&gt;`LIMIT`와 `OFFSET`을 적용합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;* 즉, 1000만 건의 &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt;와 그에 연결된 수천만 건의 &lt;/span&gt;&lt;span&gt;AuditLogDetail&lt;/span&gt;&lt;span&gt;이 있다면, 이 모든 데이터를 애플리케이션의 JVM 힙 메모리에 한꺼번에 로드하려고 시도합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;* 이 과정에서 JVM 힙 메모리가 부족해지면 &lt;/span&gt;&lt;span&gt;java.lang.OutOfMemoryError: Java heap space&lt;/span&gt;&lt;span&gt;가 발생하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;b&gt;2. &lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;@BatchSize&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;를 통한 해결&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@BatchSize&lt;/span&gt;&lt;span&gt;는 &lt;/span&gt;&lt;span&gt;FETCH JOIN&lt;/span&gt;&lt;span&gt;과는 완전히 다른 방식으로 N+1 문제를 해결하고 메모리 효율성을 높입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;* &lt;b&gt;지연 로딩 (Lazy Loading) 기반:&lt;/b&gt; &lt;/span&gt;&lt;span&gt;@BatchSize&lt;/span&gt;&lt;span&gt;는 기본적으로 연관 관계가 지연 로딩(Lazy Loading)으로 설정되어 있을 때 작동합니다. 즉, &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt; 엔티티를 조회할 때는 &lt;/span&gt;&lt;span&gt;AuditLogDetail&lt;/span&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;&amp;nbsp;&amp;nbsp; &lt;/span&gt;* &lt;b&gt;별도의 쿼리 실행:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;1. &lt;b&gt;1단계 쿼리 (메인 엔티티):&lt;/b&gt; &lt;/span&gt;&lt;span&gt;AuditLogRepositoryImpl.findByConditions&lt;/span&gt;&lt;span&gt;에서 &lt;/span&gt;&lt;span&gt;LEFT JOIN FETCH a.details&lt;/span&gt;&lt;span&gt;를 제거했기 때문에, 이제 쿼리는 &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt; 엔티티만 가져옵니다. 이 쿼리는 데이터베이스에서 &lt;/span&gt;&lt;span&gt;LIMIT&lt;/span&gt;&lt;span&gt;와 &lt;/span&gt;&lt;span&gt;OFFSET&lt;/span&gt;&lt;span&gt;을 효율적으로 적용하여 &lt;b&gt;요청된 페이지의&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;b&gt; `AuditLog` 레코드만&lt;/b&gt; 가져옵니다. 이 과정에서 대량의 데이터가 메모리에 로드되지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;2. &lt;b&gt;2단계 쿼리 (컬렉션 배치 로딩):&lt;/b&gt; 애플리케이션 코드에서 가져온 &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt; 객체들의 &lt;/span&gt;&lt;span&gt;getDetails()&lt;/span&gt;&lt;span&gt; 메서드를 호출하여 &lt;/span&gt;&lt;span&gt;AuditLogDetail&lt;/span&gt;&lt;span&gt; 컬렉션에 접근할 때, Hibernate는 &lt;/span&gt;&lt;span&gt;@BatchSize(size = 100)&lt;/span&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;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;* 예를 들어, 한 페이지에 10개의 &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt;를 가져왔다면, Hibernate는 이 10개의 &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt;에 해당하는 &lt;/span&gt;&lt;span&gt;AuditLogDetail&lt;/span&gt;&lt;span&gt;을 &lt;b&gt;한 번의 `IN` 쿼리&lt;/b&gt;로 가져옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;* 만약 한 페이지에 100개의 &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt;를 가져왔다면, Hibernate는 이 100개의 &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt;에 해당하는 &lt;/span&gt;&lt;span&gt;AuditLogDetail&lt;/span&gt;&lt;span&gt;을 &lt;b&gt;한 번의 `IN` 쿼리&lt;/b&gt;로 가져옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;* 이렇게 하면 N+1 쿼리(각 &lt;/span&gt;&lt;span&gt;AuditLog&lt;/span&gt;&lt;span&gt;마다 &lt;/span&gt;&lt;span&gt;AuditLogDetail&lt;/span&gt;&lt;span&gt;을 가져오는 쿼리)를 방지하면서도, 한 번에 메모리에 로드되는 데이터의 양을 &lt;/span&gt;&lt;span&gt;FETCH JOIN&lt;/span&gt;&lt;span&gt;처럼 전체가 아닌, &lt;b&gt;현재 페이지의 `AuditLog`에 해당하는 `AuditLogDetail`만으로 제한&lt;/b&gt;할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;b&gt;요약:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;JOIN FETCH&lt;/span&gt;&lt;span&gt;는 페이징 쿼리에서 &lt;/span&gt;&lt;span&gt;OneToMany&lt;/span&gt;&lt;span&gt; 관계를 처리할 때 모든 데이터를 메모리에 로드하여 &lt;/span&gt;&lt;span&gt;OutOfMemoryError&lt;/span&gt;&lt;span&gt;를 유발했습니다. 반면, &lt;/span&gt;&lt;span&gt;JOIN FETCH&lt;/span&gt;&lt;span&gt;를 제거하여 메인 엔티티의 페이징을 데이터베이스에 맡기고, &lt;/span&gt;&lt;span&gt;@BatchSize&lt;/span&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;&amp;nbsp; &lt;/span&gt;때마다 효율적인 배치 쿼리로 가져옴으로써, 애플리케이션의 메모리 사용량을 크게 줄여 &lt;/span&gt;&lt;span&gt;OutOfMemoryError&lt;/span&gt;&lt;span&gt;를 해결할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이제 outOfMemory 문제는 해결했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;근데 또 하나의 문제가 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;오프셋 기반 페이징의 단점은 마지막 페이지로 갈 수록 불필요한 조회가 진행되어 성능이 저하된다는 점이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-08-08 at 6.16.41 PM.png&quot; data-origin-width=&quot;1565&quot; data-origin-height=&quot;535&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FjQGW/btsPNHWBbzq/tPfheXcH72lqGw77DyekE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FjQGW/btsPNHWBbzq/tPfheXcH72lqGw77DyekE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FjQGW/btsPNHWBbzq/tPfheXcH72lqGw77DyekE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFjQGW%2FbtsPNHWBbzq%2FtPfheXcH72lqGw77DyekE0%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;1565&quot; height=&quot;535&quot; data-filename=&quot;Screenshot 2025-08-08 at 6.16.41 PM.png&quot; data-origin-width=&quot;1565&quot; data-origin-height=&quot;535&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;첫번째 페이지 조회의 경우 2898ms의 시간이 소모되지만&lt;/p&gt;
&lt;pre id=&quot;code_1754643589523&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-08-08 18:09:50.590 [https-jsse-nio-443-exec-1]  INFO com.welcommu.moduleapi.aop.ExecutionTimeLogger - ⏱️ Page com.welcommu.moduleservice.logging.AuditLogSearchService.searchLogs(ActionType,TargetType,String,String,Long,Pageable) executed in 2898 ms
2025-08-08 18:09:50.590 [https-jsse-nio-443-exec-1]  INFO com.welcommu.moduleapi.aop.ExecutionTimeLogger - ⏱️ ResponseEntity com.welcommu.moduleapi.logging.AuditLogController.searchAuditLogs(ActionType,TargetType,String,String,Long,Pageable) executed in 2898 ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;마지막 페이지의 경우 4538ms의 시간이 소모되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1754643824256&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-08-08 18:10:02.860 [https-jsse-nio-443-exec-1]  INFO com.welcommu.moduleapi.aop.ExecutionTimeLogger - ⏱️ Page com.welcommu.moduleservice.logging.AuditLogSearchService.searchLogs(ActionType,TargetType,String,String,Long,Pageable) executed in 4538 ms
2025-08-08 18:10:02.860 [https-jsse-nio-443-exec-1]  INFO com.welcommu.moduleapi.aop.ExecutionTimeLogger - ⏱️ ResponseEntity com.welcommu.moduleapi.logging.AuditLogController.searchAuditLogs(ActionType,TargetType,String,String,Long,Pageable) executed in 4538 ms&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 문제는 어떻게 해결하여야 할까?&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이 문제에 대한 해답으로 커서기반페이징을 적용하기로 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;커서 기반 페이징을 적용하고 마지막 페이지의 응답속도를 체크해보니&lt;/p&gt;
&lt;pre id=&quot;code_1754645310013&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-08-08 18:27:53.733 [https-jsse-nio-443-exec-9]  INFO com.welcommu.moduleapi.aop.ExecutionTimeLogger - ⏱️ LogsWithCursor com.welcommu.moduleservice.logging.AuditLogSearchServiceImpl.searchLogs(ActionType,TargetType,String,String,Long,LocalDateTime,Long,int) executed in 2309 ms
2025-08-08 18:27:53.734 [https-jsse-nio-443-exec-9]  INFO com.welcommu.moduleapi.aop.ExecutionTimeLogger - ⏱️ ResponseEntity com.welcommu.moduleapi.logging.AuditLogController.search(ActionType,TargetType,String,String,Long,LocalDateTime,Long,int) executed in 2311 ms&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;2309ms의 시간이 소모되는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;오프셋 기반 페이징은 100번째페이지를 조회할때 99번쨰 페이지까지 조회를 한 후 폐기한다&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 남은 100번쨰 페이지를 리턴한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그에 비해 커서 기반 페이징은 필요한 부분부터 조회를 시작해 리턴하기에 이러한 성능 차이를 가져올 수 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <author>PoroGramr</author>
      <guid isPermaLink="true">https://jspark33.tistory.com/149</guid>
      <comments>https://jspark33.tistory.com/149#entry149comment</comments>
      <pubDate>Fri, 8 Aug 2025 18:17:34 +0900</pubDate>
    </item>
    <item>
      <title>Audit Log 조회 API 성능 개선기 - 1</title>
      <link>https://jspark33.tistory.com/148</link>
      <description>&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트 관리 시스템 프로젝트를 진행하면서&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;유저, 회사, 프로젝트, 댓글 등 여러 엔티티의 생성, 수정, 삭제 내역을 조회하는 API를 개발해야 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-07-19 at 7.14.50 PM.png&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;451&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2bshE/btsPp7I0tJD/TETmnKSB2l1DWk4vMQ79iK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2bshE/btsPp7I0tJD/TETmnKSB2l1DWk4vMQ79iK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2bshE/btsPp7I0tJD/TETmnKSB2l1DWk4vMQ79iK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2bshE%2FbtsPp7I0tJD%2FTETmnKSB2l1DWk4vMQ79iK%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;753&quot; height=&quot;451&quot; data-filename=&quot;Screenshot 2025-07-19 at 7.14.50 PM.png&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;451&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;ERD는 위와 같이 구성되어 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;만약 수정의 경우 audit_log_detail을 통해 수정 전과 수정 후 데이터를 저장하고자 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;최초 구상시에는 Spring AOP를 활용해 구현하고자 했지만&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;AOP를 적용하기 위해선 현재 작동하는 비즈니스 로직들을 수정해야하는 상황이 발생해서&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;결국 AOP를 사용하지 않고 비즈니스 로직에 AuditLog에 데이터를 기록하는 로직을 추가했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;로직 설명&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;예시로 프로젝트 생성 로직의 경우&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-07-19 at 7.23.27 PM.png&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ulbUH/btsPprhxDBO/iWNKgLHKBVnAlKNl4MQ9e1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ulbUH/btsPprhxDBO/iWNKgLHKBVnAlKNl4MQ9e1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ulbUH/btsPprhxDBO/iWNKgLHKBVnAlKNl4MQ9e1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FulbUH%2FbtsPprhxDBO%2FiWNKgLHKBVnAlKNl4MQ9e1%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;730&quot; height=&quot;161&quot; data-filename=&quot;Screenshot 2025-07-19 at 7.23.27 PM.png&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;161&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-07-19 at 7.23.17 PM.png&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;109&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cO3Dac/btsPrbYcef5/CwNkdTu57flEoVcHvI1ODk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cO3Dac/btsPrbYcef5/CwNkdTu57flEoVcHvI1ODk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cO3Dac/btsPrbYcef5/CwNkdTu57flEoVcHvI1ODk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcO3Dac%2FbtsPrbYcef5%2FCwNkdTu57flEoVcHvI1ODk%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;855&quot; height=&quot;109&quot; data-filename=&quot;Screenshot 2025-07-19 at 7.23.17 PM.png&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;109&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-07-19 at 7.24.58 PM.png&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNuHql/btsPp9fK3h2/4Ghkf4LV0L2WmpGZZh2ZU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNuHql/btsPp9fK3h2/4Ghkf4LV0L2WmpGZZh2ZU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNuHql/btsPp9fK3h2/4Ghkf4LV0L2WmpGZZh2ZU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNuHql%2FbtsPp9fK3h2%2F4Ghkf4LV0L2WmpGZZh2ZU0%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;954&quot; height=&quot;230&quot; data-filename=&quot;Screenshot 2025-07-19 at 7.24.58 PM.png&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;createProjcet -&amp;gt; createAuditLog -&amp;gt; auditLogFactory&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;식의 의존 방향을 통해 생성 로그가 DB에 저장되는 방식이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트 수정의 경우&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;간단히 설명하자면&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;수정전과 수정후 스냅샷 비교를 통해 변경된 데이터를 auditLogDetail 에 저장하는 방식이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-07-19 at 7.25.48 PM.png&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;221&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vE4xw/btsPpt0a13Z/riGLOspuaxRxryuU97lCak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vE4xw/btsPpt0a13Z/riGLOspuaxRxryuU97lCak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vE4xw/btsPpt0a13Z/riGLOspuaxRxryuU97lCak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvE4xw%2FbtsPpt0a13Z%2FriGLOspuaxRxryuU97lCak%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;650&quot; height=&quot;221&quot; data-filename=&quot;Screenshot 2025-07-19 at 7.25.48 PM.png&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;221&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;발생한 문제&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;해당 로직을 구현하고 10,000개의 데이터를 바탕으로 테스트하는 과정에서&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;조회 API가 비정상적으로 느린 현상을 발견하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;어딧로그.gif&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1hgrm/btsPqaTjdNS/BueNxaKXE7aplbKLpPNs81/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1hgrm/btsPqaTjdNS/BueNxaKXE7aplbKLpPNs81/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1hgrm/btsPqaTjdNS/BueNxaKXE7aplbKLpPNs81/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b1hgrm/btsPqaTjdNS/BueNxaKXE7aplbKLpPNs81/img.gif&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;1092&quot; height=&quot;896&quot; data-filename=&quot;어딧로그.gif&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Spring AOP 를 활용해 응답시간을 확인해본 결과&lt;/p&gt;
&lt;pre id=&quot;code_1752921072500&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-04-25 11:58:29.609 [https-jsse-nio-443-exec-6]  INFO com.welcommu.moduleapi.aop.ExecutionTimeLogger - ⏱️ List com.welcommu.moduleservice.logging.AuditLogSearchService.searchLogs(ActionType,TargetType,String,String,Long) executed in 2812 ms
2025-04-25 11:58:29.609 [https-jsse-nio-443-exec-6]  INFO com.welcommu.moduleapi.aop.ExecutionTimeLogger - ⏱️ ResponseEntity com.welcommu.moduleapi.logging.AuditLogController.searchAuditLogs(ActionType,TargetType,String,String,Long) executed in 2812 ms&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;요청에서 응답까지의 시간을 확인해본 결과 평균적으로 2800ms의 시간이 소요되는 것을 확인하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;원인 분석&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;콘솔 로그에 뜨는 sql 쿼리문을 분석한 결과&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;현재 aduditLog &amp;harr; audiltLogDetail 간의 연관 관계에서&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;audiltLog를 통해 테이블 데이터를 조회할경우 audiltLogDetail의 정보도 필요하기에&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;aduditLog &amp;harr; audiltLogDetail&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;1 + N 개의 쿼리가 나가고 있는 현상을 발견하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;N + 1 문제 발생!&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752921339350&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-04-25 12:01:57.679 [https-jsse-nio-443-exec-9]  INFO com.welcommu.modulecommon.filter.JwtAuthenticationFilter - 유효한 JWT 토큰으로 인증된 사용자: admin@naver.com
2025-04-25 12:01:57.679 [https-jsse-nio-443-exec-9] DEBUG org.springframework.security.web.FilterChainProxy - Secured GET /api/auditLog/search?
Hibernate: 
    select
        al1_0.id,
        al1_0.action_type,
        al1_0.actor_id,
        al1_0.logged_at,
        al1_0.target_id,
        al1_0.target_type 
    from
        audit_log al1_0 
    where
        1=1
Hibernate: 
    select
        d1_0.audit_log_id,
        d1_0.id,
        d1_0.field_name,
        d1_0.new_value,
        d1_0.old_value 
    from
        audit_log_detail d1_0 
    where
        d1_0.audit_log_id=?
Hibernate: 
    select
        d1_0.audit_log_id,
        d1_0.id,
        d1_0.field_name,
        d1_0.new_value,
        d1_0.old_value 
    from
        audit_log_detail d1_0 
    where
        d1_0.audit_log_id=?
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;문제가 발생한 dto&lt;/p&gt;
&lt;pre id=&quot;code_1752921466722&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AuditLogResponse {

    private Long id;
    private Long actorId;
    private String targetType;
    private Long targetId;
    private String actionType;
    private LocalDateTime loggedAt;
    private List&amp;lt;AuditLogDetailResponse&amp;gt; details;

    public static AuditLogResponse from(AuditLog log) {
        return new AuditLogResponse(
            log.getId(),
            log.getActorId(),
            log.getTargetType().name(),
            log.getTargetId(),
            log.getActionType().name(),
            log.getLoggedAt(),
            log.getDetails().stream().map(AuditLogDetailResponse::from).toList()  &amp;lt;- 문제 발생 부분
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;logDetail을 불러오는 과정에서 N + 1 이 발생하는 것을 확인했다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;해결과정&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 audlitLog 조회시&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;커스텀 쿼리를 통해 여러개의 쿼리문을 보냄으로서 N + 1 문제가 발생했다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752921435561&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.welcommu.moduleinfra.logging;

import com.welcommu.moduledomain.logging.AuditLog;
import com.welcommu.moduledomain.logging.enums.ActionType;
import com.welcommu.moduledomain.logging.enums.TargetType;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom {

    private final EntityManager em;

    @Override
    public List&amp;lt;AuditLog&amp;gt; findByConditions(
        ActionType actionType,
        TargetType targetType,
        LocalDateTime startDate,
        LocalDateTime endDate,
        Long userId
    ) {
        StringBuilder sb = new StringBuilder(&quot;SELECT a FROM AuditLog a WHERE 1=1&quot;);
        Map&amp;lt;String, Object&amp;gt; params = new HashMap&amp;lt;&amp;gt;();

        if (actionType != null) {
            sb.append(&quot; AND a.actionType = :actionType&quot;);
            params.put(&quot;actionType&quot;, actionType);
        }
        if (targetType != null) {
            sb.append(&quot; AND a.targetType = :targetType&quot;);
            params.put(&quot;targetType&quot;, targetType);
        }
        if (startDate != null) {
            sb.append(&quot; AND a.loggedAt &amp;gt;= :startDate&quot;);
            params.put(&quot;startDate&quot;, startDate);
        }
        if (endDate != null) {
            sb.append(&quot; AND a.loggedAt &amp;lt;= :endDate&quot;);
            params.put(&quot;endDate&quot;, endDate);
        }
        if (userId != null) {
            sb.append(&quot; AND a.actorId = :userId&quot;);
            params.put(&quot;userId&quot;, userId);
        }

        TypedQuery&amp;lt;AuditLog&amp;gt; query = em.createQuery(sb.toString(), AuditLog.class);
        params.forEach(query::setParameter);

        return query.getResultList();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;전송하는 쿼리의 수를 줄이기 위해 &lt;b&gt;fetch join&lt;/b&gt;을 적용하여 N + 1 문제를 해결하고자 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;다른 해결책(예: @BatchSize, @EntityGraph)도 있지만, fetch join은 개발자가 데이터 로딩 시점을 명확하게 제어할 수 있다는 장점이 있기에 fetch join으로 해결하고자 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;fetch join은 JPQL 쿼리 내에서 즉시 로딩(EAGER)할 연관 관계를 명시적으로 선언하는 방식이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 개발자는 데이터가 필요한 특정 상황에서만 함께 조회하도록 직접 제어할 수 있고, 이는 애플리케이션의 동작을 예측 가능하게 만들고, 다른 곳에서 발생할 수 있는 잠재적인 성능 문제를 방지하는 효과가 있기에 이 방식을 선택했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;복잡한 동적 쿼리이기도 했고 쿼리의 동작을 명확하게 제어하고 싶었기에 이러한 방식이 더 좋다고 생각했다.&lt;/p&gt;
&lt;pre id=&quot;code_1752921593762&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.welcommu.moduleinfra.logging;

import com.welcommu.moduledomain.logging.AuditLog;
import com.welcommu.moduledomain.logging.enums.ActionType;
import com.welcommu.moduledomain.logging.enums.TargetType;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom {

    private final EntityManager em;

    @Override
    public List&amp;lt;AuditLog&amp;gt; findByConditions(
        ActionType actionType,
        TargetType targetType,
        LocalDateTime startDate,
        LocalDateTime endDate,
        Long userId
    ) {
        StringBuilder sb = new StringBuilder(&quot;SELECT DISTINCT a FROM AuditLog a LEFT JOIN FETCH a.details WHERE 1=1&quot;);
        Map&amp;lt;String, Object&amp;gt; params = new HashMap&amp;lt;&amp;gt;();

        if (actionType != null) {
            sb.append(&quot; AND a.actionType = :actionType&quot;);
            params.put(&quot;actionType&quot;, actionType);
        }
        if (targetType != null) {
            sb.append(&quot; AND a.targetType = :targetType&quot;);
            params.put(&quot;targetType&quot;, targetType);
        }
        if (startDate != null) {
            sb.append(&quot; AND a.loggedAt &amp;gt;= :startDate&quot;);
            params.put(&quot;startDate&quot;, startDate);
        }
        if (endDate != null) {
            sb.append(&quot; AND a.loggedAt &amp;lt;= :endDate&quot;);
            params.put(&quot;endDate&quot;, endDate);
        }
        if (userId != null) {
            sb.append(&quot; AND a.actorId = :userId&quot;);
            params.put(&quot;userId&quot;, userId);
        }

        TypedQuery&amp;lt;AuditLog&amp;gt; query = em.createQuery(sb.toString(), AuditLog.class);
        params.forEach(query::setParameter);

        return query.getResultList();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;변경된 내용 요약&lt;/p&gt;
&lt;pre id=&quot;code_1752921612449&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;BEFORE
StringBuilder sb = new StringBuilder(&quot;SELECT a FROM AuditLog a WHERE 1=1&quot;);

AFTER        
StringBuilder sb = new StringBuilder(&quot;SELECT DISTINCT a FROM AuditLog a LEFT JOIN FETCH a.details WHERE 1=1&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;문제 해결&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;수정후.gif&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZm0Dm/btsPpuSigvI/SGyx11hTpKWZgWbFbV82S0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZm0Dm/btsPpuSigvI/SGyx11hTpKWZgWbFbV82S0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZm0Dm/btsPpuSigvI/SGyx11hTpKWZgWbFbV82S0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bZm0Dm/btsPpuSigvI/SGyx11hTpKWZgWbFbV82S0/img.gif&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;1092&quot; height=&quot;896&quot; data-filename=&quot;수정후.gif&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;fetch join을 통해 N + 1 문제를 해결하니 응답속도가 상당히 빨라졌다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;문제 해결 후 콘솔 로그&lt;/p&gt;
&lt;pre id=&quot;code_1752921753563&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-04-25 12:12:10.635 [https-jsse-nio-443-exec-8]  INFO com.welcommu.modulecommon.filter.JwtAuthenticationFilter - 유효한 JWT 토큰으로 인증된 사용자: admin@naver.com
2025-04-25 12:12:10.636 [https-jsse-nio-443-exec-8] DEBUG org.springframework.security.web.FilterChainProxy - Secured GET /api/auditLog/search?
Hibernate: 
    select
        distinct al1_0.id,
        al1_0.action_type,
        al1_0.actor_id,
        d1_0.audit_log_id,
        d1_0.id,
        d1_0.field_name,
        d1_0.new_value,
        d1_0.old_value,
        al1_0.logged_at,
        al1_0.target_id,
        al1_0.target_type 
    from
        audit_log al1_0 
    left join
        audit_log_detail d1_0 
            on al1_0.id=d1_0.audit_log_id 
    where
        1=1&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리문도 1개가 나가는 것을 확인했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;수정 후 응답시간&lt;/p&gt;
&lt;pre id=&quot;code_1752921780291&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-04-25 12:10:11.670 [https-jsse-nio-443-exec-1]  INFO com.welcommu.moduleapi.aop.ExecutionTimeLogger - ⏱️ List com.welcommu.moduleservice.logging.AuditLogSearchService.searchLogs(ActionType,TargetType,String,String,Long) executed in 120 ms
2025-04-25 12:10:11.670 [https-jsse-nio-443-exec-1]  INFO com.welcommu.moduleapi.aop.ExecutionTimeLogger - ⏱️ ResponseEntity com.welcommu.moduleapi.logging.AuditLogController.searchAuditLogs(ActionType,TargetType,String,String,Long) executed in 120 ms&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;응답속도는 2800ms -&amp;gt; 120ms로 상당히 빨라진 모습을 확인할 수 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;1차 성능 개선 완료.&lt;/p&gt;</description>
      <category>Spring</category>
      <author>PoroGramr</author>
      <guid isPermaLink="true">https://jspark33.tistory.com/148</guid>
      <comments>https://jspark33.tistory.com/148#entry148comment</comments>
      <pubDate>Sat, 19 Jul 2025 19:43:57 +0900</pubDate>
    </item>
    <item>
      <title>Github Action 환경에서 모든 테스트코드가 실패한다.</title>
      <link>https://jspark33.tistory.com/147</link>
      <description>&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;문제상황&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Github Action을 활용해서 CI / CD를 구축하던 중&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;CI 과정에서 테스트 코드 검사를 활성화 후 파이프라인을 진행시켰다. 그런데..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;분명 로컬에서는 잘 작동하던 테스트가&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-07-18 at 1.49.43 PM.png&quot; data-origin-width=&quot;1216&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baNA4c/btsPnBYzhrq/Wa1PzIFcEKoQrLkrYYiGWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baNA4c/btsPnBYzhrq/Wa1PzIFcEKoQrLkrYYiGWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baNA4c/btsPnBYzhrq/Wa1PzIFcEKoQrLkrYYiGWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaNA4c%2FbtsPnBYzhrq%2FWa1PzIFcEKoQrLkrYYiGWK%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;1216&quot; height=&quot;678&quot; data-filename=&quot;Screenshot 2025-07-18 at 1.49.43 PM.png&quot; data-origin-width=&quot;1216&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;위 이미지 처럼 다수가 실패해 CI 가 제대로 동작하지 않게 되었다..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;원인파악 + 해결&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;1차로 확인한 원인&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;ConfigDataResourceNotFoundException이 발생하여 테스트가 실패하고 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;ConfigDataResourceNotFoundException란 스프링이 로딩할 설정파일을 읽지 못해 발생하는 익셉션이다&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-07-19 at 5.44.23 PM.png&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;359&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k1tKT/btsPpiED7BJ/bJ3uwkKpITtz5NWAGtFptK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k1tKT/btsPpiED7BJ/bJ3uwkKpITtz5NWAGtFptK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k1tKT/btsPpiED7BJ/bJ3uwkKpITtz5NWAGtFptK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk1tKT%2FbtsPpiED7BJ%2FbJ3uwkKpITtz5NWAGtFptK%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;382&quot; height=&quot;359&quot; data-filename=&quot;Screenshot 2025-07-19 at 5.44.23 PM.png&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;359&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 style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 패키지에 application-test.yml 파일을 추가하여 해당 익셉션을 해결하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-07-19 at 5.45.50 PM.png&quot; data-origin-width=&quot;435&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diwAsP/btsPpjQ42QJ/ScHbxs40suP83g0RoFi1Gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diwAsP/btsPpjQ42QJ/ScHbxs40suP83g0RoFi1Gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diwAsP/btsPpjQ42QJ/ScHbxs40suP83g0RoFi1Gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiwAsP%2FbtsPpjQ42QJ%2FScHbxs40suP83g0RoFi1Gk%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;435&quot; height=&quot;312&quot; data-filename=&quot;Screenshot 2025-07-19 at 5.45.50 PM.png&quot; data-origin-width=&quot;435&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;추가적으로 Test 클래스마다 @ActiveProfiles 어노테이션을 추가해 설정파일 옵션을 부여해주었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;2차로 확인한 원인&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: center;&quot;&gt;application-test.yml&lt;span&gt; 에서 Redis Repository를 비활성화 해놓았다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-07-19 at 5.49.26 PM.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;113&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crdoYO/btsPp91a5JZ/PoG6MZOaLggD68xOV7cTP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crdoYO/btsPp91a5JZ/PoG6MZOaLggD68xOV7cTP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crdoYO/btsPp91a5JZ/PoG6MZOaLggD68xOV7cTP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrdoYO%2FbtsPp91a5JZ%2FPoG6MZOaLggD68xOV7cTP1%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;454&quot; height=&quot;113&quot; data-filename=&quot;Screenshot 2025-07-19 at 5.49.26 PM.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;113&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;위 설정으로 인해 redis 관련 테스트들이 실행이 불가능해 테스트가 실패하고 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-07-19 at 5.50.57 PM.png&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bivxkr/btsPpPhABgV/QNWTc1IJe7LLRKLPco4kwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bivxkr/btsPpPhABgV/QNWTc1IJe7LLRKLPco4kwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bivxkr/btsPpPhABgV/QNWTc1IJe7LLRKLPco4kwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbivxkr%2FbtsPpPhABgV%2FQNWTc1IJe7LLRKLPco4kwK%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;588&quot; height=&quot;309&quot; data-filename=&quot;Screenshot 2025-07-19 at 5.50.57 PM.png&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;309&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;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@MockitoBean
private RefreshTokenRepository refreshTokenRepository;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;를 추가해 redis repository를 mock처리하여 해당 에러를 해결하였다..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-07-19 at 5.52.03 PM.png&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;716&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kCuS1/btsPqL6vqvh/KgaJvnGC6gPDUV7GsDb0U1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kCuS1/btsPqL6vqvh/KgaJvnGC6gPDUV7GsDb0U1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kCuS1/btsPqL6vqvh/KgaJvnGC6gPDUV7GsDb0U1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkCuS1%2FbtsPqL6vqvh%2FKgaJvnGC6gPDUV7GsDb0U1%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;1336&quot; height=&quot;716&quot; data-filename=&quot;Screenshot 2025-07-19 at 5.52.03 PM.png&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;716&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 style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;많은 시도 끝에 성공..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;정진&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;일단 CI가 작동은 하지만&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;아직 SpringBoot Test의 작동 방식에 대한 이해가 상당히 부족하다는 걸 깨달았다..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;테스트도 방식이 여러가지라니..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;순수 단위 테스트, 순수하지 않은 단위테스트...&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;통합테스트 등등..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;어렵다. TDD&lt;/p&gt;</description>
      <category>Spring</category>
      <author>PoroGramr</author>
      <guid isPermaLink="true">https://jspark33.tistory.com/147</guid>
      <comments>https://jspark33.tistory.com/147#entry147comment</comments>
      <pubDate>Sat, 19 Jul 2025 17:52:43 +0900</pubDate>
    </item>
    <item>
      <title>테스트 코드는 왜 짜야할까..</title>
      <link>https://jspark33.tistory.com/146</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 7월 17일 오전 12_11_27.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWq49c/btsPlxOPO6K/AuklxaQFLoqUYLsx4D5kSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWq49c/btsPlxOPO6K/AuklxaQFLoqUYLsx4D5kSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWq49c/btsPlxOPO6K/AuklxaQFLoqUYLsx4D5kSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWq49c%2FbtsPlxOPO6K%2FAuklxaQFLoqUYLsx4D5kSK%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;1536&quot; height=&quot;1024&quot; data-filename=&quot;ChatGPT Image 2025년 7월 17일 오전 12_11_27.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;text-align: center;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며..&lt;/h2&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;최근 사이드 프로젝트를 진행하면서&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 코드에 대해 공부할 겸 TDD로 프로젝트를 진행하고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지 다양한 백엔드 프로젝트를 진행해보았지만&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 코드를 작성해본 경험이 없었기에&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이번 프로젝트를 진행하면서 TDD로 개발하며 무조건! 테스트 코드를 작성하며 개발을 하고자 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: center;&quot; data-ke-size=&quot;size26&quot;&gt;그렇게 시작&lt;/h2&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;시작은 User API를 개발하며 TDD를 진행하였다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;생성, 조회 API 까지는 나름 TDD가 할만했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;컨트롤러, 서비스 모듈 별로 테스트를 작성하며 진행했다.&lt;/p&gt;
&lt;pre id=&quot;code_1752678217808&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
    public void 일반회원_회원가입() throws Exception{
        String id = &quot;username&quot;;
        String password = &quot;password&quot;;
        String nickname = &quot;nickname&quot;;

        when(userService.customerSignUp(any(CustomerSignUpRequest.class))).thenReturn(mock(
            UserResponse.class));

        mockMvc.perform(post(&quot;/api/customers&quot;)
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsBytes(new CustomerSignUpRequest(id, password,nickname)))
            .with(csrf())
        ).andDo(print())
            .andExpect(status().isOk());
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1752678257151&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
    void 일반회원_회원가입이_정상적으로_동작하는경우() {
        String id = &quot;id&quot;;
        String password = &quot;password&quot;;
        String nickname = &quot;nickname&quot;;

        when(userRepository.findById(id)).thenReturn(Optional.empty());
        when(userRepository.save(any())).thenReturn(CustomerUserFixture.get(id, password));
        when(passwordEncoder.encode(password)).thenReturn(&quot;encodedPassword&quot;);

        Assertions.assertDoesNotThrow(() -&amp;gt; userService.customerSignUp(new CustomerSignUpRequest(id,password,nickname)));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 수정 부터 굉장히 복잡해졌다..&lt;/p&gt;
&lt;pre id=&quot;code_1752678276876&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
    void 고객_회원_수정이_성공적으로_동작하는_경우() {

        // given
        User existingUser = User.ofCustomer(&quot;loginId&quot;, &quot;oldPass&quot;, &quot;oldNick&quot;);
        // anyLong() 사용
        given(userRepository.findById(anyLong()))
            .willReturn(Optional.of(existingUser));
        given(userRepository.save(any(User.class)))
            .willAnswer(invocation -&amp;gt; invocation.getArgument(0));
        given(passwordEncoder.encode(&quot;newPass&quot;)).willReturn(&quot;newPass&quot;);

        UpdateUserRequest req = new UpdateUserRequest(&quot;newPass&quot;, &quot;newNick&quot;);

        // when
        UserResponse resp = userService.updateUser(999L, req);

        // then
        assertThat(existingUser.getPassword()).isEqualTo(&quot;newPass&quot;);
        assertThat(existingUser.getNickname()).isEqualTo(&quot;newNick&quot;);
        assertThat(resp.getNickname()).isEqualTo(&quot;newNick&quot;);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: center;&quot; data-ke-size=&quot;size26&quot;&gt;장단점&lt;/h2&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그래도 스스로 TDD를 하며 느낀 장점을 적어보자면&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;1. 익셉션 처리가 제대로 되는 지 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;2. 비즈니스 로직이 제대로 작동하는 지 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;3. 리팩토링 진행 후 추가적인 파급 효과를 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;4. &lt;b&gt;심리적 안정감&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 단점도 존재한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;1. 개발속도가 상당히 느려진다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;2. 비즈니스 로직 구현보다 테스트 코드 작성하는데 시간이 더 걸린다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;뭐 어떻게 보면 단점 1,2는 같은 말이기도 하다..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;더 나아가야할 점&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;TDD의 시작은 강의를 보며 따라하는 방식으로 진행했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;해당 강의와 프로젝트의 로직이 완전히 똑같지는 않지만 나름 비슷한 부분은 참고하며 진행하고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 진행하는 프로젝트의 로직이 점점 복잡해지면서 강의를 참고하기가 어려워지고 있다...&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;AI들의 도움을 받으며 겨우 겨우 작성하고 있지만&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그래도 스스로 더욱 더 학습해나아가야 할 것 같다..&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;강의의 강사분처럼.. 혼자서 쓱쓱 테스트 코드 작성하는 개발자가 되기를 바라며..&lt;/p&gt;</description>
      <category>Spring</category>
      <author>PoroGramr</author>
      <guid isPermaLink="true">https://jspark33.tistory.com/146</guid>
      <comments>https://jspark33.tistory.com/146#entry146comment</comments>
      <pubDate>Thu, 17 Jul 2025 00:16:06 +0900</pubDate>
    </item>
  </channel>
</rss>