<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>MUNSIK BLOG</title>
    <link>https://munsik22.tistory.com/</link>
    <description>munsik22 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Sat, 18 Jul 2026 07:35:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>munsik22</managingEditor>
    <image>
      <title>MUNSIK BLOG</title>
      <url>https://tistory1.daumcdn.net/tistory/7621865/attach/60ad36a05d0f455494c0d7504d0a0473</url>
      <link>https://munsik22.tistory.com</link>
    </image>
    <item>
      <title>SSAFY 16기 합격 후기</title>
      <link>https://munsik22.tistory.com/364</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;지원 동기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 말에 진행했던 SSAFY 15기 모집에도 서울캠퍼스 임베디드 트랙에 지원했었지만 면접에서 떨어진 적이 있었다. 이후 공백기 동안 뭐라도 해야겠다는 생각으로 올해 4월부터 내일배움캠프에서 진행하는 자바 부트캠프에 참가했다. 백준에서 주로 쓰던 언어는 파이썬이었고 스프링부트도 해 본 적이 없었지만 진도를 따라가는 데 어려움은 없었다. 단기심화 과정 특성상 정신 없이 온라인 강의 주입(?)과 프로젝트를 반복했고, 교육의 질에는 만족했지만 짧은 시간동안 비대면으로 진행하는 환경이다 보니 몰입도 측면에서는 아쉬웠다. 특히 크래프톤 정글의 대면 교육 환경에 긍정적인 기억을 가지고 있던 터라 더욱 아쉬움을 느꼈던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSAFY라는 과정이 1년 동안 진행하는 것이다 보니 만만치 않을 뿐더러, 이번에 SSAFY에 들어가면 벌써 세 번째 부트캠프인데 이러다 부트캠프 낭인(?)이 되는 게 아닌가 하는 걱정도 들었다. 그럼에도 불구하고 삼성이라는 든든한 배경과 주변 지인들의 적극적인 추천, 무엇보다도 대면 환경에서 몰입하고 싶은 개인적인 열망 때문에 다시 한 번 지원하게 되었다. 다만 이번에는 임베디드 트랙이 아니라 자바 트랙으로 지원을 했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;에세이&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 지난 15기 지원 때도 에세이를 써 본 적이 있고 서류 심사는 합격했기 때문에 문제는 없었던 것 같다. 이미 다른 국비 교육 과정을 진행중이라는 사실이 합격 여부에 영향을 주지는 않을까 걱정을 하기는 했지만 지원 동기를 솔직하게 쓰기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이실직고를 하자면 이번 에세이는 Claude가 대필해줬다  물론 글감과 초안은 내가 작성했고, 클로드에게 스스로 피드백 및 퇴고를 반복 진행하도록 시킨 이후에 최종 퇴고와 오탈자 검수는 내가 직접 했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코딩 테스트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코테 자체도 지난 번에 응시한 경험이 있기 때문에 별 걱정은 하지 않았다. 당연한 얘기지만 어떤 문제가 나왔는지는 대외비이므로 언급하기가 어렵다. 주관적인 의견이지만 지난번보다는 문제가 쉬웠던 것 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;면접&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 면접. 이번 16기 지원에는 면접 스터디를 구하려고 생각했지만, 진행 중이었던 부트캠프 과정과 겹쳐서 따로 시간을 내기 어려웠기 때문에 이번에도 혼자서 준비했다. PT 면접 준비는 모든 싸피 준비생들의 바이블인 &lt;a href=&quot;https://youtu.be/DOvCIrwMPbQ?si=vXwMs8ZCJmbQ55_Y&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;강민혁 님의 PT 면접 영상&lt;/a&gt;을 참고했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IT 및 AI와 관련된 키워드들을 공부한 뒤, IT 업계와는 전혀 상관 없는 주제에 대해 PT를 하는 방식으로 준비를 했다. 모기를 예시로 들자면 다음과 같다. (양식 자체는 위 유튜브 영상을 참고했다.)&lt;/p&gt;
&lt;pre id=&quot;code_1783763746116&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;안녕하십니까 지원자 000입니다.

지금부터 모기의 스텔스 진화와 수면권 침해 해결을 주제로 발표를 시작하겠습니다. 순서는 문제인식, 방안제시, 예상되는 리스크와 해결방안 순으로 진행됩니다.

우선 결론부터 말씀드리면, LiDAR 센서와 열화상 카메라가 장착된 가정용 초소형 요격 드론을 도입하는 것을 제안합니다.

이러한 제안을 하게 된 문제 상황은 다음과 같습니다. 많은 현대인들이 모기들의 날개짓 소리로 인해 심각한 수면 장애를 겪고 있습니다. 모기들이 진화해서 불이 꺼졌을 때만 귀 근처에서 소리를 내고, 불을 켜면 바로 시야에서 사라져 사각 지대로 숨어 버리는 스텔스 회피 기동을 가지게 된 것이 문제의 요인으로 분석했습니다.

이러한 문제를 해결하기 위해 저는 인간의 육안을 뛰어넘은 초소형 요격 드론 도입을 제안드립니다. 실행 방안은 두 가지가 있습니다. 첫째, 드론에 LiDAR 센서와 열화상 카메라가 장착해 불이 꺼진 상황에서도 실시간으로 모기의 움직임을 감지합니다. 둘째, 드론에 온디바이스 AI를 탑재해 실시간으로 모기의 비행 궤적 데이터를 학습하고 위치를 예측합니다. 이로써 인간의 눈에 보이지 않은 위치에 숨은 모기를 한 번에 요격해 현대인들의 수면의 질을 보장할 것으로 기대됩니다.

물론 예상되는 리스크도 존재합니다. 초소형 하드웨어에서도 작동할 수 있는 온디바이스 AI가 시중에 존재하지 않을수도 있다는 것입니다. 이는 엣지 컴퓨팅 기술의 도입으로 모델 자체를 양자화해서 AI 모델을 가볍게 만드는 방식으로 해결할 수 있습니다.

지금까지 모기의 스텔스 진화 해결에 대해 초소형 요격 드론 도입 제안에 대해 발표했습니다.

면접을 떠나 실제 프로젝트를 하듯이 진지하게 고민을 했지만, 짧은 시간 동안 얕은 식견으로 제안한 것이라 최선의 설계가 아니라는 것을 압니다. 00 입사 후에 동료들과 선배님들의 피드백을 통해 더 실무적이고 핏한 의사결정 과정을 경험하고 싶습니다.

경청해 주셔서 감사합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복장에 관해서 짧게 언급하자면 지난번에는 캐주얼 정장, 이번에는 풀 정장으로 입고 갔었지만 돌아와서 생각해보니 복장은 슬리퍼만 아니면 상관 없었을 것 같았다. 정장은 여름이라 괜히 덥기만 했고... &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접 진행 방식 자체는 역시 지난번의 경험 덕분에 알고 있어서 떨리지 않았다. 면접 질문은 준비한 내용에서 전혀 나오지는 않았지만, 떨지 않았기 때문인지 오히려 지난번보다 여유롭게 잘 볼 수 있었던 것 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;합격 및 입과 준비&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서 안 떨었던 덕분인지 다행히도 이번에는 합격을 할 수 있었다. 2번만에 합격한 SSAFY였지만 당시 내일배움캠프 최종 프로젝트 때문에 한참 정신 없이 바빴던 터라 그렇게 신나하지는(?) 못했다. 내일배움캠프 운영진 측에 개인 사정으로 인한 하차 의사를 밝혔고, 수료를 하지 못할 것을 감안하고 있었지만 다행스럽게도 출석 일자의 80%를 채워서 조기 수료를 할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSAFY 입과 전 자취와 셔틀버스 통근을 두고 내적 갈등을 겪었다. 월세 비용 지출 vs 매일 왕복 4시간 출퇴근이라는 트레이드오프를 두고, 대학 생활 때의 통학 지옥 경험을 떠올리며 자취를 선택했다. 그래도 구미는 월세가 싼 편이라 30~40만원 정도로도 좋은 방을 구할 수 있었다. ㄷㅂ부동산을 통해서 방을 계약했는데, SSAFY 교육생은 복비를 받지 않는다고 해서 좋았다.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;SSAFY에서의 1주를 보내고&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 화요일부터 SSAFY 16기의 교육과정이 시작되었다. 자세한 내용은 대외비이므로 언급하지는 못하겠지만 앞으로의 1년 동안도 열심히 해야겠다.&lt;/p&gt;</description>
      <category>SSAFY</category>
      <category>SSAFY</category>
      <author>munsik22</author>
      <guid isPermaLink="true">https://munsik22.tistory.com/364</guid>
      <comments>https://munsik22.tistory.com/364#entry364comment</comments>
      <pubDate>Sat, 11 Jul 2026 18:46:58 +0900</pubDate>
    </item>
    <item>
      <title>3주차 5일차 작업일지 (0703) 및 최종 회고</title>
      <link>https://munsik22.tistory.com/363</link>
      <description>&lt;style&gt;
  code:not([class*=&quot;language-&quot;]) {
    font-family: &quot;SFMono-Regular&quot;, Menlo, Consolas, &quot;PT Mono&quot;, &quot;Liberation Mono&quot;, Courier, monospace;
    line-height: normal;
    background: rgba(135, 131, 120, .15);
    color: #EB5757;
    border-radius: 4px;
    font-size: 85%;
    padding: 0.2em 0.4em;
    position: relative;
    bottom: 0.065em;
  }
  pre code {
    border: none !important;
    background: #1e1e1e !important;
    color: #d4d4d4 !important;
  }
&lt;/style&gt;
&lt;h1&gt;_deploy-wave.yml waiter 관련 (삽질 기록)&lt;/h1&gt;
&lt;h2&gt;무슨 일이 있었나&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;_deploy-wave.yml&lt;/code&gt;이 &lt;code&gt;AWS_MAX_ATTEMPTS&lt;/code&gt;를 export한 뒤 &lt;code&gt;aws ecs wait services-stable&lt;/code&gt;을 쓰고 있는 걸 보고, 6/22~24 에서 이미 겪었던 &amp;quot;AWS_MAX_ATTEMPTS는 waiter 폴링 횟수에 영향을 안 준다&amp;quot;는 문제가 재발한 것으로 판단해 &lt;code&gt;describe-services&lt;/code&gt;를 직접 폴링하는 &lt;code&gt;wait_stable&lt;/code&gt; 커스텀 함수로 교체했다.&lt;/p&gt;
&lt;h2&gt;왜 쓸데없는 짓이었나&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;ai-service(wave-4)에 &lt;code&gt;max_attempts: 120&lt;/code&gt;(30분)을 준 근거 자체가 잘못됐다. 6/24일자 작업 로그는 pgvector 초기화 + Gemini 연결 때문에 기동이 오래 걸린다고 진단했지만, 실제 원인은 환경변수 설정 누락으로 인한 컨테이너 크래시 루프였다. 크래시 루프는 대기 시간을 늘린다고 해결되지 않는다.&lt;/li&gt;
&lt;li&gt;실제로 지금까지 배포에서 10분을 넘긴 적이 한 번도 없었다. 애초에 30분을 기다려야 했던 적이 없었다.&lt;/li&gt;
&lt;li&gt;즉 &lt;code&gt;max_attempts: 120&lt;/code&gt;이라는 설정도, 그걸 실제로 반영되게 만들겠다는 이번 수정도 처음부터 존재할 필요가 없는 문제를 고치려 한 것이었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;처리&lt;/h2&gt;
&lt;p&gt;코드(&lt;code&gt;_deploy-wave.yml&lt;/code&gt;)와 &lt;code&gt;docs/CD.md&lt;/code&gt;는 원래 상태(&lt;code&gt;aws ecs wait services-stable&lt;/code&gt; + &lt;code&gt;AWS_MAX_ATTEMPTS&lt;/code&gt; export)로 롤백했다. &lt;code&gt;wait_stable&lt;/code&gt; 커스텀 함수 도입은 진행하지 않는다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;config-server 중앙 설정 ddl-auto 정리&lt;/h1&gt;
&lt;h2&gt;배경&lt;/h2&gt;
&lt;p&gt;Flyway 전환 당시 서비스 로컬 &lt;code&gt;application.yml&lt;/code&gt;은 &lt;code&gt;ddl-auto: validate&lt;/code&gt;로 바꿨지만 config-server 중앙 설정(&lt;code&gt;config/*.yml&lt;/code&gt;)은 그대로 &lt;code&gt;update&lt;/code&gt;로 남아 있는 것을 발견했다.&lt;/p&gt;
&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;Spring Cloud Config는 원격(config-server) 설정이 로컬 설정보다 우선한다. 즉 서비스가 로컬에서 &lt;code&gt;validate&lt;/code&gt;로 바꿔도, config-server가 내려주는 &lt;code&gt;update&lt;/code&gt;가 이겨서 실제로는 여전히 &lt;code&gt;update&lt;/code&gt;로 동작할 수 있는 상태였다.&lt;/p&gt;
&lt;h2&gt;확인&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;config-server/src/main/resources/config/*.yml&lt;/code&gt;에서 &lt;code&gt;ddl-auto&lt;/code&gt; 값을 모두 확인했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;validate&lt;/code&gt;로 이미 맞음: &lt;code&gt;ai-service.yml&lt;/code&gt;, &lt;code&gt;auction-service.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update&lt;/code&gt;로 남아 있었음: &lt;code&gt;user-service.yml&lt;/code&gt;, &lt;code&gt;order-service.yml&lt;/code&gt;, &lt;code&gt;payment-service.yml&lt;/code&gt;, &lt;code&gt;notification-service.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bid-service.yml&lt;/code&gt;도 &lt;code&gt;update&lt;/code&gt;였지만, bid-service는 &lt;code&gt;build.gradle&lt;/code&gt;에 JPA 의존성도, JDBC 드라이버도, &lt;code&gt;@Entity&lt;/code&gt; 클래스도 없어 &lt;code&gt;datasource&lt;/code&gt;/&lt;code&gt;jpa&lt;/code&gt; 설정 블록 자체가 죽은 값이다(Flyway 전환 대상에서도 원래 제외됨).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;수정&lt;/h2&gt;
&lt;p&gt;Flyway 마이그레이션이 실제로 적용된 4개 서비스의 config-server 설정을 &lt;code&gt;validate&lt;/code&gt;로 통일했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;config-server/src/main/resources/config/user-service.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config-server/src/main/resources/config/order-service.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config-server/src/main/resources/config/payment-service.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config-server/src/main/resources/config/notification-service.yml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;config-server/src/main/resources/config/bid-service.yml&lt;/code&gt;은 DB 연결 수단이 아예 없는 서비스라 &lt;code&gt;spring.datasource&lt;/code&gt;/&lt;code&gt;spring.jpa&lt;/code&gt; 블록 전체를 삭제했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;최종 회고&lt;/h1&gt;
&lt;p&gt;개인 사정으로 오늘을 끝으로 최종 프로젝트에서 하차하게 되었다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 프로젝트를 통해 인프라 아키텍처 설계와 배포 파이프라인 구축을 처음으로 전담했다. 프로젝트 요구사항에 최적화된 아키텍처를 도출하고 배포 과정의 예기치 못한 트러블슈팅을 해결하는 과정에서 DevOps 영역의 높은 복잡도를 체감했다. 결과적으로 밤샘 작업 끝에 안정적인 배포 환경을 구축해냈다. 이번 경험을 통해 단순히 기능 코드만 작성하는 개발자를 넘어 인프라 설계, 자동화 배포, 모니터링 기반의 장애 대응력까지 갖춘 백엔드 개발자로 성장할 수 있었다고 생각한다.&lt;/p&gt;</description>
      <category>내일배움캠프/최종 프로젝트 개발일지</category>
      <author>munsik22</author>
      <guid isPermaLink="true">https://munsik22.tistory.com/363</guid>
      <comments>https://munsik22.tistory.com/363#entry363comment</comments>
      <pubDate>Fri, 3 Jul 2026 16:09:12 +0900</pubDate>
    </item>
    <item>
      <title>[Computational Thinking] 수와 표현</title>
      <link>https://munsik22.tistory.com/362</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 1&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baBgFc/dJMcabSg9mi/m9todsgkuQiMnInvbU5jB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baBgFc/dJMcabSg9mi/m9todsgkuQiMnInvbU5jB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baBgFc/dJMcabSg9mi/m9todsgkuQiMnInvbU5jB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaBgFc%2FdJMcabSg9mi%2Fm9todsgkuQiMnInvbU5jB1%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;480&quot; height=&quot;110&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 2&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1373&quot; data-origin-height=&quot;149&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YdAIv/dJMcacjk6In/T3wfy8aNSpfRAgt7aNIEkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YdAIv/dJMcacjk6In/T3wfy8aNSpfRAgt7aNIEkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YdAIv/dJMcacjk6In/T3wfy8aNSpfRAgt7aNIEkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYdAIv%2FdJMcacjk6In%2FT3wfy8aNSpfRAgt7aNIEkk%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;1373&quot; height=&quot;149&quot; data-origin-width=&quot;1373&quot; data-origin-height=&quot;149&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 3&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;944&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EeY08/dJMcaa6ZOSq/V7EYDlLBk58bTXvikGc290/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EeY08/dJMcaa6ZOSq/V7EYDlLBk58bTXvikGc290/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EeY08/dJMcaa6ZOSq/V7EYDlLBk58bTXvikGc290/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEeY08%2FdJMcaa6ZOSq%2FV7EYDlLBk58bTXvikGc290%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;1538&quot; height=&quot;944&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;944&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  컴퓨터 분야에서 로그의 밑은 항상 2이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 4&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;709&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OCSo9/dJMcadvJw5N/ylTZCmWC1tFs6v7ePKX7qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OCSo9/dJMcadvJw5N/ylTZCmWC1tFs6v7ePKX7qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OCSo9/dJMcadvJw5N/ylTZCmWC1tFs6v7ePKX7qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOCSo9%2FdJMcadvJw5N%2FylTZCmWC1tFs6v7ePKX7qk%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;480&quot; height=&quot;176&quot; data-origin-width=&quot;709&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 5&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1202&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkla98/dJMcahdXxm2/sh3OuMGn9jOX3jN99AVS5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkla98/dJMcahdXxm2/sh3OuMGn9jOX3jN99AVS5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkla98/dJMcahdXxm2/sh3OuMGn9jOX3jN99AVS5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbkla98%2FdJMcahdXxm2%2Fsh3OuMGn9jOX3jN99AVS5k%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;1202&quot; height=&quot;508&quot; data-origin-width=&quot;1202&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;후기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대학 1~2학년 때 배운 내용 같은데 수학이랑 벽 쌓고 지낸 지 좀 오래 되어서 다 까먹었다 &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 전공 짬이 있어서 그런지 이해하는 데 어려움은 (아마도) 없었던 것 같다.&lt;/p&gt;</description>
      <category>PS/SWEA</category>
      <author>munsik22</author>
      <guid isPermaLink="true">https://munsik22.tistory.com/362</guid>
      <comments>https://munsik22.tistory.com/362#entry362comment</comments>
      <pubDate>Thu, 2 Jul 2026 22:22:21 +0900</pubDate>
    </item>
    <item>
      <title>3주차 4일차 작업일지 (0702)</title>
      <link>https://munsik22.tistory.com/361</link>
      <description>&lt;style&gt;
  code:not([class*=&quot;language-&quot;]) {
    font-family: &quot;SFMono-Regular&quot;, Menlo, Consolas, &quot;PT Mono&quot;, &quot;Liberation Mono&quot;, Courier, monospace;
    line-height: normal;
    background: rgba(135, 131, 120, .15);
    color: #EB5757;
    border-radius: 4px;
    font-size: 85%;
    padding: 0.2em 0.4em;
    position: relative;
    bottom: 0.065em;
  }
  pre code {
    border: none !important;
    background: #1e1e1e !important;
    color: #d4d4d4 !important;
  }
&lt;/style&gt;

&lt;h1&gt;Redis 관리자 접근 허용&lt;/h1&gt;
&lt;h2&gt;무엇을 했나&lt;/h2&gt;
&lt;p&gt;관리자가 Private Subnet에 있는 RDS와 Redis를 직접 조작하지 못하는 문제를 해결했다. RDS는 이미 모니터링 EC2를 거쳐 접근하는 길이 있었지만 Redis는 없어서, Redis에도 같은 방식의 길을 새로 열었다.&lt;/p&gt;
&lt;h2&gt;바꾼 파일 (sanji-infra 레포)&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;modules/network/main.tf&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;redis_in_from_monitoring&lt;/code&gt; 보안 그룹 규칙 추가&lt;/li&gt;
&lt;li&gt;모니터링 EC2에서 Redis(6379)로 들어가는 걸 허용&lt;/li&gt;
&lt;li&gt;RDS 규칙(&lt;code&gt;rds_in_from_monitoring&lt;/code&gt;)과 같은 방식으로, dev/prod 구분 없이 동일하게 적용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;terraform validate&lt;/code&gt; 통과 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;docs/DEPLOY.md&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;6-3 절에 관리자가 RDS/Redis에 접속하는 방법(SSM 포트포워딩) 추가&lt;/li&gt;
&lt;li&gt;별도 Bastion 서버 없이 모니터링 EC2를 징검다리로 써서 &lt;code&gt;aws ssm start-session&lt;/code&gt;으로 로컬 포트를 RDS/Redis 포트에 연결하는 명령 예시 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;왜 이렇게 했나&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;처음에는 dev 환경에서만 여는 방식을 검토했으나, 검토 후 dev/prod 구분 없이 단순하게 열기로 결정&lt;/li&gt;
&lt;li&gt;실제 접근 통제는 보안 그룹이 아니라 모니터링 EC2에 SSM 세션을 열 수 있는 IAM 권한으로 하기 때문에, SG를 dev/prod 구분 없이 열어도 안전함&lt;/li&gt;
&lt;li&gt;새 Bastion 서버를 만들지 않고 이미 떠 있는 모니터링 EC2와 이미 붙어 있는 SSM 권한을 그대로 재활용&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;추가 작업: 접속용 쉘 스크립트 작성&lt;/h2&gt;
&lt;p&gt;RDS/Redis에 SSM 포트포워딩으로 접속하는 걸 매번 손으로 명령어를 치지 않도록 스크립트로 만들었다.&lt;/p&gt;
&lt;h3&gt;만든 파일 (sanji-infra 레포)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;scripts/connect-rds.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scripts/connect-redis.sh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;사용법&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bash scripts/connect-rds.sh &amp;lt;dev|prod&amp;gt; [로컬포트]
bash scripts/connect-redis.sh &amp;lt;dev|prod&amp;gt; [로컬포트]&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;첫 번째 인자로 dev/prod 환경을 지정한다 (필수).&lt;/li&gt;
&lt;li&gt;두 번째 인자로 로컬 포트를 바꿀 수 있다 (생략 시 RDS는 5432, Redis는 6379).&lt;/li&gt;
&lt;li&gt;기존 &lt;code&gt;scripts/db-init.sh&lt;/code&gt;와 같은 방식(&lt;code&gt;aws.exe&lt;/code&gt;, 태그로 모니터링 EC2 조회 후 SSM 명령 실행)을 따라 만들었다.&lt;/li&gt;
&lt;li&gt;동작 순서: 태그(&lt;code&gt;Role=monitoring&lt;/code&gt;, &lt;code&gt;Environment=&amp;lt;env&amp;gt;&lt;/code&gt;)로 모니터링 EC2 조회, RDS/Redis 엔드포인트 조회, &lt;code&gt;aws ssm start-session&lt;/code&gt;으로 포트포워딩 시작&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docs/DEPLOY.md&lt;/code&gt; 6-3 절도 수동 명령어 대신 이 스크립트를 쓰도록 갱신함&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;추가 수정: RDS 접속 안내 보완&lt;/h2&gt;
&lt;p&gt;실제 접속 시도에서 두 가지 문제가 확인되어 안내 문구를 보완했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-U&lt;/code&gt;, &lt;code&gt;-d&lt;/code&gt;를 안 주면 psql이 OS 로그인 계정을 유저/DB 이름으로 써서 인증에 실패함 (실제 계정은 &lt;code&gt;sanji&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;RDS 기본 파라미터 그룹이 SSL을 강제해서 &lt;code&gt;sslmode=require&lt;/code&gt; 없이는 &lt;code&gt;no pg_hba.conf entry ... no encryption&lt;/code&gt; 오류 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;scripts/connect-rds.sh&lt;/code&gt; 상단 주석과 &lt;code&gt;docs/DEPLOY.md&lt;/code&gt; 6-3 절에 올바른 psql 접속 명령(&lt;code&gt;sslmode=require&lt;/code&gt; 포함)과 SSM에서 비밀번호 조회하는 명령을 추가했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;SSM 스크립트 파일명 변경&lt;/h1&gt;
&lt;h2&gt;무엇을 했나&lt;/h2&gt;
&lt;p&gt;sanji-infra 레포의 SSM 스크립트 파일명과 관련 문서를 업데이트했다.&lt;/p&gt;
&lt;h2&gt;바꾼 파일 (sanji-infra 레포)&lt;/h2&gt;
&lt;h3&gt;파일명 변경 (scripts/)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ssm-backup.sh&lt;/code&gt; -&amp;gt; &lt;code&gt;ssm-pull.sh&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SSM 파라미터 값을 파일로 가져오는(pull) 스크립트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ssm-restore.sh&lt;/code&gt; -&amp;gt; &lt;code&gt;ssm-push.sh&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파일의 SSM 파라미터 값을 AWS에 적용하는(push) 스크립트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;문서 업데이트&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;README.md&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;배포 가이드 섹션의 명령어 변경&lt;/li&gt;
&lt;li&gt;파일 구성 섹션의 파일명 및 설명 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;docs/DEPLOY.md&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;4단계(SSM 파라미터 값 주입)의 명령어 변경&lt;/li&gt;
&lt;li&gt;10단계(SSM 파라미터 백업/복구)의 명령어 및 설명 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;왜 이렇게 했나&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;backup/restore&lt;/code&gt; 보다 &lt;code&gt;pull/push&lt;/code&gt;가 더 직관적이고 명확함&lt;/li&gt;
&lt;li&gt;pull은 &amp;quot;AWS에서 로컬로 가져오기&amp;quot;, push는 &amp;quot;로컬에서 AWS로 보내기&amp;quot;&lt;/li&gt;
&lt;li&gt;전체 레포의 일관성을 위해 문서도 함께 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;GitHub Actions Node.js 20 지원 종료 대응&lt;/h1&gt;
&lt;h2&gt;배경&lt;/h2&gt;
&lt;p&gt;GitHub Actions에서 &amp;quot;Node.js 20 is deprecated&amp;quot; 경고가 발생. GitHub는 Node.js 20의 수명이 끝남에 따라&lt;br&gt;2026년 6월 16일부터 러너 기본 런타임을 Node24로 전환하고, 9월 16일에는 Node20을 완전히 제거할 예정.&lt;br&gt;현재 워크플로에서 쓰는 액션들의 태그를 실제로 확인한 결과, 아래 액션들이 아직 Node20 런타임으로 고정된 메이저 버전을 쓰고 있었음.&lt;/p&gt;
&lt;h2&gt;확인 방법&lt;/h2&gt;
&lt;p&gt;각 액션 저장소의 &lt;code&gt;action.yml&lt;/code&gt;을 GitHub API로 직접 조회해서 &lt;code&gt;runs.using&lt;/code&gt; 값이 &lt;code&gt;node20&lt;/code&gt;인지 &lt;code&gt;node24&lt;/code&gt;인지 확인했다.&lt;/p&gt;
&lt;h2&gt;변경 내역&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;액션&lt;/th&gt;
&lt;th&gt;기존&lt;/th&gt;
&lt;th&gt;변경 후&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;actions/checkout&lt;/td&gt;
&lt;td&gt;v3, v4&lt;/td&gt;
&lt;td&gt;v7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;actions/setup-java&lt;/td&gt;
&lt;td&gt;v4&lt;/td&gt;
&lt;td&gt;v5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;actions/cache&lt;/td&gt;
&lt;td&gt;v4&lt;/td&gt;
&lt;td&gt;v6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;actions/setup-node&lt;/td&gt;
&lt;td&gt;v3&lt;/td&gt;
&lt;td&gt;v6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;actions/github-script&lt;/td&gt;
&lt;td&gt;v7&lt;/td&gt;
&lt;td&gt;v9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws-actions/configure-aws-credentials&lt;/td&gt;
&lt;td&gt;v4&lt;/td&gt;
&lt;td&gt;v6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;변경 파일&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.github/workflows/gemini_review.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.github/workflows/deploy-ec2.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.github/workflows/deploy-ecs.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.github/workflows/_deploy-wave.yml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;추가 작업: Gemini 리뷰 모델 환경변수화&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;gemini_review.yml&lt;/code&gt;에서 하드코딩되어 있던 모델명을 환경변수로 뺐다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Run Gemini Review&lt;/code&gt; 스텝의 &lt;code&gt;env&lt;/code&gt;에 &lt;code&gt;GEMINI_MODEL: ${{ vars.GEMINI_MODEL || &amp;#39;google/gemini-3.1-pro-preview&amp;#39; }}&lt;/code&gt; 추가&lt;/li&gt;
&lt;li&gt;스크립트 안의 &lt;code&gt;model: &amp;quot;google/gemini-3.1-pro-preview&amp;quot;&lt;/code&gt;를 &lt;code&gt;model: process.env.GEMINI_MODEL&lt;/code&gt;로 변경&lt;/li&gt;
&lt;li&gt;GitHub 저장소 변수(Repository variable) &lt;code&gt;GEMINI_MODEL&lt;/code&gt;을 설정하면 그 값을 쓰고, 없으면 기존 기본값을 그대로 사용&lt;/li&gt;
&lt;li&gt;변경 파일: &lt;code&gt;.github/workflows/gemini_review.yml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;별도 레포인 &lt;code&gt;sanji-infra&lt;/code&gt;의 &lt;code&gt;gemini_review.yml&lt;/code&gt;(Terraform 코드 리뷰용)에도 동일하게 적용했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Run Gemini Review&lt;/code&gt; 스텝 &lt;code&gt;env&lt;/code&gt;에 &lt;code&gt;GEMINI_MODEL: ${{ vars.GEMINI_MODEL || &amp;#39;google/gemini-3-flash-preview&amp;#39; }}&lt;/code&gt; 추가&lt;/li&gt;
&lt;li&gt;스크립트 안의 &lt;code&gt;model: &amp;quot;google/gemini-3-flash-preview&amp;quot;&lt;/code&gt;를 &lt;code&gt;model: process.env.GEMINI_MODEL&lt;/code&gt;로 변경&lt;/li&gt;
&lt;li&gt;이 레포는 기본 모델이 &lt;code&gt;google/gemini-3-flash-preview&lt;/code&gt;로, &lt;code&gt;sanji-jk&lt;/code&gt;의 &lt;code&gt;gemini_review.yml&lt;/code&gt;과는 기본값이 다름&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sanji-infra/README.md&lt;/code&gt;, &lt;code&gt;sanji-infra/docs/DEPLOY.md&lt;/code&gt;에는 모델명이 문서화되어 있지 않아 별도 수정 없음&lt;/li&gt;
&lt;li&gt;변경 파일: &lt;code&gt;sanji-infra/.github/workflows/gemini_review.yml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;Langfuse 관리자 접근 허용&lt;/h1&gt;
&lt;h2&gt;무엇을 했나&lt;/h2&gt;
&lt;p&gt;Langfuse(LLM 트레이싱 도구)가 모니터링 EC2 위에 3001 포트로 떠 있지만 보안 그룹에 열려 있지 않아 브라우저로 접근할 수 없던 문제를 해결했다. Grafana(3000)와 같은 방식으로 관리자 IP(&lt;code&gt;admin_cidr&lt;/code&gt;)에 3001 포트를 열었다.&lt;/p&gt;
&lt;h2&gt;바꾼 파일 (sanji-infra 레포)&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;modules/network/main.tf&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;monitoring_in_admin&lt;/code&gt; 보안 그룹 규칙의 포트 목록에 &lt;code&gt;3001&lt;/code&gt; 추가 (&lt;code&gt;[&amp;quot;3000&amp;quot;, &amp;quot;3001&amp;quot;, &amp;quot;9090&amp;quot;, &amp;quot;8080&amp;quot;, &amp;quot;22&amp;quot;]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;terraform validate&lt;/code&gt; 통과 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;modules/network/variables.tf&lt;/code&gt;, &lt;code&gt;envs/prod/variables.tf&lt;/code&gt;, &lt;code&gt;envs/dev/variables.tf&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;admin_cidr&lt;/code&gt; 변수 설명에 Langfuse 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;envs/prod/outputs.tf&lt;/code&gt;, &lt;code&gt;envs/dev/outputs.tf&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;langfuse_url&lt;/code&gt; 출력값 추가 (&lt;code&gt;http://&amp;lt;모니터링 EC2 공인 IP&amp;gt;:3001&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;docs/DEPLOY.md&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;admin_cidr&lt;/code&gt; 안내 주석에 Langfuse 추가&lt;/li&gt;
&lt;li&gt;접속 주소 표에 Langfuse 행 추가&lt;/li&gt;
&lt;li&gt;트러블슈팅 표의 &amp;quot;Grafana 접속 안 됨&amp;quot; 항목을 &amp;quot;Grafana/Langfuse 접속 안 됨&amp;quot;으로 확장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;envs/prod/terraform.tfvars.example&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;admin_cidr&lt;/code&gt; 안내 주석에 Langfuse 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;왜 이렇게 했나&lt;/h2&gt;
&lt;p&gt;RDS/Redis처럼 SSM 포트포워딩으로 접근하는 방법도 검토했으나, Langfuse는 모니터링 EC2 자기 자신에서 도는 서비스라 매번 SSM 세션을 여는 것보다 Grafana처럼 보안 그룹으로 상시 열어두는 방식을 선택했다.&lt;/p&gt;
&lt;h2&gt;추가 작업: kafka-ui 클러스터 조회 실패 수정&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;http://&amp;lt;모니터링 EC2&amp;gt;:8080/ui/clusters/prod/all-topics&lt;/code&gt; 페이지가 안 뜨는 문제를 진단하고 원인을 고쳤다.&lt;/p&gt;
&lt;h3&gt;진단 과정&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;모니터링 EC2 상태, 공인 IP, 보안 그룹의 8080 규칙을 확인 → 전부 정상 (네트워크 문제 아님)&lt;/li&gt;
&lt;li&gt;루트 경로(&lt;code&gt;:8080/&lt;/code&gt;)가 뜨면서 컨테이너 자체는 살아있음을 확인&lt;/li&gt;
&lt;li&gt;SSM으로 모니터링 EC2에 접속해 &lt;code&gt;docker ps&lt;/code&gt;, kafka-ui 로그, 컨테이너 실제 환경변수, SSM 파라미터 값을 조회&lt;/li&gt;
&lt;li&gt;로그에서 &lt;code&gt;Invalid url in bootstrap.servers: 10.0.1.153&lt;/code&gt; 에러 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;원인&lt;/h3&gt;
&lt;p&gt;Kafka가 3-브로커 구성으로 바뀌면서 &lt;code&gt;/sanji/prod/kafka/private-ip&lt;/code&gt; SSM 값이 &lt;code&gt;10.0.1.153,10.0.1.58,10.0.1.191&lt;/code&gt;(콤마로 구분된 IP 3개)이 됐다. 그런데 &lt;code&gt;docker-compose.monitoring.yml&lt;/code&gt;의 kafka-ui 설정은 이 문자열 전체 뒤에 &lt;code&gt;:9092&lt;/code&gt;를 한 번만 붙이고 있어서, 실제 컨테이너에 주입된 값이 &lt;code&gt;10.0.1.153,10.0.1.58,10.0.1.191:9092&lt;/code&gt;가 됐다. 앞의 두 IP에 포트가 없어 kafka-ui가 유효하지 않은 주소로 판단해 &amp;quot;prod&amp;quot; 클러스터 정보를 가져오지 못했다.&lt;/p&gt;
&lt;p&gt;Prometheus 설정(&lt;code&gt;scripts/deploy-monitoring.sh&lt;/code&gt;)은 이미 IP마다 포트를 따로 붙이는 루프 로직이 있어서 같은 문제가 없었다.&lt;/p&gt;
&lt;h3&gt;바꾼 파일 (sanji-jk 레포)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;scripts/deploy-monitoring.sh&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IP마다 &lt;code&gt;:9092&lt;/code&gt;를 붙여 &lt;code&gt;KAFKA_BOOTSTRAP_SERVERS&lt;/code&gt; 변수를 새로 만들고 &lt;code&gt;.env&lt;/code&gt;에 기록&lt;/li&gt;
&lt;li&gt;기존 Prometheus용 IP 분리 루프(&lt;code&gt;KAFKA_IPS&lt;/code&gt;)를 재사용하도록 위치 조정 (중복 제거)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;docker-compose.monitoring.yml&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;kafka-ui의 &lt;code&gt;KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS&lt;/code&gt;를 &lt;code&gt;${KAFKA_PRIVATE_IP}:9092&lt;/code&gt; 대신 &lt;code&gt;${KAFKA_BOOTSTRAP_SERVERS}&lt;/code&gt;로 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bash -n&lt;/code&gt;으로 스크립트 문법 확인 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;Gateway에 Swagger 라우트 추가&lt;/h1&gt;
&lt;h2&gt;배경&lt;/h2&gt;
&lt;p&gt;배포 환경에서 swagger-ui에 접속할 방법이 없다는 문제가 있었다. ALB는 gateway(8000 포트)로만 트래픽을 보내고, 각 서비스 포트는 보안 그룹에서 ALB/모니터링 EC2 외에는 막혀 있어서 직접 접근이 불가능했다. 임시 방편으로 SSM 포트포워딩(모니터링 EC2를 발판 삼아 터널링)을 고려했지만, 어차피 재배포를 해야하는 것은 똑같았기 때문에 상시 접근 가능한 방식인 &amp;quot;gateway에 swagger 라우트 추가&amp;quot;를 선택했다.&lt;/p&gt;
&lt;h2&gt;무엇을 했나&lt;/h2&gt;
&lt;p&gt;gateway-server가 각 서비스의 OpenAPI 문서(&lt;code&gt;/v3/api-docs&lt;/code&gt;)를 대신 가져와서, gateway 자체의 swagger-ui 한 화면에서 서비스를 드롭다운으로 골라볼 수 있게 구성했다. swagger-ui 화면 자체는 gateway가 직접 서빙하고, 문서 데이터만 각 서비스로부터 프록시해오는 방식이다. (Spring Cloud Gateway + springdoc 공식 연동 패턴)&lt;/p&gt;
&lt;h2&gt;바꾼 파일&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;gateway-server/build.gradle&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;springdoc-openapi-starter-webflux-ui:2.8.9&lt;/code&gt; 의존성 추가 (gateway는 WebFlux 기반이라 webflux 버전 사용, 각 서비스가 쓰는 &lt;code&gt;springdoc-openapi-starter-webmvc-ui&lt;/code&gt;와는 다른 아티팩트)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;config-server/src/main/resources/config/gateway-server.yml&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;springdoc 의존성을 가진 6개 서비스(auction, bid, order, payment, notification, ai)에 대해 &lt;code&gt;/v3/api-docs/{서비스명}&lt;/code&gt; 라우트 추가. &lt;code&gt;SetPath=/v3/api-docs&lt;/code&gt; 필터로 실제 서비스가 기대하는 경로로 바꿔서 전달&lt;/li&gt;
&lt;li&gt;&lt;code&gt;springdoc.api-docs.enabled: false&lt;/code&gt; 설정 (gateway 자신은 REST API가 없으므로 자체 문서 생성 불필요)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;springdoc.swagger-ui.urls&lt;/code&gt;에 6개 서비스 문서 URL 목록 등록&lt;/li&gt;
&lt;li&gt;user-service는 springdoc 의존성이 없어 목록에서 제외&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;gateway-server/src/main/java/.../config/SecurityConfig.java&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/swagger-ui.html&lt;/code&gt;, &lt;code&gt;/swagger-ui/**&lt;/code&gt;, &lt;code&gt;/v3/api-docs/**&lt;/code&gt;, &lt;code&gt;/webjars/**&lt;/code&gt; 경로를 &lt;code&gt;permitAll&lt;/code&gt;로 열어서 JWT 없이 문서 화면에 접근 가능하도록 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;확인한 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;./gradlew :gateway-server:compileJava&lt;/code&gt; 성공 (새 의존성 정상 해석, 컴파일 문제 없음)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;접속 방법 (배포 반영 후)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;https://&amp;lt;ALB 주소&amp;gt;/swagger-ui.html&lt;/code&gt; 접속 후 화면 우측 상단 드롭다운에서 서비스 선택&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;LANGFUSE_AUTH_HEADER 운영 배포 연결&lt;/h1&gt;
&lt;h2&gt;무엇을 했나&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ai-service&lt;/code&gt;가 Langfuse로 트레이싱을 보낼 때 쓰는 &lt;code&gt;LANGFUSE_AUTH_HEADER&lt;/code&gt;가 로컬 &lt;code&gt;docker-compose.yml&lt;/code&gt;에만 연결되어 있고, 실제 운영 배포(AWS ECS Fargate)에는 전혀 주입되지 않고 있던 문제를 확인하고 연결했다.&lt;/p&gt;
&lt;h2&gt;확인한 문제&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sanji-infra/bootstrap/main.tf&lt;/code&gt;의 &lt;code&gt;secret_keys&lt;/code&gt;, &lt;code&gt;sanji-infra/modules/ssm/main.tf&lt;/code&gt;의 &lt;code&gt;app_secret_paths&lt;/code&gt;, &lt;code&gt;sanji-infra/modules/ecs/main.tf&lt;/code&gt;의 &lt;code&gt;ai-service&lt;/code&gt; 컨테이너 정의(&lt;code&gt;secrets&lt;/code&gt;, &lt;code&gt;extra_env&lt;/code&gt;) 어디에도 &lt;code&gt;LANGFUSE_AUTH_HEADER&lt;/code&gt;/&lt;code&gt;LANGFUSE_HOST&lt;/code&gt;가 없었다.&lt;/li&gt;
&lt;li&gt;그래서 운영 &lt;code&gt;ai-service&lt;/code&gt; 태스크는 &lt;code&gt;application.yml&lt;/code&gt; 기본값(빈 문자열)으로 떠 있었고, Langfuse로 트레이싱이 전송되지 않는 상태였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;바꾼 파일 (sanji-infra 레포)&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;bootstrap/main.tf&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;secret_keys&lt;/code&gt; 목록에 &lt;code&gt;&amp;quot;langfuse/auth-header&amp;quot;&lt;/code&gt; 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;modules/ssm/main.tf&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app_secret_paths&lt;/code&gt;에 &lt;code&gt;&amp;quot;langfuse-auth-header&amp;quot; = &amp;quot;/${var.project}/${var.environment}/langfuse/auth-header&amp;quot;&lt;/code&gt; 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;modules/ecs/main.tf&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ai-service&lt;/code&gt; 블록의 &lt;code&gt;secrets&lt;/code&gt;에 &lt;code&gt;LANGFUSE_AUTH_HEADER = &amp;quot;langfuse-auth-header&amp;quot;&lt;/code&gt; 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extra_env&lt;/code&gt;에 &lt;code&gt;LANGFUSE_HOST = &amp;quot;http://${var.monitoring_private_ip}:3000&amp;quot;&lt;/code&gt; 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scripts/ssm-init.sh&lt;/code&gt;, &lt;code&gt;scripts/ssm-push.sh&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;두 스크립트가 서로 존재하지 않는 파일명(&lt;code&gt;ssm-restore.sh&lt;/code&gt;, &lt;code&gt;ssm-backup.sh&lt;/code&gt;)을 안내하던 오류 수정 (실제 파일명 &lt;code&gt;ssm-init.sh&lt;/code&gt; / &lt;code&gt;ssm-push.sh&lt;/code&gt;에 맞게 정리)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;진행 중 발견한 별개 문제와 해결&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;bootstrap&lt;/code&gt; 폴더에서 &lt;code&gt;terraform apply&lt;/code&gt;를 시도하니 기존 시크릿 14개가 전부 삭제 대상으로 잡히는 에러가 발생했다. 원인은 이전 &amp;quot;dev/prod 환경 분리&amp;quot; 리팩터링에서 SSM 파라미터 키 형식을 &lt;code&gt;keycloak/client-secret&lt;/code&gt; → &lt;code&gt;prod/keycloak/client-secret&lt;/code&gt;처럼 바꾸면서 &lt;code&gt;terraform state mv&lt;/code&gt;를 하지 않아 상태 파일과 코드가 달라졌던 것이었다.&lt;/p&gt;
&lt;p&gt;기존 시크릿 14개에 대해 &lt;code&gt;terraform state mv&lt;/code&gt;로 상태 파일 내 키 이름만 &lt;code&gt;prod/&lt;/code&gt; 접두사가 붙도록 옮겼고, 이후 &lt;code&gt;terraform plan&lt;/code&gt; 결과 &lt;code&gt;16 to add, 0 to change, 0 to destroy&lt;/code&gt;로 확인되어 기존 시크릿 값 유실 없이 정리되었다.&lt;/p&gt;
&lt;h2&gt;추가 수정: LANGFUSE_HOST 포트 오류, 보안 그룹 누락&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;LANGFUSE_HOST&lt;/code&gt;가 잘못된 걸 발견해 고쳤다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;포트 오류&lt;/strong&gt;: &lt;code&gt;http://${var.monitoring_private_ip}:3000&lt;/code&gt;으로 넣었으나, &lt;code&gt;docker-compose.monitoring.yml&lt;/code&gt;에서 Langfuse는 &lt;code&gt;&amp;quot;3001:3000&amp;quot;&lt;/code&gt;으로 호스트 3001 포트에 매핑되어 있다(컨테이너 내부만 3000). ai-service는 같은 호스트가 아니라 네트워크 너머로 접근하므로 호스트 포트인 3001을 써야 한다. &lt;code&gt;:3001&lt;/code&gt;로 수정했다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;보안 그룹 누락&lt;/strong&gt;: &lt;code&gt;modules/network/main.tf&lt;/code&gt;의 &lt;code&gt;monitoring&lt;/code&gt; 보안 그룹에는 3001 포트가 &lt;code&gt;admin_cidr&lt;/code&gt;(관리자 접근용)에만 열려 있고 ECS 태스크에서의 접근은 허용되어 있지 않았다. &lt;code&gt;monitoring_in_loki_from_ecs&lt;/code&gt;(Loki 3100, ECS→모니터링)와 같은 패턴으로 &lt;code&gt;monitoring_in_langfuse_from_ecs&lt;/code&gt; 규칙을 추가해 ECS 보안 그룹에서 3001 포트로의 접근을 허용했다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;envs/prod&lt;/code&gt;에서 &lt;code&gt;terraform validate&lt;/code&gt; 통과 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;발견한 개선 방향&lt;/h2&gt;
&lt;p&gt;Keycloak은 &lt;code&gt;scripts/keycloak-setup.sh&lt;/code&gt;로 realm/client-secret을 자동 재설정하지만, Langfuse는 그런 스크립트가 없어서 RDS가 destroy/재생성될 때마다 사람이 직접 Langfuse UI에서 프로젝트를 새로 만들고 &lt;code&gt;LANGFUSE_AUTH_HEADER&lt;/code&gt;를 재발급해야 하는 상태다.&lt;/p&gt;
&lt;p&gt;찾아보니 Langfuse는 &amp;quot;Headless Initialization&amp;quot; 기능으로 &lt;code&gt;LANGFUSE_INIT_ORG_ID&lt;/code&gt;, &lt;code&gt;LANGFUSE_INIT_PROJECT_ID&lt;/code&gt;, &lt;code&gt;LANGFUSE_INIT_PROJECT_PUBLIC_KEY&lt;/code&gt;, &lt;code&gt;LANGFUSE_INIT_PROJECT_SECRET_KEY&lt;/code&gt;, &lt;code&gt;LANGFUSE_INIT_USER_EMAIL&lt;/code&gt;, &lt;code&gt;LANGFUSE_INIT_USER_PASSWORD&lt;/code&gt; 환경변수를 지원한다. 이 값들을 SSM에 고정값으로 미리 넣고 &lt;code&gt;docker-compose.monitoring.yml&lt;/code&gt;의 langfuse 서비스에 주입하면, 컨테이너가 뜰 때마다 같은 프로젝트/키를 자동으로 재생성해서 Keycloak의 수동 스크립트 없이도 &lt;code&gt;LANGFUSE_AUTH_HEADER&lt;/code&gt; 값이 영구 고정된다.&lt;/p&gt;
&lt;h2&gt;추가 작업: Headless Initialization 구현&lt;/h2&gt;
&lt;p&gt;위 개선 방향을 실제로 적용했다. Langfuse 소스 코드(&lt;code&gt;web/src/initialize.ts&lt;/code&gt;, &lt;code&gt;web/src/env.mjs&lt;/code&gt;)를 직접 확인해 &lt;code&gt;LANGFUSE_INIT_PROJECT_PUBLIC_KEY&lt;/code&gt;/&lt;code&gt;SECRET_KEY&lt;/code&gt;에 형식 검증이 없음(임의 문자열 허용)을 확인한 뒤 아래와 같이 반영했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sanji-infra/bootstrap/main.tf&lt;/code&gt;: &lt;code&gt;secret_keys&lt;/code&gt;에 &lt;code&gt;langfuse/public-key&lt;/code&gt;, &lt;code&gt;langfuse/secret-key&lt;/code&gt;, &lt;code&gt;langfuse/init-user-password&lt;/code&gt; 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sanji-infra/modules/ssm/main.tf&lt;/code&gt;: &lt;code&gt;app_secret_paths&lt;/code&gt;에 동일한 3개 경로 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scripts/deploy-monitoring.sh&lt;/code&gt;: 위 3개 값을 SSM에서 조회해 &lt;code&gt;.env&lt;/code&gt;에 기록하도록 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-compose.monitoring.yml&lt;/code&gt;: langfuse 서비스에 &lt;code&gt;LANGFUSE_INIT_*&lt;/code&gt; 9개 변수 추가. 조직/프로젝트 ID는 &lt;code&gt;sanji&lt;/code&gt;/&lt;code&gt;sanji-ai&lt;/code&gt;로 고정, 관리자 이메일은 &lt;code&gt;admin@sanji.local&lt;/code&gt;로 고정, 공개키/비밀키/비밀번호는 &lt;code&gt;.env&lt;/code&gt;에서 주입&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sanji-infra/docs/DEPLOY.md&lt;/code&gt;: SSM 파라미터 표에 3개 행 추가, &amp;quot;6-3. Langfuse 설정&amp;quot; 섹션 신설 (뒤 섹션 번호 6-3 -&amp;gt; 6-4로 밀림), 기존 &amp;quot;6-3단계&amp;quot; 오탈자를 &amp;quot;6-2단계&amp;quot;로 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;terraform validate&lt;/code&gt;(bootstrap, envs/prod)와 bootstrap &lt;code&gt;terraform plan&lt;/code&gt;(&lt;code&gt;6 to add, 0 to change, 0 to destroy&lt;/code&gt;)으로 기존 값 손실 없이 반영됨을 확인했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;ssm-push CHANGE_ME 확인 절차 추가&lt;/h1&gt;
&lt;h2&gt;작업 내용&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sanji-infra/scripts/ssm-push.sh&lt;/code&gt;에서 파라미터 값이 &lt;code&gt;CHANGE_ME&lt;/code&gt;인 경우, 기존에는 그냥 건너뛰기만 하고 넘어갔다.&lt;br&gt;이제는 실제로 파라미터를 적용하기 전에 &lt;code&gt;CHANGE_ME&lt;/code&gt;로 남아있는 파라미터 목록을 먼저 보여주고, 사용자가 &lt;code&gt;yes&lt;/code&gt;를 입력해야만 다음 단계로 진행하도록 변경했다.&lt;/p&gt;
&lt;h3&gt;동작 방식&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;백업 파일(&lt;code&gt;ssm-backup.json&lt;/code&gt;)을 한 번 먼저 훑어서 값이 &lt;code&gt;CHANGE_ME&lt;/code&gt;인 파라미터 이름을 모두 수집한다.&lt;/li&gt;
&lt;li&gt;하나라도 있으면 아래 형식으로 출력하고 입력을 기다린다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;아래 파라미터 값이 CHANGE_ME로 지정되어 있습니다.
- /sanji/prod/...

그래도 진행하시겠습니까?
- Only enter &amp;#39;yes&amp;#39; to continue:&lt;/code&gt;&lt;/pre&gt;&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;code&gt;yes&lt;/code&gt;가 아니면 스크립트를 중단한다(종료 코드 1).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yes&lt;/code&gt;를 입력하면 기존 로직대로 파라미터를 하나씩 처리하며, &lt;code&gt;CHANGE_ME&lt;/code&gt;인 파라미터는 여전히 건너뛴다(값을 실수로 덮어쓰지 않기 위함).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;변경 파일&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sanji-infra/scripts/ssm-push.sh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내일배움캠프/최종 프로젝트 개발일지</category>
      <author>munsik22</author>
      <guid isPermaLink="true">https://munsik22.tistory.com/361</guid>
      <comments>https://munsik22.tistory.com/361#entry361comment</comments>
      <pubDate>Thu, 2 Jul 2026 19:27:32 +0900</pubDate>
    </item>
    <item>
      <title>3주차 3일차 작업일지 (0701)</title>
      <link>https://munsik22.tistory.com/360</link>
      <description>&lt;h1&gt;keycloak-setup.sh 자동화&lt;/h1&gt;
&lt;h2&gt;작업 내용&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;keycloak-setup.sh&lt;/code&gt;를 메인 레포로 이동하고, Deploy EC2 워크플로우에 자동 실행 스텝을 추가했다.&lt;/p&gt;
&lt;h2&gt;변경 파일&lt;/h2&gt;
&lt;h3&gt;생성&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;scripts/keycloak-setup.sh&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;sanji-infra의 동일 스크립트를 가져와서 아래 사항 수정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aws.exe&lt;/code&gt; -&amp;gt; &lt;code&gt;aws&lt;/code&gt; (GitHub Actions runner는 Linux)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tr -d &amp;#39;\r&amp;#39;&lt;/code&gt; 제거 (Windows 전용)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REGION&lt;/code&gt; 환경변수 주입 지원 (&lt;code&gt;REGION:-ap-northeast-2&lt;/code&gt; 폴백)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INSTANCE_ID&lt;/code&gt; 환경변수 주입 지원 (없으면 AWS CLI로 자동 조회)&lt;/li&gt;
&lt;li&gt;멱등 처리 추가: sanjijk realm이 이미 Keycloak DB에 있으면 &lt;code&gt;SKIP:realm_exists&lt;/code&gt; 출력 후 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;수정&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.github/workflows/deploy-ec2.yml&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Setup Keycloak realm and client secret&lt;/code&gt; 스텝 추가&lt;/li&gt;
&lt;li&gt;조건: &lt;code&gt;inputs.target != &amp;#39;kafka&amp;#39;&lt;/code&gt; (monitoring 또는 both 배포 시에만 실행, push 트리거 포함)&lt;/li&gt;
&lt;li&gt;모니터링 EC2 배포 완료 직후 실행되어 realm-export.json 배포 순서 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sanji-infra/README.md&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;3번(재배포 절차)에서 수동 실행 안내 제거, 자동 처리 설명으로 교체&lt;/li&gt;
&lt;li&gt;파일 구성 목록에서 keycloak-setup.sh 항목 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sanji-infra/docs/DEPLOY.md&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;6-2 Keycloak 설정 절 수동 실행 → 자동 처리로 교체&lt;/li&gt;
&lt;li&gt;체크리스트 9번 자동 처리로 교체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;삭제&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sanji-infra/scripts/keycloak-setup.sh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;추가 수정 (코드 리뷰 반영)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;scripts/keycloak-setup.sh&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;[치명적 버그]&lt;/strong&gt; &lt;code&gt;STATUS=$((...))&lt;/code&gt; → &lt;code&gt;STATUS=$(...)&lt;/code&gt; (산술 확장 오용으로 스크립트 강제 종료됨)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[보안]&lt;/strong&gt; client-secret을 stdout에 평문 출력하던 방식 제거, EC2에서 SSM에 직접 저장하도록 변경&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[보안]&lt;/strong&gt; admin 패스워드를 &lt;code&gt;-d &amp;quot;password=...&amp;quot;&lt;/code&gt; 방식 → &lt;code&gt;--data-urlencode&lt;/code&gt; 방식으로 변경 (특수문자 대응)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[버그]&lt;/strong&gt; &lt;code&gt;aws ecs list-tasks&lt;/code&gt;에 &lt;code&gt;--desired-status RUNNING&lt;/code&gt; 추가 (STOPPED 태스크 반환 방지)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[유지보수]&lt;/strong&gt; &lt;code&gt;ECS_CLUSTER&lt;/code&gt;, &lt;code&gt;KEYCLOAK_REALM&lt;/code&gt;, &lt;code&gt;KEYCLOAK_CLIENT_ID&lt;/code&gt;, &lt;code&gt;SSM_PARAM_ADMIN_PASSWORD&lt;/code&gt; 하드코딩 → 환경변수 + 기본값으로 변경&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[유지보수]&lt;/strong&gt; &lt;code&gt;TimedOut&lt;/code&gt; 상태 처리 추가, &lt;code&gt;--timeout-seconds 600&lt;/code&gt; send-command에 명시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scripts/keycloak-setup.sh&lt;/code&gt; (2차 리뷰 반영)&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;[버그]&lt;/strong&gt; &lt;code&gt;CLIENT_UUID&lt;/code&gt;, &lt;code&gt;NEW_SECRET&lt;/code&gt; null/빈 문자열 검증 누락 → put-parameter 전 방어 로직 추가&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[버그]&lt;/strong&gt; 폴링 루프 &lt;code&gt;seq 1 60&lt;/code&gt; (300초) → &lt;code&gt;seq 1 120&lt;/code&gt; (600초)로 send-command 타임아웃과 동기화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[개선]&lt;/strong&gt; realm import HTTP 응답 코드 &lt;code&gt;!= &amp;quot;201&amp;quot;&lt;/code&gt; → &lt;code&gt;&amp;lt; 200 || &amp;gt;= 300&lt;/code&gt; (2xx 전체 허용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sanji-infra/modules/iam/main.tf&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;EC2 IAM 역할에 &lt;code&gt;ssm:PutParameter&lt;/code&gt; 추가 (경로: &lt;code&gt;/{project}/*/keycloak/client-secret&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;EC2 IAM 역할에 &lt;code&gt;kms:GenerateDataKey&lt;/code&gt; 추가 (SecureString PutParameter에 필요)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;추가 수정 (보안 그룹 누락 발견)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sanji-infra/modules/network/main.tf&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ecs_in_from_monitoring&lt;/code&gt; 규칙에 포트 18080 추가&lt;/li&gt;
&lt;li&gt;Keycloak은 ECS 퍼블릭 서브넷에 있고 포트 18080을 씀&lt;/li&gt;
&lt;li&gt;기존 규칙에 18080이 빠져 있어 모니터링 EC2에서 Keycloak API를 호출할 수 없었음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;멱등성 동작 정리&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;최초 배포 / destroy 후 재배포&lt;/td&gt;
&lt;td&gt;realm 없음 → import + client-secret 발급 + SSM 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일반 main push (모니터링 재배포)&lt;/td&gt;
&lt;td&gt;realm 이미 존재 → SKIP, SSM 건드리지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h1&gt;Kafka Producer / 복제 계수 설정 추가&lt;/h1&gt;
&lt;p&gt;동료 코드 리뷰를 통해 Kafka 3중 브로커 구성에서 누락된 설정 두 가지를 발견했다.&lt;/p&gt;
&lt;h2&gt;발견된 문제&lt;/h2&gt;
&lt;h3&gt;문제 1. Producer &lt;code&gt;acks&lt;/code&gt;, &lt;code&gt;retries&lt;/code&gt; 누락&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;영향 서비스:&lt;/strong&gt; &lt;code&gt;bid-service&lt;/code&gt;, &lt;code&gt;auction-service&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;acks=all&lt;/code&gt; 없으면: 리더 브로커 1개가 응답하면 전송 완료로 간주. 팔로워 복제 전에 리더 장애 발생 시 입찰 이벤트 데이터 소실 가능&lt;/li&gt;
&lt;li&gt;&lt;code&gt;retries&lt;/code&gt; 없으면: 브로커 리더 교체 중 일시적 실패 시 재시도 없이 이벤트 발행 실패 (Kafka 3.7 기본값은 Integer.MAX_VALUE이나 명시적으로 설정하는 것이 안전)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;현재 코드:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# bid-service, auction-service 공통 - acks/retries 없음
spring:
  kafka:
    producer:
      key-serializer: ...
      value-serializer: ...&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;문제 2. 복제 계수 환경변수 누락&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;영향 파일:&lt;/strong&gt; &lt;code&gt;docker-compose.kafka.yml&lt;/code&gt;, &lt;code&gt;docker-compose.kafka-local.yml&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KAFKA_DEFAULT_REPLICATION_FACTOR&lt;/code&gt; 미설정 시 토픽이 기본값 &lt;code&gt;replication-factor=1&lt;/code&gt;로 생성됨&lt;/li&gt;
&lt;li&gt;브로커 3대 구성의 의미가 없어지고, 해당 브로커 1대 장애 시 토픽 접근 불가&lt;/li&gt;
&lt;li&gt;내부 토픽(&lt;code&gt;__consumer_offsets&lt;/code&gt;, 트랜잭션 로그)도 동일 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;작업 내역&lt;/h2&gt;
&lt;h3&gt;bid-service producer 설정 추가&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;파일:&lt;/strong&gt; &lt;code&gt;services/bid-service/src/main/resources/application.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  kafka:
    producer:
      acks: all      # 추가
      retries: 3     # 추가&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;auction-service producer 설정 추가&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;파일:&lt;/strong&gt; &lt;code&gt;services/auction-service/src/main/resources/application.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  kafka:
    producer:
      acks: all      # 추가
      retries: 3     # 추가&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;프로덕션 Kafka 복제 계수 설정 추가&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;파일:&lt;/strong&gt; &lt;code&gt;docker-compose.kafka.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;KAFKA_DEFAULT_REPLICATION_FACTOR: 3
KAFKA_MIN_INSYNC_REPLICAS: 2
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h1&gt;ECS 가중치(Fargate Spot) 조정&lt;/h1&gt;
&lt;h2&gt;변경 내용&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/modules/ecs/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aws_ecs_cluster_capacity_providers&lt;/code&gt;: &lt;code&gt;FARGATE&lt;/code&gt;에 &lt;code&gt;base=1&lt;/code&gt; 추가, &lt;code&gt;FARGATE_SPOT weight=3&lt;/code&gt; 블록 신규 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aws_ecs_service.app&lt;/code&gt; (일반 서비스 9개): &lt;code&gt;launch_type = &amp;quot;FARGATE&amp;quot;&lt;/code&gt; 제거 + &lt;code&gt;capacity_provider_strategy&lt;/code&gt; 블록 2개 추가&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FARGATE base=1 weight=1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FARGATE_SPOT weight=3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aws_ecs_service.bid&lt;/code&gt;, &lt;code&gt;aws_ecs_service.keycloak&lt;/code&gt;: 변경 없음 (&lt;code&gt;launch_type = &amp;quot;FARGATE&amp;quot;&lt;/code&gt; 유지)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;결정 사항&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;bid: WebSocket 장기 연결 서비스라 Spot 회수 시 STOMP 세션 전체 끊김 -&amp;gt; On-Demand 고정&lt;/li&gt;
&lt;li&gt;keycloak: 인증 서버라 태스크 중단 시 로그인 전체 불가 -&amp;gt; On-Demand 고정&lt;/li&gt;
&lt;li&gt;일반 App 9개: &lt;code&gt;desired_count=1&lt;/code&gt;이면 &lt;code&gt;base=1&lt;/code&gt;로 On-Demand로 뜸, 향후 스케일아웃 시 추가 태스크부터 Spot 배분&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;비고&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;launch_type&lt;/code&gt;을 명시하면 클러스터 기본 Capacity Provider 전략을 우회해 Spot이 아예 쓰이지 않는다.&lt;br&gt;서비스 레벨에서 &lt;code&gt;capacity_provider_strategy&lt;/code&gt;로 교체해야 실제 Spot이 적용된다.&lt;/p&gt;
&lt;h2&gt;코드 리뷰 반영 (Fargate Spot 구조 개선)&lt;/h2&gt;
&lt;h3&gt;변경 내용&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/modules/ecs/variables.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fargate_on_demand_base&lt;/code&gt; 변수 추가 (prod 기본값 1, dev는 환경 폴더에서 0으로 재정의)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fargate_spot_weight&lt;/code&gt; 변수 추가 (기본값 3)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/modules/ecs/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aws_ecs_cluster_capacity_providers&lt;/code&gt;: base/weight 하드코딩을 변수로 교체&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aws_ecs_service.app&lt;/code&gt;: 서비스 레벨 &lt;code&gt;capacity_provider_strategy&lt;/code&gt; 블록 제거 (클러스터 기본 전략 위임으로 중복 제거)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;base&lt;/code&gt; 동작 설명 주석 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/envs/dev/variables.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fargate_on_demand_base = 0&lt;/code&gt; (100% Spot)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fargate_spot_weight = 3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/envs/prod/variables.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fargate_on_demand_base = 1&lt;/code&gt; (On-Demand 1대 보장)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fargate_spot_weight = 3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/envs/dev/main.tf&lt;/code&gt;, &lt;code&gt;sanji-infra/envs/prod/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ecs 모듈 호출에 &lt;code&gt;fargate_on_demand_base&lt;/code&gt;, &lt;code&gt;fargate_spot_weight&lt;/code&gt; 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;코드 리뷰 반영 2차 (서비스 레벨 명시 복원 + 100% Spot)&lt;/h2&gt;
&lt;h3&gt;변경 내용&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/modules/ecs/variables.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fargate_on_demand_weight&lt;/code&gt; 변수 추가 (기본값 1)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/modules/ecs/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aws_ecs_cluster_capacity_providers&lt;/code&gt;: FARGATE weight를 &lt;code&gt;var.fargate_on_demand_weight&lt;/code&gt;로 교체&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aws_ecs_service.app&lt;/code&gt;: &lt;code&gt;capacity_provider_strategy&lt;/code&gt; 블록 재추가 (의도 명시 목적)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/envs/dev/variables.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fargate_on_demand_weight = 0&lt;/code&gt; (FARGATE weight=0으로 100% Spot)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/envs/prod/variables.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fargate_on_demand_weight = 1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/envs/dev/main.tf&lt;/code&gt;, &lt;code&gt;sanji-infra/envs/prod/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ecs 모듈 호출에 &lt;code&gt;fargate_on_demand_weight&lt;/code&gt; 추가 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;결정 사항&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;weight=0&lt;/code&gt;은 AWS ECS API 기준 유효한 값 (범위 0~1000, 단 전략 내 하나 이상은 weight &amp;gt; 0 필요)&lt;/li&gt;
&lt;li&gt;dev: &lt;code&gt;FARGATE weight=0&lt;/code&gt; → 태스크 배치 대상 제외 → 100% FARGATE_SPOT&lt;/li&gt;
&lt;li&gt;서비스 레벨 strategy는 클러스터 기본값과 동일하더라도 명시적으로 유지 (서비스 코드만 봐도 Spot 여부 파악 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;terraform apply 에러 수정 (bash 경로 + 스크립트 경로)&lt;/h1&gt;
&lt;h2&gt;증상&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;envs/prod&lt;/code&gt;에서 &lt;code&gt;terraform apply&lt;/code&gt; 실행 시 &lt;code&gt;null_resource.db_schema_init&lt;/code&gt; local-exec 에러 발생&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error running command &amp;#39;bash ./scripts/db-schema-init.sh&amp;#39;: exec: &amp;quot;/bin/bash&amp;quot;: executable file not found in %PATH%.&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;원인&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;interpreter = [&amp;quot;/bin/bash&amp;quot;, &amp;quot;-c&amp;quot;]&lt;/code&gt; - Windows에 &lt;code&gt;/bin/bash&lt;/code&gt; 경로 자체가 없음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;command = &amp;quot;bash ${path.root}/scripts/db-schema-init.sh&amp;quot;&lt;/code&gt; - 모듈 안에서 &lt;code&gt;path.root&lt;/code&gt;는 &lt;code&gt;envs/prod/&lt;/code&gt;를 가리키므로 &lt;code&gt;envs/prod/scripts/db-schema-init.sh&lt;/code&gt;를 찾지만, 실제 스크립트는 &lt;code&gt;sanji-infra/scripts/&lt;/code&gt;에 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;변경 내용&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/modules/data/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;interpreter = [&amp;quot;/bin/bash&amp;quot;, &amp;quot;-c&amp;quot;]&lt;/code&gt; 제거 후 &lt;code&gt;interpreter = [var.bash_path]&lt;/code&gt;로 교체&lt;/li&gt;
&lt;li&gt;&lt;code&gt;command = &amp;quot;bash ${path.root}/scripts/...&amp;quot;&lt;/code&gt; → &lt;code&gt;command = &amp;quot;${path.module}/../../scripts/db-schema-init.sh&amp;quot;&lt;/code&gt; (&lt;code&gt;path.module&lt;/code&gt; = &lt;code&gt;modules/data/&lt;/code&gt; 절대경로 기준으로 올바르게 위치 지정)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/modules/data/variables.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bash_path&lt;/code&gt; 변수 추가 (기본값 &lt;code&gt;/bin/bash&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/envs/prod/variables.tf&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;sanji-infra/envs/dev/variables.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bash_path&lt;/code&gt; 변수 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/envs/prod/main.tf&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;sanji-infra/envs/dev/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;module &amp;quot;data&amp;quot;에 &lt;code&gt;bash_path = var.bash_path&lt;/code&gt; 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/envs/prod/terraform.tfvars.example&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows 로컬 실행 방법 주석 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/docs/INTRODUCTION.md&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;section 9 코드 예시를 수정 후 코드로 업데이트, &lt;code&gt;path.module&lt;/code&gt; vs &lt;code&gt;path.root&lt;/code&gt; 차이 및 &lt;code&gt;bash_path&lt;/code&gt; 사용법 설명 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/docs/DEPLOY.md&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;section 11 자주 겪는 문제 표에 &lt;code&gt;/bin/bash&lt;/code&gt; 에러 + 해결법 항목 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;조치&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sanji-infra/envs/prod/terraform.tfvars&lt;/code&gt;에 아래 한 줄 추가 필요 (Windows 로컬 실행 시)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bash_path = &amp;quot;C:/Program Files/Git/bin/bash.exe&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h1&gt;Deploy EC2 kafka 배포 실패 수정 (SSM 다중 인스턴스 처리)&lt;/h1&gt;
&lt;h2&gt;증상&lt;/h2&gt;
&lt;p&gt;GitHub Actions run #28494220661 &lt;code&gt;Send deploy command via SSM&lt;/code&gt; 단계 실패&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ValidationException: Value &amp;#39;i-xxx\ti-yyy\ti-zzz&amp;#39; at &amp;#39;instanceId&amp;#39; failed to satisfy constraint:
Member must satisfy regular expression pattern: (^i-(\w{8}|\w{17})$)&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;원인&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;send_and_wait&lt;/code&gt; 함수가 &lt;code&gt;aws ec2 describe-instances --output text&lt;/code&gt;로 인스턴스 ID를 조회할 때 Kafka 3대의 ID가 탭 구분으로 한 변수에 들어온다.&lt;br&gt;&lt;code&gt;aws ssm wait command-executed --instance-id&lt;/code&gt; 는 ID 하나만 받으므로 ValidationException 발생했다.&lt;/p&gt;
&lt;h2&gt;변경 내용&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;.github/workflows/deploy-ec2.yml&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;send_and_wait&lt;/code&gt; 함수의 인스턴스 ID 조회를 &lt;code&gt;--output json | jq -r &amp;#39;.[]&amp;#39;&lt;/code&gt; + &lt;code&gt;mapfile -t&lt;/code&gt; 배열로 변경&lt;/li&gt;
&lt;li&gt;인스턴스별 wait/상태확인/로그출력을 루프(&lt;code&gt;for iid in &amp;quot;${instance_ids[@]}&amp;quot;&lt;/code&gt;)로 처리&lt;/li&gt;
&lt;li&gt;어느 인스턴스라도 실패하면 &lt;code&gt;overall_status&lt;/code&gt;에 기록 후 루프 완료 시 &lt;code&gt;return 1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docs/CD.md&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kafka 다중 인스턴스 SSM 처리 방식 설명 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KAFKA_PRIVATE_IP&lt;/code&gt; SSM 포맷 설명 업데이트 (포트 없는 IP 목록)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;Prometheus kafka 타겟 오류 수정 (bootstrap_servers → IP 전용)&lt;/h1&gt;
&lt;h2&gt;증상&lt;/h2&gt;
&lt;p&gt;Grafana Prometheus 타겟 페이지에서 kafka job 전체 DOWN&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Get &amp;quot;http://10.0.1.117:9092,10.0.1.137:9092,10.0.1.169:9092:9100/metrics&amp;quot;:
dial tcp: too many colons in address&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;원인&lt;/h2&gt;
&lt;p&gt;SSM &lt;code&gt;/sanji/prod/kafka/private-ip&lt;/code&gt; 파라미터에 &lt;code&gt;kafka_bootstrap_servers&lt;/code&gt; 값(&lt;code&gt;10.0.1.117:9092,10.0.1.137:9092,10.0.1.169:9092&lt;/code&gt;)이 저장되어 있었다.&lt;br&gt;&lt;code&gt;deploy-monitoring.sh&lt;/code&gt;가 이 값을 단일 IP처럼 취급해 &lt;code&gt;:7071&lt;/code&gt;, &lt;code&gt;:9100&lt;/code&gt;을 덧붙이면서 잘못된 주소(&lt;code&gt;IP:9092,...:9100&lt;/code&gt;)가 만들어졌다.&lt;/p&gt;
&lt;h2&gt;변경 내용&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/modules/ssm/variables.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kafka_bootstrap_servers&lt;/code&gt; 변수명 → &lt;code&gt;kafka_private_ips&lt;/code&gt;로 변경 (포트 없는 IP만 저장 의미 명시)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/modules/ssm/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aws_ssm_parameter.kafka_private_ip&lt;/code&gt; value: &lt;code&gt;var.kafka_bootstrap_servers&lt;/code&gt; → &lt;code&gt;var.kafka_private_ips&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/envs/prod/main.tf&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;sanji-infra/envs/dev/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;module &amp;quot;ssm&amp;quot; 호출: &lt;code&gt;kafka_bootstrap_servers = module.compute_ec2.kafka_bootstrap_servers&lt;/code&gt; → &lt;code&gt;kafka_private_ips = join(&amp;quot;,&amp;quot;, module.compute_ec2.kafka_private_ips)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;scripts/deploy-monitoring.sh&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;envsubst&lt;/code&gt;를 제거하고 루프 + here-doc으로 prometheus.prod.yml 직접 생성&lt;/li&gt;
&lt;li&gt;각 IP 주소에서 &lt;code&gt;${addr%%:*}&lt;/code&gt;로 포트를 벗겨냄 (SSM 포맷에 관계없이 안전하게 처리)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/docs/DEPLOY.md&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;Prometheus kafka 타겟 Down&amp;quot; 문제 설명 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;조치&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;envs/prod&lt;/code&gt;에서 &lt;code&gt;terraform apply&lt;/code&gt; (SSM 파라미터 포맷 갱신)&lt;/li&gt;
&lt;li&gt;Deploy EC2 워크플로우 재실행 (prometheus.yml 재생성)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;참고: ecs-apps DOWN은 별도 이슈 아님&lt;/h2&gt;
&lt;p&gt;ECS 배포 직후 태스크가 새 IP로 교체되면서 &lt;code&gt;ecs-targets.json&lt;/code&gt;에 예전 IP가 남아 &amp;quot;no route to host&amp;quot;이 발생했는데, &lt;code&gt;ecs-discovery.sh cron&lt;/code&gt;이 5분마다 자동 갱신하므로 조금만 기다리면 UP으로 알아서 바뀐다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;Deploy EC2 실패 수정 (ecs:ListTasks IAM 권한 부족)&lt;/h1&gt;
&lt;h2&gt;증상&lt;/h2&gt;
&lt;p&gt;GitHub Actions &lt;code&gt;Deploy EC2&lt;/code&gt; run #28491781498 에서 &lt;code&gt;Setup Keycloak realm and client secret&lt;/code&gt; 단계 실패&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AccessDeniedException when calling the ListTasks operation:
... is not authorized to perform: ecs:ListTasks
on resource: arn:aws:ecs:ap-northeast-2:598537771389:container-instance/sanji-prod-cluster/*&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;원인&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;modules/iam/main.tf&lt;/code&gt;의 &lt;code&gt;EcsDeployService&lt;/code&gt; 정책이 &lt;code&gt;ecs:ListTasks&lt;/code&gt;를 &lt;code&gt;service/*&lt;/code&gt;, &lt;code&gt;task/*&lt;/code&gt; ARN에만 허용한다.&lt;br&gt;&lt;code&gt;aws ecs list-tasks --cluster ... --service-name keycloak&lt;/code&gt; 호출 시 AWS가 &lt;code&gt;container-instance/&lt;/code&gt;와 &lt;code&gt;cluster/&lt;/code&gt; ARN에 대해서도 권한을 확인하는데, 이 두 ARN이 resource 목록에 빠져 있었다.&lt;/p&gt;
&lt;h2&gt;변경 내용&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/modules/iam/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EcsDeployService&lt;/code&gt; statement의 resources에 두 항목을 추가했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;arn:aws:ecs:...:cluster/${var.name}-cluster&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arn:aws:ecs:...:container-instance/${var.name}-cluster/*&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;조치&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;envs/prod&lt;/code&gt;에서 &lt;code&gt;terraform apply&lt;/code&gt;로 IAM 정책 반영 후 Deploy EC2 워크플로우를 재실행했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;terraform plan 경고 제거 (cpu_core_count deprecated)&lt;/h1&gt;
&lt;h2&gt;증상&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;envs/prod&lt;/code&gt;에서 &lt;code&gt;terraform plan&lt;/code&gt; 실행 시 아래 경고가 발생했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Warning: Deprecated value used
  on ..\..\modules\compute-ec2\outputs.tf line 3, in output &amp;quot;kafka_instances&amp;quot;:
   3:   value = aws_instance.kafka
The deprecation originates from module.compute_ec2.aws_instance.kafka[0].cpu_core_count&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;원인&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;kafka_instances&lt;/code&gt; output이 &lt;code&gt;aws_instance.kafka&lt;/code&gt; 전체 객체를 그대로 내보내고 있었다.&lt;br&gt;&lt;code&gt;aws_instance&lt;/code&gt; 객체 안에 AWS 프로바이더가 deprecated 처리한 &lt;code&gt;cpu_core_count&lt;/code&gt; 속성이 포함되어 있어 경고가 발생했다.&lt;/p&gt;
&lt;h2&gt;변경 내용&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/modules/compute-ec2/outputs.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kafka_instances&lt;/code&gt; value를 전체 객체 &lt;code&gt;aws_instance.kafka&lt;/code&gt;에서 &lt;code&gt;[for i in aws_instance.kafka : { id = i.id }]&lt;/code&gt;로 교체&lt;/li&gt;
&lt;li&gt;호출하는 쪽(&lt;code&gt;envs/dev/main.tf&lt;/code&gt;, &lt;code&gt;envs/prod/main.tf&lt;/code&gt;)은 &lt;code&gt;.id&lt;/code&gt;만 쓰고 있어 수정 없이 동작함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;Deploy EC2 kafka 배포 실패 수정 (ec2:DescribeTags IAM 권한 부족)&lt;/h1&gt;
&lt;h2&gt;증상&lt;/h2&gt;
&lt;p&gt;GitHub Actions run #28494525772 &lt;code&gt;Send deploy command via SSM&lt;/code&gt; 단계 실패&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;An error occurred (UnauthorizedOperation) when calling the DescribeTags operation:
... is not authorized to perform: ec2:DescribeTags because no identity-based policy allows the ec2:DescribeTags action&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;원인&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;scripts/deploy-kafka.sh&lt;/code&gt;가 EC2 태그에서 &lt;code&gt;KafkaNodeId&lt;/code&gt; 값을 읽기 위해 &lt;code&gt;ec2:DescribeTags&lt;/code&gt;를 호출하는데, Kafka EC2 인스턴스 역할 &lt;code&gt;sanji-prod-ec2-role&lt;/code&gt;에 해당 권한이 없었다.&lt;/p&gt;
&lt;h2&gt;변경 내용&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/modules/iam/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ec2_grafana_ec2&lt;/code&gt; 정책의 Action 목록에 &lt;code&gt;ec2:DescribeTags&lt;/code&gt; 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/docs/DEPLOY.md&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;자주 겪는 문제 표에 &lt;code&gt;ec2:DescribeTags&lt;/code&gt; 에러 항목 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;조치&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;envs/prod&lt;/code&gt;에서 &lt;code&gt;terraform apply&lt;/code&gt; 후 Deploy EC2 워크플로우 재실행 (kafka 대상)&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;배포 환경에서 경매가 진행중 상태로 안 바뀌는 문제&lt;/h1&gt;
&lt;h2&gt;증상&lt;/h2&gt;
&lt;p&gt;배포 환경(prod)에서 경매 시작 시간이 지나도 경매 상태가 READY(대기중)에서 PROGRESS(진행중)로 바뀌지 않았다.&lt;/p&gt;
&lt;h2&gt;원인 조사&lt;/h2&gt;
&lt;h3&gt;코드 확인 내용&lt;/h3&gt;
&lt;p&gt;경매 상태를 시간에 맞춰 자동으로 바꾸는 코드는 &lt;code&gt;services/auction-service&lt;/code&gt;에 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AuctionScheduleManager&lt;/code&gt;: 경매 시작 시간이 되면 실행할 작업을 서버 메모리에 등록해두는 역할&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AuctionSchedulerJobService.startAuction()&lt;/code&gt;: 실제로 READY를 PROGRESS로 바꾸는 함수&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AuctionScheduleRecovery&lt;/code&gt;: 서버가 다시 켜졌을 때, 놓친 예약 작업을 다시 등록하는 역할. 다만 이미 시작 시간이 지난 READY 경매는 경고 로그만 남기고 다시 시작시키지 않음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AuctionRecoveryScheduler&lt;/code&gt;: 5분마다 도는 감시 작업. 그런데 이 작업은 종료 시간이 지난 PROGRESS 경매만 확인하고, 시작 시간이 지난 READY 경매는 확인하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 부분(READY 경매를 감시하는 로직이 없음)은 코드상 확인된 사실이지만, 이번에 실제로 이게 원인인지는 아직 확인하지 못했다.&lt;/p&gt;
&lt;h3&gt;AWS 실제 상태 확인 (aws cli 사용)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;auction-service&lt;/code&gt;는 ECS에 인스턴스 1개로만 떠 있음 (desiredCount=1). 여러 인스턴스 때문에 문제가 생긴다는 가설은 이걸로 배제됨&lt;/li&gt;
&lt;li&gt;이 인스턴스는 오늘 03:56 UTC(12:56 KST)에 새로 시작됨&lt;/li&gt;
&lt;li&gt;이 인스턴스 로그에 Kafka 서버 3대 전부에 연결이 안 된다는 경고가 계속 찍히고 있음&lt;/li&gt;
&lt;li&gt;Kafka EC2 3대의 실제 사설 IP와 로그에 나온 IP는 일치함 (IP 설정 문제는 아님)&lt;/li&gt;
&lt;li&gt;Kafka EC2 3대는 오늘 03:21 UTC에 새로 켜진 것으로 확인됨 (auction-service 재시작 35분 전)&lt;/li&gt;
&lt;li&gt;코드상 경매 시작 시 Kafka로 직접 메시지를 보내지 않고 DB(Outbox 테이블)에만 저장하므로, Kafka 연결 실패가 상태 변경 자체를 막는 구조는 아닌 것으로 보임. 다만 아직 확정된 결론은 아님&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;원인 확정 (타임존 미설정)&lt;/h2&gt;
&lt;h3&gt;재확인 계기&lt;/h3&gt;
&lt;p&gt;서버 재시작과 무관하게 경매 시작 시간을 몇 분 뒤로 바꾼 뒤에도 상태가 안 바뀌는 것을 확인했다. 이걸로 앞서 세운 &amp;quot;서버 재시작 때문에 예약이 사라짐&amp;quot; 가설은 폐기했다.&lt;/p&gt;
&lt;h3&gt;확인 방법&lt;/h3&gt;
&lt;p&gt;CloudWatch 로그에서 실제 경매 ID(&lt;code&gt;019f1c1d-4654-7f4d-845d-71bc4ae69e7b&lt;/code&gt;)로 필터링해서 확인했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;예약 등록 로그(&lt;code&gt;AuctionScheduleManager&lt;/code&gt;)는 여러 번 찍힘. 가장 최근 건: 05:42:33 UTC에 등록, 목표 시간(scheduledAt)은 &amp;quot;14:43&amp;quot;&lt;/li&gt;
&lt;li&gt;실제 실행 로그(&lt;code&gt;AuctionSchedulerJobService&lt;/code&gt;)는 로그 전체 기록을 통틀어 단 한 번도 없음&lt;/li&gt;
&lt;li&gt;확인 시점(05:45 UTC)은 목표 시간(14:43)을 이미 지난 상태였는데도 실행 안 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;원인&lt;/h2&gt;
&lt;p&gt;배포 환경(ECS)에 타임존 설정이 없어서, 서버가 시간을 한국시간이 아니라 UTC로 인식하고 있었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;로컬(&lt;code&gt;docker-compose.yml&lt;/code&gt;)의 auction-service에는 &lt;code&gt;TZ: Asia/Seoul&lt;/code&gt;이 설정되어 있음&lt;/li&gt;
&lt;li&gt;배포 환경(ECS 태스크 정의)에는 TZ 환경변수가 없음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dockerfile&lt;/code&gt;에도 &lt;code&gt;-Duser.timezone&lt;/code&gt; 같은 설정이 없음&lt;/li&gt;
&lt;li&gt;베이스 이미지(&lt;code&gt;eclipse-temurin:21-jre-alpine&lt;/code&gt;)의 기본 시간대는 UTC&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;경매 시작 시간은 시간대 정보 없이 숫자만(예: &amp;quot;14:43&amp;quot;) 저장된다. 로컬은 서버가 이걸 한국시간으로 알아서 맞게 처리하지만, 배포 환경은 서버가 이걸 UTC로 알아버려서 실제로는 한국시간 밤 23:43(9시간 뒤)로 예약된다. 그래서 사용자가 아무리 기다려도 경매가 시작되지 않는 것처럼 보인다.&lt;/p&gt;
&lt;h2&gt;조치&lt;/h2&gt;
&lt;h3&gt;변경 내용&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sanji-infra/modules/ecs/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;common_env&lt;/code&gt;에 &lt;code&gt;TZ = &amp;quot;Asia/Seoul&amp;quot;&lt;/code&gt; 추가 (일반 서비스 9개 + bid-service에 공통 적용)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;발견한 문제: apply만으로는 실제 반영 안 됨&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;aws_ecs_service&lt;/code&gt;(app, bid)의 &lt;code&gt;lifecycle { ignore_changes = [desired_count, task_definition] }&lt;/code&gt; 때문에, terraform apply로 새 리비전이 &amp;quot;등록&amp;quot;만 되고 실제 서비스는 여전히 옛 리비전을 쓴다. 서비스별로 &lt;code&gt;aws ecs update-service --task-definition &amp;lt;새 리비전&amp;gt;&lt;/code&gt;을 직접 해줘야 반영된다.&lt;/p&gt;
&lt;h2&gt;결과&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Wave 1(config-server, discovery-server, gateway-server): &lt;code&gt;update-service&lt;/code&gt;로 새 리비전 전환 완료, &lt;code&gt;wait services-stable&lt;/code&gt;로 안정화 확인&lt;/li&gt;
&lt;li&gt;Wave 2 이후(user, auction, order, payment, bid, notification, ai, 총 7개): 권한 정책에 막혀서 진행 못함 (&amp;quot;CI/CD 파이프라인을 안 거치고 직접 update-service 반복 실행&amp;quot;이 사용자의 일반적인 승인 범위를 넘는다는 이유). 사용자와 다음 진행 방법 논의 중&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;CI/CD 배포 로직 구조적 결함 발견 및 수정&lt;/h1&gt;
&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;_deploy-wave.yml&lt;/code&gt;의 배포 로직이 &amp;quot;서비스에 지금 붙어있는 태스크 정의&amp;quot;를 베이스로 복사해서 이미지 태그만 SHA로 교체하는 방식이었다. 이 때문에 Terraform이 환경변수(&lt;code&gt;TZ&lt;/code&gt; 등)나 cpu/memory 같은 걸 바꿔도, 배포할 때마다 옛 정의(변경 전)를 계속 복제하게 되어 영원히 반영이 안 된다.&lt;/p&gt;
&lt;p&gt;과거 작업일지 확인 결과, 이 방식은 &amp;quot;terraform apply가 CI/CD가 올려둔 최신 이미지 리비전을 옛 걸로 되돌리는 문제&amp;quot;를 막기 위해 일부러 만든 것이었다. 그 의도 자체는 맞지만, 구현 방법(&amp;quot;현재 붙어있는 정의&amp;quot;를 베이스로 삼음) 때문에 이런 부작용이 생긴 것으로, 그때는 검토가 안 됐던 것으로 보인다.&lt;/p&gt;
&lt;h2&gt;변경 내용&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;.github/workflows/_deploy-wave.yml&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;deploy_one()&lt;/code&gt; 함수에서 &lt;code&gt;aws ecs describe-services&lt;/code&gt;로 &amp;quot;서비스에 현재 붙어있는 정의&amp;quot;를 조회하던 부분을 제거&lt;/li&gt;
&lt;li&gt;대신 family 이름(&lt;code&gt;sanji-prod-{서비스명}&lt;/code&gt;)으로 &lt;code&gt;aws ecs describe-task-definition&lt;/code&gt;을 호출해서 &amp;quot;Terraform이 등록한 최신 리비전&amp;quot;을 베이스로 삼도록 변경&lt;/li&gt;
&lt;li&gt;이제부터는 Terraform이 바꾸는 모든 설정이 다음 배포 때 자동으로 반영됨 (수동 동기화 불필요)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;Kafka 브로커 3대 연결 불가 원인 조사 및 수정&lt;/h1&gt;
&lt;h2&gt;증상&lt;/h2&gt;
&lt;p&gt;auction-service 로그에 Kafka 브로커 3대 전부 연결 실패(&lt;code&gt;could not be established&lt;/code&gt;)가 계속 찍혔다. &lt;code&gt;Deploy EC2&lt;/code&gt;(kafka 대상)를 재실행해도 컨테이너는 정상 기동됐지만 연결 실패 문제는 해결되지 않았다.&lt;/p&gt;
&lt;h2&gt;확인 과정&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Kafka EC2 3대 모두 &lt;code&gt;docker ps&lt;/code&gt;로 컨테이너 정상 기동 확인 (재시작 반복 없음)&lt;/li&gt;
&lt;li&gt;9092 포트는 3대 모두 호스트에서 정상 리스닝 확인 (&lt;code&gt;ss -tlnp&lt;/code&gt;, &lt;code&gt;docker port&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;컨테이너 로그 확인 결과, 3대가 서로 9093(RAFT 컨트롤러 포트)으로 계속 연결 실패 중&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ss -tlnp&lt;/code&gt;에 9093 리스닝 자체가 없음 (호스트에 노출 안 됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;원인&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;docker-compose.kafka.yml&lt;/code&gt;의 &lt;code&gt;ports&lt;/code&gt;에 9092, 7071만 매핑되어 있고 9093이 빠져 있었다. 보안그룹은 9093을 이미 허용하고 있었지만, 실제 컨테이너-호스트 포트 매핑이 없어서 브로커 3대(서로 다른 EC2)가 RAFT quorum을 영원히 형성 못하는 상태였다.&lt;/p&gt;
&lt;h2&gt;변경 내용&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker-compose.kafka.yml&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ports&lt;/code&gt;에 &lt;code&gt;9093:9093&lt;/code&gt; 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;Deploy ECS 서비스 선택 수동 배포 추가&lt;/h1&gt;
&lt;h2&gt;변경 내용&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;.github/workflows/deploy-ecs.yml&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;workflow_dispatch.inputs.services&lt;/code&gt; 추가 (공백 구분 서비스 목록, 비워두면 전체 10개)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;plan&lt;/code&gt; 잡 추가: 입력값을 받아서 전체 목록 + wave별 목록으로 나눠 출력&lt;/li&gt;
&lt;li&gt;&lt;code&gt;build&lt;/code&gt; 잡: 선택된 서비스 이미지만 빌드/push하도록 변경&lt;/li&gt;
&lt;li&gt;wave 1~4 잡: 선택된 서비스가 하나도 없는 wave는 스킵, &lt;code&gt;success()&lt;/code&gt; 조건 추가로 앞 wave 실패 시 이후 wave도 중단&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내일배움캠프/최종 프로젝트 개발일지</category>
      <author>munsik22</author>
      <guid isPermaLink="true">https://munsik22.tistory.com/360</guid>
      <comments>https://munsik22.tistory.com/360#entry360comment</comments>
      <pubDate>Thu, 2 Jul 2026 10:04:18 +0900</pubDate>
    </item>
    <item>
      <title>[Computational Thinking] 논리와 증명</title>
      <link>https://munsik22.tistory.com/359</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 1&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1857&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HbWXm/dJMcacXXDhq/uUbknKkfHkEqtOlYIUN25K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HbWXm/dJMcacXXDhq/uUbknKkfHkEqtOlYIUN25K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HbWXm/dJMcacXXDhq/uUbknKkfHkEqtOlYIUN25K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHbWXm%2FdJMcacXXDhq%2FuUbknKkfHkEqtOlYIUN25K%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;1857&quot; height=&quot;840&quot; data-origin-width=&quot;1857&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 2&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E3yAb/dJMcahEVw93/ek8GnzF3OqS9jM6wwunFMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E3yAb/dJMcahEVw93/ek8GnzF3OqS9jM6wwunFMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E3yAb/dJMcahEVw93/ek8GnzF3OqS9jM6wwunFMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE3yAb%2FdJMcahEVw93%2Fek8GnzF3OqS9jM6wwunFMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1816&quot; height=&quot;903&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 3&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1790&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VHJwA/dJMcabY6u4E/OmuLWKz7Vi9HCrScg56rx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VHJwA/dJMcabY6u4E/OmuLWKz7Vi9HCrScg56rx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VHJwA/dJMcabY6u4E/OmuLWKz7Vi9HCrScg56rx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVHJwA%2FdJMcabY6u4E%2FOmuLWKz7Vi9HCrScg56rx1%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;1790&quot; height=&quot;728&quot; data-origin-width=&quot;1790&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 4&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1646&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTkTiy/dJMcaiKAFLR/yCx5kqkz2UrpQaGwUpU6mK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTkTiy/dJMcaiKAFLR/yCx5kqkz2UrpQaGwUpU6mK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTkTiy/dJMcaiKAFLR/yCx5kqkz2UrpQaGwUpU6mK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTkTiy%2FdJMcaiKAFLR%2FyCx5kqkz2UrpQaGwUpU6mK%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;1646&quot; height=&quot;469&quot; data-origin-width=&quot;1646&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 5&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxrFTr/dJMcadoXXpf/J7ITINEjZdrdnV1Wxg6uik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxrFTr/dJMcadoXXpf/J7ITINEjZdrdnV1Wxg6uik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxrFTr/dJMcadoXXpf/J7ITINEjZdrdnV1Wxg6uik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxrFTr%2FdJMcadoXXpf%2FJ7ITINEjZdrdnV1Wxg6uik%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;540&quot; height=&quot;338&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1243&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dM9Hnp/dJMcacqaCUT/3M854CplIUiwlbatgydP6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dM9Hnp/dJMcacqaCUT/3M854CplIUiwlbatgydP6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dM9Hnp/dJMcacqaCUT/3M854CplIUiwlbatgydP6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdM9Hnp%2FdJMcacqaCUT%2F3M854CplIUiwlbatgydP6k%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;540&quot; height=&quot;344&quot; data-origin-width=&quot;1243&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 6&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckJtLO/dJMcag0i7S2/pjOPCv2akoCy3UbhQEgJdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckJtLO/dJMcag0i7S2/pjOPCv2akoCy3UbhQEgJdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckJtLO/dJMcag0i7S2/pjOPCv2akoCy3UbhQEgJdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckJtLO%2FdJMcag0i7S2%2FpjOPCv2akoCy3UbhQEgJdK%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;540&quot; height=&quot;433&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 7&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciSivp/dJMcadP2i2d/H60k7urPJEckbtJCyiJl80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciSivp/dJMcadP2i2d/H60k7urPJEckbtJCyiJl80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciSivp/dJMcadP2i2d/H60k7urPJEckbtJCyiJl80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciSivp%2FdJMcadP2i2d%2FH60k7urPJEckbtJCyiJl80%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;540&quot; height=&quot;241&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 8&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;401&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5LYzk/dJMcahyduO3/i0BY5gwtb4YzzvJDqJmmFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5LYzk/dJMcahyduO3/i0BY5gwtb4YzzvJDqJmmFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5LYzk/dJMcahyduO3/i0BY5gwtb4YzzvJDqJmmFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5LYzk%2FdJMcahyduO3%2Fi0BY5gwtb4YzzvJDqJmmFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1288&quot; height=&quot;401&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;401&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 9&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcwlQ0/dJMcaaFUmlB/UEwVI419j6e1Szdbn3NHJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcwlQ0/dJMcaaFUmlB/UEwVI419j6e1Szdbn3NHJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcwlQ0/dJMcaaFUmlB/UEwVI419j6e1Szdbn3NHJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcwlQ0%2FdJMcaaFUmlB%2FUEwVI419j6e1Szdbn3NHJ0%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;540&quot; height=&quot;347&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 10&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1208&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/67QsT/dJMcadJflRJ/vDCSuY7DlWE87R2pGrtxeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/67QsT/dJMcadJflRJ/vDCSuY7DlWE87R2pGrtxeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/67QsT/dJMcadJflRJ/vDCSuY7DlWE87R2pGrtxeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F67QsT%2FdJMcadJflRJ%2FvDCSuY7DlWE87R2pGrtxeK%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;540&quot; height=&quot;182&quot; data-origin-width=&quot;1208&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 11&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1479&quot; data-origin-height=&quot;682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/81YkE/dJMcajv1S66/HKteGOvteLNo2MrrvfQht0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/81YkE/dJMcajv1S66/HKteGOvteLNo2MrrvfQht0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/81YkE/dJMcajv1S66/HKteGOvteLNo2MrrvfQht0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F81YkE%2FdJMcajv1S66%2FHKteGOvteLNo2MrrvfQht0%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;1479&quot; height=&quot;682&quot; data-origin-width=&quot;1479&quot; data-origin-height=&quot;682&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 12&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;603&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJlWle/dJMcafNZAb0/7qqifCAbjHN6lSzCNkWkF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJlWle/dJMcafNZAb0/7qqifCAbjHN6lSzCNkWkF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJlWle/dJMcafNZAb0/7qqifCAbjHN6lSzCNkWkF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJlWle%2FdJMcafNZAb0%2F7qqifCAbjHN6lSzCNkWkF1%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;640&quot; height=&quot;332&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;603&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 13&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ymxWy/dJMb99Usl7s/GCyZ4gwzV9ku7lPSVOjeZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ymxWy/dJMb99Usl7s/GCyZ4gwzV9ku7lPSVOjeZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ymxWy/dJMb99Usl7s/GCyZ4gwzV9ku7lPSVOjeZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FymxWy%2FdJMb99Usl7s%2FGCyZ4gwzV9ku7lPSVOjeZ0%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;640&quot; height=&quot;239&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 14&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;629&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pIKpc/dJMb99UsmiY/GCWDOvCKxm9b51KKDkKiVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pIKpc/dJMb99UsmiY/GCWDOvCKxm9b51KKDkKiVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pIKpc/dJMb99UsmiY/GCWDOvCKxm9b51KKDkKiVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpIKpc%2FdJMb99UsmiY%2FGCWDOvCKxm9b51KKDkKiVK%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;540&quot; height=&quot;427&quot; data-origin-width=&quot;629&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 15&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buMiV0/dJMcad3z42e/UIjdOqdA3KPZ70zmBrVFuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buMiV0/dJMcad3z42e/UIjdOqdA3KPZ70zmBrVFuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buMiV0/dJMcad3z42e/UIjdOqdA3KPZ70zmBrVFuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuMiV0%2FdJMcad3z42e%2FUIjdOqdA3KPZ70zmBrVFuk%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;640&quot; height=&quot;302&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 16&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1gWsi/dJMcagF9w9L/ozJn94C40AH1aNjlcvX6i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1gWsi/dJMcagF9w9L/ozJn94C40AH1aNjlcvX6i0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1gWsi/dJMcagF9w9L/ozJn94C40AH1aNjlcvX6i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1gWsi%2FdJMcagF9w9L%2FozJn94C40AH1aNjlcvX6i0%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;1368&quot; height=&quot;903&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 17&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br1a9L/dJMcafHegFL/QJ7ur5lP4ZJhDbP51v1h5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br1a9L/dJMcafHegFL/QJ7ur5lP4ZJhDbP51v1h5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br1a9L/dJMcafHegFL/QJ7ur5lP4ZJhDbP51v1h5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr1a9L%2FdJMcafHegFL%2FQJ7ur5lP4ZJhDbP51v1h5K%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;1444&quot; height=&quot;702&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 18&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1257&quot; data-origin-height=&quot;971&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AOgvZ/dJMcahZgIkD/rGxSEbhS88kk8hg8DjkNh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AOgvZ/dJMcahZgIkD/rGxSEbhS88kk8hg8DjkNh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AOgvZ/dJMcahZgIkD/rGxSEbhS88kk8hg8DjkNh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAOgvZ%2FdJMcahZgIkD%2FrGxSEbhS88kk8hg8DjkNh1%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;1257&quot; height=&quot;971&quot; data-origin-width=&quot;1257&quot; data-origin-height=&quot;971&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 19&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1759&quot; data-origin-height=&quot;1017&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgli1V/dJMcahyfeaT/2gBlCp3W6SwlIOm129SsrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgli1V/dJMcahyfeaT/2gBlCp3W6SwlIOm129SsrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgli1V/dJMcahyfeaT/2gBlCp3W6SwlIOm129SsrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdgli1V%2FdJMcahyfeaT%2F2gBlCp3W6SwlIOm129SsrK%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;1759&quot; height=&quot;1017&quot; data-origin-width=&quot;1759&quot; data-origin-height=&quot;1017&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 20&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;993&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7DIUq/dJMcad3BNYC/0wGPII04pxkbcIIrkOMuHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7DIUq/dJMcad3BNYC/0wGPII04pxkbcIIrkOMuHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7DIUq/dJMcad3BNYC/0wGPII04pxkbcIIrkOMuHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7DIUq%2FdJMcad3BNYC%2F0wGPII04pxkbcIIrkOMuHK%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;1722&quot; height=&quot;993&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;993&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 21&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1483&quot; data-origin-height=&quot;699&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bT06z1/dJMcajpaVNJ/CjTkPjeSKSfsz8RTppTzTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bT06z1/dJMcajpaVNJ/CjTkPjeSKSfsz8RTppTzTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bT06z1/dJMcajpaVNJ/CjTkPjeSKSfsz8RTppTzTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbT06z1%2FdJMcajpaVNJ%2FCjTkPjeSKSfsz8RTppTzTk%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;1483&quot; height=&quot;699&quot; data-origin-width=&quot;1483&quot; data-origin-height=&quot;699&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 22&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1575&quot; data-origin-height=&quot;1032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coOOMm/dJMb9971cWD/9lkog2QyKifFfiv1QhYMQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coOOMm/dJMb9971cWD/9lkog2QyKifFfiv1QhYMQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coOOMm/dJMb9971cWD/9lkog2QyKifFfiv1QhYMQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoOOMm%2FdJMb9971cWD%2F9lkog2QyKifFfiv1QhYMQ0%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;1575&quot; height=&quot;1032&quot; data-origin-width=&quot;1575&quot; data-origin-height=&quot;1032&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sK0w7/dJMcag0kOvP/6ZhTLj1D04giEsMt5ftBb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sK0w7/dJMcag0kOvP/6ZhTLj1D04giEsMt5ftBb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sK0w7/dJMcag0kOvP/6ZhTLj1D04giEsMt5ftBb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsK0w7%2FdJMcag0kOvP%2F6ZhTLj1D04giEsMt5ftBb1%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;1598&quot; height=&quot;389&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;389&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 23&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;447&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6kCAM/dJMcaiDUZPc/H542UNUAX4Nux9jOsRk3Ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6kCAM/dJMcaiDUZPc/H542UNUAX4Nux9jOsRk3Ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6kCAM/dJMcaiDUZPc/H542UNUAX4Nux9jOsRk3Ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6kCAM%2FdJMcaiDUZPc%2FH542UNUAX4Nux9jOsRk3Ak%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;1084&quot; height=&quot;447&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;447&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>PS/SWEA</category>
      <author>munsik22</author>
      <guid isPermaLink="true">https://munsik22.tistory.com/359</guid>
      <comments>https://munsik22.tistory.com/359#entry359comment</comments>
      <pubDate>Tue, 30 Jun 2026 20:43:10 +0900</pubDate>
    </item>
    <item>
      <title>3주차 2일차 작업일지 (0630)</title>
      <link>https://munsik22.tistory.com/358</link>
      <description>&lt;h1&gt;로컬 Docker Compose를 이용한 KRaft 3-브로커 클러스터 동작 검증&lt;/h1&gt;
&lt;h2&gt;작업 내용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;로컬 테스트용 Docker Compose 작성&lt;/strong&gt;: &lt;code&gt;docker-compose.kafka-local.yml&lt;/code&gt; 생성&lt;ul&gt;
&lt;li&gt;apache/kafka:3.7.0 이미지 사용&lt;/li&gt;
&lt;li&gt;브로커 3대(node 1, 2, 3) 구성&lt;/li&gt;
&lt;li&gt;KRaft 모드(Zookeeper-less) 설정 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;클러스터 형성 검증&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kafka-metadata-quorum.sh&lt;/code&gt;를 통해 3개 노드가 투표자(Voter)로 정상 등록됨을 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 무결성 검증&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;복제 계정(replication-factor) 3인 토픽 생성 및 메시지 발행/소비 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;가용성(High Availability) 검증&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;현재 리더 브로커를 &lt;code&gt;docker stop&lt;/code&gt;으로 강제 종료&lt;/li&gt;
&lt;li&gt;나머지 2개 브로커가 자동으로 새로운 리더를 선출함 확인&lt;/li&gt;
&lt;li&gt;리더 장애 상황에서도 데이터 소비가 중단 없이 가능함을 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;결과&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;로컬 환경에서 KRaft 3중화 설정이 올바름을 검증 완료&lt;/li&gt;
&lt;li&gt;이를 토대로 AWS EC2 3중화 작업을 진행할 수 있는 기반 마련&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;생성 데이터&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker-compose.kafka-local.yml&lt;/code&gt;: 로컬 검증용 설정 파일 (프로젝트 루트)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;AWS 환경 Kafka 3-브로커(KRaft HA) 구성&lt;/h1&gt;
&lt;h2&gt;변경 사항 세부 내용&lt;/h2&gt;
&lt;h3&gt;1. EC2 인스턴스 3대로 확장&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;파일: &lt;code&gt;ec2.tf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;기존: &lt;code&gt;count&lt;/code&gt; 선언 없이 1대만 프로비저닝하고 태그 이름이 고정적(&lt;code&gt;sanji-kafka&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;변경: &lt;code&gt;count = 3&lt;/code&gt;을 선언하여 인스턴스를 3대로 확장. 각 인스턴스의 태그를 인덱스를 사용하여 &lt;code&gt;sanji-kafka-1&lt;/code&gt;, &lt;code&gt;sanji-kafka-2&lt;/code&gt;, &lt;code&gt;sanji-kafka-3&lt;/code&gt; 형태로 변경하고, 스크립트에서 컨테이너 ID를 파악할 수 있도록 &lt;code&gt;KafkaNodeId&lt;/code&gt; 태그(1, 2, 3)를 추가 설정함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 보안 그룹(Security Group) 규칙 추가&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;파일: &lt;code&gt;security_groups.tf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;변경: 브로커 간 내부 통신용 규칙(&lt;code&gt;kafka_in_self&lt;/code&gt;) 추가&lt;ul&gt;
&lt;li&gt;프로토콜: &lt;code&gt;all (-1)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;소스: 자기 자신(kafka SG)&lt;/li&gt;
&lt;li&gt;설명: Quorum(9093) 및 데이터(9092) 통신을 위해 3대의 브로커가 서로 통신할 수 있도록 내부 방화벽을 엶&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. SSM Parameter Store 변경&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;파일: &lt;code&gt;ssm.tf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;기존: &lt;code&gt;private-ip&lt;/code&gt;를 단일 값으로 저장&lt;/li&gt;
&lt;li&gt;변경:&lt;ol&gt;
&lt;li&gt;&lt;code&gt;kafka_private_ip&lt;/code&gt;: 애플리케이션 서비스들(Application.yml의 KAFKA_BOOTSTRAP_SERVERS)이 바라볼 수 있도록 3대의 IP 주소를 쉼표로 연결(&lt;code&gt;ip1:9092,ip2:9092,ip3:9092&lt;/code&gt; 형태)하는 로직으로 변경&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kafka_quorum_voters&lt;/code&gt; 파라미터 신규 추가: KRaft 컨트롤러 투표자 구성을 위해 &lt;code&gt;1@ip1:9093,2@ip2:9093,3@ip3:9093&lt;/code&gt; 형태의 리스트 값을 만들어 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 배포용 스크립트 수정&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;파일: &lt;code&gt;scripts/deploy-kafka.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;변경: SSM을 통해 가져오는 파라미터 로드에 &lt;code&gt;KAFKA_CONTROLLER_QUORUM_VOTERS&lt;/code&gt; 추가. 또한, 여러 개의 EC2에서 공통으로 사용되는 스크립트이므로, &lt;code&gt;aws ec2 describe-tags&lt;/code&gt; 명령어를 추가해 자신의 &lt;code&gt;InstanceId&lt;/code&gt;에 해당하는 &lt;code&gt;KafkaNodeId&lt;/code&gt; 값을 읽어 &lt;code&gt;.env&lt;/code&gt;의 &lt;code&gt;KAFKA_NODE_ID&lt;/code&gt;로 기록하도록 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. Docker Compose 파일 수정&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;파일: &lt;code&gt;docker-compose.kafka.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;기존: 브로커 1대 기준으로 &lt;code&gt;KAFKA_NODE_ID: 1&lt;/code&gt;, &lt;code&gt;KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093&lt;/code&gt; 설정 사용&lt;/li&gt;
&lt;li&gt;변경: 배포 스크립트에서 자동 주입(&lt;code&gt;KAFKA_NODE_ID: ${KAFKA_NODE_ID}&lt;/code&gt;, &lt;code&gt;KAFKA_CONTROLLER_QUORUM_VOTERS: ${KAFKA_CONTROLLER_QUORUM_VOTERS}&lt;/code&gt;)받아 동작하도록 변수 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;코드 리뷰 반영&lt;/h2&gt;
&lt;h3&gt;변경 파일: &lt;code&gt;scripts/deploy-kafka.sh&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 불필요한 &lt;code&gt;MAC_MAC&lt;/code&gt; 변수 제거&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기존: IMDS에서 MAC 주소를 조회해 &lt;code&gt;MAC_MAC&lt;/code&gt; 변수에 저장&lt;/li&gt;
&lt;li&gt;변경: 스크립트 내에서 전혀 사용되지 않는 변수이므로 해당 줄 삭제. 불필요한 네트워크 요청 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 필수 변수 Fail-fast 검증 로직 추가&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기존: AWS 명령어 결과가 빈 문자열이거나 &lt;code&gt;None&lt;/code&gt;이어도 그대로 &lt;code&gt;.env&lt;/code&gt;에 기록한 뒤 컨테이너 실행&lt;/li&gt;
&lt;li&gt;변경: &lt;code&gt;.env&lt;/code&gt; 생성 전, &lt;code&gt;KAFKA_PRIVATE_IP&lt;/code&gt; / &lt;code&gt;KAFKA_NODE_ID&lt;/code&gt; / &lt;code&gt;KAFKA_CLUSTER_ID&lt;/code&gt; / &lt;code&gt;KAFKA_CONTROLLER_QUORUM_VOTERS&lt;/code&gt; 4개 변수를 순서대로 확인. 하나라도 비어 있거나 &lt;code&gt;None&lt;/code&gt;이면 에러 메시지를 출력하고 즉시 &lt;code&gt;exit 1&lt;/code&gt;로 배포 중단&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;KAFKA_NODE_ID&lt;/code&gt; Fallback 기본값 제거&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기존: &lt;code&gt;.env&lt;/code&gt; 작성 시 &lt;code&gt;KAFKA_NODE_ID=${KAFKA_NODE_ID:-1}&lt;/code&gt; 형태로 조회 실패 시 기본값 &lt;code&gt;1&lt;/code&gt; 강제 할당&lt;/li&gt;
&lt;li&gt;변경: Fail-fast 검증 통과 후에만 &lt;code&gt;.env&lt;/code&gt;를 생성하므로 기본값 표현식 불필요. &lt;code&gt;KAFKA_NODE_ID=${KAFKA_NODE_ID}&lt;/code&gt; 로 단순화. 3-브로커 환경에서 중복 Node ID로 인한 클러스터 장애 예방&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;Terraform 모듈화&lt;/h1&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;Terraform 루트에 평평하게 펼쳐진 &lt;code&gt;.tf&lt;/code&gt; 파일들을 6개 모듈로 분리했다.&lt;/p&gt;
&lt;h2&gt;만들어진 모듈 구조&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;root/modules/
  network/        VPC, 서브넷, IGW, 라우팅 테이블, 보안 그룹 전체
  edge/           ALB, 대상 그룹, HTTP/HTTPS 리스너
  data/           RDS(PostgreSQL), ElastiCache(Redis), DB 스키마 초기화
  compute-ec2/    Kafka EC2 3대, 모니터링 EC2
  ecs/            ECR, ECS 클러스터, 서비스 9개 + bid + keycloak
  observability/  CloudWatch 경보, SNS 알림&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;루트에 남은 파일&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;파일&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iam.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ECS, EC2, GitHub Actions 역할이 모두 얽혀 있어 루트에 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssm.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;compute-ec2, data 두 모듈 출력을 동시에 참조해서 루트에 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;locals.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt; 접두사 한 줄만 남음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;variables.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;변경 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;outputs.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모듈 출력을 참조하도록 전면 재작성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;main.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;provider + data sources + 모듈 6개 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;삭제된 루트 파일 (모듈로 이동)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;network.tf&lt;/code&gt;, &lt;code&gt;security_groups.tf&lt;/code&gt;, &lt;code&gt;alb.tf&lt;/code&gt;, &lt;code&gt;rds.tf&lt;/code&gt;, &lt;code&gt;elasticache.tf&lt;/code&gt;,&lt;br&gt;&lt;code&gt;ec2.tf&lt;/code&gt;, &lt;code&gt;ecs_cluster.tf&lt;/code&gt;, &lt;code&gt;ecs_services.tf&lt;/code&gt;, &lt;code&gt;ecs_bid.tf&lt;/code&gt;, &lt;code&gt;ecs_keycloak.tf&lt;/code&gt;,&lt;br&gt;&lt;code&gt;ecr.tf&lt;/code&gt;, &lt;code&gt;cloudwatch.tf&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;함께 수정한 버그&lt;/h2&gt;
&lt;p&gt;기존 코드에서 &lt;code&gt;aws_instance.kafka.private_ip&lt;/code&gt;를 단일 값으로 쓰던 부분이 Kafka 3중화(&lt;code&gt;count = 3&lt;/code&gt;) 이후 깨져 있었다. 모듈화하면서 &lt;code&gt;compute-ec2&lt;/code&gt; 모듈의 &lt;code&gt;kafka_bootstrap_servers&lt;/code&gt; output에서 3대 IP를 쉼표로 합쳐 출력하고, &lt;code&gt;ecs&lt;/code&gt; 모듈이 이 값을 &lt;code&gt;SPRING_KAFKA_BOOTSTRAPSERVERS&lt;/code&gt; 환경변수로 받도록 고쳤다.&lt;/p&gt;
&lt;h2&gt;검증 결과&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;terraform init -backend=false  # 성공
terraform validate              # Success! The configuration is valid.&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;코드 리뷰 반영&lt;/h2&gt;
&lt;h3&gt;1. &lt;code&gt;modules/data/main.tf&lt;/code&gt;: null_resource 트리거에 스크립트 해시 추가&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;db-schema-init.sh&lt;/code&gt; 파일을 수정해도 &lt;code&gt;null_resource&lt;/code&gt;가 재실행되지 않는 문제를 수정했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;triggers = {
  rds_id      = aws_db_instance.main.id
  script_hash = filemd5(&amp;quot;${path.root}/scripts/db-schema-init.sh&amp;quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. &lt;code&gt;modules/network/main.tf&lt;/code&gt;: 보안 그룹 포트 범위 최소화&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;from_port = 0, to_port = 65535&lt;/code&gt;로 과도하게 열려 있던 모니터링 인바운드 규칙 2개를 수정했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ecs_in_from_monitoring&lt;/strong&gt;: 실제 ECS 서비스 포트만 열도록 &lt;code&gt;for_each&lt;/code&gt;로 교체&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;포트&lt;/th&gt;
&lt;th&gt;서비스&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;8000&lt;/td&gt;
&lt;td&gt;gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8761&lt;/td&gt;
&lt;td&gt;Eureka&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8888&lt;/td&gt;
&lt;td&gt;Config Server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19091~19097&lt;/td&gt;
&lt;td&gt;마이크로서비스, bid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Keycloak(18080)은 &lt;code&gt;ecs-discovery.sh&lt;/code&gt;의 &lt;code&gt;SKIP&lt;/code&gt; 목록에 있어 Prometheus가 스크레이프하지 않으므로 제외했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;kafka_in_from_monitoring&lt;/strong&gt;: 실제 사용 포트만 허용 (&lt;code&gt;docker-compose.kafka.yml&lt;/code&gt; 기준)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;포트&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;9092&lt;/td&gt;
&lt;td&gt;Kafka UI 브로커 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7071&lt;/td&gt;
&lt;td&gt;JMX Exporter (Prometheus scrape)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9100&lt;/td&gt;
&lt;td&gt;Node Exporter (EC2 메트릭)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h1&gt;dev/prod 환경 분리&lt;/h1&gt;
&lt;h2&gt;새 폴더 구조&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;root/
  envs/
    dev/     (새 폴더)
    prod/    (새 폴더)
  modules/   (기존 유지)
  bootstrap/ (기존 유지)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;envs/dev/&lt;/code&gt;와 &lt;code&gt;envs/prod/&lt;/code&gt; 모두 &lt;code&gt;../../modules/&lt;/code&gt;를 공유한다. 각 폴더에서 &lt;code&gt;terraform init &amp;amp;&amp;amp; terraform apply&lt;/code&gt;를 실행하면 된다.&lt;/p&gt;
&lt;h2&gt;상태 파일 분리&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;환경&lt;/th&gt;
&lt;th&gt;S3 키&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;prod&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prod/terraform.tfstate&lt;/code&gt; (기존과 동일)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dev&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev/terraform.tfstate&lt;/code&gt; (신규)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;환경별 차이&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;dev&lt;/th&gt;
&lt;th&gt;prod&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dev&lt;/td&gt;
&lt;td&gt;prod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spring_profile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dev&lt;/td&gt;
&lt;td&gt;prod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kafka_count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kafka_instance_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;t3.micro&lt;/td&gt;
&lt;td&gt;t3.medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kafka_volume_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20 GB&lt;/td&gt;
&lt;td&gt;30 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;monitoring_instance_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;t3.micro&lt;/td&gt;
&lt;td&gt;t3.medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;monitoring_volume_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30 GB&lt;/td&gt;
&lt;td&gt;50 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bid_min_capacity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bid_max_capacity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enable_github_oidc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC CIDR&lt;/td&gt;
&lt;td&gt;10.1.0.0/16&lt;/td&gt;
&lt;td&gt;10.0.0.0/16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;함께 수정한 파일&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;modules/compute-ec2/&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;count = 3&lt;/code&gt;이 하드코딩되어 있던 Kafka EC2를 &lt;code&gt;count = var.kafka_count&lt;/code&gt;로 변경했다. 루트 변수 기본값은 3이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;modules/data/main.tf&lt;/code&gt; + &lt;code&gt;modules/data/variables.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;filemd5(&amp;quot;${path.root}/scripts/...&amp;quot;)&lt;/code&gt; 경로가 실행 위치에 따라 달라지는 문제를 수정했다. 모듈이 직접 파일을 읽는 대신, 호출하는 쪽(각 env의 main.tf)에서 해시를 계산해 &lt;code&gt;db_init_script_hash&lt;/code&gt; 변수로 넘긴다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;envs/prod/main.tf&lt;/code&gt; : &lt;code&gt;filemd5(&amp;quot;${path.root}/../../scripts/db-schema-init.sh&amp;quot;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;envs/dev/main.tf&lt;/code&gt;  : &lt;code&gt;filemd5(&amp;quot;${path.root}/../../scripts/db-schema-init.sh&amp;quot;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;루트 &lt;code&gt;main.tf&lt;/code&gt;      : &lt;code&gt;filemd5(&amp;quot;${path.root}/scripts/db-schema-init.sh&amp;quot;)&lt;/code&gt; (기존과 동일)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;bootstrap/main.tf&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SSM 파라미터를 환경별로 함께 생성하도록 수정했다. 기존에는 &lt;code&gt;/sanji/prod/...&lt;/code&gt; 경로만 하드코딩되어 있었다. 이제 &lt;code&gt;secret_keys&lt;/code&gt; 목록을 한 번 정의하고, &lt;code&gt;merge()&lt;/code&gt;로 prod와 dev 경로를 모두 만든다.&lt;/p&gt;
&lt;h3&gt;검증 결과&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;terraform validate   # 루트(.terraform/)   - Success!
terraform validate   # envs/prod/          - Success!
terraform validate   # envs/dev/           - Success!&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;IAM s3:ListBucket 권한 범위 축소&lt;/h2&gt;
&lt;p&gt;코드 리뷰에서 지적된 &lt;code&gt;ec2_ssm_read&lt;/code&gt; 정책의 &lt;code&gt;s3:ListBucket&lt;/code&gt; 권한 범위 문제를 수정했다.&lt;/p&gt;
&lt;h3&gt;문제&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;s3:ListBucket&lt;/code&gt;이 버킷 전체(&lt;code&gt;arn:aws:s3:::sanji-terraform-state&lt;/code&gt;)에 열려 있어 EC2와 GitHub Actions가 &lt;code&gt;prod/terraform.tfstate&lt;/code&gt; 등 민감한 경로까지 열람할 수 있었다.&lt;/p&gt;
&lt;h3&gt;수정&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;s3:prefix&lt;/code&gt; 조건을 추가해 &lt;code&gt;deploy/*&lt;/code&gt; 경로만 목록 조회가 가능하도록 제한했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;condition {
  test     = &amp;quot;StringLike&amp;quot;
  variable = &amp;quot;s3:prefix&amp;quot;
  values   = [&amp;quot;deploy/*&amp;quot;]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;aws s3 sync&lt;/code&gt;가 내부적으로 &lt;code&gt;ListObjects&lt;/code&gt;를 호출하므로 &lt;code&gt;s3:ListBucket&lt;/code&gt; 자체는 필요하기 때문에 조건만 추가했다.&lt;/p&gt;
&lt;h3&gt;수정 파일&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;파일&lt;/th&gt;
&lt;th&gt;수정 위치&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.terraform/iam.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ec2_ssm_read&lt;/code&gt;, &lt;code&gt;github_actions&lt;/code&gt; 정책 각 1곳&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.terraform/envs/prod/iam.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.terraform/envs/dev/iam.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;동일&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;검증 결과&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;terraform validate   # 루트(.terraform/)   - Success!
terraform validate   # envs/prod/          - Success!
terraform validate   # envs/dev/           - Success!&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;IAM / SSM 코드 중복 제거 (modules/iam, modules/ssm 추출)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;envs/prod/iam.tf&lt;/code&gt;와 &lt;code&gt;envs/dev/iam.tf&lt;/code&gt;가 100% 동일하고, &lt;code&gt;ssm.tf&lt;/code&gt;도 마찬가지였다. IAM 정책 변경 시 두 파일을 모두 수정해야 하는 문제를 해결했다.&lt;/p&gt;
&lt;h3&gt;새 모듈&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모듈&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;modules/iam/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ECS 실행/태스크 역할, EC2 인스턴스 역할, GitHub Actions OIDC 역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;modules/ssm/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SSM 파라미터 생성(db_password, kafka_private_ip, kafka_quorum_voters) + bootstrap 시크릿 읽기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;변경 사항&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;modules/iam/main.tf&lt;/code&gt;, &lt;code&gt;variables.tf&lt;/code&gt;, &lt;code&gt;outputs.tf&lt;/code&gt; 신규 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;modules/ssm/main.tf&lt;/code&gt;, &lt;code&gt;variables.tf&lt;/code&gt;, &lt;code&gt;outputs.tf&lt;/code&gt; 신규 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;envs/prod/main.tf&lt;/code&gt;, &lt;code&gt;envs/dev/main.tf&lt;/code&gt;: &lt;code&gt;module &amp;quot;iam&amp;quot;&lt;/code&gt;, &lt;code&gt;module &amp;quot;ssm&amp;quot;&lt;/code&gt; 호출 추가, 직접 리소스 참조를 모듈 output으로 교체&lt;/li&gt;
&lt;li&gt;&lt;code&gt;envs/prod/outputs.tf&lt;/code&gt;, &lt;code&gt;envs/dev/outputs.tf&lt;/code&gt;: &lt;code&gt;aws_iam_role.github_actions[0].arn&lt;/code&gt; → &lt;code&gt;module.iam.github_actions_role_arn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;envs/prod/iam.tf&lt;/code&gt;, &lt;code&gt;envs/dev/iam.tf&lt;/code&gt;, &lt;code&gt;envs/prod/ssm.tf&lt;/code&gt;, &lt;code&gt;envs/dev/ssm.tf&lt;/code&gt; 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;검증 결과&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;terraform validate   # envs/prod/   - Success!
terraform validate   # envs/dev/    - Success!&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;SSM 시크릿 목록 단일화&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;bootstrap/main.tf&lt;/code&gt;의 &lt;code&gt;secret_keys&lt;/code&gt;(14개)와 &lt;code&gt;modules/ssm/main.tf&lt;/code&gt;의 &lt;code&gt;app_secret_paths&lt;/code&gt;(9개)가 달라, ECS에서 쓸 새 시크릿을 추가할 때 두 파일을 모두 수정해야 했다.&lt;/p&gt;
&lt;h3&gt;변경 사항&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;modules/ssm/main.tf&lt;/code&gt;의 &lt;code&gt;app_secret_paths&lt;/code&gt;를 bootstrap의 &lt;code&gt;secret_keys&lt;/code&gt; 전체(14개)와 동일하게 맞췄다.&lt;/p&gt;
&lt;p&gt;추가된 5개 키:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kafka/cluster-id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;monitoring/grafana-admin-password&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;monitoring/slack-webhook-url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;langfuse/nextauth-secret&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;langfuse/salt&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 키들은 &lt;code&gt;secret_arns&lt;/code&gt; 맵에 포함되지만, ECS 태스크 정의가 명시적으로 참조하지 않으므로 컨테이너에 주입되지 않는다. 이제 새 시크릿을 추가할 때 &lt;code&gt;bootstrap/main.tf&lt;/code&gt;와 &lt;code&gt;modules/ssm/main.tf&lt;/code&gt; 두 곳을 수정하면 된다. 단, EC2 전용 시크릿은 bootstrap만 수정하면 된다.&lt;/p&gt;
&lt;h3&gt;검증 결과&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;terraform validate   # envs/prod/   - Success!
terraform validate   # envs/dev/    - Success!&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;루트 .tf 파일 삭제&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;envs/&lt;/code&gt;가 진입점이 된 이후에도 루트에 &lt;code&gt;main.tf&lt;/code&gt;, &lt;code&gt;iam.tf&lt;/code&gt; 등이 남아 있어 실수로 루트에서 &lt;code&gt;terraform apply&lt;/code&gt;를 실행하면 state가 꼬일 위험이 있었다.&lt;/p&gt;
&lt;p&gt;루트 &lt;code&gt;versions.tf&lt;/code&gt;의 backend key가 &lt;code&gt;prod/terraform.tfstate&lt;/code&gt;로 &lt;code&gt;envs/prod/&lt;/code&gt;와 동일하므로, 삭제해도 &lt;code&gt;envs/prod/&lt;/code&gt;가 같은 state를 그대로 이어받는다.&lt;/p&gt;
&lt;h3&gt;삭제 파일&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;main.tf&lt;/code&gt;, &lt;code&gt;variables.tf&lt;/code&gt;, &lt;code&gt;iam.tf&lt;/code&gt;, &lt;code&gt;ssm.tf&lt;/code&gt;, &lt;code&gt;locals.tf&lt;/code&gt;, &lt;code&gt;outputs.tf&lt;/code&gt;, &lt;code&gt;versions.tf&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;루트에는 이제 &lt;code&gt;bootstrap/&lt;/code&gt;, &lt;code&gt;scripts/&lt;/code&gt;, &lt;code&gt;docs/&lt;/code&gt;, &lt;code&gt;envs/&lt;/code&gt;, &lt;code&gt;modules/&lt;/code&gt;만 남는다.&lt;/p&gt;</description>
      <category>내일배움캠프/최종 프로젝트 개발일지</category>
      <author>munsik22</author>
      <guid isPermaLink="true">https://munsik22.tistory.com/358</guid>
      <comments>https://munsik22.tistory.com/358#entry358comment</comments>
      <pubDate>Tue, 30 Jun 2026 15:04:47 +0900</pubDate>
    </item>
    <item>
      <title>Flyway로 DB 스키마 버전 관리하기</title>
      <link>https://munsik22.tistory.com/357</link>
      <description>&lt;div&gt;
&lt;style&gt;
  #article-view div[data-ke-type='moreLess'] {
  	background-color: transparent;
    border: none;
    padding: 0px;
  }
  .btn-toggle-moreless {
  	color: #000000DE !important;
  }
  .moreless-content {
  	padding: 12px;
  }
  .h3-title {
  	background-color: #e5f2fc;
    border-radius: 0.3rem;
    padding: 0px 4px;
  }
  code:not([class*=&quot;language-&quot;]) {
    font-family: &quot;SFMono-Regular&quot;, Menlo, Consolas, &quot;PT Mono&quot;, &quot;Liberation Mono&quot;, Courier, monospace;
    line-height: normal;
    background: rgba(135, 131, 120, .15);
    color: #EB5757;
    border-radius: 4px;
    font-size: 85%;
    padding: 0.2em 0.4em;
    position: relative;
    bottom: 0.065em;
  }
  pre code {
    border: none !important;
    background: #1e1e1e !important;
    color: #d4d4d4 !important;
  }
&lt;/style&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;tfile.svg&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;970&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mdAYc/dJMcagMSb54/XlLVw2cQD2VFIEMiiyoCjK/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mdAYc/dJMcagMSb54/XlLVw2cQD2VFIEMiiyoCjK/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mdAYc/dJMcagMSb54/XlLVw2cQD2VFIEMiiyoCjK/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmdAYc%2FdJMcagMSb54%2FXlLVw2cQD2VFIEMiiyoCjK%2Ftfile.svg&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;240&quot; height=&quot;228&quot; data-filename=&quot;tfile.svg&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;970&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 class=&quot;h3-title&quot; data-ke-size=&quot;size23&quot;&gt;  Flyway란?&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Flyway&lt;/b&gt;는 오픈소스 데이터베이스 마이그레이션 도구로, 일종의 데이터베이스를 위한 Git(버전 관리 시스템)이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 Git으로 버전을 관리한다. &quot;이 파일 언제 어떻게 바뀌었는지&quot; 기록이 남고, 과거 버전으로 돌아갈 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 DB 테이블 구조(스키마)는 어떨까? 보통은 직접 DB에 접속해서 &lt;code&gt;ALTER TABLE&lt;/code&gt;을 치거나, JPA의 &lt;code&gt;ddl-auto: update&lt;/code&gt;가 알아서 바꾸도록 내버려둔다. 이 방법은 &quot;지금 DB가 어떤 상태인지&quot; 기록이 없고, 팀원마다 DB 상태가 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway는 이 문제를 해결한다. SQL 파일에 &quot;V1 초기 테이블 생성&quot;, &quot;V2 컬럼 추가&quot; 같은 변경 이력을 순서대로 적어두면, Flyway가 아직 적용 안 된 것만 골라서 DB에 실행해준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ddl-auto와 뭐가 다른가요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot JPA의 &lt;code&gt;ddl-auto&lt;/code&gt;는 아래와 같은 선택지를 제공한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 값 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 동작 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;앱 시작 때마다 테이블을 새로 만듦 (기존 데이터 삭제됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;엔티티 코드를 보고 부족한 컬럼만 추가 (삭제는 안 함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;validate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;엔티티 코드와 DB 스키마가 일치하는지만 확인, 변경 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;none&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;아무것도 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;update&lt;/code&gt;는 편하지만 다음과 같은 문제가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컬럼 삭제는 알아서 안 해준다. 코드에서 필드를 지워도 DB에는 남는다.&lt;/li&gt;
&lt;li&gt;&quot;이 테이블이 언제, 왜 바뀌었는지&quot; 기록이 없다.&lt;/li&gt;
&lt;li&gt;운영 DB에서 쓰면 예상치 못한 스키마 변경이 일어날 수 있어 위험하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway를 쓰면 스키마 변경 내역이 SQL 파일로 남고, 어느 환경에서 앱을 띄워도 같은 순서로 마이그레이션이 적용되어 DB 상태가 일정하다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;어떻게 작동하나요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;flyway_schema_history 테이블&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway는 처음 실행될 때 &lt;code&gt;flyway_schema_history&lt;/code&gt;라는 테이블을 만든다. 이 테이블이 &quot;어떤 마이그레이션 파일까지 적용했는지&quot; 기록하는 장부다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;version | description  | installed_on        | success
--------|--------------|---------------------|--------
1       | init         | 2025-01-01 00:00:00 | true
2       | add column   | 2025-02-01 00:00:00 | true&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;앱이 시작될 때마다 Flyway는 이 장부를 보고 아직 실행 안 된 파일만 골라서 순서대로 실행합니다. 이미 실행된 파일은 건너뛴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;마이그레이션 파일 이름 규칙&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 이름이 Flyway에게 &quot;어떤 버전인지, 어떤 종류인지&quot;를 알려준다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;V{버전}__{설명}.sql&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 class=&quot;pgsql&quot;&gt;&lt;code&gt;V1__init.sql          -- 1번 버전, 초기 테이블 생성
V2__add_index.sql     -- 2번 버전, 인덱스 추가
V3__alter_column.sql  -- 3번 버전, 컬럼 변경&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;V&lt;/code&gt;는 버전 마이그레이션(한 번만 실행)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_&lt;/code&gt; 두 개임에 주의 (&lt;code&gt;__&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;버전 번호는 숫자뿐 아니라 &lt;code&gt;V1.1&lt;/code&gt;, &lt;code&gt;V1.2&lt;/code&gt;처럼 소수점도 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;실행 순서&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;앱 시작
  -&amp;gt; Flyway 활성화
  -&amp;gt; flyway_schema_history 확인
  -&amp;gt; 아직 실행 안 된 V*.sql 파일 탐색
  -&amp;gt; 버전 순서대로 실행
  -&amp;gt; 완료 후 장부에 기록
  -&amp;gt; JPA ddl-auto: validate 실행 (엔티티 코드와 DB 일치 확인)
  -&amp;gt; 앱 정상 기동&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;h3-title&quot; data-ke-size=&quot;size23&quot;&gt;  Spring Boot에서 사용하는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 의존성 추가 (build.gradle)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway 10.x부터는 PostgreSQL 지원 모듈이 별도로 분리되어 있기 때문에 두 개 모두 추가해야 한다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.flywaydb:flyway-core'
    implementation 'org.flywaydb:flyway-database-postgresql'
}&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;MySQL을 쓴다면 &lt;code&gt;flyway-database-postgresql&lt;/code&gt; 대신 &lt;code&gt;flyway-mysql&lt;/code&gt;을 추가한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. application.yml 설정&lt;/h4&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: myuser
    password: mypassword

  jpa:
    hibernate:
      ddl-auto: validate  # Flyway가 스키마를 만들어주므로 validate만 사용

  flyway:
    enabled: true
    schemas: my_schema        # 마이그레이션할 스키마
    create-schemas: true      # 스키마가 없으면 자동으로 만들어줌
    locations: classpath:db/migration  # SQL 파일 위치
    baseline-on-migrate: true  # 이미 데이터가 있는 DB에 처음 Flyway를 도입할 때 필요&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. SQL 마이그레이션 파일 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;src/main/resources/db/migration/V1__init.sql&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE IF NOT EXISTS p_users
(
    id         UUID         NOT NULL,
    username   VARCHAR(50)  NOT NULL,
    email      VARCHAR(255) NOT NULL,
    created_at TIMESTAMP    NOT NULL,
    PRIMARY KEY (id),
    CONSTRAINT uq_users_email UNIQUE (email)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일 하나가 &quot;V1 마이그레이션&quot;이 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 설정 옵션 정리&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 설정 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 기본값 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 설명 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;Flyway 활성화 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;schemas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(없음)&lt;/td&gt;
&lt;td&gt;마이그레이션할 스키마 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create-schemas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;스키마가 없으면 자동 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;locations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;classpath:db/migration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SQL 파일 위치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;baseline-on-migrate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;기존 DB에 Flyway 처음 도입 시 true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;out-of-order&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;순서가 뒤섞인 마이그레이션 허용 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;validate-on-migrate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;실행 전 체크섬 검증 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;baseline-on-migrate가 뭔가요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 테이블이 있는 DB에 처음으로 Flyway를 도입할 때 필요한 옵션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway는 기본적으로 &quot;스키마는 비어 있어야 내가 V1부터 만들 수 있다&quot;고 생각한다. 그런데 이미 운영 중인 DB에는 테이블이 잔뜩 있습니다. 이 상태에서 Flyway를 켜면 아래 오류가 난다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Found non-empty schema(s) &quot;my_schema&quot; but no schema history table.
Use baseline() or set baselineOnMigrate to true...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;baseline-on-migrate: true&lt;/code&gt;를 설정하면 Flyway가 &quot;현재 DB 상태를 V1 베이스라인으로 등록하고, 그 이후 버전부터만 실행할게&quot;라고 처리한다. 기존 테이블에는 손대지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단, 처음 도입 이후에는 true로 두어도 문제 없다.&lt;/b&gt; 베이스라인은 &lt;code&gt;flyway_schema_history&lt;/code&gt;가 없을 때만 동작한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;로컬 개발 환경에서 H2 쓰는 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;H2 인메모리 DB를 쓰는 로컬 환경에서는 Flyway를 끄는 것이 편하다. H2는 PostgreSQL SQL 문법과 달라서 마이그레이션 파일이 실행 오류를 낼 수 있다.&lt;/p&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;code&gt;application-local.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;spring:
  flyway:
    enabled: false
  jpa:
    hibernate:
      ddl-auto: create-drop  # 로컬에서는 앱 시작/종료 때 테이블 자동 관리&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마이그레이션 파일 수정하면 안 되는 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 DB에 적용된 파일(flyway_schema_history에 기록된 것)을 수정하면 Flyway가 체크섬 오류를 낸다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Validate failed: Migration checksum mismatch for migration version 1&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;Flyway는 파일이 처음 적용될 때 내용의 해시값(체크섬)을 저장해두고, 다음 실행 때 파일이 변경됐는지 확인한다. &lt;b&gt;이미 배포된 마이그레이션 파일은 절대 수정하지 말고, 새 버전 파일(V2, V3...)을 추가해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 class=&quot;h3-title&quot; data-ke-size=&quot;size23&quot;&gt;  정리&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 항목 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 내용 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flyway 역할&lt;/td&gt;
&lt;td&gt;SQL 파일을 버전 순서대로 DB에 자동 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;핵심 테이블&lt;/td&gt;
&lt;td&gt;&lt;code&gt;flyway_schema_history&lt;/code&gt; (적용 이력 장부)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일 이름 형식&lt;/td&gt;
&lt;td&gt;&lt;code&gt;V{버전}__{설명}.sql&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ddl-auto 조합&lt;/td&gt;
&lt;td&gt;Flyway 쓸 때는 &lt;code&gt;validate&lt;/code&gt; 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이미 있는 DB 도입&lt;/td&gt;
&lt;td&gt;&lt;code&gt;baseline-on-migrate: true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로컬 H2 환경&lt;/td&gt;
&lt;td&gt;&lt;code&gt;flyway.enabled: false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>내일배움캠프/내배캠 개발일지</category>
      <category>내일배움캠프</category>
      <author>munsik22</author>
      <guid isPermaLink="true">https://munsik22.tistory.com/357</guid>
      <comments>https://munsik22.tistory.com/357#entry357comment</comments>
      <pubDate>Mon, 29 Jun 2026 14:36:09 +0900</pubDate>
    </item>
    <item>
      <title>3주차 1일차 작업일지 (0629)</title>
      <link>https://munsik22.tistory.com/356</link>
      <description>&lt;h1&gt;SHA 이미지 태그 + Task Definition 방식 전환 작업 로그&lt;/h1&gt;
&lt;h2&gt;변경 파일 목록&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;파일&lt;/th&gt;
&lt;th&gt;변경 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.github/workflows/deploy-ecs.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;빌드: SHA + latest 두 태그 push / 배포: task def 리비전 교체 방식으로 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.terraform/ecs_services.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws_ecs_service.app&lt;/code&gt; lifecycle에 &lt;code&gt;task_definition&lt;/code&gt; ignore_changes 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.terraform/ecs_bid.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws_ecs_service.bid&lt;/code&gt; lifecycle에 &lt;code&gt;task_definition&lt;/code&gt; ignore_changes 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.terraform/variables.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;container_image_tag&lt;/code&gt; 설명 업데이트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docs/INFRASTRUCTURE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;[7] 이미지 태그 전략 절을 &amp;quot;SHA 태그 채택&amp;quot;으로 업데이트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;변경 상세&lt;/h2&gt;
&lt;h3&gt;deploy-ecs.yml: 빌드 잡&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;전: docker build ... -t &amp;quot;${IMAGE}:latest&amp;quot;
    docker push &amp;quot;${IMAGE}:latest&amp;quot;

후: IMAGE_TAG=&amp;quot;${GITHUB_SHA::7}&amp;quot;
    docker build ... -t &amp;quot;${IMAGE}:${IMAGE_TAG}&amp;quot; -t &amp;quot;${IMAGE}:latest&amp;quot;
    docker push &amp;quot;${IMAGE}:${IMAGE_TAG}&amp;quot;
    docker push &amp;quot;${IMAGE}:latest&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;deploy-ecs.yml: 각 wave 배포 잡 (wave-1~4 공통)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;전: aws ecs update-service --force-new-deployment

후: 1. describe-services → 현재 task def ARN 조회
    2. describe-task-definition → task def JSON 가져오기
    3. jq로 이미지 태그를 SHA로 교체 + 등록 불가 필드(taskDefinitionArn 등) 제거
    4. register-task-definition → 새 리비전 등록
    5. update-service --task-definition &amp;lt;새 리비전&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;wave-4(ai-service)는 함수 없이 같은 흐름을 직접 작성.&lt;/p&gt;
&lt;h3&gt;Terraform: ECS 서비스 lifecycle&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ecs_services.tf&lt;/code&gt;와 &lt;code&gt;ecs_bid.tf&lt;/code&gt; 두 파일에 동일하게 적용:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;전: ignore_changes = [desired_count]
후: ignore_changes = [desired_count, task_definition]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;CI/CD가 task def 리비전을 직접 교체하므로 Terraform이 이전 리비전으로 되돌리지 않도록 막는 것이 목적.&lt;/p&gt;
&lt;h2&gt;주의 사항&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Keycloak(&lt;code&gt;ecs_keycloak.tf&lt;/code&gt;)은 공개 이미지(quay.io)를 쓰고 CI/CD 빌드 대상이 아니라 변경하지 않음.&lt;/li&gt;
&lt;li&gt;wave 구조(vCPU 한도 대응)는 그대로 유지됨. 배포 명령만 교체.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;container_image_tag&lt;/code&gt; Terraform 변수는 Terraform 최초 apply 시에만 쓰이고, 이후 운영에서는 CI/CD가 SHA 리비전으로 덮어씀.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;wave deploy 중복 제거 (reusable workflow 추출)&lt;/h2&gt;
&lt;h3&gt;변경 파일&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;파일&lt;/th&gt;
&lt;th&gt;변경 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.github/workflows/deploy-ecs.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wave 잡: 재사용 가능 워크플로 호출로 교체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.github/workflows/_deploy-wave.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(신규) 재사용 가능 워크플로: SHA 태그 기반 배포 로직 통합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;변경 상세&lt;/h3&gt;
&lt;p&gt;wave마다 반복되던 deploy 로직(50줄 × 4회)을 &lt;code&gt;_deploy-wave.yml&lt;/code&gt;로 추출.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;전: runs-on + Configure AWS credentials + Deploy 스텝 (함수 50줄 × 4회 반복)

후: uses: ./.github/workflows/_deploy-wave.yml
    with:
      services: &amp;#39;서비스 목록&amp;#39;
      max_attempts: 120  # wave-4만&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vars.AWS_ROLE_ARN&lt;/code&gt;은 같은 레포의 재사용 가능 워크플로에서 자동으로 사용 가능(별도 전달 불필요).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;Flyway DB 마이그레이션 작업 로그&lt;/h1&gt;
&lt;h2&gt;작업 개요&lt;/h2&gt;
&lt;p&gt;기존 JPA &lt;code&gt;ddl-auto: update&lt;/code&gt; 방식을 Flyway 기반 마이그레이션으로 전환&lt;/p&gt;
&lt;h2&gt;변경 사항&lt;/h2&gt;
&lt;h3&gt;의존성 추가 (각 서비스 build.gradle)&lt;/h3&gt;
&lt;p&gt;Flyway 10.x는 PostgreSQL 지원 모듈이 분리되어 있어 두 라이브러리를 모두 추가해야 함.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;services/user-service/build.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;services/auction-service/build.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;services/order-service/build.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;services/payment-service/build.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;services/notification-service/build.gradle&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;추가 내용:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-groovy&quot;&gt;implementation &amp;#39;org.flywaydb:flyway-core&amp;#39;
implementation &amp;#39;org.flywaydb:flyway-database-postgresql&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;SQL 마이그레이션 파일 생성&lt;/h3&gt;
&lt;p&gt;각 서비스의 &lt;code&gt;src/main/resources/db/migration/V1__init.sql&lt;/code&gt; 신규 생성&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;서비스&lt;/th&gt;
&lt;th&gt;스키마&lt;/th&gt;
&lt;th&gt;생성 테이블&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;user-service&lt;/td&gt;
&lt;td&gt;user_schema&lt;/td&gt;
&lt;td&gt;p_users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;auction-service&lt;/td&gt;
&lt;td&gt;auction_schema&lt;/td&gt;
&lt;td&gt;p_products, p_auctions, p_auction_outbox, shedlock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;order-service&lt;/td&gt;
&lt;td&gt;order_schema&lt;/td&gt;
&lt;td&gt;p_orders, p_order_outbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;payment-service&lt;/td&gt;
&lt;td&gt;payment_schema&lt;/td&gt;
&lt;td&gt;p_payments, p_payment_history, p_outbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;notification-service&lt;/td&gt;
&lt;td&gt;notification_schema&lt;/td&gt;
&lt;td&gt;p_notification_logs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;설정 파일 변경&lt;/h3&gt;
&lt;h4&gt;ddl-auto: update → validate 변경&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;파일&lt;/th&gt;
&lt;th&gt;변경 전&lt;/th&gt;
&lt;th&gt;변경 후&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;user-service/application.yml&lt;/td&gt;
&lt;td&gt;update&lt;/td&gt;
&lt;td&gt;validate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;order-service/application.yml&lt;/td&gt;
&lt;td&gt;update&lt;/td&gt;
&lt;td&gt;validate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;payment-service/application.yml&lt;/td&gt;
&lt;td&gt;update&lt;/td&gt;
&lt;td&gt;validate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;notification-service/application.yml&lt;/td&gt;
&lt;td&gt;create&lt;/td&gt;
&lt;td&gt;validate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;config-server/config/auction-service.yml&lt;/td&gt;
&lt;td&gt;update&lt;/td&gt;
&lt;td&gt;validate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h4&gt;Flyway 설정 추가 (각 application.yml)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  flyway:
    schemas: &amp;lt;서비스별_스키마명&amp;gt;
    create-schemas: true
    locations: classpath:db/migration&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;auction-service 로컬 프로파일 처리&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;application-local.yml&lt;/code&gt;은 H2 인메모리 DB를 사용하므로 Flyway 비활성화:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  flyway:
    enabled: false&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;추가 작업: ai-service Flyway 적용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;services/ai-service/build.gradle&lt;/code&gt; - flyway-core, flyway-database-postgresql 의존성 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;services/ai-service/src/main/resources/db/migration/V1__init.sql&lt;/code&gt; 생성 (p_chat_sessions, p_chat_messages)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;services/ai-service/src/main/resources/application.yml&lt;/code&gt; - ddl-auto: create -&amp;gt; validate, Flyway 설정 추가 (ai_schema)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config-server/src/main/resources/config/ai-service.yml&lt;/code&gt; - 동일하게 반영&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;참고: &lt;code&gt;vector_store&lt;/code&gt; 테이블은 Spring AI pgvector의 &lt;code&gt;initialize-schema: true&lt;/code&gt;가 자체 관리하므로 Flyway 대상에서 제외.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;keycloak, langfuse는 외부 서비스로 자체 스키마 관리 → 적용 불필요&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;추가 작업: db-init.sh / init-schemas.sql 정리&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;db/init/init-schemas.sql&lt;/code&gt;이 기존에 모든 스키마와 shedlock 테이블을 수동으로 만들던 역할을 하고 있었음.&lt;/p&gt;
&lt;p&gt;Flyway 도입 후 역할 분리:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;대상&lt;/th&gt;
&lt;th&gt;기존 방식&lt;/th&gt;
&lt;th&gt;변경 후&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;6개 서비스 스키마 생성&lt;/td&gt;
&lt;td&gt;init-schemas.sql&lt;/td&gt;
&lt;td&gt;Flyway &lt;code&gt;create-schemas: true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;각 서비스 테이블 생성&lt;/td&gt;
&lt;td&gt;없음 (ddl-auto: update)&lt;/td&gt;
&lt;td&gt;Flyway V1__init.sql&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shedlock 테이블&lt;/td&gt;
&lt;td&gt;init-schemas.sql&lt;/td&gt;
&lt;td&gt;auction-service V1__init.sql&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keycloak_schema&lt;/td&gt;
&lt;td&gt;init-schemas.sql&lt;/td&gt;
&lt;td&gt;init-schemas.sql 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;langfuse_schema&lt;/td&gt;
&lt;td&gt;init-schemas.sql&lt;/td&gt;
&lt;td&gt;init-schemas.sql 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;db/init/init-schemas.sql&lt;/code&gt; 에서 Spring 서비스 스키마 및 shedlock 항목 제거, keycloak/langfuse 스키마만 남김.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;db-init.sh&lt;/code&gt;는 Keycloak, Langfuse가 시작하기 전에 스키마를 미리 만들어야 하므로 계속 사용.&lt;/p&gt;
&lt;h2&gt;추가 작업: db-init.sh 수동 개입 완전 제거&lt;/h2&gt;
&lt;p&gt;RDS가 private subnet에 있어 Terraform PostgreSQL 프로바이더 직접 연결 불가. 기존 SSM 방식을 Terraform &lt;code&gt;null_resource&lt;/code&gt;로 통합.&lt;/p&gt;
&lt;h3&gt;변경 파일&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.terraform/versions.tf&lt;/code&gt; - &lt;code&gt;null&lt;/code&gt; 프로바이더 추가 (&lt;code&gt;hashicorp/null ~&amp;gt; 3.0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.terraform/rds.tf&lt;/code&gt; - &lt;code&gt;null_resource.db_schema_init&lt;/code&gt; 추가&lt;ul&gt;
&lt;li&gt;&lt;code&gt;depends_on&lt;/code&gt;: RDS + 모니터링 EC2 + SSM 비밀번호 파라미터&lt;/li&gt;
&lt;li&gt;&lt;code&gt;triggers&lt;/code&gt;: RDS ID 변경 시에만 재실행 (매 apply마다 실행 방지)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;local-exec&lt;/code&gt;: env vars로 값 주입 후 &lt;code&gt;db-schema-init.sh&lt;/code&gt; 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.terraform/scripts/db-schema-init.sh&lt;/code&gt; 신규 생성 - Terraform 자동 호출용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.terraform/scripts/db-init.sh&lt;/code&gt; - 비상 수동 복구용임을 주석으로 명시&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;동작 흐름&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;terraform apply
  → aws_db_instance.main 생성
  → aws_instance.monitoring 생성
  → null_resource.db_schema_init 실행
      → SSM Agent 준비 대기
      → 모니터링 EC2에 SSM Run Command 전송
          → EC2에서 SSM으로 DB 비밀번호 조회
          → psql로 keycloak_schema / langfuse_schema 생성
      → 완료 확인&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이후 &lt;code&gt;terraform apply&lt;/code&gt;에서는 RDS ID가 바뀌지 않는 한 재실행 안 됨.&lt;/p&gt;
&lt;h4&gt;null_resource란?&lt;/h4&gt;
&lt;p&gt;Terraform 리소스인데 실제 인프라를 만들지 않는 빈 껍데기 리소스. 리소스 생성 없이 셸 명령만 실행하고 싶을 때 사용.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;일반 리소스: &lt;code&gt;terraform apply&lt;/code&gt; 시 EC2, RDS 등을 실제로 AWS에 생성&lt;/li&gt;
&lt;li&gt;null_resource: 아무것도 만들지 않고 &lt;code&gt;local-exec&lt;/code&gt; 프로비저너로 명령(스크립트)만 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;triggers&lt;/code&gt;에 적은 값이 바뀔 때만 재실행됨. RDS ID를 트리거로 걸었으므로 RDS가 새로 만들어질 때만 스키마 초기화 스크립트가 실행되고, 이후 apply에서는 건너뜀.&lt;/p&gt;
&lt;h2&gt;트러블슈팅: 컨테이너 재시작 루프&lt;/h2&gt;
&lt;h3&gt;원인&lt;/h3&gt;
&lt;p&gt;기존 &lt;code&gt;ddl-auto: update&lt;/code&gt;로 이미 테이블이 만들어진 DB에 Flyway를 처음 도입하면, Flyway가 &lt;code&gt;flyway_schema_history&lt;/code&gt; 테이블 없이 스키마가 비어있지 않다는 것을 감지하고 오류 발생&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Found non-empty schema(s) &amp;quot;user_schema&amp;quot; but no schema history table.
Use baseline() or set baselineOnMigrate to true to initialize the schema history table.&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;해결&lt;/h3&gt;
&lt;p&gt;모든 서비스 Flyway 설정에 &lt;code&gt;baseline-on-migrate: true&lt;/code&gt; 추가.&lt;/p&gt;
&lt;p&gt;기존 스키마를 version 1 베이스라인으로 등록하고 이후 마이그레이션만 실행하는 방식으로 전환.&lt;/p&gt;
&lt;p&gt;변경 파일:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;services/user-service/src/main/resources/application.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;services/order-service/src/main/resources/application.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;services/payment-service/src/main/resources/application.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;services/notification-service/src/main/resources/application.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;services/ai-service/src/main/resources/application.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config-server/src/main/resources/config/auction-service.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config-server/src/main/resources/config/ai-service.yml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;주의사항&lt;/h3&gt;
&lt;p&gt;모든 서비스 Dockerfile이 빌드된 JAR을 복사하는 방식(&lt;code&gt;COPY build/libs/*.jar&lt;/code&gt;)이라, yml 변경 후 반드시 아래 순서로 진행해야 함.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Gradle 재빌드 (&lt;code&gt;./gradlew bootJar&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Docker 이미지 재빌드 (&lt;code&gt;docker compose build&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;컨테이너 재시작 (&lt;code&gt;docker compose up -d&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;auction-service는 Flyway 설정을 config-server에서 받으므로, &lt;code&gt;config-server/config/auction-service.yml&lt;/code&gt; 변경 시 &lt;strong&gt;config-server JAR도 함께 재빌드&lt;/strong&gt; 후 config-server를 먼저 재시작해야 auction-service에 반영됨.&lt;/p&gt;
&lt;h2&gt;코드리뷰 반영: V1__init.sql 수정&lt;/h2&gt;
&lt;h3&gt;auction-service&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;p_products.quantity VARCHAR(255)&lt;/code&gt; → &lt;code&gt;INTEGER&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p_auctions.start_price / final_price / bid_unit INTEGER&lt;/code&gt; → &lt;code&gt;BIGINT&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;order-service&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;p_orders.user_name VARCHAR(10)&lt;/code&gt; → &lt;code&gt;VARCHAR(50)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p_orders.amount INTEGER&lt;/code&gt; → &lt;code&gt;BIGINT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p_order_outbox&lt;/code&gt; 인덱스 추가: &lt;code&gt;(status, created_at)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;payment-service&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;p_payments.amount / original_amount / cancel_amount INTEGER&lt;/code&gt; → &lt;code&gt;BIGINT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p_payment_history.amount INTEGER&lt;/code&gt; → &lt;code&gt;BIGINT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p_payments.card_number&lt;/code&gt;: 마스킹된 번호만 저장한다는 주석 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p_outbox&lt;/code&gt; 인덱스 추가: &lt;code&gt;(status, created_at)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;ai-service&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;p_chat_messages&lt;/code&gt; 인덱스 추가: &lt;code&gt;(session_id)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;TABLES.md 검증 및 롤백&lt;/h2&gt;
&lt;p&gt;TABLES.md가 엔티티 코드 기반 명세서이므로 코드리뷰 반영 내용 일부를 롤백.&lt;/p&gt;
&lt;h3&gt;롤백 항목 (엔티티 코드와 불일치)&lt;/h3&gt;
&lt;p&gt;엔티티가 &lt;code&gt;int&lt;/code&gt; 타입으로 선언되어 있어 DB를 BIGINT로 변경하면 &lt;code&gt;ddl-auto: validate&lt;/code&gt; 단계에서 기동 실패.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;auction-service: &lt;code&gt;start_price / final_price / bid_unit&lt;/code&gt; BIGINT → INTEGER 롤백&lt;/li&gt;
&lt;li&gt;order-service: &lt;code&gt;user_name&lt;/code&gt; VARCHAR(50) → VARCHAR(10) 롤백, &lt;code&gt;amount&lt;/code&gt; BIGINT → INTEGER 롤백&lt;/li&gt;
&lt;li&gt;payment-service: &lt;code&gt;amount / original_amount / cancel_amount&lt;/code&gt; BIGINT → INTEGER 롤백, &lt;code&gt;p_payment_history.amount&lt;/code&gt; BIGINT → INTEGER 롤백&lt;/li&gt;
&lt;li&gt;TABLES.md: 위 항목 모두 원복&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;유지 항목 (엔티티와 무관한 추가)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;p_products.quantity&lt;/code&gt; VARCHAR(255) 유지 (TABLES.md: &amp;quot;100kg&amp;quot; 형식 문자열, INTEGER 변경은 잘못)&lt;/li&gt;
&lt;li&gt;아웃박스 인덱스 3개 유지 (order, payment, ai-service)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p_payments.card_number&lt;/code&gt; 주석 유지&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내일배움캠프/최종 프로젝트 개발일지</category>
      <author>munsik22</author>
      <guid isPermaLink="true">https://munsik22.tistory.com/356</guid>
      <comments>https://munsik22.tistory.com/356#entry356comment</comments>
      <pubDate>Mon, 29 Jun 2026 14:19:10 +0900</pubDate>
    </item>
    <item>
      <title>2주차 2~5일차 작업일지 (0623 ~ 0626)</title>
      <link>https://munsik22.tistory.com/355</link>
      <description>&lt;h2&gt;6월 23일&lt;/h2&gt;
&lt;h3&gt;AWS 인프라 전체 최초 구축&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; 서비스를 올릴 AWS 인프라가 전혀 없었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; Terraform으로 VPC, ALB, RDS(PostgreSQL 16.9), Redis(ElastiCache), ECS 클러스터, ECR, IAM, SSM Parameter Store, EC2(Kafka, 모니터링) 등 총 155개 리소스를 생성했다. RDS 버전 16.4가 ap-northeast-2 리전에서 미지원이어서 16.9로 교체 후 재시도했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; AWS 인프라 전체 구축 완료. SSM Run Command로 RDS 스키마 9개와 테이블을 초기화했고, EC2 Docker 설치 및 SSM Agent 연결, ECS 서비스 11개 배포까지 마쳤다.&lt;/p&gt;
&lt;h3&gt;Terraform EC2 AMI 교체 방지&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; &lt;code&gt;most_recent = true&lt;/code&gt;로 AMI를 조회하도록 되어 있어, 아마존이 새 AMI를 배포할 때마다 다음 &lt;code&gt;terraform apply&lt;/code&gt; 시 Kafka EC2와 모니터링 EC2가 통째로 교체될 수 있었다. 두 서버는 디스크에 데이터를 보관하는 서버라 교체 시 데이터가 날아간다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; 두 EC2 리소스에 &lt;code&gt;lifecycle { ignore_changes = [ami] }&lt;/code&gt;를 추가했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; AMI가 바뀌어도 Terraform이 기존 서버를 그대로 유지한다. 의도적으로 업그레이드할 때만 이 블록을 제거하고 apply하면 된다.&lt;/p&gt;
&lt;h3&gt;config-server 프로파일 설정 오류 수정&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; ECS에서 &lt;code&gt;SPRING_PROFILES_ACTIVE=prod&lt;/code&gt;로 컨테이너를 실행하면 config-server &lt;code&gt;application.yml&lt;/code&gt;의 &lt;code&gt;spring.profiles.active: native&lt;/code&gt; 설정이 덮어써진다. native 프로파일이 비활성화되면 Spring Cloud Config Server가 Git 백엔드로 폴백하고 URI가 없어서 크래시가 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; Terraform ECS 태스크 정의에서 config-server에만 &lt;code&gt;SPRING_PROFILES_ACTIVE=prod,native&lt;/code&gt;를 환경변수로 주입했다. (&lt;code&gt;spring.profiles.include&lt;/code&gt;는 Spring Boot 2.4부터 profile-specific 파일에서 사용 불가라 앱 코드 수정 대신 이 방법을 택했다.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; config-server가 prod + native 프로파일로 정상 기동. classpath의 설정 파일들을 읽어 나머지 서비스에 배포한다.&lt;/p&gt;
&lt;h3&gt;ECS wave 배포 도입 + waiter 타임아웃 수정&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; 11개 서비스를 동시에 롤링 배포하면 old 태스크 + new 태스크가 겹치며 Fargate vCPU 한도(30)를 초과해 일부 서비스가 배포되지 못했다. 또한 &lt;code&gt;AWS_MAX_ATTEMPTS=80&lt;/code&gt;을 설정했지만 &lt;code&gt;aws ecs wait services-stable&lt;/code&gt;의 폴링은 botocore 내부에서 40회×15초=10분으로 고정되어 있어 이 환경변수가 전혀 영향을 주지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; 서비스를 3개 wave로 나눠 순차 배포하는 방식을 도입했다. &lt;code&gt;aws ecs wait services-stable&lt;/code&gt; 대신 &lt;code&gt;wait_stable&lt;/code&gt; 커스텀 폴링 함수로 교체해 최대 20분(ai-service는 30분)까지 대기하도록 했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; Deploy ECS 워크플로우 최종 성공. wave별 진행 상황을 GitHub Actions UI에서 별도 job으로 확인할 수 있다.&lt;/p&gt;
&lt;h3&gt;Prometheus + Grafana 설정 수정&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제 1&lt;/strong&gt; &lt;code&gt;prometheus.prod.yml&lt;/code&gt;에 &lt;code&gt;ecs_sd_configs&lt;/code&gt;를 사용했는데, 공식 Prometheus는 ECS SD를 지원하지 않는 필드라 크래시가 발생했다. Prometheus 크래시에 &lt;code&gt;depends_on&lt;/code&gt;으로 걸린 Grafana도 연쇄 실패했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결 1&lt;/strong&gt; &lt;code&gt;ecs_sd_configs&lt;/code&gt; 대신 &lt;code&gt;file_sd_configs&lt;/code&gt; + 5분 주기 cron 스크립트(&lt;code&gt;ecs-discovery.sh&lt;/code&gt;) 방식으로 교체했다. 스크립트가 AWS CLI로 ECS 태스크 IP를 조회해 JSON 파일로 저장하면 Prometheus가 이 파일을 읽는다. 서비스별로 포트가 다 달라 PORT_MAP을 스크립트에 추가했다. Prometheus 이미지를 &lt;code&gt;latest&lt;/code&gt;에서 &lt;code&gt;v2.55.1&lt;/code&gt;로 버전 고정했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;문제 2&lt;/strong&gt; Grafana에 &lt;code&gt;SLACK_WEBHOOK_URL&lt;/code&gt;이 주입되지 않아(빈 값) Slack contact point 초기화에 실패하며 재시작을 반복했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결 2&lt;/strong&gt; &lt;code&gt;deploy-ec2.yml&lt;/code&gt;에서 SSM Parameter Store 값을 조회해 환경변수로 주입하도록 수정했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; Prometheus, Grafana 모두 정상 기동. Spring Boot Observability 대시보드에 ECS 앱 지표가 표시된다.&lt;/p&gt;
&lt;h3&gt;로컬 Docker 환경 수정&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; 여러 서비스의 환경변수 설정이 누락되거나 잘못되어 있었다. user-service는 &lt;code&gt;SPRING_DATA_REDIS_HOST&lt;/code&gt;가 빈 값으로 전달되어 Redis 연결 실패. payment-service는 Toss 환경변수 이름이 &lt;code&gt;application.yml&lt;/code&gt;과 &lt;code&gt;docker-compose.yml&lt;/code&gt; 사이에 달랐다. notification-service는 &lt;code&gt;SLACK_BOT_TOKEN&lt;/code&gt;이 컨테이너에 전달되지 않았다. &lt;code&gt;locals.tf&lt;/code&gt;의 Redis 플래그도 실제 코드와 맞지 않아 4개 서비스가 잘못 설정되어 있었다(user-service/payment-service/notification-service는 false, auction-service는 true로 반대로 되어 있었다).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; user-service에 Redis 호스트를 컨테이너 이름 &lt;code&gt;redis&lt;/code&gt;로 직접 지정했다. payment-service에 Toss 환경변수 이름 매핑을 추가했다. notification-service에 &lt;code&gt;SLACK_BOT_TOKEN&lt;/code&gt; 전달을 추가했다. &lt;code&gt;locals.tf&lt;/code&gt; Redis 플래그를 코드 실제 사용 여부에 맞게 수정했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; 로컬 Docker 전체 19개 서비스 정상 기동 확인.&lt;/p&gt;
&lt;h3&gt;AI 코드 리뷰 개선 (GitHub Actions)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; &lt;code&gt;gemini_review.yml&lt;/code&gt;이 diff.txt만 AI에 전달해 파일의 전체 맥락을 볼 수 없었다. 이미 코드에 있는 내용을 AI가 누락됐다고 피드백하는 오탐이 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; 변경된 파일의 전체 내용도 프롬프트에 함께 전달하도록 수정했다. 500줄 이하 파일은 전체, 초과 파일은 선언부(1~80줄) + hunk 위치 기반 변경 구간을 추출한다. 추출 로직을 shell 스크립트로 작성했다가 Node.js로 전환했다(diff 1회 파싱으로 O(M+N) 달성, 겹치는 hunk 범위 병합).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; &amp;quot;이미 있는 코드를 추가하라&amp;quot;는 중복 피드백이 제거됐다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6월 24일&lt;/h2&gt;
&lt;h3&gt;db-init 실행 호스트 변경 (Kafka EC2 → 모니터링 EC2)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; DB 초기화 스크립트가 Kafka EC2에서 실행되도록 되어 있었다. Kafka가 다중 클러스터로 확장되면 어느 Kafka에서 실행하는지 특정할 수 없고, Kafka 브로커가 DB 초기화 책임까지 겸하는 역할 오염이 생긴다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; 단일 인스턴스로 유지되는 모니터링 EC2로 실행 호스트를 변경했다. RDS 보안 그룹 허용 출처도 Kafka → 모니터링으로 함께 변경했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; DB 초기화 책임이 단일 관리 서버로 명확하게 분리됐다.&lt;/p&gt;
&lt;h3&gt;deploy-ec2.yml 리팩터링&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; 워크플로우 안 단일 step에 SSM 환경변수 조회, .env 생성, 설정 파일 치환, crontab 등록, Docker Compose 실행 등 모든 책임이 몰려 있었다. &lt;code&gt;REPO_DIR&lt;/code&gt;과 &lt;code&gt;REGION&lt;/code&gt;이 4곳에 하드코딩되어 있어 경로 변경 시 모두 찾아 수정해야 했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; 배포 로직을 &lt;code&gt;scripts/deploy-kafka.sh&lt;/code&gt;, &lt;code&gt;scripts/deploy-monitoring.sh&lt;/code&gt;로 분리했다. 공통 환경변수는 &lt;code&gt;scripts/env.sh&lt;/code&gt; 한 곳에서 관리하고 각 스크립트에서 source해서 쓰도록 했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; 워크플로우는 git pull + 스크립트 호출 두 줄만 남았다. 환경변수 중복이 제거됐다.&lt;/p&gt;
&lt;h3&gt;EC2 ECR 권한 제거&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; EC2 인스턴스 역할에 &lt;code&gt;AmazonEC2ContainerRegistryReadOnly&lt;/code&gt; 권한이 붙어 있었는데, EC2에서 실행하는 컨테이너는 전부 Docker Hub / ghcr.io 퍼블릭 이미지라 ECR에 접근할 일이 없다. 사용하지 않는 권한은 보안 원칙상 제거해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; &lt;code&gt;iam.tf&lt;/code&gt;에서 해당 권한 attachment를 제거했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; EC2 역할의 권한이 실제 필요한 것만 남았다. ECR은 ECS Fargate만 사용한다는 것이 코드와 IAM 모두에서 명확해졌다.&lt;/p&gt;
&lt;h3&gt;SSM Parameter Store bootstrap 마이그레이션&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; &lt;code&gt;terraform destroy&lt;/code&gt; 시 SSM 파라미터가 함께 삭제되어, 다음에 인프라를 다시 올릴 때 시크릿 값을 수동으로 모두 다시 입력해야 했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; 자주 변경하지 않는 시크릿 14개를 &lt;code&gt;bootstrap/main.tf&lt;/code&gt;로 이동하고 &lt;code&gt;prevent_destroy = true&lt;/code&gt; + &lt;code&gt;ignore_changes = [value]&lt;/code&gt;를 적용했다. bootstrap은 destroy 대상에 포함되지 않으므로 값이 AWS에 보존된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; &lt;code&gt;terraform destroy&lt;/code&gt; 후 재구축 시 SSM 파라미터를 다시 입력할 필요가 없어졌다.&lt;/p&gt;
&lt;h3&gt;ai-service 배포 Wave 분리&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; ai-service는 기동 시 pgvector 스키마 초기화 + Gemini API 연결 초기화가 추가로 일어나 다른 서비스보다 기동 시간이 훨씬 길다. Wave 3에서 다른 서비스와 함께 배포하면 ai-service 타임아웃이 같은 Wave 전체를 실패로 만들었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; ai-service를 Wave 3에서 분리해 Wave 4(단독)로 이동하고, 최대 대기 시간을 30분으로 늘렸다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; ai-service 타임아웃이 다른 서비스 배포에 영향을 주지 않게 됐다.&lt;/p&gt;
&lt;h3&gt;db-init.sh / keycloak-setup.sh 버그 수정 (총 5차)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; 여러 스크립트에 걸쳐 버그가 겹쳐 있었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SSM Run Command JSON 안에 리터럴 &lt;code&gt;&amp;quot;&lt;/code&gt;가 삽입되어 파싱 오류 발생&lt;/li&gt;
&lt;li&gt;레포 경로가 &lt;code&gt;sanji-jk&lt;/code&gt;로 잘못 하드코딩되어 실제 경로 &lt;code&gt;san-ji-jik-kyeng&lt;/code&gt;와 불일치 (여러 스크립트에 걸쳐)&lt;/li&gt;
&lt;li&gt;keycloak-setup.sh 헬스 체크 URL이 관리 포트(9000)를 바라보고 있어 포트 포워딩(18080)과 불일치&lt;/li&gt;
&lt;li&gt;SSM 포트 포워딩이 실제로 포트를 바인딩하지 않아 Keycloak에 접근 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; &lt;code&gt;echo \&amp;quot;...\&amp;quot;&lt;/code&gt; → &lt;code&gt;printf&lt;/code&gt;로 쌍따옴표 문제를 제거했다. 경로 오타를 전체 스크립트에 걸쳐 수정했다. keycloak-setup.sh를 SSM 포트 포워딩 방식에서 SSM Run Command로 모니터링 EC2에서 직접 Keycloak API를 호출하는 방식으로 전환했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; db-init 정상 실행 확인(&amp;quot;완료. 스키마 초기화 성공.&amp;quot;). Keycloak 설정 스크립트도 정상 실행.&lt;/p&gt;
&lt;h3&gt;EC2 IAM 권한 추가 (rds:DescribeDBInstances)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; &lt;code&gt;deploy-monitoring.sh&lt;/code&gt;에서 &lt;code&gt;aws rds describe-db-instances&lt;/code&gt;로 RDS 엔드포인트를 조회하는데, EC2 역할에 해당 권한이 없어 &lt;code&gt;AccessDenied&lt;/code&gt;로 배포가 실패했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; &lt;code&gt;iam.tf&lt;/code&gt;에 EC2 역할용 &lt;code&gt;rds:DescribeDBInstances&lt;/code&gt; 권한 정책을 추가하고 Terraform으로 즉시 반영했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; deploy-ec2 워크플로우 정상 완료.&lt;/p&gt;
&lt;h3&gt;EC2 배포 S3 전환&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; EC2 배포 시 &lt;code&gt;git clone&lt;/code&gt;으로 레포를 받아오는 방식을 사용했는데, 이 레포의 기본 브랜치가 &lt;code&gt;dev&lt;/code&gt;라서 &lt;code&gt;git clone&lt;/code&gt;이 dev를 받아왔다. 배포 스크립트는 main에만 있어 &lt;code&gt;exit 127&lt;/code&gt;로 실패했다. 또한 EC2가 JMX Exporter JAR를 직접 Maven Central에서 wget해오는 구조라 외부 인터넷 직접 의존이 생겼다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; git clone 대신 GHA 러너에서 배포에 필요한 파일을 S3에 업로드하고, EC2는 &lt;code&gt;aws s3 sync&lt;/code&gt;로 받도록 전환했다. JMX Exporter JAR도 GHA 러너에서 다운로드해 S3에 올리고, SHA256 무결성 검증 후 EC2에 배포한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; 브랜치 문제 없이 항상 최신 main의 파일이 EC2에 배포된다. EC2의 Maven Central 직접 의존성이 제거됐다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6월 25일&lt;/h2&gt;
&lt;h3&gt;Keycloak 연결 오류 수정 (로컬)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; 로컬에서 user-service가 Keycloak에 &lt;code&gt;localhost:18080&lt;/code&gt;으로 연결을 시도해 &lt;code&gt;Connection refused&lt;/code&gt;가 발생했다. Docker 컨테이너 내부에서 &lt;code&gt;localhost&lt;/code&gt;는 자기 자신(user-service 컨테이너)을 가리키기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; &lt;code&gt;.env&lt;/code&gt;의 &lt;code&gt;KEYCLOAK_SERVER_URL&lt;/code&gt;, &lt;code&gt;KEYCLOAK_ISSUER_URI&lt;/code&gt;를 &lt;code&gt;localhost&lt;/code&gt; 대신 Docker Compose 서비스 이름 &lt;code&gt;keycloak&lt;/code&gt;으로 변경했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; 로컬에서 Keycloak 연동 정상 동작.&lt;/p&gt;
&lt;h3&gt;order-service outbox JSONB 타입 오류 수정&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; &lt;code&gt;p_order_outbox&lt;/code&gt; 테이블의 &lt;code&gt;payload&lt;/code&gt; 컬럼이 &lt;code&gt;jsonb&lt;/code&gt; 타입인데, &lt;code&gt;OrderOutbox.java&lt;/code&gt;의 &lt;code&gt;payload&lt;/code&gt; 필드에 &lt;code&gt;@JdbcTypeCode(SqlTypes.JSON)&lt;/code&gt; 어노테이션이 없었다. Hibernate가 이 필드를 &lt;code&gt;varchar&lt;/code&gt;로 바인딩해 PostgreSQL이 묵시적 형변환을 거부하면서 보증금 주문 생성 INSERT가 실패했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; &lt;code&gt;OrderOutbox.java&lt;/code&gt;에 &lt;code&gt;@JdbcTypeCode(SqlTypes.JSON)&lt;/code&gt; 어노테이션을 추가했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; 보증금 주문 생성 시 outbox INSERT 오류 해소.&lt;/p&gt;
&lt;h3&gt;Kafka healthcheck 추가 (로컬)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; Kafka에 healthcheck가 없어 컨슈머 서비스들이 Kafka가 완전히 준비되기 전에 접속을 시도했다. 접속 실패 후 재시도할 때마다 &lt;code&gt;__consumer_offsets&lt;/code&gt; 자동 생성 로그가 반복됐다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; Kafka에 &lt;code&gt;kafka-topics.sh --list&lt;/code&gt;를 사용한 healthcheck를 추가하고, Kafka에 의존하는 6개 서비스(auction, bid, order, payment, notification, kafka-ui)의 &lt;code&gt;depends_on condition&lt;/code&gt;을 &lt;code&gt;service_started&lt;/code&gt; → &lt;code&gt;service_healthy&lt;/code&gt;로 변경했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; Kafka가 완전히 준비된 후에 컨슈머 서비스들이 기동해 불필요한 재연결 로그가 사라졌다.&lt;/p&gt;
&lt;h3&gt;Kafka 직렬화 타입 헤더 문제 수정 (핵심 버그)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; order-service outbox 릴레이에서 &lt;code&gt;Object.class&lt;/code&gt;로 역직렬화하면 Jackson이 &lt;code&gt;LinkedHashMap&lt;/code&gt;을 만들고, 메시지 헤더에 &lt;code&gt;__TypeId__: java.util.LinkedHashMap&lt;/code&gt;이 붙는다. payment-service의 &lt;code&gt;JsonDeserializer&lt;/code&gt;가 이 헤더를 보고 &lt;code&gt;LinkedHashMap&lt;/code&gt;으로 역직렬화하려 하지만, 리스너가 기대하는 타입(예: &lt;code&gt;WinningCreatedEvent&lt;/code&gt;)으로 변환할 방법이 없어 예외가 발생했다. 3번 재시도 후 DLT로 이동하면서 이벤트가 &amp;quot;전달되지 않은 것처럼&amp;quot; 보였다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;order-service: &lt;code&gt;deserializePayload()&lt;/code&gt; 메서드로 이벤트 타입별 DTO 클래스로 역직렬화하도록 수정. producer에서 &lt;code&gt;JsonSerializer.ADD_TYPE_INFO_HEADERS = false&lt;/code&gt; 설정(마이크로서비스 간에는 producer 클래스가 consumer classpath에 없으므로 타입 헤더가 유해하다).&lt;/li&gt;
&lt;li&gt;payment-service: &lt;code&gt;JsonDeserializer&lt;/code&gt; → &lt;code&gt;StringDeserializer&lt;/code&gt; + &lt;code&gt;StringJsonMessageConverter&lt;/code&gt;로 교체해 리스너 파라미터 타입으로 직접 역직렬화.&lt;/li&gt;
&lt;li&gt;order-service: payment-service 패턴으로 Kafka consumer 방식을 통일했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; Kafka 이벤트가 DLT로 빠지지 않고 consumer에서 정상 처리됨. order-service → payment-service 이벤트 전달 정상화.&lt;/p&gt;
&lt;h3&gt;보증금 결제 confirm 404 수정&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; &lt;code&gt;PaymentService.createDepositPayment()&lt;/code&gt;에서 &lt;code&gt;tossOrderId&lt;/code&gt;를 내부 UUID(&lt;code&gt;event.orderId().toString()&lt;/code&gt;)로 저장했다. 그런데 Toss 위젯에 전달하는 주문 번호는 &lt;code&gt;ORD-XXXXXXXX&lt;/code&gt; 형식의 &lt;code&gt;orderNumber&lt;/code&gt;다. confirm 요청이 &lt;code&gt;orderNumber&lt;/code&gt;로 들어오면 UUID가 저장된 레코드를 찾지 못해 404가 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; 이벤트 DTO(&lt;code&gt;DepositCreatedEvent&lt;/code&gt;, &lt;code&gt;WinningCreatedEvent&lt;/code&gt;)에 &lt;code&gt;orderNumber&lt;/code&gt; 필드를 추가하고, &lt;code&gt;PaymentService&lt;/code&gt;에서 &lt;code&gt;tossOrderId&lt;/code&gt;를 &lt;code&gt;event.orderNumber()&lt;/code&gt;로 참조하도록 수정했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; 보증금 결제 confirm 정상 처리.&lt;/p&gt;
&lt;h3&gt;결제 완료 후 주문 상태 미업데이트 수정&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; payment-service outbox 릴레이도 &lt;code&gt;Object.class&lt;/code&gt; → &lt;code&gt;LinkedHashMap&lt;/code&gt; 문제가 있었다. order-service의 &lt;code&gt;PaymentEventConsumer&lt;/code&gt;가 &lt;code&gt;LinkedHashMap&lt;/code&gt;을 &lt;code&gt;PaymentCompletedEvent&lt;/code&gt;로 변환하지 못해 &lt;code&gt;MessageConversionException&lt;/code&gt; 발생 → 3회 재시도 후 DLT 이동 → 결제 완료 후 주문 상태가 &lt;code&gt;PAYMENT_SUCCESS&lt;/code&gt;로 업데이트되지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; &lt;code&gt;PaymentEventConsumer&lt;/code&gt;를 &lt;code&gt;ConsumerRecord + objectMapper.convertValue()&lt;/code&gt; 방식에서 &lt;code&gt;StringJsonMessageConverter&lt;/code&gt; 기반 타입 직접 수신 방식으로 교체했다. &lt;code&gt;KafkaConsumerConfig&lt;/code&gt;도 payment-service와 동일한 구조로 통일했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; 결제 완료 후 주문 상태가 &lt;code&gt;PAYMENT_SUCCESS&lt;/code&gt;로 정상 업데이트됨.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6월 26일&lt;/h2&gt;
&lt;h3&gt;모니터링 설정 버그 수정&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; 여러 모니터링 설정 버그가 겹쳐 있었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;deploy-monitoring.sh&lt;/code&gt;에서 모니터링 EC2의 private IP를 IMDS에서 조회하는 코드와 &lt;code&gt;envsubst&lt;/code&gt; 인자에 &lt;code&gt;$MONITORING_PRIVATE_IP&lt;/code&gt;가 빠져, &lt;code&gt;prometheus.prod.yml&lt;/code&gt;에 리터럴 &lt;code&gt;${MONITORING_PRIVATE_IP}:9100&lt;/code&gt;이 그대로 남아 Prometheus가 잘못된 주소를 타겟으로 읽었다.&lt;/li&gt;
&lt;li&gt;Langfuse의 &lt;code&gt;NEXTAUTH_URL: ${LANGFUSE_URL}&lt;/code&gt; 변수명이 배포 스크립트의 &lt;code&gt;LANGFUSE_NEXTAUTH_URL&lt;/code&gt;과 달라 빈 값으로 기동됐다.&lt;/li&gt;
&lt;li&gt;Grafana Spring Boot Observability 대시보드 PromQL에 Config Server 정규식 경로(&lt;code&gt;profiles:&lt;/code&gt;)와 Spring Security가 URI 패턴을 못 잡은 &lt;code&gt;UNKNOWN&lt;/code&gt; 요청이 노이즈로 표시됐다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-compose.monitoring.yml&lt;/code&gt;에 Node Exporter 서비스가 없어 &lt;code&gt;monitoring&lt;/code&gt; Prometheus job이 항상 DOWN이었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; IMDS에서 private IP 조회 코드와 &lt;code&gt;envsubst&lt;/code&gt; 인자를 추가했다. Langfuse 변수명을 맞췄다. PromQL 전체에 &lt;code&gt;uri!~&amp;quot;.*profiles:.*&amp;quot;&lt;/code&gt;, &lt;code&gt;uri!=&amp;quot;UNKNOWN&amp;quot;&lt;/code&gt; 필터를 추가했다. Node Exporter 서비스를 &lt;code&gt;docker-compose.monitoring.yml&lt;/code&gt;에 추가했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; Prometheus가 Kafka, 모니터링 EC2, ECS 앱 지표를 정상 수집한다. Grafana 대시보드에서 노이즈가 제거됐다.&lt;/p&gt;
&lt;h3&gt;회원가입 500 에러 수정&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt; AWS에서 회원가입 API를 호출하면 Gateway가 &lt;code&gt;Connection refused: /169.254.172.2:19091&lt;/code&gt;로 실패했다. ECS Fargate(&lt;code&gt;awsvpc&lt;/code&gt; 네트워크 모드)에서는 컨테이너에 네트워크 인터페이스가 여러 개 붙는다. Spring Boot가 인터페이스를 스캔할 때 순서에 따라 ECS 링크-로컬 주소(&lt;code&gt;169.254.172.2&lt;/code&gt;)를 먼저 집어오면 Eureka에 &lt;code&gt;169.254.172.2:19091&lt;/code&gt;로 등록된다. 이 주소는 해당 컨테이너 내부에서만 유효해 다른 컨테이너에서 도달할 수 없다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt; &lt;code&gt;spring.cloud.inetutils.preferred-networks: 10.0&lt;/code&gt; 설정을 config-server 공통 설정(&lt;code&gt;config/application.yml&lt;/code&gt;)에 추가했다. Spring이 인터페이스를 스캔할 때 VPC CIDR(&lt;code&gt;10.0.0.0/16&lt;/code&gt;) 대역을 우선 선택한다. 로컬 환경에서는 &lt;code&gt;10.0&lt;/code&gt; 대역이 없으므로 자동으로 일반 인터페이스로 폴백한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; Eureka에 VPC 내부 IP(&lt;code&gt;10.0.x.x&lt;/code&gt;)가 등록되어 Gateway 라우팅이 정상화됐다. 회원가입 500 에러 해소.&lt;/p&gt;</description>
      <category>내일배움캠프/최종 프로젝트 개발일지</category>
      <author>munsik22</author>
      <guid isPermaLink="true">https://munsik22.tistory.com/355</guid>
      <comments>https://munsik22.tistory.com/355#entry355comment</comments>
      <pubDate>Fri, 26 Jun 2026 19:31:36 +0900</pubDate>
    </item>
  </channel>
</rss>