<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Dev96</title>
        <link>https://velog.io/</link>
        <description>다양한 경험과 실무의 깊이로 평가받고 싶은 사람들을 위해 기록합니다. 실무에서 부딪히며 배운 것들이 가장 오래 남는다고 믿습니다.</description>
        <lastBuildDate>Mon, 09 Feb 2026 11:19:58 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Dev96</title>
            <url>https://velog.velcdn.com/images/dev-hsl-960221/profile/e90eb100-ee09-4aee-9854-e64108a7b506/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Dev96. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev-hsl-960221" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[🚀 TDD부터 Testcontainers까지...]]></title>
            <link>https://velog.io/@dev-hsl-960221/TDD%EB%B6%80%ED%84%B0-Testcontainers%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@dev-hsl-960221/TDD%EB%B6%80%ED%84%B0-Testcontainers%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Mon, 09 Feb 2026 11:19:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
</blockquote>
<p><strong>TDD(Test Driven Development)</strong>의 개념부터  <strong>Testcontainers</strong>가 왜 등장했는지 그리고 특정 버전과 호환되지 않아 발생하는 실제 문제와 해결 전략까지 정리해본다.</p>
<hr>
<h2 id="1️⃣-tddtest-driven-development란">1️⃣ TDD(Test Driven Development)란?</h2>
<p>TDD는 테스트를 먼저 작성하고 그 테스트를 통과하는 코드를 작성하는 개발 방식이다.</p>
<h3 id="tdd의-기본-사이클-red-→-green-→-refactor">TDD의 기본 사이클 (Red → Green → Refactor)</h3>
<ol>
<li><strong>Red</strong><ul>
<li>실패하는 테스트를 먼저 작성</li>
</ul>
</li>
<li><strong>Green</strong><ul>
<li>테스트를 통과시키는 최소한의 코드 작성</li>
</ul>
</li>
<li><strong>Refactor</strong><ul>
<li>중복 제거, 구조 개선</li>
</ul>
</li>
</ol>
<hr>
<h2 id="2️⃣-tdd-장단점">2️⃣ TDD 장단점</h2>
<h3 id="✅-장점">✅ 장점</h3>
<ul>
<li>요구사항이 코드로 명확해진다 ⭐⭐⭐</li>
<li>회귀 버그 방지</li>
</ul>
<h3 id="❌-단점">❌ 단점</h3>
<ul>
<li>초기 개발 속도가 느리다 </li>
<li>테스트 작성에 대한 러닝 커브 ⭐⭐</li>
<li><strong>외부 의존성(DB, Redis, Kafka 등)이 많을수록 난이도 증가</strong>  ⭐⭐⭐</li>
</ul>
<p>👉 이 지점에서 <strong>Testcontainers</strong>가 등장한다.</p>
<hr>
<h2 id="3️⃣-기존-테스트-방식의-한계">3️⃣ 기존 테스트 방식의 한계</h2>
<p>보통 이런 방식으로 테스트를 작성해왔다.</p>
<ul>
<li>H2 같은 In-memory DB 사용</li>
<li>로컬 DB에 직접 의존</li>
<li>*<em>개발자마다 다른 환경 *</em> ⭐⭐⭐</li>
</ul>
<p>👉 <strong>운영 환경과 테스트 환경의 불일치</strong>가 가장 큰 문제다.</p>
<hr>
<h2 id="4️⃣-testcontainers란">4️⃣ Testcontainers란?</h2>
<p><strong>Testcontainers는 테스트 코드에서 Docker 컨테이너를 직접 띄워 사용하는 라이브러리</strong>다.</p>
<h3 id="핵심-개념">핵심 개념</h3>
<ul>
<li>테스트 실행 시<ul>
<li>실제 DB / 메시지 브로커 / 인메모리 캐시 ... 등 컨테이너 실행  ⭐⭐</li>
<li>테스트 종료 후 자동 정리 </li>
</ul>
</li>
<li><strong>운영 환경과 동일한 버전 사용 가능</strong></li>
</ul>
<hr>
<h2 id="5️⃣-testcontainers-기본-사용-예-spring-boot--junit--javakotlin">5️⃣ Testcontainers 기본 사용 예 (Spring Boot + JUnit + Java/Kotlin)</h2>
<h3 id="gradle-의존성-및-configuration">Gradle 의존성 및 Configuration</h3>
<pre><code class="language-java">testImplementation(&quot;org.testcontainers:junit-jupiter&quot;)
testImplementation(&quot;org.testcontainers:mariadb&quot;)


-------------

@Testcontainers
@ActiveProfiles(&quot;test&quot;)
public abstract class TestContainerConfig {

    @Container
    protected static final MariaDBContainer&lt;?&gt; mariaDBContainer = new MariaDBContainer&lt;&gt;(&quot;mariadb:latest&quot;)
            .withDatabaseName(TestDBConfig.DB_NAME)
            .withUsername(TestDBConfig.USERNAME)
            .withPassword(TestDBConfig.PASSWORD);

    @DynamicPropertySource
    static void overrideProperties(DynamicPropertyRegistry registry) {
        registry.add(&quot;spring.datasource.url&quot;, mariaDBContainer::getJdbcUrl);
        registry.add(&quot;spring.datasource.username&quot;, mariaDBContainer::getUsername);
        registry.add(&quot;spring.datasource.password&quot;, mariaDBContainer::getPassword);
    }
}

</code></pre>
<hr>
<h2 id="❗testcontainers-문제-발생">❗Testcontainers 문제 발생</h2>
<blockquote>
<p><strong>Could not find a valid Docker environment. Please see logs and check configuration</strong> 라는 오류가 갑자기 잘되다가 발생</p>
</blockquote>
<pre><code>Caused by: java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configuration
    at org.testcontainers.dockerclient.DockerClientProviderStrategy.lambda$getFirstValidStrategy$7(DockerClientProviderStrategy.java:274)
    at java.base/java.util.Optional.orElseThrow(Optional.java:403)
    at org.testcontainers.dockerclient.DockerClientProviderStrategy.getFirstValidStrategy(DockerClientProviderStrategy.java:265)</code></pre><h3 id="🚨-1-docker-cli-버전-문제">🚨 1. Docker CLI 버전 문제</h3>
<h4 id="❌-docker-29x-버전과-호환문제">❌ Docker 29.x 버전과 호환문제</h4>
<h4 id="✅-해결-방법">✅ 해결 방법</h4>
<ul>
<li><strong>Docker Desktop 28.x 이하 버전 사용</strong></li>
<li>** Spring Boot 3.5.8 이상으로 업그레이드 **</li>
<li>** docker.sock 경로 수정 ** <pre><code>sudo ln -s $HOME/.docker/run/docker.sock /var/run/docker.sock</code></pre></li>
</ul>
<hr>
<h2 id="6️⃣-testcontainers-장점">6️⃣ Testcontainers 장점</h2>
<ul>
<li><strong>실제 DB 동작과 100% 동일</strong> ⭐⭐</li>
<li>CI/CD 환경에서도 재현 가능</li>
<li><strong>로컬 환경 오염 없음</strong> ⭐⭐⭐</li>
<li><strong>테스트 간 독립성 보장</strong> ⭐⭐⭐</li>
</ul>
<hr>
<h2 id="7️⃣-마무리">7️⃣ 마무리</h2>
<blockquote>
<p><strong>TDD</strong>를 도입하다가 포기하는 가장 큰 이유는 환경과 외부 의존성이다. <strong>Testcontainers</strong>는 이 문제를 거의 완벽하게 해결해 주지만
대신 버전 관리라는 새로운 책임을 요구한다. 잘 통제된 Testcontainers 환경은 테스트를 믿을 수 있게 만드는 마지막 퍼즐이라고 생각한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네이버 클라우드] 백엔드 개발자 채용 후기 ]]></title>
            <link>https://velog.io/@dev-hsl-960221/%EB%84%A4%EC%9D%B4%EB%B2%84-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%B1%84%EC%9A%A9-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@dev-hsl-960221/%EB%84%A4%EC%9D%B4%EB%B2%84-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%B1%84%EC%9A%A9-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Tue, 16 Dec 2025 07:51:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>네이버 클라우드 백엔드 개발자 채용 과정에 참여하면서 경험한 내용을 
정리했습니다. </p>
</blockquote>
<p>*<em>서류 → 기업문화적합도 검사 및 직무테스트 → 기술 면접 → 컬처 핏 및 레퍼체크 → 최종합격 *</em> 순서로 진행되었습니다.</p>
<hr>
<h2 id="📌-지원-배경">📌 지원 배경</h2>
<ul>
<li>지원 포지션:     [NAVER Cloud] AI Voice Call 서비스 BE 개발 (경력)</li>
<li>지원 경력 조건 : 3년 이상</li>
</ul>
<p>네이버 클라우드는 국내에서 드물게 <strong>대규모 트래픽 + 클라우드 네이티브 환경</strong>을 직접 경험할 수 있는 회사라고 생각했고 스타트업 회사를 다니면서 Iot 또는 웨어러블 디바이스 관련 소프트웨어를 개발에 대한 좋은기억이 있어 지원하게됨</p>
<hr>
<h2 id="🧾-서류-전형">🧾 서류 전형</h2>
<ul>
<li>결과: 합격 </li>
<li>소요 기간: 2~3시간 이내</li>
</ul>
<blockquote>
<p>운이 좋게 합격을 했던것 같다. 서류는 참고로 <strong>&quot;Naver Careers&quot;</strong> 홈페이지를 통해 접수하면된다.  </p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/15c7545c-46e0-4306-8747-f5a39ed0b496/image.png" alt=""></p>
<hr>
<h2 id="💻-기업문화적합도-검사">💻 기업문화적합도 검사</h2>
<p>서류 전형 합격 이후, <strong>기업문화적합도 검사</strong>를 진행하게 되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/07a6bb7a-2d88-43be-b447-2fbf24390fa7/image.png" alt=""></p>
<ul>
<li>기업문화적합도 검사는 <strong>두 가지 응답 방식이 섞여 있는 형태</strong>였습니다.<h4 id="1️⃣-동의-정도-선택형">1️⃣ 동의 정도 선택형</h4>
</li>
<li>문항에 대해 <strong>전혀 그렇지 않다 ~ 매우 그렇다 (A~D)</strong> 중 하나를 선택</li>
<li>한 세트당 <strong>4개 문항</strong>에 응답</li>
</ul>
<p>👉 일관성과 솔직함을 보는 느낌이 강했습니다.</p>
<h4 id="2️⃣-상대-비교-선택형">2️⃣ 상대 비교 선택형</h4>
<ul>
<li>여러 문항 중 <strong>가장 나와 가까운 문항 1개 + 가장 먼 문항 1개</strong> 선택</li>
<li>한 세트당 <strong>2개 문항</strong> 선택</li>
</ul>
<p>👉 단순히 좋게 답하는 것보다는 <strong>자기 성향을 명확히 드러내는 응답</strong>이 중요</p>
<h3 id="⚠️-응시-시-유의사항">⚠️ 응시 시 유의사항</h3>
<ul>
<li>정답이 있는 검사가 아니며 <strong>솔직한 응답이 가장 중요</strong></li>
<li>응답이 불성실하거나 일관성이 없을 경우 결과가 무효 처리될 수 있다고 안내받음</li>
<li>조용한 환경에서 충분한 시간을 확보하고 응시하는 것을 권장</li>
<li>생각하면서 문항을 선택할 여유가 부족함</li>
</ul>
<hr>
<h2 id="💻-온라인-직무-테스트codility">💻 온라인 직무 테스트(Codility)</h2>
<p>기업문화적합도 검사 이후 <strong>온라인 직무 테스트(코딩 테스트)</strong> 안내 메일을 통해 테스트를 진행했습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/6a37aab3-0cba-48cb-90cd-805f514f5905/image.png" alt=""></p>
<ul>
<li>플랫폼: Codility</li>
<li>진행 방식: 온라인 코딩 테스트</li>
<li>응시 기한: 지정된 기간 내 자율 응시</li>
</ul>
<h3 id="📌-출제된-문제-유형">📌 출제된 문제 유형</h3>
<h4 id="1️⃣-토너먼트-방식의-게임-문제">1️⃣ 토너먼트 방식의 게임 문제</h4>
<ul>
<li>시뮬레이션 기반 완전탐색</li>
</ul>
<h4 id="2️⃣-팰린드롬-문제">2️⃣ 팰린드롬 문제</h4>
<ul>
<li>주어진 문자열을 앞 뒤로 같은지 확인하여 팰린드롬 판별</li>
</ul>
<h4 id="3️⃣-문자열-압축-최적화-문제">3️⃣ 문자열 압축 최적화 문제</h4>
<ul>
<li>약간 야매..식 브루트포스 알고리즘으로 문제를 푼 기억 ( 너무 어려웠다 )</li>
</ul>
<h3 id="⚠️-응시-시-유의사항-1">⚠️ 응시 시 유의사항</h3>
<p>안내 메일에서 강조된 주의사항은 다음과 같았습니다.</p>
<ul>
<li>타인의 도움, 웹 서핑, 코드 복사 <strong>금지</strong></li>
<li>테스트 종료 전 문제 내용 공유 <strong>금지</strong></li>
<li>시험 시작 후 중단 시 <strong>재응시 불가</strong></li>
<li>기한 내 미응시 또는 최종 제출 미완료 시 <strong>불합격 처리</strong></li>
<li>안정적인 인터넷 환경에서 응시 권장</li>
</ul>
<p>👉 일반적인 코딩 테스트보다 <strong>부정행위 및 환경 조건에 대한 안내가 비교적 엄격</strong>한 편이었습니다.</p>
<hr>
<h2 id="✅-준비하면서-느낀-점--팁">✅ 준비하면서 느낀 점 &amp; 팁</h2>
<ul>
<li>기업문화적합도 검사 유형이 낯설고 어색한 느낌이 많이 들고 시간이 상대적으로 부족 대답못하고 넘어간 경우도 생겼다.</li>
<li>코딩테스트 문제는 총 3문제 사고와 생각을 좀 많이 해야됨</li>
<li>평소에 알고리즘 문제를 안풀다보니 난이도가 상당히 높았다고 생각이 듬.</li>
</ul>
<hr>
<h2 id="✍️-마무리">✍️ 마무리</h2>
<p>비슷한 포지션을 준비하시는 분들께 조금이나마 도움이 되었으면 좋겠습니다 🙂</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[💸 DynamoDB 비용 절감 여정]]></title>
            <link>https://velog.io/@dev-hsl-960221/DynamoDB-%EB%B9%84%EC%9A%A9-%EC%A0%88%EA%B0%90-%EC%97%AC%EC%A0%95</link>
            <guid>https://velog.io/@dev-hsl-960221/DynamoDB-%EB%B9%84%EC%9A%A9-%EC%A0%88%EA%B0%90-%EC%97%AC%EC%A0%95</guid>
            <pubDate>Wed, 22 Oct 2025 05:51:35 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>최근 AWS 비용을 점검하면서 DynamoDB의 청구 금액이 예상보다 높게 나오고 있다는 걸 확인했다. 특히 개발 환경(dev)과 운영 환경(prod) 모두에 동일한 방식으로 세팅되어 있어 불필요한 리소스 낭비가 있다고 문득 생각이듬. 이번 글에서는 제가 실제로 진행한 DynamoDB 비용 절감 여정을 공유하려고 합니다.</p>
</blockquote>
<h2 id="🧾-해결방안-탐색-및-해결">🧾 해결방안 탐색 및 해결</h2>
<h3 id="🪄-step-1-standard-→-standard-ia-전환-개발-환경">🪄 Step 1. Standard → Standard-IA 전환 (개발 환경)</h3>
<blockquote>
<p>먼저 개발 환경의 모든 테이블을 Standard → Standard-IA(Infrequent Access) 스토리지 클래스로 변경했습니다.</p>
</blockquote>
<ul>
<li>Standard-IA 특징<ul>
<li>자주 접근하지 않는 데이터를 위해 설계</li>
<li>GB당 저장 비용은 저렴</li>
<li>단 읽기/쓰기 요청이 많이 발생했을때 추가 비용 발생 가능성 높음</li>
</ul>
</li>
</ul>
<h4 id="👉-dev-환경은-테스트-시에만-접근하므로-ia로-변경-시-읽기쓰기-요청은-적지만-저장-데이터는-많기-때문에-확실히-유리했다">👉 dev 환경은 테스트 시에만 접근하므로 IA로 변경 시 읽기/쓰기 요청은 적지만 저장 데이터는 많기 때문에 확실히 유리했다.</h4>
<hr>
<h3 id="🕒-step-2-ttl--dynamodb-streams--firehose">🕒 Step 2. TTL + DynamoDB Streams + Firehose</h3>
<blockquote>
<p>다음은 오래된 데이터(Old Data) 정리 단계입니다. 데이터를 삭제하는 대신 추적 가능성을 유지하면서도 비용을 줄이는 방식을 택했습니다.</p>
</blockquote>
<ul>
<li>TTL(Time To Live) 설정</li>
<li>DynamoDB Streams 활성화</li>
<li>Lambda → Firehose → S3 파이프라인 구축<ul>
<li>Lambda : DynamoDB Stream [REMOVE] 이벤트 수신 후 Firehose로 JSON 전송</li>
<li>Firehose : JSON → Parquet 포맷 변환</li>
<li>S3(Data Lake) : 장기 보관용 저장소로 아카이빙 →  Glue(Data Catalog) 구조화해서 Athena에서 SQL 조회도 가능</li>
</ul>
</li>
</ul>
<h4 id="👉-삭제된-데이터도-로그-형태로-s3에-안전하게-보관">👉 삭제된 데이터도 로그 형태로 S3에 안전하게 보관</h4>
<hr>
<h3 id="⚙️-step-3-on-demand-→-provisioned-전환-운영-환경">⚙️ Step 3. On-Demand → Provisioned 전환 (운영 환경)</h3>
<blockquote>
<p>운영 환경(prod)에서는 트래픽이 꾸준했기 때문에 온디맨드(On-Demand) 과금 방식이 오히려 비효율적이었습니다. 평균 RCU/WCU(읽기/쓰기 용량 단위)가 안정적으로 유지되는 상태라면 프로비저닝(Provisioned) 모드로 전환하는 것이 훨씬 저렴합니다.</p>
</blockquote>
<ul>
<li>Before : On-demand (요청당 과금)</li>
<li>After : Provisioned (고정 용량 + Auto Scaling)</li>
</ul>
<h4 id="👉-auto-scaling도-같이-설정하여-트래픽-급증-시-자동으로-용량을-확장">👉 Auto Scaling도 같이 설정하여 트래픽 급증 시 자동으로 용량을 확장</h4>
<hr>
<h2 id="💭-회고">💭 회고</h2>
<blockquote>
<p>솔직히 말하면 이번 프로젝트에서 DynamoDB에 대해 깊이 이해하지 못한 채 개발에만 집중해왔던 것 같다. 테이블 만들고 데이터 넣고 조회만 되면 “됐다”라고 생각했던것같다. 당시엔 <strong>“온디맨드(On-Demand) 방식이면 내가 쓴 만큼만 요금이 나오겠지”</strong> 라고 단순하게 생각했다. 하지만 청구서를 보고 많은걸 느꼈던것같다. 이번 비용 절감 작업을 하면서 느꼈던 건 운영 효율도 결국 개발의 일부고 단순히 코드만 잘 짜는게 아니라 서비스의 동작 원리와 과금 구조를 이해하는것이 진짜 백엔드 개발자의 완성이라는 걸 새삼 깨달았다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[🚀 Amazon Q Developer + MCP 환경 구성 가이드]]></title>
            <link>https://velog.io/@dev-hsl-960221/Amazon-Q-Developer-MCP-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1-%EA%B0%80%EC%9D%B4%EB%93%9C-Windows-%EA%B8%B0%EC%A4%80</link>
            <guid>https://velog.io/@dev-hsl-960221/Amazon-Q-Developer-MCP-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1-%EA%B0%80%EC%9D%B4%EB%93%9C-Windows-%EA%B8%B0%EC%A4%80</guid>
            <pubDate>Fri, 17 Oct 2025 05:00:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>Amazon Q Developer</strong>는 개발자를 위한 AI 대화형 비서로 AWS 리소스 관리, 문서 검색, 코드 생성, 인프라 구성까지 VS Code 안에서 대화형으로 수행할 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="🧩-1-vs-code-설치">🧩 1. VS Code 설치</h2>
<blockquote>
<p>Amazon Q Developer는 <strong>VS Code 확장(Extension)</strong> 형태로 동작합니다.</p>
</blockquote>
<ul>
<li><a href="https://code.visualstudio.com/">Visual Studio Code 다운로드</a></li>
<li>운영체제(Windows, macOS, Linux)에 맞게 설치</li>
</ul>
<p>설치 후 실행하면 Amazon Q Extension 을 추가할 수 있습니다.<br>(<code>Extensions → Amazon Q</code> 검색)</p>
<hr>
<h2 id="⚙️-2-uvx-설치-windows-환경">⚙️ 2. uvx 설치 (Windows 환경)</h2>
<blockquote>
<p>Amazon Q Developer의 MCP 서버를 설치하려면 Python 패키지 관리자 <strong>uvx</strong>가 필요합니다.</p>
</blockquote>
<h3 id="🪟-powershell-실행-정책-설정">🪟 PowerShell 실행 정책 설정</h3>
<p><code>Set-ExecutionPolicy RemoteSigned -Scope CurrentUser</code></p>
<h3 id="📦-uv-설치">📦 uv 설치</h3>
<p><code>-ExecutionPolicy Bypass -c &quot;irm https://astral.sh/uv/install.ps1 | iex&quot;</code></p>
<h3 id="✅-설치-확인">✅ 설치 확인</h3>
<p><code>uv --version</code></p>
<hr>
<h2 id="🔑-3-amazon-credential-설정-msi-설치-방식">🔑 3. Amazon Credential 설정 (MSI 설치 방식)</h2>
<blockquote>
<p>Amazon Q Developer는 AWS 계정 권한을 이용해 AWS 모든 리소스를 직접 다룹니다.</p>
</blockquote>
<h3 id="3-1-aws-cli-v2-다운로드-및-설치">3-1. AWS CLI v2 다운로드 및 설치</h3>
<ul>
<li><a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">AWS CLI v2 (Windows MSI)</a>  </li>
<li>다운로드 → 설치 진행</li>
</ul>
<h3 id="3-2-설치-확인">3-2. 설치 확인</h3>
<p><code>aws --version</code></p>
<blockquote>
<p>*<em>예시 출력: *</em><code>aws-cli/2.15.30 Python/3.11.7 Windows/10 exe/AMD64 prompt/off</code></p>
</blockquote>
<h3 id="3-3-aws-자격-증명-설정">3-3. AWS 자격 증명 설정</h3>
<p><code>aws configure</code></p>
<blockquote>
<ul>
<li>입력 항목</li>
</ul>
</blockquote>
<ul>
<li>AWS Access Key ID</li>
<li>AWS Secret Access Key</li>
<li>Default region name</li>
<li>Output format </li>
</ul>
<hr>
<h2 id="🧠-4-mcp-서버-설치">🧠 4. MCP 서버 설치</h2>
<blockquote>
<p>Amazon Q Developer는 MCP (Model Context Protocol) 을 기반으로 AWS API, 문서, 지식베이스에 접근합니다. 각 MCP 서버는 AI에게 AWS 리소스 컨텍스트를 제공합니다.</p>
</blockquote>
<h3 id="4-1-aws-documentation-mcp-server">4-1. AWS Documentation MCP Server</h3>
<ul>
<li>AWS 공식 문서를 직접 참조할 수 있게 하는 MCP 서버</li>
<li><a href="https://awslabs.github.io/mcp/servers/aws-documentation-mcp-server">참고 링크</a>  <h3 id="4-2-aws-api-mcp-server">4-2. AWS API MCP Server</h3>
</li>
<li>AWS API 호출 정보 (예: EC2, Lambda 등) 에 접근할 수 있게 하는 서버</li>
<li><a href="https://awslabs.github.io/mcp/servers/aws-api-mcp-server">참고 링크</a>  <h3 id="4-3-aws-knowledge-mcp-server">4-3. AWS Knowledge MCP Server</h3>
</li>
<li>AWS 내부 기술 문서, 아키텍처 가이드, FAQ 지식 베이스 등을 조회할 수 있게 해주는 MCP 서버</li>
<li><a href="https://awslabs.github.io/mcp/servers/aws-knowledge-mcp-server">참고 링크</a>  </li>
</ul>
<hr>
<h2 id="🚀-5-amazon-q-developer-실전-사용-결과">🚀 5. Amazon Q Developer 실전 사용 결과</h2>
<h3 id="q-dynamodb에서-비용이-많이-발생되는것-같은데-비용-발생-원인-파악과-해결방안">Q. DynamoDB에서 비용이 많이 발생되는것 같은데 비용 발생 원인 파악과 해결방안</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/7750f16f-e75c-4438-ab2a-dc5e84743328/image.png" alt=""></p>
<h3 id="a-분석-→-원인-→-해결-→-절감-효과">A. 분석 → 원인 → 해결 → 절감 효과</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/c617df76-110d-41e5-b2dd-5b18a8d5ef3d/image.png" alt=""></p>
<h4 id="✅-ai를-기반으로-클라우드-환경-구축-가능하다">✅ AI를 기반으로 클라우드 환경 구축 가능하다.</h4>
<h4 id="✅-aws-리소스-관리-코드-생성-인프라-자동화를-대화형으로-통합할-수-있다">✅ AWS 리소스 관리, 코드 생성, 인프라 자동화를 <strong>대화형</strong>으로 통합할 수 있다.</h4>
<h4 id="✅-spring-boot--aws--terraform-환경에서-iacinfrastructure-as-code를-자주-다루는-개발자에게-강력한-생산성-향상을-제공">✅ Spring Boot + AWS + Terraform 환경에서 IaC(Infrastructure as Code)를 자주 다루는 개발자에게 강력한 생산성 향상을 제공</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[🚀 복잡한 시스템을 다루는 4가지 핵심 개념]]></title>
            <link>https://velog.io/@dev-hsl-960221/%EB%B3%B5%EC%9E%A1%ED%95%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%EB%8B%A4%EB%A3%A8%EB%8A%94-4%EA%B0%80%EC%A7%80-%ED%95%B5%EC%8B%AC-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@dev-hsl-960221/%EB%B3%B5%EC%9E%A1%ED%95%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%EB%8B%A4%EB%A3%A8%EB%8A%94-4%EA%B0%80%EC%A7%80-%ED%95%B5%EC%8B%AC-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Wed, 17 Sep 2025 06:52:00 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>현대의 소프트웨어는 단순 CRUD를 넘어서 <strong>복잡한 도메인 로직</strong>과 <strong>높은 확장성 요구</strong>를 동시에 만족해야 합니다.<br>이 과정에서 자주 등장하는 개념이 바로 <strong>DDD, 에그리거트(Aggregate), 바운디드 컨텍스트(Bounded Context), CQRS, 이벤트 소싱(Event Sourcing)</strong> 입니다.  </p>
</blockquote>
<p>이 글에서는 이 다섯 가지 개념이 <strong>어떻게 연결되는지 큰 그림과 흐름을</strong> 다루도록 하겠습니다.</p>
<hr>
<h2 id="📖-1-ddd-domain-driven-design">📖 1. DDD (Domain-Driven Design)</h2>
<blockquote>
<p>DDD의 핵심은 <strong>도메인 전문가와 개발자가 같은 언어(유비쿼터스 언어)를 사용하여 소프트웨어를 설계</strong>하는 것입니다.<br>단순한 데이터 중심 설계가 아니라 <strong>도메인 로직 중심</strong>으로 모델을 만들고, 코드와 비즈니스 개념이 일치하도록 하는 것이 목표입니다.</p>
</blockquote>
<p>DDD의 주요 개념:</p>
<ul>
<li><strong>유비쿼터스 언어</strong>: 도메인 전문가와 개발자가 공유하는 용어 체계</li>
<li><strong>바운디드 컨텍스트</strong>: 모델의 경계를 명확히 하여 혼란 방지</li>
<li><strong>에그리거트</strong>: 경계 내부의 일관성을 유지하는 단위</li>
</ul>
<hr>
<h2 id="📖-2-바운디드-컨텍스트-bounded-context">📖 2. 바운디드 컨텍스트 (Bounded Context)</h2>
<blockquote>
<p>현실 세계의 비즈니스는 크고 복잡합니다. 예를 들어 &quot;회원(Member)&quot;이라는 단어도 
<strong>결제 시스템</strong>에서는 &quot;고객(Customer)&quot; 
<strong>인사 시스템</strong>에서는 &quot;직원(Employee)&quot; 으로 다르게 쓰일 수 있습니다.  </p>
</blockquote>
<p>이를 해결하는 방법이 <strong>바운디드 컨텍스트</strong>입니다.</p>
<ul>
<li>각 컨텍스트는 <strong>자체적인 모델과 의미</strong>를 가집니다.</li>
<li>서로 다른 컨텍스트는 명시적인 인터페이스(API, 이벤트)로 통신합니다.</li>
<li>결과적으로 <strong>경계를 명확히 나눔으로써 모델의 혼란과 충돌을 줄입니다.</strong></li>
</ul>
<p>예시)</p>
<pre><code>            [ 비즈니스 전체 도메인 ]
                  ┌──────────────┐
                  │   회사 운영   │
                  └──────────────┘
                           │
        ┌──────────────────┼──────────────────┐
        │                                      │
 ┌─────────────┐                       ┌───────────────┐
   결제 시스템                            인사 시스템 
   Payment BC                               HR BC     
 └─────────────┘                       └───────────────┘
        │                                      │
 ┌───────────────┐                       ┌───────────────┐
     Customer                               Employee     
      (고객)                                  (직원)       
    - 이름                                  - 이름        
    - 결제수단                              - 부서        
    - 구매내역                              - 급여        
 └───────────────┘                       └───────────────┘
        │                                      │
        └──────────── 명시적 인터페이스(API/이벤트
</code></pre><hr>
<h2 id="📖-3-에그리거트-aggregate">📖 3. 에그리거트 (Aggregate)</h2>
<blockquote>
<p>바운디드 컨텍스트 내부에서도 모든 객체가 뒤엉켜 있으면 유지보수가 어렵습니다.<br>그래서 <strong>일관성 경계(Consistency Boundary)</strong>를 정하고 그 안에서 항상 규칙이 지켜지도록 하는 단위가 <strong>에그리거트</strong>입니다.  </p>
</blockquote>
<ul>
<li><strong>Aggregate Root</strong>: 외부에서 접근 가능한 유일한 진입점</li>
<li><strong>내부 엔티티(Value Object 포함)</strong>: Root를 통해서만 접근/수정 가능</li>
</ul>
<p>예시)</p>
<ul>
<li><code>Order</code> (Root) + <code>OrderItem</code> (내부 엔티티)  </li>
<li><code>Post</code> (Root) + <code>Comment</code> (내부 엔티티)  </li>
<li><code>Account</code> (Root) + <code>Transaction</code> (내부 엔티티)</li>
</ul>
<p>👉 핵심은 <strong>트랜잭션은 Aggregate 단위로 보장</strong>된다는 점입니다.</p>
<hr>
<h2 id="📖-4-cqrs-command-query-responsibility-segregation">📖 4. CQRS (Command Query Responsibility Segregation)</h2>
<blockquote>
<p>일반적으로 하나의 모델로 읽기와 쓰기를 동시에 처리합니다.<br>하지만 트래픽이 커지고 복잡도가 올라가면 <strong>쓰기(Command)와 읽기(Query)</strong>를 분리하는 CQRS 패턴이 등장합니다.</p>
</blockquote>
<ul>
<li><strong>Command 모델</strong>: 상태 변경 담당 (도메인 규칙, 트랜잭션)</li>
<li><strong>Query 모델</strong>: 데이터 조회 담당 (성능 최적화, 캐싱, 별도 저장소)</li>
</ul>
<p>장점:</p>
<ul>
<li>읽기와 쓰기를 따로 확장 가능</li>
<li>복잡한 도메인 로직과 단순 조회 로직 분리</li>
</ul>
<p>단점:</p>
<ul>
<li>결국 <strong>Eventual Consistency(최종적 일관성)</strong> 문제를 다뤄야 함</li>
<li>단순 CRUD 시스템에는 과도한 설계</li>
</ul>
<p>예시)</p>
<pre><code>UserRepository는 쓰기용 DB(JPA 기반)
UserReadRepository는 읽기용 DB(캐시 or 조회 전용 DB)로 나눌 수 있다.</code></pre><hr>
<h2 id="📖-5-이벤트-소싱-event-sourcing">📖 5. 이벤트 소싱 (Event Sourcing)</h2>
<blockquote>
<p>일반적으로 DB에는 &quot;현재 상태&quot;만 저장합니다.<br>이벤트 소싱은 다른 접근을 택합니다. <strong>상태 변화를 만든 모든 이벤트를 저장</strong>합니다.  </p>
</blockquote>
<p>장점:</p>
<ul>
<li>모든 변경 이력을 100% 보존 (추적 용이)</li>
<li>원하는 시점의 상태 복원 가능</li>
<li>이벤트를 다른 시스템에 쉽게 전달 가능 (이벤트 드리븐 아키텍처)</li>
</ul>
<p>단점:</p>
<ul>
<li>이벤트가 많아지면 리플레이 비용 발생 → 스냅샷 필요</li>
<li>이벤트 스키마 진화 관리 어려움</li>
</ul>
<p>예시)</p>
<p>사용자가 주문을 생성/결제/취소하는 과정을 이벤트로 저장</p>
<hr>
<h2 id="📖-6-개념들의-연결">📖 6. 개념들의 연결</h2>
<blockquote>
<p>이 다섯 가지 개념은 따로 노는 것이 아니라 <strong>하나의 스토리</strong>를 이룹니다.</p>
</blockquote>
<ul>
<li><strong>DDD</strong>: 복잡한 도메인을 다루기 위한 철학  </li>
<li><strong>바운디드 컨텍스트</strong>: 모델의 경계를 나누는 방법  </li>
<li><strong>에그리거트</strong>: 경계 내부의 일관성을 지키는 단위  </li>
<li><strong>CQRS</strong>: 확장성을 위한 읽기/쓰기 분리  </li>
<li><strong>이벤트 소싱</strong>: CQRS와 잘 어울리는 상태 관리 방식</li>
</ul>
<blockquote>
<p><strong>DDD → BC → Aggregate → CQRS → Event Sourcing</strong>  이런 흐름으로 연결됩니다.</p>
</blockquote>
<hr>
<h2 id="📖-7-언제-쓰면-좋을까">📖 7. 언제 쓰면 좋을까?</h2>
<ul>
<li>단순한 CRUD 서비스 → 필요 없음 (오버엔지니어링)  </li>
<li><strong>복잡한 비즈니스 규칙 + 높은 확장성 요구 → 적합</strong>  </li>
<li>*<em>금융, 이커머스, 로깅/추적 시스템 → 이벤트 소싱 + CQRS 강력 추천  *</em></li>
</ul>
<hr>
<h2 id="📖-8-마무리">📖 8. 마무리</h2>
<blockquote>
<p>모두 <strong>복잡한 도메인을 효과적으로 다루기 위한 도구</strong>입니다.<br>하지만 &quot;무조건 쓰는 패턴&quot;이 아니라 <strong>도메인 특성과 요구사항에 맞춰 선택적으로 적용</strong>해야 합니다.  </p>
</blockquote>
<hr>
<p>혹시 궁금한 부분이나 더 보고 싶은 주제가 있다면 댓글로 남겨주세요 🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🚀 Spring Boot 3 + Swagger(OpenAPI 3) 적용 가이드]]></title>
            <link>https://velog.io/@dev-hsl-960221/Spring-Boot-3-SwaggerOpenAPI-3-%EC%A0%81%EC%9A%A9-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@dev-hsl-960221/Spring-Boot-3-SwaggerOpenAPI-3-%EC%A0%81%EC%9A%A9-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Thu, 11 Sep 2025 07:50:14 GMT</pubDate>
            <description><![CDATA[<h2 id="📝-개요">📝 개요</h2>
<blockquote>
<p>백엔드 API를 개발하다 보면 프론트엔드/QA 팀과 <strong>API 명세를 문서로 공유</strong>하는 일이 필수적입니다.<br>과거에는 Excel이나 Wiki에 수동으로 작성했지만, 이제는 <strong>Swagger(OpenAPI)</strong> 를 통해 API 문서를 자동으로 생성하고 유지보수할 수 있습니다.  </p>
</blockquote>
<p>이번 글에서는 <strong>Spring Boot 3.4 + Java 21 + Gradle</strong> 환경을 기준으로,  Swagger를 적용하고 운영할 때의 <strong>Best Practice</strong>를 소개합니다.  </p>
<hr>
<h2 id="🔧-1-의존성-추가">🔧 1. 의존성 추가</h2>
<p><code>build.gradle</code>에 OpenAPI 관련 의존성을 추가합니다.<br>Spring Boot 3에서는 <code>springdoc-openapi</code> 라이브러리가 가장 많이 사용됩니다.  </p>
<pre><code class="language-gradle">dependencies {
    implementation &#39;org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0&#39;
}
</code></pre>
<hr>
<h2 id="📌-2-swagger-ui-접근-방법">📌 2. Swagger UI 접근 방법</h2>
<p>의존성만 추가하면 자동으로 <strong>Swagger UI</strong> 가 활성화됩니다.</p>
<p>기본 경로: http://{{호스트}}:{{포트}}/swagger-ui.html</p>
<p>OpenAPI 문서(JSON): http://{{호스트}}:{{포트}}/v1/api-docs</p>
<p>회사 내 <strong>보안 정책에 따라 접근 권한을 제어</strong>하거나, <strong>프로필별로 설정을 분리</strong>하는 것이 중요합니다.</p>
<hr>
<h2 id="⚙️-3-swagger-설정-커스터마이징">⚙️ 3. Swagger 설정 커스터마이징</h2>
<pre><code>/**
 * Swagger UI 상단에 표시되는 API 정보가 이 부분에서 설정
 */
@Configuration
@OpenAPIDefinition(
        info = @Info(
                title = &quot;테스트 전용 API&quot;,
                description = &quot;개인적으로 테스트하기 위한 API 문서입니다&quot;,
                version = &quot;v1&quot;
        )
)
public class SwaggerConfiguration {

}



/**
 * Swagger 화면에서 사용자 편의성을 높이는 옵션
 * Swagger UI에서 사용자/관리자 API를 탭으로 구분
 */

springdoc:
  default-consumes-media-type: application/json
  default-produces-media-type: application/json
  api-docs:
    path: /v1/api-docs
  swagger-ui:
    path: /swagger-ui.html
    operations-sorter: alpha
    tags-sorter: alpha
    display-query-params-without-oauth2: true
    disable-swagger-default-url: true
    doc-expansion: list
    defaultModelsExpandDepth: 1
    defaultModelExpandDepth: 2
    defaultModelRendering: model
    show-extensions: true
    show-common-extensions: true
    display-request-duration: true
    try-it-out-enabled: true
  group-configs:
    - group: 사용자
      paths-to-match:
        - /v1/member/**
        - /v1/board/**
    - group: 관리자
      paths-to-match:
        - /v1/admin/**</code></pre><hr>
<h2 id="🛠-4-controller-예제">🛠 4. Controller 예제</h2>
<pre><code>/**
 * 사용자 생성 API
 */
    @Operation(
            summary = &quot;사용자 생성&quot;,
            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
                    required = true,
                    content = @Content(
                            mediaType = &quot;application/json&quot;,
                            schema = @Schema(implementation = CreateMemberRequest.class)
                    )
            ),
            responses = {
                    @ApiResponse(
                            responseCode = &quot;201&quot;,
                            description = &quot;사용자 생성 완료&quot;,
                            content = {
                                    @Content(
                                            mediaType = &quot;application/json&quot;,
                                            schema = @Schema(implementation = CommonResponse.class)
                                    ),
                            }
                    ),
            }
    )
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public CommonResponse createMember(@Valid @RequestBody CreateMemberRequest createMemberRequest) {
        Member member = memberWebMapper.toDomain(createMemberRequest);
        Member resultMember = createMemberUseCase.createMember(member);
        CreateMemberResponse createMemberResponse = memberWebMapper.toCreateMemberResponse(resultMember);
        CommonResponse commonResponse = new CommonResponse&lt;&gt;(&quot;사용자 생성이 정상적으로 처리됐습니다&quot;, createMemberResponse);
        return commonResponse;
    }</code></pre><hr>
<h2 id="👉-5-활용-사례">👉 5. 활용 사례</h2>
<blockquote>
<ul>
<li><strong>백엔드(Spring Boot)</strong> → /v1/api-docs 제공</li>
<li><strong>프론트엔드</strong> → OpenAPI Generator, orval, swagger-typescript-api 등을 활용하여<ul>
<li>타입 정의</li>
<li>API 호출 코드</li>
<li>React Query/SWR 훅 자동으로 생성 가능 </li>
</ul>
</li>
</ul>
</blockquote>
<hr>
<h2 id="🎯-6-마무리">🎯 6. 마무리</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/6cf44ae6-68c8-4c39-b881-7f75fc62362e/image.png" alt=""></p>
<blockquote>
<p>Swagger(OpenAPI)는 <strong>API 개발/운영에서 협업 효율을 극대화</strong>하는 도구입니다.<br>Spring Boot에서는 <code>springdoc-openapi</code>를 통해 쉽게 적용할 수 있고,<br>보안/프로필 분리/문서 관리까지 신경쓴다면 <strong>실무에 바로 적용 가능한 베스트 프랙티스</strong>가 됩니다.  </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[✨ 헥사고날 아키텍처(Ports & Adapters)]]></title>
            <link>https://velog.io/@dev-hsl-960221/%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98Ports-Adapters</link>
            <guid>https://velog.io/@dev-hsl-960221/%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98Ports-Adapters</guid>
            <pubDate>Mon, 18 Aug 2025 09:40:30 GMT</pubDate>
            <description><![CDATA[<h1 id="🏛️-헥사고날-아키텍처란">🏛️ 헥사고날 아키텍처란?</h1>
<blockquote>
<p>&quot;도메인 로직을 외부 의존성으로부터 격리하여 
더 유연하고 테스트 가능한 애플리케이션을 만든다!&quot;</p>
</blockquote>
<ul>
<li><p>예시</p>
<ul>
<li><p><strong>UserService</strong>가 곧바로 <strong>JPA Repository와 EmailClient에 의존</strong></p>
<pre><code>@Service
public class UserService {

private final UserRepository userRepository; // JPA Repository
private final EmailClient emailClient;       // 외부 Email API

</code></pre></li>
</ul>
</li>
</ul>
<pre><code>...</code></pre><p>}</p>
<pre><code>---

## ⚡ 핵심 개념
- 🧩 **Domain (코어 비즈니스 로직)**
- 🔌 **Ports (인터페이스, 추상화)**
- 🔄 **Adapters (외부 연동: DB, API, 메시징 등)**

---


### 📊 아키텍처 비교

| 구조 | 특징 | 문제점 |
|------|------|--------|
| 🏗️ Layered | Controller → Service → Repository | 외부 의존성(DB, 외부 API)에 강하게 묶임 |
| 🔷 Hexagonal | Domain 중심, Ports &amp; Adapters | 외부 교체 용이, 테스트 용이 |
---

### 🗂️ 실제 나의 토이 프로젝트 레이어 구조 (예시)
</code></pre><p>📦 user                                     Bounded Context (User 도메인 전체 경계)
 ┣ 📂 adapter                              Adapter Layer (외부 세계와 연결 지점)
 ┃  ┣ 📂 in                                Inbound Adapter (들어오는 요청 처리)
 ┃  ┃  ┗ 📂 web                            REST API Controller, Request/Response DTO
 ┃  ┗ 📂 out                               Outbound Adapter (외부 자원 연동)
 ┃     ┗ 📂 persistence                    JPA Entity, Repository, Mapper (DB 연동)
 ┣ 📂 application                          Application Layer (유즈케이스 중심)
 ┃  ┣ 📂 port                              Port (도메인 ↔ Adapter 느슨한 연결/계약)
 ┃  ┃  ┣ 📂 in                             Inbound Port (UseCase 인터페이스)
 ┃  ┃  ┗ 📂 out                            Outbound Port (Persistence 인터페이스)
 ┃  ┗ 📂 service                           Application Service (유즈케이스 구현체)
 ┗ 📂 domain                               Domain Layer (핵심 비즈니스 모델/로직)</p>
<pre><code>
---

## 💡 정리 &amp; 회고
- ✅ 장점: 테스트 용이, 도메인 로직의 독립성 확보
- ⚠️ 단점: 구조가 복잡함. 소규모 프로젝트에는 과한 느낌
- 🧭 개인적으로는 중장기 유지보수 프로젝트에서 큰 힘이 됨

</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[🚀 PRD 정의]]></title>
            <link>https://velog.io/@dev-hsl-960221/PRD-%EC%A0%95%EC%9D%98</link>
            <guid>https://velog.io/@dev-hsl-960221/PRD-%EC%A0%95%EC%9D%98</guid>
            <pubDate>Wed, 13 Aug 2025 01:57:03 GMT</pubDate>
            <description><![CDATA[<h1 id="🔍-prdproduct-requirements-document란-무엇인가">🔍 PRD(Product Requirements Document)란 무엇인가?</h1>
<blockquote>
<p>제품을 기획하고 개발할 때, <strong>&quot;무엇을&quot;, &quot;왜&quot;, &quot;어떻게 만들 것인가&quot;</strong>에 대한 방향성을 명확히 하기 위해 작성하는 문서가 <strong>PRD</strong> 입니다.</p>
</blockquote>
<h3 id="🤔-개발이-시작되기-전까지-우리는-종종-다음과-같은-질문을-받거나-합니다">🤔 개발이 시작되기 전까지 우리는 종종 다음과 같은 질문을 받거나 합니다.</h3>
<ul>
<li>이 기능은 <strong>왜</strong> 필요한가요?</li>
<li><strong>어떤 유저</strong>를 위한 기능인가요?</li>
<li><strong>어떤 상황</strong>에서 <strong>어떤 조건</strong>으로 동작하나요?</li>
</ul>
<p>이 질문들에 대한 공통된 답을 팀 전체가 할 수 있도록 해주는 것이 바로 <strong>PRD</strong> 입니다.</p>
<hr>
<h2 id="🧩-prd의-핵심-구성-요소">🧩 PRD의 핵심 구성 요소</h2>
<p>아래는 대부분의 조직에서 공통적으로 사용하는 PRD 기본 구성입니다. 상황에 따라 유연하게 커스터마이징할 수 있습니다.</p>
<h3 id="1-✨-목표goal">1. ✨ 목표(Goal)</h3>
<ul>
<li>이 문서를 작성하는 목적과 해당 기능/제품이 기여할 비즈니스 목표<ul>
<li>예 : <strong>&quot;GPS를 기반으로 사용자의 위치에 따른 지역 날씨정보를 실시간으로 제공해줌으로써 편의성을 제공&quot;</strong></li>
</ul>
</li>
</ul>
<h3 id="2-👤-대상-사용자user-persona">2. 👤 대상 사용자(User Persona)</h3>
<ul>
<li>주요 사용자 유형과 어떤 문제를 겪고 있는지 그리고 어떤 가치를 제공할 것인지<ul>
<li>예 : <strong>&quot;출퇴근 직장인, 학부모, 학생, 러너&quot;</strong></li>
</ul>
</li>
</ul>
<h3 id="3-📚-배경background--context">3. 📚 배경(Background &amp; Context)</h3>
<ul>
<li>기존의 문제 상황 또는 시장 조사나 데이터 기반 인사이트<ul>
<li>예 : 사내 VOC 분석 및 사용자 설문 결과에서도 “<strong>실제 느낌을 반영한 날씨 정보가 부족하다</strong>”는 피드백 다수 존재</li>
</ul>
</li>
</ul>
<h3 id="4-📌-요구사항requirements">4. 📌 요구사항(Requirements)</h3>
<h4 id="기능적-요구사항">기능적 요구사항</h4>
<ul>
<li>유저 플로우 (시나리오 기반)<ul>
<li>예 : <strong>&quot;사용자는 Kakao 또는 Google 계정을 통해 로그인할 수 있어야 한다.&quot;</strong></li>
</ul>
</li>
</ul>
<h4 id="비기능적-요구사항">비기능적 요구사항</h4>
<ul>
<li>성능, 보안, 확장성 등<ul>
<li>예 : <strong>&quot;로그인 응답 시간은 200ms 이내 여야한다.&quot;</strong></li>
</ul>
</li>
</ul>
<h3 id="5-✅-성공-기준success-metrics">5. ✅ 성공 기준(Success Metrics)</h3>
<ul>
<li>이 기능이 성공했다고 말할 수 있는 기준<ul>
<li>예 : <strong>&quot; DAU 증가, 전환율 개선, 사용자 만족도&quot;</strong></li>
</ul>
</li>
</ul>
<h3 id="6-🔄-스코프-정의">6. 🔄 스코프 정의</h3>
<ul>
<li>포함 범위 (In Scope)<ul>
<li>예 : <strong>&quot;이번 스프린트에서는 OOO까지가 in-scope 입니다.&quot;</strong></li>
</ul>
</li>
<li>제외 범위 (Out of Scope)<ul>
<li>예 : <strong>&quot;해당 기능은 out-of-scope 이지만 다음 사이클에서 다룰 예정입니다.&quot;</strong></li>
</ul>
</li>
</ul>
<h3 id="7-🎨-와이어프레임--ux-흐름optional">7. 🎨 와이어프레임 / UX 흐름(Optional)</h3>
<ul>
<li>기본적인 화면 구조</li>
<li>인터랙션 애니메이션 </li>
<li>스크린 플로우</li>
</ul>
<hr>
<h2 id="💡-실무에서의-prd-작성-팁">💡 실무에서의 PRD 작성 팁</h2>
<ul>
<li><strong>모호함은 줄이고 구체적으로 작성하기</strong> </li>
<li><strong>개발자, 디자이너, PM 모두를 위한 문서이기 때문에 기술적, 기획적, 디자인 관점에서 누구나 이해할 수 있어야 한다.</strong></li>
<li><strong>PRD는 살아있는 문서이기때문에 항상 최신 업데이트하기</strong></li>
</ul>
<hr>
<h2 id="🙋-prd는-왜-중요한가">🙋 PRD는 왜 중요한가?</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>명확한 커뮤니케이션</td>
<td>모든 팀원이 같은 방향을 바라볼 수 있음</td>
</tr>
<tr>
<td>개발 낭비 최소화</td>
<td>중복 기능/비효율적인 구현 방지</td>
</tr>
<tr>
<td>QA 및 테스트 기준 수립</td>
<td>테스트 케이스 기준 문서로도 활용 가능</td>
</tr>
<tr>
<td>이후 회고의 기준점</td>
<td>의도 vs 결과 비교가 쉬워짐</td>
</tr>
</tbody></table>
<hr>
<h2 id="🧠-마무리">🧠 마무리</h2>
<p>PRD는 단순한 기획 문서가 아닙니다. 개발자와 디자이너, 기획자, 심지어 마케팅팀까지 같은 이해를 공유하도록 도와주는 <strong>소통의 도구</strong> 이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[💻 WebClient vs HttpClient & ReactorClientHttpConnector]]></title>
            <link>https://velog.io/@dev-hsl-960221/WebClient-vs-HttpClient-ReactorClientHttpConnector</link>
            <guid>https://velog.io/@dev-hsl-960221/WebClient-vs-HttpClient-ReactorClientHttpConnector</guid>
            <pubDate>Fri, 08 Aug 2025 08:52:37 GMT</pubDate>
            <description><![CDATA[<h1 id="1️⃣-정의">1️⃣ 정의</h1>
<h3 id="💻-webclient">💻 WebClient</h3>
<ul>
<li>Spring Boot에서 제공하는 <strong>고수준(High-level) 비동기·논블로킹 기반 HTTP 클라이언트</strong></li>
<li><strong>Spring WebFlux</strong> 기반</li>
<li>응답 모델 <strong>자동 역직렬화</strong> (Jackson 등)</li>
<li>메소드 체이닝 방식</li>
</ul>
<h3 id="📡-httpclient">📡 HttpClient</h3>
<ul>
<li><strong>저수준(Low-level) 비동기 HTTP 클라이언트</strong></li>
<li><strong>Reactor-Netty</strong> 기반</li>
<li>응답은 ByteBuf/ByteBuffer 형태로 제공 → JSON은 <strong>직접 파싱</strong> 필요</li>
<li>커넥션 풀, 타임아웃, SSL 등 <strong>세부 튜닝 가능</strong></li>
</ul>
<h3 id="🧩-reactorclienthttpconnector">🧩 ReactorClientHttpConnector</h3>
<ul>
<li><strong>WebClient</strong>와 <strong>HttpClient</strong>를 연결하는 <strong>어댑터/브리지</strong> 역할</li>
<li><code>HttpClient</code>에서 세부 설정한 후, 이를 WebClient에 주입</li>
</ul>
<hr>
<h2 id="2️⃣-주요-차이점">2️⃣ 주요 차이점</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>💻 WebClient</th>
<th>📡 HttpClient</th>
</tr>
</thead>
<tbody><tr>
<td>레벨</td>
<td>High-level</td>
<td>Low-level</td>
</tr>
<tr>
<td>기반</td>
<td>Spring WebFlux</td>
<td>reactor-netty</td>
</tr>
<tr>
<td>직렬화</td>
<td>자동(Jackson 등)</td>
<td>수동(직접 파싱)</td>
</tr>
<tr>
<td>커스터마이징</td>
<td>제한적(커넥터 통해 가능)</td>
<td>자유도 높음</td>
</tr>
<tr>
<td>러닝커브</td>
<td>중</td>
<td>상</td>
</tr>
<tr>
<td>권장 용도</td>
<td>일반 REST API 호출</td>
<td>네트워크 세부 튜닝, 특수 상황</td>
</tr>
</tbody></table>
<hr>
<h2 id="3️⃣-장단점">3️⃣ 장단점</h2>
<h3 id="💻-webclient-1">💻 WebClient</h3>
<p><strong>장점 ✅</strong></p>
<ul>
<li>자동 직렬화/역직렬화</li>
<li>필터, 리트라이, 백프레셔 등 리액티브 패턴 쉽게 구현</li>
<li>코드 간결</li>
</ul>
<p><strong>단점 ⚠️</strong></p>
<ul>
<li>세부 네트워크 설정은 <code>HttpClient</code> 의존</li>
<li>리액티브 프로그래밍 러닝 커브 존재</li>
</ul>
<h3 id="📡-httpclient-1">📡 HttpClient</h3>
<p><strong>장점 ✅</strong></p>
<ul>
<li>세부 네트워크 설정 가능(ConnectionProvider, 타임아웃, TLS, 프록시 등)</li>
<li>대규모 트래픽·특수 프로토콜에 대응 가능</li>
</ul>
<p><strong>단점 ⚠️</strong></p>
<ul>
<li>JSON 파싱, 에러 처리, 리트라이 직접 구현 필요</li>
<li>코드 복잡도 증가</li>
</ul>
<hr>
<h2 id="4️⃣-httpclient--webclient--reactorclienthttpconnector-실전-예시">4️⃣ HttpClient + WebClient &amp; ReactorClientHttpConnector 실전 예시</h2>
<pre><code>    /**
     * 공통 커넥션 풀 설정 (커넥션 재사용을 위함)
     */
    @Bean
    public ConnectionProvider connectionProvider() {
        return ConnectionProvider.builder(&quot;test-pool&quot;)
                .maxConnections(100)
                .pendingAcquireMaxCount(500)
                .pendingAcquireTimeout(Duration.ofSeconds(30))
                .build();
    }




    /**
     * Webclient 정의 (ReactorClientHttpConnector 활용하여
     */
    @Bean
    public HttpClient createHttpClient(ConnectionProvider connectionProvider) {

        SslContext sslContext;
        try {
            sslContext = SslContextBuilder.forClient().build();
        } catch (SSLException e) {
            throw new CustomRuntimeException(ErrorEnum.SSL_CONTEXT_ERROR);
        }
        return HttpClient.create(connectionProvider)
                .secure(sslContextSpec -&gt; sslContextSpec.sslContext(sslContext).handshakeTimeout(Duration.ofSeconds(30)))
                .responseTimeout(Duration.ofSeconds(90))
                .doOnConnected(conn -&gt; conn
                        .addHandlerLast(new ReadTimeoutHandler(90))
                        .addHandlerLast(new WriteTimeoutHandler(5)));

    }    



    /**
     * 공통 HttpClient (응답 타임아웃 설정 포함)
     */    
    @Bean
    public WebClient testWebClient(HttpClient httpClient) {

        DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(&quot;https://example.com/test&quot;);
        factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);

        //WebClient 기본 버퍼가 256KB 때문에 초과하면 BufferLimitException 발생
        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .uriBuilderFactory(factory)
                .baseUrl(&quot;https://example.com/test&quot;)
                .exchangeStrategies(ExchangeStrategies.builder()
                        .codecs(configurer -&gt; configurer
                                .defaultCodecs()
                                .maxInMemorySize(10 * 1024 * 1024)) // ✅ 최대 10MB
                        .build())
                .build();
    }</code></pre><hr>
<h2 id="5️⃣-mono-기반의-비동기·논블로킹-호출">5️⃣ Mono 기반의 비동기·논블로킹 호출</h2>
<pre><code>@Component
@RequiredArgsConstructor
public class TestProvider {


    private final WebClient testWebClient;


    /**
     * Mono 활용한 비동기 호출
     */
    private Mono&lt;TestDTO&gt; test() {

        return testWebClient.get()
                .uri(uriBuilder -&gt; uriBuilder
                        .queryParam(&quot;serviceKey&quot;, &quot;testKey&quot;)
                        .queryParam(&quot;pageNo&quot;, 1)
                        .queryParam(&quot;numOfRows&quot;, 1000)
                        .queryParam(&quot;returnType&quot;, &quot;JSON&quot;)
                        .build())
                .retrieve()
                .bodyToMono(TestDTO.class)
                .doOnError(TimeoutException.class, e -&gt; log.error(&quot;⏳ TimeoutException 발생 : {}&quot;, e.getMessage()))
                .doOnError(IllegalStateException.class, e -&gt; log.error(&quot;⏳ IllegalStateException 발생 : {}&quot;, e.getMessage()))
                .doOnError(WebClientResponseException.class, e -&gt; log.error(&quot;⏳ WebClientResponseException 발생 : {}&quot;, e.getMessage()))
                .retryWhen(Retry.fixedDelay(5, Duration.ofSeconds(5))
                        .filter(throwable -&gt; throwable instanceof TimeoutException || throwable instanceof IllegalStateException || throwable instanceof WebClientResponseException)
                        .doBeforeRetry(retrySignal -&gt; {
                            Throwable failure = retrySignal.failure();
                            log.warn(&quot;🔁 [ 테스트 서드파티  {}회차 재시도] 이유: {}&quot;,
                                    retrySignal.totalRetries() + 1,
                                    failure.getMessage());
                        }));
    }
}

</code></pre><hr>
<h2 id="결론-🎯">결론 🎯</h2>
<ul>
<li>일반적인 REST API 호출 → WebClient 단독 사용</li>
<li>세밀한 네트워크 제어 필요 시 → HttpClient 설정 후 ReactorClientHttpConnector로 WebClient에 주입</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[💡 개발 문화와 조직의 태도에 대한 회고]]></title>
            <link>https://velog.io/@dev-hsl-960221/%EA%B0%9C%EB%B0%9C-%EB%AC%B8%ED%99%94%EC%99%80-%EC%A1%B0%EC%A7%81%EC%9D%98-%ED%83%9C%EB%8F%84%EC%97%90-%EB%8C%80%ED%95%9C-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dev-hsl-960221/%EA%B0%9C%EB%B0%9C-%EB%AC%B8%ED%99%94%EC%99%80-%EC%A1%B0%EC%A7%81%EC%9D%98-%ED%83%9C%EB%8F%84%EC%97%90-%EB%8C%80%ED%95%9C-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 07 Aug 2025 09:08:50 GMT</pubDate>
            <description><![CDATA[<h2 id="1-📌-코드-리뷰가-없다는-것의-의미">1. 📌 코드 리뷰가 없다는 것의 의미</h2>
<blockquote>
<p>코드 리뷰가 없다는 것은 <strong>그 누구도 내 코드에 책임지지 않는다는 뜻</strong>일수도 있다고 생각했다.</p>
</blockquote>
<h3 id="❗-그-순간의-깨달음">❗ 그 순간의 깨달음</h3>
<ul>
<li>코드리뷰가 존재하지 않는 조직은 <strong>빠른 구현만을 미덕</strong>으로 여기는 경향이 있음</li>
<li>품질, 안정성은 <strong>논의조차 되지 않았음</strong></li>
<li>코드에 대한 <strong>집단적 신뢰 구조가 부재</strong></li>
</ul>
<hr>
<h2 id="2-🔥-장애에-대한-무감각한-대응">2. 🔥 장애에 대한 무감각한 대응</h2>
<blockquote>
<p>시스템보다 <strong>문제를 대하는 조직의 태도</strong>가 더 무서웠다.</p>
</blockquote>
<ul>
<li>충분하지 않은 회귀테스트 및 엣지 테스트 진행, 테스트 코드 미작성 → 애플리케이션 장애 발생</li>
<li>장애 후 팀의 회고 : &quot;<strong>장애가 되는 부분을 어떻게 해결할지 보다는 서비스에서 빼는 방향으로 생각</strong>&quot;</li>
</ul>
<h3 id="⚠-조직의-대응-방식">⚠ 조직의 대응 방식</h3>
<ul>
<li>근본 원인 분석 없음</li>
<li>재발 방지 대책 없음</li>
<li>“장애에 대해서 해결방안을 제시하거나 생각하기보다는 제거한다”는 <strong>임시 방편</strong>만 남음</li>
</ul>
<hr>
<h2 id="3-🧱-변화에-둔감한-조직-무력한-개인">3. 🧱 변화에 둔감한 조직, 무력한 개인</h2>
<blockquote>
<p>“지금 잘 돌아가는데 왜 고치냐”는 말은 <strong>성장을 가로막는 가장 단단한 벽</strong>이었다. </p>
</blockquote>
<ul>
<li>언제 터질지 모르는 시한폭탄</li>
<li>레거시 코드를 리팩토링 하려는 자세가 안보임</li>
<li>새로운 기술 제안도 &quot;우린 그거 안 써봤어 또는 두려움&quot;</li>
<li>변화에 둔감한 조직에서 <strong>도전은 유난으로 취급됨</strong></li>
</ul>
<h3 id="😞-자존감을-갉아먹는-환경">😞 자존감을 갉아먹는 환경</h3>
<ul>
<li><strong>성장이 불가능한 토양</strong> 위에 서 있었음</li>
<li>이것은 <strong>건강한 책임감이 아닌 조직이 만든 가스라이팅</strong></li>
</ul>
<hr>
<h2 id="4-🧭-오너십과-문제-중심-성장에-대한-생각">4. 🧭 오너십과 문제 중심 성장에 대한 생각</h2>
<blockquote>
<p>오너십은 <strong>열정</strong>과 <strong>책임</strong>을 요구한다기보다 <strong>문제 중심의 성장을 위해 시야를 넓혀주는 말</strong>이라고 생각한다.</p>
</blockquote>
<ul>
<li>업무를 ‘내 일’처럼 생각하고 주도적으로 접근하는 태도</li>
<li>단순한 ‘시키는 일’ 수행이 아니라 전체 맥락 속 역할을 이해하려는 자세</li>
</ul>
<h3 id="🧠-오너십을-기르는-방법">🧠 오너십을 기르는 방법</h3>
<ul>
<li>스스로 &quot;<strong>왜 이 작업이 필요한가?</strong>&quot;를 생각하고 정의해보기</li>
<li>전체 업무 흐름 속에서 <strong>내가 맡은 작업의 위치와 맥락</strong> 이해하기</li>
<li>작업을 진행하기 전/후로 <strong>사용자 입장에서 시나리오를 상상</strong>해보기</li>
<li>문제가 발생했을 때 <strong>기술적 수정에 그치지 않고 본질을 되짚어보기</strong></li>
</ul>
<hr>
<h2 id="5-❓-왜를-묻는-습관">5. ❓ &#39;왜&#39;를 묻는 습관</h2>
<blockquote>
<p>&#39;왜?&#39;라는 질문은 단순한 호기심이 아닌 <strong>업무 맥락을 더 깊게 이해하려는 시도</strong>다.</p>
</blockquote>
<ul>
<li>왜 이 기능이 필요한가?</li>
<li>왜 이 방식으로 구현해야 하는가?</li>
<li>왜 지금 이 시점에서 이 기능을 진행하는가?</li>
</ul>
<h3 id="⚠-왜를-묻지-않는-개발자의-특징">⚠ &#39;왜&#39;를 묻지 않는 개발자의 특징</h3>
<ul>
<li>시킨 일만 수행</li>
<li>문서만 보고 구현</li>
<li>피드백 오면 고치고 끝</li>
<li>팀원들과 소통 없이 개인 단위로 개발</li>
</ul>
<hr>
<h2 id="6-🧩-기술보다-맥락을-먼저-이해하는-개발자">6. 🧩 기술보다 맥락을 먼저 이해하는 개발자</h2>
<blockquote>
<p>기술 중심 사고보다 <strong>문제 중심 사고</strong>가 우선이다.</p>
</blockquote>
<ul>
<li>문제 정의와 문제를 어떻게 해결할지 방법을 생각하면서 기술을 선택하는 자세가 필요함 </li>
<li>기술 선택은 <strong>개인의 흥미나 유행</strong>이 아니라 <strong>문제를 해결하기 위한 맥락과 필요에 따른 결정</strong>이어야 한다.</li>
</ul>
<hr>
<h2 id="7-🌱-성장에-대한-관점">7. 🌱 성장에 대한 관점</h2>
<blockquote>
<p>성장은 어느 날 <strong>갑자기</strong> 오지 않는다.</p>
</blockquote>
<ul>
<li>별것 아닌 듯 지나쳤던 요소 하나하나를 <strong>주의 깊게 바라보기 시작할 때</strong></li>
<li>작은 의문과 고민들이 쌓여 진짜 성장으로 이어진다.</li>
</ul>
<hr>
<h1 id="📝-한-줄-결론">📝 한 줄 결론</h1>
<p>좋은 개발자는 <strong>“비즈니스 문제를 정확히 인식하고 그 해결을 위한 코드를 고민하고 조직과 커뮤니케이션하며 함께 성장하는 사람”</strong>이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🧩 SpringBoot + MapStruct로 시작하는 객체 매핑 자동화]]></title>
            <link>https://velog.io/@dev-hsl-960221/SpringBoot-MapStruct%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EA%B0%9D%EC%B2%B4-%EB%A7%A4%ED%95%91-%EC%9E%90%EB%8F%99%ED%99%94</link>
            <guid>https://velog.io/@dev-hsl-960221/SpringBoot-MapStruct%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EA%B0%9D%EC%B2%B4-%EB%A7%A4%ED%95%91-%EC%9E%90%EB%8F%99%ED%99%94</guid>
            <pubDate>Wed, 30 Jul 2025 07:21:09 GMT</pubDate>
            <description><![CDATA[<p>팀 프로젝트에서 매번 DTO 매핑을 수동으로 하다 보니 피로감이 쌓였습니다.
 그래서 도입한 MapStruct! 이 글에서는 그 개념부터 실전 코드까지 소개해보겠습니다.</p>
<blockquote>
<p>MapStruct로 빠르고 깔끔하게 매핑 자동화하기!</p>
</blockquote>
<hr>
<h2 id="📌-mapstruct란">📌 MapStruct란?</h2>
<p><strong>MapStruct</strong>는 Java Bean 간의 매핑 코드를 <strong>컴파일 타임에 자동 생성</strong>해주는 객체 매핑 라이브러리입니다.<br>Lombok처럼 <strong>Annotation 기반</strong>으로 정의하면, <code>@Mapper</code> 인터페이스 구현체를 자동으로 만들어줍니다.</p>
<blockquote>
<p>✔️ 수동 매핑에서 해방되고, 성능도 뛰어난 객체 매퍼입니다.</p>
</blockquote>
<hr>
<h2 id="✅-mapstruct의-주요-특징">✅ MapStruct의 주요 특징</h2>
<ul>
<li><strong>컴파일 타임 생성</strong>: 리플렉션을 사용하지 않아 성능이 빠름</li>
<li><strong>타입 안정성</strong>: 컴파일 오류로 잘못된 매핑을 사전에 방지</li>
<li><strong>간결한 코드</strong>: 반복적인 매핑 로직 제거</li>
<li><strong>커스터마이징 가능</strong>: 커스텀 매핑, 조건부 매핑 등 유연한 설정 지원</li>
</ul>
<hr>
<h2 id="🌟-mapstruct의-장점">🌟 MapStruct의 장점</h2>
<table>
<thead>
<tr>
<th>장점</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>✅ 퍼포먼스 우수</td>
<td>컴파일 타임에 코드를 생성 → 런타임 오버헤드 없음</td>
</tr>
<tr>
<td>✅ 반복 제거</td>
<td>DTO ↔ Entity 간 매핑 코드 자동 생성</td>
</tr>
<tr>
<td>✅ 오류 방지</td>
<td>필드 누락, 타입 불일치 시 컴파일 오류 유도</td>
</tr>
<tr>
<td>✅ 유지보수 용이</td>
<td>명시적 매핑으로 추적 및 테스트 용이</td>
</tr>
</tbody></table>
<hr>
<h2 id="⚠️-mapstruct의-단점">⚠️ MapStruct의 단점</h2>
<table>
<thead>
<tr>
<th>단점</th>
<th>보완 방법</th>
</tr>
</thead>
<tbody><tr>
<td>❌ 런타임 유연성 부족</td>
<td>컴파일 타임 기반이라 동적 매핑은 어려움</td>
</tr>
<tr>
<td>❌ Lombok과의 궁합 이슈</td>
<td>일부 IDE 설정 및 버전에 따라 컴파일 순서 충돌 가능</td>
</tr>
<tr>
<td>❌ 고급 로직 처리 어려움</td>
<td>복잡한 매핑은 <code>@AfterMapping</code>, <code>expression</code> 등을 직접 작성해야 함</td>
</tr>
</tbody></table>
<hr>
<h2 id="🛠️-실무-적용-예시">🛠️ 실무 적용 예시</h2>
<p>아래는 실무에서 사용한 MapStruct 기반 DTO ↔ Entity 매핑 예시입니다.</p>
<pre><code>
// com.example.demo.member.adapter.in.web


@Mapper(componentModel = &quot;spring&quot;)
public interface MemberWebMapper {

    Member toDomain(CreateMemberRequest createMemberRequest);

    CreateMemberResponse toCreateMemberResponse(Member member);

    GetMemberResponse toGetMemberResponse(Member member);

    default Member toDomain(String memberId) {
        return Member.builder()
                .id(memberId)
                .build();
    }
}

// com.example.demo.member.adapter.out.persistence


@Mapper(componentModel = &quot;spring&quot;)
public interface MemberPersistenceMapper {

    MemberJpaEntity toJpaEntity(Member member);

    Member toDomain(MemberJpaEntity memberJpaEntity);

}
</code></pre><hr>
<h4 id="📂-헥사고날-아키텍처-기반-책임분리">📂 헥사고날 아키텍처 기반 책임분리</h4>
<blockquote>
<p><strong>Persistence 영역과 사용자 useCase영역을 분리 이후 MapStruct 사용</strong></p>
</blockquote>
<hr>
<h2 id="🧪-테스트-코드-예시">🧪 테스트 코드 예시</h2>
<pre><code>
@DisplayName(&quot;Member Domain Unit Test&quot;)
public class MemberTest {

    private final MemberWebMapper mapper = new MemberWebMapperImpl();

    @Test
    void 회원_정상_생성_요청에서_도메인_변환() {
        // given
        CreateMemberRequest request = new CreateMemberRequest(
                &quot;user@example.com&quot;,
                &quot;QWERasdf1234!&quot;,
                &quot;홍길동&quot;,
                GenderEnum.MALE,
                &quot;010-1234-5678&quot;,
                &quot;서울특별시 강남구 테헤란로 123&quot;
        );

        // when
        Member member = mapper.toDomain(request);

        // then
        assertAll(
                () -&gt; assertEquals(&quot;user@example.com&quot;, member.getEmail()),
                () -&gt; assertEquals(&quot;QWERasdf1234!&quot;, member.getPassword()),
                () -&gt; assertEquals(&quot;홍길동&quot;, member.getName()),
                () -&gt; assertEquals(GenderEnum.MALE, member.getGender()),
                () -&gt; assertEquals(&quot;010-1234-5678&quot;, member.getPhoneNumber()),
                () -&gt; assertEquals(&quot;서울특별시 강남구 테헤란로 123&quot;, member.getAddress())
        );

    }
}</code></pre><hr>
<h2 id="📚-마무리">📚 마무리</h2>
<blockquote>
<p>MapStruct는 반복적인 DTO ↔ Entity 매핑 코드를 <strong>컴파일 타임에 자동으로 생성</strong>해줌으로써<br>개발자의 생산성을 높이고, 코드의 안정성과 유지보수성을 동시에 잡을 수 있는 훌륭한 도구입니다.
특히 Spring Boot와 결합하면 <code>@Mapper(componentModel = &quot;spring&quot;)</code> 설정 하나로 DI까지 연동되기 때문에<br>복잡한 변환 로직 없이 <strong>깔끔하고 명확한 코드 구조</strong>를 유지할 수 있습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔄 Flyway로 시작하는 DB 마이그레이션 버전 관리]]></title>
            <link>https://velog.io/@dev-hsl-960221/Flyway%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-DB-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@dev-hsl-960221/Flyway%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-DB-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Thu, 24 Jul 2025 09:49:13 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-flyway란">📌 Flyway란?</h2>
<p><strong>Flyway</strong>는 데이터베이스 스키마의 변경사항을 <strong>버전 관리</strong>할 수 있게 도와주는 오픈소스 마이그레이션 도구입니다.<br>SQL 파일 또는 Java 코드를 통해 마이그레이션을 정의하고, 프로젝트 빌드 또는 실행 시 자동으로 DB 변경을 적용할 수 있습니다.</p>
<blockquote>
<p>✔️ SQL도 Git처럼 버전 관리해보자<br>✔️ 즉, DB를 코드처럼 관리할 수 있게 해줍니다.</p>
</blockquote>
<hr>
<h2 id="✅-flyway의-주요-특징">✅ Flyway의 주요 특징</h2>
<ul>
<li><strong>버전 기반의 마이그레이션 관리</strong> (<code>V1__init.sql</code>, <code>V2__add_table.sql</code>)</li>
<li><strong>자동 적용</strong>: Spring Boot 실행 시 마이그레이션 자동 수행</li>
<li><strong>검증 기능</strong>: 마이그레이션 파일 변경 여부 탐지 및 무결성 검사</li>
<li><strong>다양한 DB 지원</strong>: MySQL, PostgreSQL, Oracle, MSSQL, H2 등 대부분 지원</li>
<li><strong>SQL 또는 Java 코드 기반 작성 가능</strong></li>
</ul>
<hr>
<h2 id="🌟-flyway의-장점">🌟 Flyway의 장점</h2>
<table>
<thead>
<tr>
<th>장점</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>✅ 버전 관리</td>
<td>마이그레이션마다 고유 버전 부여 (<code>V1__</code>, <code>V2__</code> 등)</td>
</tr>
<tr>
<td>✅ 협업에 유리</td>
<td>팀원 간 DB 변경 이력을 코드로 공유 가능</td>
</tr>
<tr>
<td>✅ CI/CD 연동</td>
<td>배포 파이프라인에 통합하여 자동 적용 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="⚠️-flyway의-단점">⚠️ Flyway의 단점</h2>
<table>
<thead>
<tr>
<th>단점</th>
<th>보완 방법</th>
</tr>
</thead>
<tbody><tr>
<td>❌ 롤백 기능 없음</td>
<td>되돌리는 SQL을 별도로 관리해야 함</td>
</tr>
<tr>
<td>❌ 초기 도입 시 진입장벽</td>
<td>레거시 DB라면 <code>baseline</code> 설정 필요</td>
</tr>
<tr>
<td>❌ 마이그레이션 순서 충돌 위험</td>
<td>PR 병합 시 파일 버전 충돌 주의 필요</td>
</tr>
</tbody></table>
<hr>
<h2 id="🛠️-spring-boot--flyway-실전-적용기">🛠️ Spring Boot + Flyway 실전 적용기</h2>
<h3 id="1-gradle-의존성-추가">1. Gradle 의존성 추가</h3>
<pre><code>dependencies {
        implementation &#39;org.flywaydb:flyway-core&#39;
        implementation &#39;org.flywaydb:flyway-mysql&#39;
}</code></pre><h3 id="2-applicationyml-설정">2. application.yml 설정</h3>
<pre><code>spring:
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true
    validate-on-migrate: true</code></pre><h3 id="3-마이그레이션-파일-작성">3. 마이그레이션 파일 작성</h3>
<pre><code>-- src/main/resources/db/migration/V1__create_member_table.sql

CREATE TABLE member
(
    id           VARCHAR(26)  NOT NULL PRIMARY KEY COMMENT &#39;ULID 기반 식별자&#39;,
    email        VARCHAR(254) NOT NULL COMMENT &#39;이메일 (RFC 5321 표준)&#39;,
    password     VARCHAR(128) NOT NULL COMMENT &#39;비밀번호&#39;,
    name         VARCHAR(50)  NOT NULL COMMENT &#39;회원 이름&#39;,
    gender       VARCHAR(10)  NOT NULL COMMENT &#39;성별&#39;,
    phone_number VARCHAR(20)  NOT NULL COMMENT &#39;전화번호&#39;,
    address      VARCHAR(200) NOT NULL COMMENT &#39;주소&#39;,
    is_deleted   TINYINT(1)   NOT NULL COMMENT &#39;삭제 유무&#39;,
    deleted_at   DATETIME     NULL COMMENT &#39;삭제 일시&#39;,
    created_at   DATETIME     NOT NULL COMMENT &#39;생성 일시&#39;,
    updated_at   DATETIME     NOT NULL COMMENT &#39;수정 일시&#39;,


    CONSTRAINT chk_gender CHECK (gender IN (&#39;MALE&#39;, &#39;FEMALE&#39;))
);</code></pre><hr>
<pre><code>-- src/main/resources/db/migration/V2__add_email_to_user.sql


ALTER TABLE member ADD CONSTRAINT uq_member_email UNIQUE (email);
</code></pre><h3 id="4-실행-시-로그-확인">4. 실행 시 로그 확인</h3>
<pre><code>2025-07-24T18:40:04.281+09:00  INFO 27328 --- [  restartedMain] org.flywaydb.core.FlywayExecutor         : Database: jdbc:mariadb://localhost/local?user=localUser&amp;password=********&amp;serverTimezone=Asia/Seoul (MariaDB 11.7)
2025-07-24T18:40:04.302+09:00  WARN 27328 --- [  restartedMain] o.f.c.internal.database.base.Database    : Flyway upgrade recommended: MariaDB 11.7 is newer than this version of Flyway and support has not been tested. The latest supported version of MariaDB is 11.2.
</code></pre><hr>
<h4 id="📂-flyway-마이그레이션-파일명-규칙">📂 Flyway 마이그레이션 파일명 규칙</h4>
<p>Flyway는 마이그레이션 파일명을 통해 <strong>버전과 설명을 파악</strong>하고, 순서에 따라 실행합니다.</p>
<blockquote>
<p><strong>파일명 규칙: V{버전}__{설명}.sql (밑줄 2개)</strong></p>
</blockquote>
<hr>
<h2 id="📚-마무리">📚 마무리</h2>
<blockquote>
<p>Flyway는 DB 변경 사항을 안전하게 추적하고 자동화할 수 있는 강력한 도구입니다.
수동 SQL 관리로 인한 혼선을 줄이고, 팀원 간의 협업 효율도 높일 수 있습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[📈 Grafana + Prometheus Metric 데이터 시각화]]></title>
            <link>https://velog.io/@dev-hsl-960221/Grafana-Prometheus-Metric-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%EA%B0%81%ED%99%94</link>
            <guid>https://velog.io/@dev-hsl-960221/Grafana-Prometheus-Metric-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%EA%B0%81%ED%99%94</guid>
            <pubDate>Wed, 02 Jul 2025 08:51:15 GMT</pubDate>
            <description><![CDATA[<h1 id="🔹-시작하기앞서">🔹 시작하기앞서</h1>
<blockquote>
<p><strong>이전글에서 Docker Compose를 활용하여 Loki + Prometheus 구축하는 글을 보고 오는걸 참고하길 바란다.</strong></p>
</blockquote>
<p><a href="https://velog.io/@dev-hsl-960221/Docker-Compose%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-PrometheusLokiGrafana-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9A%B4%EC%98%81">https://velog.io/@dev-hsl-960221/Docker-Compose%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-PrometheusLokiGrafana-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9A%B4%EC%98%81</a></p>
<h2 id="1️⃣-grafana--prometheus-연동">1️⃣ Grafana + Prometheus 연동</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/b59c2d71-7ad0-459a-8030-50690daa1025/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/51b3c6c8-b905-449b-93d8-596b4617be62/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/618aaeb0-ae19-42d2-9322-9bb8c91cee6e/image.png" alt=""></p>
<blockquote>
</blockquote>
<h3 id="호스트주소가-아닌-prometheus-키워드로-사용❓"><strong>호스트주소가 아닌 prometheus 키워드로 사용❓</strong></h3>
<p><strong>※ Docker Compose에서 여러 서비스를 정의할 때, 같은 네트워크에 속한 컨테이너끼리는 서비스 이름으로 서로를 호출할 수 있습니다.</strong></p>
<hr>
<h2 id="2️⃣-대시보드-생성">2️⃣ 대시보드 생성</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/3bbc6dbd-3c37-4b16-8949-1a5487a4ed1b/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/4f755384-afdb-43d0-9dd3-8ecb9c56dcf4/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/dc7a31bd-f8cb-449e-a327-ad0f584aa4ea/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/82422839-6e5a-4e27-b573-022733e0a7d3/image.png" alt=""></p>
<hr>
<h2 id="3️⃣-대시보드-셋팅">3️⃣ 대시보드 셋팅</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/9682dce1-cc4a-4ba5-a799-c754fa00fee7/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/d2fef20d-6330-41aa-992d-d7d8825fa310/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>✅ <strong>앱 서비스들에 대한 환경변수를 job으로 설정</strong> 
<strong>※ 여기서 말하는 앱 서비스들은 ECS FARGATE 환경으로 구성된 TASK(POD)임 ECS 서비스 내 기능중에서 서비스 디스커버리를 사용하여 FARGATE IP를 찾을 수 있음(DNS)</strong></p>
<hr>
<h2 id="4️⃣-prometheus-metric-데이터-시각화">4️⃣ Prometheus Metric 데이터 시각화</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/29b7f961-8cdc-4964-9d6b-ff32b91d8132/image.png" alt=""></p>
<blockquote>
<p><strong>PROMQL을 활용하여 생성</strong></p>
</blockquote>
<table>
<thead>
<tr>
<th>Title</th>
<th>사용 예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Total Requests</strong></td>
<td><strong>📄 총 앤드포인트 요청 수</strong> <img src="https://velog.velcdn.com/images/dev-hsl-960221/post/746df7ee-f6c5-42fd-b65d-b9d278847f95/image.png" alt=""></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td><strong>Total Exceptions</strong></td>
<td>*<em>📄 총 에러 수 *</em><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/5a6abad3-92b0-4793-b5ab-e772ab1c1a62/image.png" alt=""></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td><strong>200 Responses</strong></td>
<td>*<em>📄 200 응답수 *</em><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/69c3cf67-0347-4bf4-a3ba-2e4340d10aec/image.png" alt=""></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td><strong>4xx Responses</strong></td>
<td>*<em>📄 4XX 응답수 *</em><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/ddba6a5c-c0c0-4642-b94f-0eada8add2db/image.png" alt=""></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td><strong>5xx Responses</strong></td>
<td>*<em>📄 5XX 응답수 *</em><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/b15798bb-2d8d-4be2-962f-bb685e32356a/image.png" alt=""></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td><strong>Requests Average Duration</strong></td>
<td>*<em>📄 각 앤드포인트별 응답속도 평균 *</em><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/8eb93a40-2c1e-4f03-9e85-6d3c6a257789/image.png" alt=""></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td><strong>Requests Count</strong></td>
<td>*<em>📄 각 앤드포인트별 요청수 *</em><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/cc14df79-9a99-440c-813d-3f2204936492/image.png" alt=""></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td>*<em>5XX Responses Rate *</em></td>
<td>*<em>📄 모든 앤드포인트 5xx 에러 발생 시간 *</em><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/c5c6b8f6-faa4-4990-963b-5434854a0779/image.png" alt=""></td>
</tr>
</tbody></table>
<hr>
<h1 id="🔹-결론">🔹 결론</h1>
<h4 id="⭐-강력한-시계열-데이터-수집-및-시각화">⭐ 강력한 시계열 데이터 수집 및 시각화</h4>
<h4 id="⭐-오픈소스-기반의-높은-확장성과-유연성">⭐ 오픈소스 기반의 높은 확장성과 유연성</h4>
<h4 id="⭐-promql-쿼리-결과를-바로-대시보드로-시각화-가능">⭐ PromQL 쿼리 결과를 바로 대시보드로 시각화 가능</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[📜 Grafana + Loki 로그수집 데이터 시각화]]></title>
            <link>https://velog.io/@dev-hsl-960221/Grafana-Loki-%EB%A1%9C%EA%B7%B8%EC%88%98%EC%A7%91-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%EA%B0%81%ED%99%94</link>
            <guid>https://velog.io/@dev-hsl-960221/Grafana-Loki-%EB%A1%9C%EA%B7%B8%EC%88%98%EC%A7%91-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%EA%B0%81%ED%99%94</guid>
            <pubDate>Tue, 22 Apr 2025 10:24:08 GMT</pubDate>
            <description><![CDATA[<h1 id="🔹-시작하기앞서">🔹 시작하기앞서</h1>
<blockquote>
<p><strong>이전글에서 Docker Compose를 활용하여 Loki + Grafana 구축하는 글을 보고 오는걸 참고하길 바란다.</strong></p>
</blockquote>
<p><a href="https://velog.io/@dev-hsl-960221/Docker-Compose%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-PrometheusLokiGrafana-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9A%B4%EC%98%81">https://velog.io/@dev-hsl-960221/Docker-Compose%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-PrometheusLokiGrafana-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9A%B4%EC%98%81</a></p>
<h2 id="1️⃣-grafana--loki-연동">1️⃣ Grafana + Loki 연동</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/b59c2d71-7ad0-459a-8030-50690daa1025/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/51b3c6c8-b905-449b-93d8-596b4617be62/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/41760c91-474b-4f32-9cf8-ecfbf028f439/image.png" alt=""></p>
<blockquote>
</blockquote>
<h3 id="호스트주소가-아닌-loki-키워드로-사용❓"><strong>호스트주소가 아닌 loki 키워드로 사용❓</strong></h3>
<p><strong>※ Docker Compose에서 여러 서비스를 정의할 때, 같은 네트워크에 속한 컨테이너끼리는 서비스 이름으로 서로를 호출할 수 있습니다.</strong></p>
<hr>
<h2 id="2️⃣-대시보드-생성">2️⃣ 대시보드 생성</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/3bbc6dbd-3c37-4b16-8949-1a5487a4ed1b/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/4f755384-afdb-43d0-9dd3-8ecb9c56dcf4/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/dc7a31bd-f8cb-449e-a327-ad0f584aa4ea/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/5d7c6379-7049-4a86-bcd9-c478f417493f/image.png" alt=""></p>
<hr>
<h2 id="3️⃣-query-및-시각화">3️⃣ Query 및 시각화</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/4f4523e7-0f77-49f6-ba63-c61543968450/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/0b4dfee8-1042-447a-9930-609b9979b6a8/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>✅ <strong>앱 서비스들에 대한 환경변수를 app으로 설정</strong> </p>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/f275f584-8a20-4387-b092-310f0ae45460/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/03ab6523-a689-44cb-9eb7-934ceaccc8c3/image.png" alt=""></p>
<blockquote>
</blockquote>
<h3 id="🏷️-label-filters-라벨-필터">🏷️ <strong>Label Filters (라벨 필터)</strong></h3>
<h4 id="-loki는-로그를-라벨label로-구분해서-저장합니다">* Loki는 로그를 라벨(label)로 구분해서 저장합니다.</h4>
<h4 id="-appnameapp은-특정-애플리케이션app에-해당하는-로그만-필터링해서-조회하겠다는-의미입니다">* appName=&quot;$app&quot;은 특정 애플리케이션(app)에 해당하는 로그만 필터링해서 조회하겠다는 의미입니다.</h4>
<h4 id="-여기서-app은-grafana에서-정의한-변수variable이며-대시보드-상단에서-선택할-수-있습니다">* 여기서 $app은 Grafana에서 정의한 <strong>변수(variable)</strong>이며, 대시보드 상단에서 선택할 수 있습니다.</h4>
<blockquote>
</blockquote>
<h3 id="🔍-logfmt-파서-설정"><strong>🔍 Logfmt 파서 설정</strong></h3>
<h4 id="-logfmt는-keyvalue-형식의-로그를-구조화된-형태로-파싱합니다">* logfmt는 key=value 형식의 로그를 구조화된 형태로 파싱합니다.</h4>
<h4 id="-파싱된-키는-grafana에서-필터링-테이블-출력-시각화-등에-활용할-수-있어요">* 파싱된 키는 Grafana에서 필터링, 테이블 출력, 시각화 등에 활용할 수 있어요.</h4>
<blockquote>
</blockquote>
<h3 id="⚙️-추가-옵션-설명"><strong>⚙️ 추가 옵션 설명</strong></h3>
<h4 id="strict"><strong>Strict</strong></h4>
<p>체크 시 logfmt 파싱 오류가 있는 로그는 무시합니다.</p>
<blockquote>
</blockquote>
<h4 id="keep-empty"><strong>Keep empty</strong></h4>
<p>값이 비어 있는 키도 유지해서 파싱합니다. (예: traceId=)</p>
<blockquote>
</blockquote>
<h4 id="-expression"><strong>+ Expression</strong></h4>
<p>파싱된 필드를 조건으로 추가 필터링이 가능합니다.</p>
<hr>
<h2 id="4️⃣-grafana-시각화-결과">4️⃣ Grafana 시각화 결과</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/a137f6df-4bea-41f1-923d-a79a630796a2/image.png" alt=""></p>
<hr>
<h1 id="🔹-결론">🔹 결론</h1>
<h4 id="⭐-로그-시각화-통합">⭐ 로그 시각화 통합</h4>
<h4 id="⭐-인덱싱이-거의-없기-때문에-리소스-사용량이-적고-저장-비용도-낮음">⭐ 인덱싱이 거의 없기 때문에 리소스 사용량이 적고, 저장 비용도 낮음.</h4>
<h4 id="⭐-운영-중인-시스템에서-로그-기반-이벤트를-빠르게-감지하고-대응할-수-있음">⭐ 운영 중인 시스템에서 로그 기반 이벤트를 빠르게 감지하고 대응할 수 있음.</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[📦 ECR + ECS + FARGATE를 통한 클러스터 구축 및 서비스 운영]]></title>
            <link>https://velog.io/@dev-hsl-960221/ECR-ECS-FARGATE%EB%A5%BC-%ED%86%B5%ED%95%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9A%B4%EC%98%81</link>
            <guid>https://velog.io/@dev-hsl-960221/ECR-ECS-FARGATE%EB%A5%BC-%ED%86%B5%ED%95%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9A%B4%EC%98%81</guid>
            <pubDate>Tue, 08 Apr 2025 08:29:06 GMT</pubDate>
            <description><![CDATA[<h1 id="📌-워크로드">📌 워크로드</h1>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/9cce4a4b-ffa3-42e2-b90b-3bb3a3496150/image.png" alt=""></p>
<h1 id="🔹ecr-elastic-container-registry">🔹ECR (Elastic Container Registry)</h1>
<h3 id="ecr은-aws의-docker-이미지-저장소이다-aws에-최적화되어-있는-docker-hub이다">ECR은 AWS의 Docker 이미지 저장소이다. AWS에 최적화되어 있는 Docker Hub이다.</h3>
<blockquote>
</blockquote>
<ul>
<li>Docker 이미지 저장 및 관리</li>
<li>ECS, EKS, CodePipeline 등과 통합</li>
<li>프라이빗/퍼블릭 이미지 레지스트리 지원</li>
<li>고속 이미지 다운로드 (S3 기반 저장)</li>
</ul>
<h1 id="🔹ecs-elastic-container-service">🔹ECS (Elastic Container Service)</h1>
<h3 id="ecs는-aws의-리소스를-활용하는-컨테이너-오케스트레이션-서비스입니다-kubernetes와-비슷한-역할을-하며-docker-컨테이너를-실행-관리-확장하는-일을-자동으로-처리해줍니다">ECS는 AWS의 리소스를 활용하는 컨테이너 오케스트레이션 서비스입니다. Kubernetes와 비슷한 역할을 하며, Docker 컨테이너를 실행, 관리, 확장하는 일을 자동으로 처리해줍니다.</h3>
<blockquote>
</blockquote>
<ul>
<li>클러스터 (Cluster) : 컨테이너를 배포할 서버 그룹 (Fargate 또는 EC2 인스턴스 기반)</li>
<li>태스크 (Task) : 컨테이너 정의 (도커 이미지, 환경변수, 포트 등 포함)</li>
<li>서비스 (Service) : 태스크를 실행시키고 상태를 유지시켜주는 역할 (Auto-scaling, Load balancing 가능)</li>
<li>런타임 방식 :
  <strong>- EC2: EC2 인스턴스 위에서 컨테이너 실행</strong>
  <strong>- Fargate: 서버를 신경쓰지 않고 컨테이너만 실행 (서버리스 방식)</strong></li>
</ul>
<hr>
<h2 id="1️⃣-ecr-생성">1️⃣ ECR 생성</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/279ba4c7-ee95-4727-a450-6a499694c957/image.png" alt=""></p>
<hr>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/9be14d48-a1d3-463c-a097-2b8ceb35bcc8/image.png" alt=""></p>
<hr>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/306f9dfb-2381-405b-b622-6d8d85af3c7a/image.png" alt=""></p>
<hr>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/293ac571-c6d4-463b-9069-55e88e02962f/image.png" alt=""></p>
<blockquote>
</blockquote>
<h3 id="✅-mutable-기본값">✅ Mutable (기본값)</h3>
<p><strong>이미지 태그를 덮어쓸 수 있음</strong>
👉 latest 태그에 새로운 이미지를 계속 덮어쓰기 가능</p>
<h3 id="🔒-immutable">🔒 Immutable</h3>
<p><strong>이미지 태그를 한 번 등록하면 덮어쓸 수 없음</strong>
👉 태그가 고정되어 배포 안정성 확보에 유리</p>
<hr>
<h2 id="2️⃣-ecs-cluster-생성">2️⃣ ECS Cluster 생성</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/b6e5ba6b-4f00-4e42-bb81-a1943bf7f1bb/image.png" alt=""></p>
<hr>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/4e9dd477-b1e1-4611-98e7-59436b350d2f/image.png" alt=""></p>
<hr>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/02dfec87-48df-4b5c-99b5-7c346f681a00/image.png" alt=""></p>
<hr>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/1baec5f7-8fdd-4256-bd0a-ba8c9fa61f39/image.png" alt=""></p>
<blockquote>
</blockquote>
<h3 id="☁️-aws-ecs---fargate-vs-ec2">☁️ AWS ECS - Fargate vs EC2</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>Fargate</th>
<th>EC2</th>
</tr>
</thead>
<tbody><tr>
<td>방식</td>
<td>🟢 서버리스 (자동 관리)</td>
<td>⚙️ 수동 관리</td>
</tr>
<tr>
<td>요금</td>
<td>💸 사용한 만큼만 과금</td>
<td>💰 인스턴스 지속 비용 발생</td>
</tr>
<tr>
<td>추천 사용처</td>
<td>🔹 유지보수 최소화</td>
<td>🔸 고정 리소스가 필요한 경우</td>
</tr>
</tbody></table>
<blockquote>
</blockquote>
<h3 id="📊-ecs-container-insights-옵션-비교">📊 ECS Container Insights 옵션 비교</h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Enhanced Observability</td>
<td>✅ <strong>추천 옵션</strong><br>🔍 태스크/컨테이너 수준까지 상세 메트릭 제공<br>⚡ 빠른 문제 해결 및 성능 분석에 유리</td>
</tr>
<tr>
<td>기본 Container Insights</td>
<td>📈 클러스터 및 서비스 수준의 집계 메트릭 제공<br>🔎 Logs Insights로 추가 분석 가능</td>
</tr>
<tr>
<td>Off (기본값)</td>
<td>💤 CloudWatch 기본 메트릭만 수집<br>🛠️ 최소한의 모니터링만 필요할 때 사용</td>
</tr>
</tbody></table>
<p>✅ <strong>추천</strong></p>
<ul>
<li><p>운영 중인 ECS 서비스를 잘 <strong>모니터링하고 디버깅</strong>하려면<br>➤ <code>Enhanced Observability</code> 사용하세요.</p>
</li>
<li><p>비용이나 성능 고려로 <strong>기본 수준만 필요</strong>하다면<br>➤ <code>Container Insights</code> 또는 <code>Off</code> 선택도 가능</p>
</li>
</ul>
<hr>
<h2 id="3️⃣-ecs-task-definitions-생성">3️⃣ ECS Task definitions 생성</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/ecd32528-b288-489b-91dd-96f4e964673d/image.png" alt=""></p>
<hr>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/9f41ff67-f934-4012-9e2a-8138f928efb8/image.png" alt=""></p>
<hr>
<h2 id="⚙️-ecs-task-definition---infrastructure-requirements-주요-항목-설명">⚙️ ECS Task Definition - Infrastructure requirements 주요 항목 설명</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/7af3a6bc-19e5-4757-9691-68d63b13ec60/image.png" alt=""></p>
<blockquote>
</blockquote>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Task definition family</strong></td>
<td>태스크 정의 이름</td>
</tr>
<tr>
<td><strong>Launch type</strong></td>
<td>🚀 실행 방식 선택 (예: <code>FARGATE</code>, <code>EC2</code>) <br> ➤ Fargate: 서버리스 환경에서 실행 <br> ➤ EC2: 사용자 EC2 인스턴스에서 실행</td>
</tr>
<tr>
<td><strong>Operating system / Architecture</strong></td>
<td>🖥️ OS 및 아키텍처 지정 <br> ➤ 예: <code>Linux/x86_64</code>, <code>Linux/ARM64</code>, <code>Windows</code> 등</td>
</tr>
<tr>
<td><strong>Task size (Fargate 전용)</strong></td>
<td>📦 CPU &amp; Memory 리소스 설정 <br> ➤ 예: <code>0.5 vCPU / 1GB RAM</code> <br> ➤ 조합에 따라 비용과 성능 달라짐</td>
</tr>
</tbody></table>
<p>✅ <strong>Tips</strong></p>
<ul>
<li><strong>Launch type</strong>은 클러스터에서 어떤 환경에서 실행할지 결정하는 핵심 옵션입니다.</li>
<li><strong>OS/Arch</strong>는 사용하는 컨테이너 이미지에 맞춰 정확히 선택해야 실행 오류가 없습니다.</li>
<li><strong>Fargate 사용 시</strong>에는 반드시 Task size 설정이 필요합니다.<br>➤ <code>vCPU</code>와 <code>Memory</code>를 적절히 조합하여 비용 최적화하세요.</li>
</ul>
<hr>
<h2 id="⚙️-ecs-task-definition---container-details-주요-항목-설명">⚙️ ECS Task Definition - Container Details 주요 항목 설명</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/ef6418a2-9a72-4fb2-98f6-f9621e3366cd/image.png" alt=""></p>
<blockquote>
</blockquote>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Name</strong></td>
<td>컨테이너 이름 지정<br>➤ 최대 255자 (영문, 숫자, <code>-</code>, <code>_</code> 허용)</td>
</tr>
<tr>
<td><strong>Image URI</strong></td>
<td>사용할 컨테이너 이미지 URI<br>예: <code>123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/app:tag</code>  <br>➤ ECR Repository URI</td>
</tr>
<tr>
<td><strong>Essential Container</strong></td>
<td>✅ 필수 컨테이너 여부 설정<br>➤ 하나 이상의 컨테이너는 <strong>반드시 Essential</strong>로 지정되어야 함</td>
</tr>
<tr>
<td><strong>Private Registry</strong></td>
<td>🔐 프라이빗 이미지 레지스트리 사용 시, <strong>Secrets Manager를 통해 인증 정보 저장 필요</strong></td>
</tr>
<tr>
<td><strong>Port Mappings</strong></td>
<td>🔁 외부 통신을 위한 포트 매핑<br>예: 컨테이너 포트 <code>80</code>, 프로토콜 <code>TCP</code>, 앱 프로토콜 <code>HTTP</code></td>
</tr>
<tr>
<td><strong>Read-only Root FS</strong></td>
<td>🔒 루트 파일시스템을 읽기 전용으로 설정 (보안 강화 목적)</td>
</tr>
<tr>
<td><strong>CPU / Memory Limits</strong></td>
<td>⚙️ 컨테이너 단위 자원 제한 설정<br>➤ Task-level과 별도 설정 가능<br>➤ 초과 시 종료될 수 있음</td>
</tr>
<tr>
<td><strong>GPU</strong></td>
<td>🎮 GPU 리소스 할당 (선택 사항, 필요 시 사용)</td>
</tr>
</tbody></table>
<p>✅ <strong>Tips</strong></p>
<ul>
<li><code>Essential</code> 컨테이너가 종료되면 전체 Task도 중지됨 → 중요 서비스는 반드시 Essential로!</li>
<li><strong>CPU/Memory 제한</strong>은 컨테이너가 개별적으로 사용하는 최대치 → Task 전체 리소스와 합쳐서 고려해야 함</li>
<li><strong>Read-only Root FS</strong> 옵션은 보안 강화를 위해 사용되며, 대부분의 앱에서는 켜도 문제 없음</li>
</ul>
<hr>
<h2 id="🛠️-ecs-task-definition---선택-설정-optional-settings">🛠️ ECS Task Definition - 선택 설정 (Optional Settings)</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/c959e17e-8e3c-411f-b919-a9a8aa34d79a/image.png" alt=""></p>
<blockquote>
<p>대부분의 경우 생략해도 무방하지만, <strong>특정 상황</strong>에서 유용하게 사용됩니다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명 및 사용 예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Logging</strong></td>
<td>📄 CloudWatch 로그 설정 <br>➤ 로그가 필요한 경우 지정된 log group으로 출력 가능</td>
</tr>
<tr>
<td><strong>Restart policy</strong></td>
<td>🔁 개별 컨테이너 재시작 설정 <br>➤ Task 전체가 아닌 <strong>컨테이너 단위 재시작</strong> 용도</td>
</tr>
<tr>
<td><strong>HealthCheck</strong></td>
<td>❤️ 컨테이너의 헬스체크 설정 <br>➤ 지정된 명령어 기준으로 컨테이너 상태 판단</td>
</tr>
<tr>
<td><strong>Startup dependency ordering</strong></td>
<td>⏱️ 여러 컨테이너 실행 순서 제어 <br>➤ DB → App 순서 등 의존성 있을 때 사용</td>
</tr>
<tr>
<td><strong>Container timeouts</strong></td>
<td>⌛ 시작/종료 시 타임아웃 지정 <br>➤ 느린 초기화가 있는 앱에 유용</td>
</tr>
<tr>
<td><strong>Container network settings</strong></td>
<td>🌐 IP, DNS 등 네트워크 상세 설정 <br>➤ 특별한 네트워크 구성이 필요할 때 사용</td>
</tr>
<tr>
<td><strong>Docker configuration</strong></td>
<td>⚙️ 추가 Docker 옵션 설정 <br>➤ log driver, sysctl 등 설정이 필요할 때</td>
</tr>
<tr>
<td><strong>Resource limits (Ulimits)</strong></td>
<td>📊 리눅스의 ulimit 설정 <br>➤ 열 수 있는 파일 수 제한 등 세밀한 리소스 제어에 사용</td>
</tr>
<tr>
<td><strong>Docker labels</strong></td>
<td>🏷️ 메타데이터 태그 부여 <br>➤ 관리, 모니터링, 정책 적용 시 활용 가능</td>
</tr>
</tbody></table>
<p>✅ <strong>요약</strong></p>
<ul>
<li>일반적인 웹/백엔드 앱: <strong>기본 설정만으로 충분</strong>  </li>
<li>로깅, 재시작 정책, 헬스체크 등은 <strong>운영환경에서 점진적으로 적용</strong>하면 좋습니다.</li>
</ul>
<hr>
<h2 id="4️⃣-ecs-service-생성">4️⃣ ECS Service 생성</h2>
<blockquote>
<p> 👉 클러스터 상세 페이지 상단 메뉴에서 Services 탭을 선택합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/d7dbbefc-97f1-4d1d-b219-ac221e81080b/image.png" alt=""></p>
<hr>
<blockquote>
<p>👉 Create 또는 서비스 생성하기 버튼을 클릭합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/2d38a1fc-b7f9-499c-9c23-8a62d3a84d02/image.png" alt=""></p>
<hr>
<h2 id="🛠️-ecs-service-environment--deployment-configuration-주요-항목-설명">🛠️ ECS Service Environment &amp; Deployment configuration 주요 항목 설명</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/74816d01-a69f-4503-98e8-b420b704472b/image.png" alt=""></p>
<blockquote>
</blockquote>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명 및 사용 예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Compute options</strong></td>
<td>⚙️ 작업 배치 전략 <br>➤ 여러 용량 공급자(Capacity Provider) 조합 가능</td>
</tr>
<tr>
<td><strong>Launch type</strong></td>
<td>🚀 작업 실행 환경 선택 <br>➤ 일반적으로 <code>FARGATE</code> 사용</td>
</tr>
<tr>
<td><strong>Platform version</strong></td>
<td>🧩 플랫폼 버전 <br>➤ <code>LATEST</code> 권장 (최신 기능과 보안 업데이트 포함)</td>
</tr>
<tr>
<td><strong>Task definition family</strong></td>
<td>📜 실행할 작업 정의 그룹 <br>➤ 여러 revision을 가진 작업 정의 패밀리 선택</td>
</tr>
<tr>
<td><strong>Task definition revision</strong></td>
<td>🔁 작업 정의 리비전 선택 <br>➤ 빈 칸으로 두면 최신 revision 사용</td>
</tr>
<tr>
<td><strong>Service name</strong></td>
<td>🏷️ 클러스터 내 유일한 서비스명 <br>➤ 영문, 숫자, <code>_</code>, <code>-</code> 가능, 최대 255자</td>
</tr>
<tr>
<td><strong>Service type</strong></td>
<td>🛠️ 배포 유형 <br>➤ <code>Replica</code> (원하는 수만큼 실행), <code>Daemon</code> (인스턴스당 1개 실행)</td>
</tr>
<tr>
<td><strong>Desired tasks</strong></td>
<td>🔢 실행할 작업 수 지정 <br>➤ 예: <code>1</code> → 하나의 Task 실행</td>
</tr>
<tr>
<td><strong>Availability Zone rebalancing</strong></td>
<td>🌍 가용 영역 자동 재분산 <br>➤ AZ 간 작업 균형을 자동으로 조정</td>
</tr>
<tr>
<td><strong>Health check grace period</strong></td>
<td>❤️ 헬스체크 유예 시간 <br>➤ 초기 부팅 시간이 긴 앱에 유용</td>
</tr>
<tr>
<td><strong>Deployment options</strong></td>
<td>🚀 배포 전략 및 실패 감지 설정 <br>➤ 무중단 배포를 위한 설정, 배포 실패 시 롤백 여부 등을 지정 Rolling, Blue/Green</td>
</tr>
<tr>
<td><strong>Deployment failure detection</strong></td>
<td>🚨 배포 실패 감지 <br>➤ 롤백 또는 경고 알림 등에 사용 가능</td>
</tr>
</tbody></table>
<p>✅ <strong>Tips</strong></p>
<p>Deployment circuit breaker는 꼭 켜두는 것을 추천합니다.
→ 신규 배포 실패 시 자동으로 이전 상태로 복구되어 장애 방지에 유리합니다.</p>
<hr>
<h2 id="🛠️-ecs-service-networking-주요-항목-설명">🛠️ ECS Service Networking 주요 항목 설명</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/820a5b91-a86c-445f-a4cb-1e6484c145fe/image.png" alt=""></p>
<blockquote>
</blockquote>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명 및 사용 예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>VPC</strong></td>
<td>🌐 작업(Task)이 실행될 <strong>VPC(가상 네트워크)</strong> 선택 <br>➤ 보안 그룹, 서브넷 등 네트워크 자원과 연결됨</td>
</tr>
<tr>
<td><strong>Subnets</strong></td>
<td>🧱 작업을 실행할 서브넷 선택 <br>➤ 퍼블릭/프라이빗 서브넷 선택 가능, 일반적으로 <strong>멀티 AZ</strong>로 2개 이상 선택</td>
</tr>
<tr>
<td><strong>Security group</strong></td>
<td>🔐 방화벽 역할을 하는 보안 그룹 선택 <br>➤ 인바운드/아웃바운드 트래픽 제어, 기존 그룹 선택 또는 새로 생성 가능</td>
</tr>
<tr>
<td><strong>Public IP</strong></td>
<td>🌍 퍼블릭 IP 자동 할당 여부 <br>➤ 인터넷 접근이 필요한 경우 <code>Turned on</code>으로 설정 (퍼블릭 서브넷에서 사용)</td>
</tr>
</tbody></table>
<p>✅ <strong>Tips</strong></p>
<p>VPC/Subnet
→ 퍼블릭 서브넷을 사용하는 경우 퍼블릭 IP 할당을 켜야 외부 통신 가능
→ 프라이빗 서브넷에서는 NAT Gateway 등을 통해 외부 통신 필요</p>
<p>Security group
→ 최소 포트만 열고, 특정 IP만 허용하는 식으로 보안 원칙 준수</p>
<p>Public IP
→ 외부 API 호출, S3 접근 등 인터넷 접속이 필요한 경우 반드시 켜야 함</p>
<hr>
<h2 id="🛠️-ecs-service-load-balancing-주요-항목-설명">🛠️ ECS Service Load balancing 주요 항목 설명</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/85b85a37-2257-4b07-9918-d928bba844b1/image.png" alt=""></p>
<blockquote>
</blockquote>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명 및 사용 예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Use load balancing</strong></td>
<td>⚖️ 로드 밸런서를 사용할지 여부 선택 <br>➤ 여러 Task 간 트래픽을 균등하게 분산</td>
</tr>
<tr>
<td><strong>VPC</strong></td>
<td>🌐 로드 밸런서가 속할 VPC <br>➤ 서비스에서 사용하는 VPC와 동일해야 함 (<code>awsvpc</code> 모드 기준)</td>
</tr>
<tr>
<td><strong>Load balancer type</strong></td>
<td>📚 로드 밸런서 종류 선택 <br>➤ <code>Application Load Balancer(ALB)</code> 또는 <code>Network Load Balancer(NLB)</code></td>
</tr>
<tr>
<td></td>
<td>- ALB: HTTP/HTTPS 기반, 경로 기반 라우팅 가능 <br>➤ 웹 애플리케이션에 적합</td>
</tr>
<tr>
<td></td>
<td>- NLB: TCP/UDP 기반, 빠른 처리 성능 <br>➤ 실시간 게임, 메시징 등 네트워크 지향 서비스에 적합</td>
</tr>
<tr>
<td><strong>Container</strong></td>
<td>📦 로드 밸런싱할 대상 컨테이너와 포트 지정 <br>➤ 예: <code>dev-ad-front-container 3000:3000</code></td>
</tr>
<tr>
<td><strong>Load balancer name</strong></td>
<td>🏷️ ALB 또는 NLB 이름 지정 <br>➤ 유일해야 하며 AWS 리소스 명명 규칙을 따라야 함</td>
</tr>
<tr>
<td><strong>Listener</strong></td>
<td>🎧 클라이언트 요청을 수신할 포트 및 프로토콜 <br>➤ 예: <code>HTTP : 80</code>, <code>HTTPS : 443</code> 등</td>
</tr>
<tr>
<td><strong>Target group</strong></td>
<td>🎯 요청을 분산시킬 ECS 작업(Task) 그룹 <br>➤ 새로 만들거나 기존 Target Group 사용 가능</td>
</tr>
<tr>
<td><strong>Target group name</strong></td>
<td>🏷️ 대상 그룹 이름 지정 <br>➤ ALB가 트래픽을 전달할 ECS Task 컨테이너 그룹 식별자</td>
</tr>
<tr>
<td><strong>Deregistration delay</strong></td>
<td>⏳ 등록 해제 대기 시간(초) <br>➤ 종료되는 Task가 drain 상태를 유지하는 시간 (기본 300초)</td>
</tr>
<tr>
<td><strong>Health check protocol</strong></td>
<td>❤️ 헬스 체크에 사용할 프로토콜 <br>➤ 일반적으로 <code>HTTP</code> 사용</td>
</tr>
<tr>
<td><strong>Health check path</strong></td>
<td>🔍 헬스 체크 경로 <br>➤ 예: <code>/</code> 또는 <code>/health</code> 와 같이 컨테이너가 정상임을 확인할 수 있는 경로</td>
</tr>
</tbody></table>
<p>✅ <strong>Tips</strong></p>
<ul>
<li><p>ALB vs NLB</p>
<ul>
<li>웹 서비스나 API 서버 → ALB 추천</li>
<li>고성능 TCP 서비스 → NLB 고려</li>
</ul>
</li>
<li><p>Health check 경로는 200 OK 응답을 반환해야 정상으로 간주되므로 /health, /actuator/health 등의 경로로 설정 필요</p>
</li>
<li><p>Deregistration delay는 무중단 배포를 위해 Task가 종료되기 전 클라이언트 요청을 모두 처리할 수 있도록 일정 시간 대기하는 설정입니다.</p>
</li>
<li><p>Listener 설정이 잘못되면 ALB가 요청을 수신하지 못하므로 주의하세요.</p>
</li>
</ul>
<hr>
<h2 id="🛠️-ecs-service-service-discovery-주요-항목-설명">🛠️ ECS Service Service Discovery 주요 항목 설명</h2>
<blockquote>
</blockquote>
<h4 id="-👉-ecs-내에서-서비스-간-통신을-dns-기반으로-자동화하는-기능입니다-특히-서비스-이름-기반으로-다른-서비스의-ip-주소를-찾을-수-있게-해주는-기능-">** 👉 ECS 내에서 서비스 간 통신을 DNS 기반으로 자동화하는 기능입니다. 특히 서비스 이름 기반으로 다른 서비스의 IP 주소를 찾을 수 있게 해주는 기능 **</h4>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/985e6bde-a4b7-43a2-894d-fc409a612e07/image.png" alt=""></p>
<blockquote>
</blockquote>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명 및 사용 예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>기능 요약</strong></td>
<td>- <strong>Cloud Map</strong>을 통해 ECS 서비스에 도메인 이름을 부여 <br> - 서비스가 시작/종료될 때 자동으로 등록/해제</td>
</tr>
<tr>
<td><strong>VPC 연결</strong></td>
<td>🌐 Service Discovery는 <strong>VPC 내부 전용 DNS</strong>로 작동 <br> ➤ 외부 공개 DNS 아님</td>
</tr>
<tr>
<td><strong>사용 목적</strong></td>
<td>- 서비스 간 로드밸런서 없이 직접 접근 <br> - 예: <code>http://user-service.local:8080</code></td>
</tr>
<tr>
<td><strong>Namespace</strong></td>
<td>🗂️ Cloud Map에서 생성된 네임스페이스 사용 <br>➤ 예: <code>local</code>, <code>internal</code>, <code>svc.cluster.local</code> 등</td>
</tr>
<tr>
<td><strong>Service Name</strong></td>
<td>🏷️ 해당 ECS 서비스에 부여할 DNS 이름 <br> ➤ 예: <code>user-service</code> → <code>user-service.local</code></td>
</tr>
<tr>
<td><strong>DNS Record Type</strong></td>
<td>📘 A 또는 SRV 타입 선택 <br>➤ 일반적으로 <code>A</code> 사용 (IP 주소 반환)</td>
</tr>
<tr>
<td><strong>TTL (Time To Live)</strong></td>
<td>⏱️ DNS 캐시 유지 시간 설정 <br> ➤ 기본값은 보통 60초 정도, 변경 가능</td>
</tr>
</tbody></table>
<hr>
<h1 id="🔹-결론">🔹 결론</h1>
<p>⭐ 서버 관리가 필요 없는 완전한 서버리스 아키텍처</p>
<ul>
<li>EC2 관리 불필요 → 오토스케일, 패치, 유지보수 부담 ↓, 인프라 비용은 실제 사용한 리소스만큼만 지불</li>
</ul>
<p>⭐ 서비스 간 통신 구조가 간단</p>
<ul>
<li>Service Discovery를 활용하면 DNS 기반으로 다른 ECS 서비스에 바로 접근 가능  </li>
<li>로드밸런서 없이도 마이크로서비스 연결 가능 ( ※ EC2 프로메테우스 환경에서 dns_sd_configs 활용 가능 )</li>
</ul>
<p>⭐ 높은 확장성과 가용성</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🐳 Docker Compose를 활용한 Prometheus+Loki+Grafana 구축 및 서비스 운영]]></title>
            <link>https://velog.io/@dev-hsl-960221/Docker-Compose%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-PrometheusLokiGrafana-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9A%B4%EC%98%81</link>
            <guid>https://velog.io/@dev-hsl-960221/Docker-Compose%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-PrometheusLokiGrafana-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9A%B4%EC%98%81</guid>
            <pubDate>Thu, 27 Mar 2025 08:11:56 GMT</pubDate>
            <description><![CDATA[<h1 id="📌-워크로드">📌 워크로드</h1>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/e2881982-3c7b-42cf-bc11-15a0a88a807c/image.png" alt=""></p>
<h2 id="1-docker-compse-설정">1) docker-compse 설정</h2>
<pre><code>services:

  prometheus:
    image: prom/prometheus:latest                                                  # Prometheus 최신 이미지 사용
    container_name: prometheus                                                     # 컨테이너 이름 지정
    ports:
      - &quot;9090:9090&quot;                                                                # 호스트 포트 9090 → 컨테이너 포트 9090
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml               # 호스트의 설정 파일을 컨테이너로 마운트   
    command:
      - --config.file=/etc/prometheus/prometheus.yml                             # 설정 파일 경로 지정 (옵션)                   
    networks:
      - prometheus                                                               # prometheus 네트워크에 연결

  loki:
    image: loki:latest                                                            # Loki 최신 이미지 사용 (공식 이미지는 grafana/loki 사용 권장)
    container_name: loki                                                          # 컨테이너 이름 지정
    user: &quot;$UID:$GID&quot;                                                             # 현재 사용자 권한으로 실행 (권한 문제 방지)
    ports:
      - &quot;3100:3100&quot;                                                               # Loki 포트 매핑 (기본 포트 3100)
    volumes:
      - ./loki/loki.yml:/etc/loki/local-config.yaml                               # Loki 설정 파일 마운트       
      - ./loki:/var/loki                                                        # 로그 데이터 저장 디렉토리 마운트
    command: -config.file=/etc/loki/local-config.yaml                             # 설정 파일 지정                     
    networks:
      - loki                                                                      # loki 네트워크에 연결

  grafana:
    image: grafana:latest                                                         # Grafana 최신 이미지
    container_name: grafana                                                      # 컨테이너 이름 지정
    user: &quot;$UID:$GID&quot;                                                             # 현재 사용자 권한으로 실행
    ports:
      - &quot;3000:3000&quot;                                                               # Grafana 웹 포트 (기본 3000)
    volumes:
      - ./grafana:/var/lib/grafana                                                # Grafana 데이터 디렉토리 마운트 (대시보드, 설정 등 저장)                             
    depends_on:
      - prometheus                                                                # Prometheus 서비스가 먼저 시작되도록 설정
      - loki                                                                      # Loki 서비스가 먼저 시작되도록 설정
    networks:
      - prometheus                                                                # Prometheus 네트워크에 연결 (메트릭 수집용)
      - loki                                                                      # Loki 네트워크에 연결 (로그 수집용)

networks:                                                                        # 사용자 정의 네트워크 설정
  prometheus:
    driver: bridge                                                                # 기본 브리지 네트워크 사용
  loki:
    driver: bridge                                                               # 로그 관련 컨테이너용 네트워크
</code></pre><blockquote>
</blockquote>
<h3 id="✅-docker-compose-down"><strong>✅ docker-compose down</strong></h3>
<ul>
<li>현재 실행 중인 모든 컨테이너, 네트워크, 볼륨 등을 종료 및 삭제합니다.</li>
<li>기본적으로 네트워크와 컨테이너를 삭제하고, 볼륨은 유지됩니다. (--volumes 옵션을 주면 볼륨도 삭제됨)<blockquote>
</blockquote>
<h3 id="✅--docker-compose-up--d"><strong>✅  docker-compose up -d</strong></h3>
</li>
<li>docker-compose.yml에 정의된 서비스를 백그라운드(-d)로 실행합니다.</li>
<li>변경된 부분만 다시 빌드하고 실행합니다.</li>
</ul>
<hr>
<h2 id="2-spring-application-에서-프로메테우스-서버로-메트릭-데이터를-export">2) Spring Application 에서 프로메테우스 서버로 메트릭 데이터를 Export</h2>
<h3 id="1-buildgradle-의존성-추가">1. build.gradle 의존성 추가</h3>
<pre><code>implementation &#39;io.micrometer:micrometer-registry-prometheus&#39;</code></pre><h3 id="2-yml-파일-설정">2. yml 파일 설정</h3>
<pre><code>management:
  endpoints:
    web:
      exposure:
        include: prometheus
  endpoint:
    prometheus:
      enabled: true</code></pre><hr>
<h2 id="3-spring-application-에서-loki-서버로-로그를-전달하는-appender-설정">3) Spring Application 에서 Loki 서버로 로그를 전달하는 Appender 설정</h2>
<h3 id="1-buildgradle-의존성-추가-1">1. build.gradle 의존성 추가</h3>
<pre><code>implementation &#39;com.github.loki4j:loki-logback-appender:1.5.1&#39;</code></pre><h3 id="2-logback-설정">2. logback 설정</h3>
<pre><code>&lt;!-- Loki로 WARN과 ERROR 레벨의 로그만 전송하는 appender 설정 --&gt;
&lt;appender name=&quot;LOKI&quot; class=&quot;com.github.loki4j.logback.Loki4jAppender&quot;&gt;

    &lt;!-- 로그 레벨 필터 설정: WARN 이상만 전송됨 (WARN, ERROR) --&gt;
    &lt;filter class=&quot;ch.qos.logback.classic.filter.ThresholdFilter&quot;&gt;
        &lt;level&gt;WARN&lt;/level&gt; &lt;!-- WARN과 ERROR 레벨만 전송 --&gt;
    &lt;/filter&gt;

    &lt;!-- Loki HTTP Push 설정 --&gt;
    &lt;http&gt;
        &lt;!-- Loki 수신 URL (IP 또는 도메인) --&gt;
        &lt;url&gt;http://{Loki 서버 URL}:3100/loki/api/v1/push&lt;/url&gt;
    &lt;/http&gt;

    &lt;!-- 로그 포맷 설정 --&gt;
    &lt;format&gt;
        &lt;!-- 라벨(label): Loki의 로그 분류 기준으로 사용됨 --&gt;
        &lt;label&gt;
            &lt;!-- 예: appName, host, 로그레벨 등을 라벨로 전송 --&gt;
            &lt;pattern&gt;appName=weather-v2-dev,host=${HOSTNAME},level=%level&lt;/pattern&gt;
        &lt;/label&gt;

        &lt;!-- 메시지 포맷: 실제 로그 내용 --&gt;
        &lt;message&gt;
            &lt;!-- 로그 출력 형식 설정 (스레드명, 로그레벨, 로거, 메시지) --&gt;
            &lt;pattern&gt; [%thread] %-5level %logger{36} - %msg%n&lt;/pattern&gt;
        &lt;/message&gt;
    &lt;/format&gt;

&lt;/appender&gt;</code></pre><hr>
<h2 id="4-preometheusyml-설정">4) preometheus.yml 설정</h2>
<h2 id="🚨ecs--fargate-서비스-디스커버리-연동-시--dns_sd_config--cloud-map-사용">🚨ECS + FARGATE 서비스 디스커버리 연동 시 : dns_sd_config + Cloud Map 사용</h2>
<pre><code>global:
  scrape_interval: 1s                             # 메트릭 수집 주기 (기본: 1초)
  evaluation_interval: 1s                         # 알림 룰 평가 주기 (기본: 1초)



  - job_name: &quot;test-one&quot;              
    metrics_path: &quot;/actuator/prometheus&quot;
    static_configs:
      - targets: [&quot;test.application.com&quot;]
    relabel_configs:
      - source_labels: [&#39;__address__&#39;]
        target_label: &#39;instance&#39;
        replacement: &#39;샘플테스트(첫번째)&#39;
    scheme: https

  - job_name: &quot;test-two&quot;                      
    metrics_path: &quot;/actuator/prometheus&quot;
    dns_sd_configs:                              # DNS 기반 서비스 디스커버리 사용
      - names:
          - &#39;test-two-api.test-two.dev&#39;           # 서비스 도메인 {
        type: &#39;A&#39;                                # A 레코드 조회
        port: 80                                 # HTTP 포트
    relabel_configs:
      - source_labels: [&#39;__address__&#39;]
        target_label: &#39;instance&#39;
    scheme: https       
</code></pre><blockquote>
</blockquote>
<h3 id="✅-static_configs"><strong>✅ static_configs</strong></h3>
<h4 id="정적인-타겟-설정--ip나-도메인-주소를-직접-명시">정적인 타겟 설정 – IP나 도메인 주소를 직접 명시</h4>
<ul>
<li>타겟을 수동으로 입력</li>
<li>IP 또는 도메인 주소가 고정되어 있을 때 사용</li>
<li>간단한 구조, 테스트/개발 환경에서 많이 사용<h3 id="사용-예">사용 예</h3>
</li>
<li>EC2, ECS, 로드밸런서 등에 도메인이나 IP로 접근 가능할 때</li>
<li>수가 많지 않고 변경되지 않는 서비스들</li>
</ul>
<hr>
<h3 id="✅-dns_sd_configs"><strong>✅ dns_sd_configs</strong></h3>
<h4 id="dns-기반의-서비스-디스커버리--dns-쿼리를-통해-동적으로-타겟을-찾음">DNS 기반의 서비스 디스커버리 – DNS 쿼리를 통해 동적으로 타겟을 찾음</h4>
<ul>
<li>서비스가 등록된 도메인을 기준으로 DNS A/AAAA 레코드를 조회</li>
<li>클러스터 내부 서비스(DNS가 자동 관리되는 환경, 예: Kubernetes, ECS 서비스 디스커버리 등)에 적합</li>
<li>대상이 늘어나거나 줄어드는 경우 자동 반영 가능 ( ※핵심 )<h3 id="사용-예-1">사용 예</h3>
</li>
<li>AWS ECS, Kubernetes 등 동적으로 생성되는 서비스</li>
<li>DNS를 통해 클러스터 서비스 엔드포인트를 노출할 때</li>
</ul>
<hr>
<h2 id="5-loki-설정">5) Loki 설정</h2>
<pre><code>auth_enabled: false                                                  # 인증 기능 비활성화 (기본적으로 내부 네트워크에서 사용 시 인증 생략 가능)

server:
  http_listen_port: 3100                                              # Loki 서버가 HTTP 요청을 수신할 포트 설정

common:
  instance_addr: 127.0.0.1                                          # Loki 인스턴스의 주소 (gossip ring에 사용)
  path_prefix: /var/loki                                              # Loki가 내부적으로 파일을 저장할 기본 경로
  storage:
    filesystem:                                                      # 로컬 파일 시스템 기반 저장소 사용
      chunks_directory: /var/loki/chunks                              # 로그 청크 데이터를 저장할 디렉토리
      rules_directory: /var/loki/rules                                 # 룰 파일 저장 경로 (알림 룰 등)
  replication_factor: 1                                              # 복제 개수 설정, 1은 단일 노드에서 사용
  ring:
    kvstore:
      store: inmemory                                                  # 키-값 저장소로 인메모리 방식 사용 (단일 노드에 적합)

query_range:
  results_cache:
    cache:
      embedded_cache:
        enabled: true                                                  # 쿼리 결과에 대한 캐시 기능 활성화
        max_size_mb: 100                                             # 캐시 최대 크기 (MB 단위)

schema_config:
  configs:
    - from: 2020-10-24                                              # 이 날짜부터 적용할 스키마 설정
      store: tsdb                                                      # 저장 방식으로 TSDB(Time Series Database) 사용
      object_store: s3                                              # 청크 데이터를 저장할 오브젝트 스토어 타입
      schema: v12                                                      # Loki 스키마 버전 (최신 버전 중 하나)
      index:
        prefix: index_                                              # 인덱스 프리픽스 (S3에 저장될 키 이름의 접두어)
        period: 24h                                                  # 인덱스를 하루 단위로 생성

storage_config:
  aws:
    s3: tnear-loki-log                                              # 사용할 S3 버킷 이름
    region: ap-northeast-2                                          # S3 리전 (서울)
    access_key_id: {AWS_ACCESS_KEY}                                  # AWS 접근 키 (보안상 주의 필요)
    secret_access_key: {AWS_SECRET_ACCESS_KEY}                      # AWS 비밀 접근 키 (보안상 주의 필요)
</code></pre><hr>
<h2 id="6-grafana-접속">6) Grafana 접속</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/3a5c1351-135f-4968-9ded-c54e3d9c99ba/image.png" alt=""></p>
<hr>
<h1 id="🔹-결론">🔹 결론</h1>
<h4 id="⭐-prometheus-메트릭-데이터--loki-로그를-연동하여-시각화">⭐ Prometheus 메트릭 데이터 + Loki 로그를 연동하여 시각화</h4>
<h4 id="⭐-loki는-레이블-기반의-최소-인덱싱-구조-사용">⭐ Loki는 레이블 기반의 최소 인덱싱 구조 사용,</h4>
<h4 id="⭐-prometheus는-자체-db와-압축-구조로-고성능-고효율적임">⭐ Prometheus는 자체 DB와 압축 구조로 고성능 고효율적임</h4>
<h4 id="⭐-loki와-prometheus-설정-파일-기반-운영이-쉬움">⭐ Loki와 Prometheus 설정 파일 기반 운영이 쉬움</h4>
<h4 id="⭐-오픈소스-기반으로-별도-라이선스-비용이-없다">⭐ 오픈소스 기반으로 별도 라이선스 비용이 없다.</h4>
<hr>
<h2 id="시각화-대시보드-구성에-관해서는-다음-글에서-진행하도록-하겠다-😊😊😊😊😊😊😊😊">시각화 대시보드 구성에 관해서는 다음 글에서 진행하도록 하겠다. 😊😊😊😊😊😊😊😊</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[🤖 Jenkins Pipeline을 활용한 CI/CD 구현 및 Webhook]]></title>
            <link>https://velog.io/@dev-hsl-960221/Jenkins-Pipeline%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-CICD-%EA%B5%AC%ED%98%84-%EB%B0%8F-Webhook</link>
            <guid>https://velog.io/@dev-hsl-960221/Jenkins-Pipeline%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-CICD-%EA%B5%AC%ED%98%84-%EB%B0%8F-Webhook</guid>
            <pubDate>Wed, 26 Mar 2025 10:06:12 GMT</pubDate>
            <description><![CDATA[<h1 id="📌-워크로드-상황-정리">📌 워크로드 상황 정리</h1>
<blockquote>
<p>*<em>1. 특정 EC2 인스턴스 내 Docker로 Jenkins 컨테이너 운영
    2. Jenkins Pipeline 내에서  Git Branch Checkout 정의
    3. Jenkins Pipeline 내에서  AWS CLI 사용자 정의
    4. Jenkins Pipeline 내에서  Dockerfile 정의
    5. Jenkins Pipeline 내에서  Slack Notification 정의
    6. Jenkins Pipeline 내에서  Build Gradle 정의
    7. Jenkins Pipeline 내에서  ECR Registry &amp; ECS Deploy 정의 
    8. Jenkins Pipeline 내에서  커밋 로그 추출 Slack WebWook 정의
    *</em></p>
</blockquote>
<hr>
<h2 id="🔹-파이프라인을-사용하기-앞서-필요한-plugin">🔹 파이프라인을 사용하기 앞서 필요한 Plugin</h2>
<h3 id="1️⃣-aws-credentials-plugin">1️⃣ AWS Credentials Plugin</h3>
<h3 id="2️⃣-pipeline-aws-steps-plugin">2️⃣ Pipeline: AWS Steps Plugin</h3>
<h3 id="3️⃣-slack-notification-plugin">3️⃣ Slack Notification Plugin</h3>
<h3 id="4️⃣-git-plugin">4️⃣ Git plugin</h3>
<hr>
<h3 id="🔧-파이프라인-구성-요소-설명">🔧 파이프라인 구성 요소 설명</h3>
<blockquote>
</blockquote>
<ul>
<li>agent        👉 어디서 실행할지를 정의 (any, label, docker, 등)</li>
<li>environment    👉 공통 환경변수 설정</li>
<li>stages        👉 단계들을 그룹핑</li>
<li>stage        👉 개별 작업 단계 (예: 빌드, 테스트, 배포 등)</li>
<li>steps        👉 해당 단계에서 수행할 작업들</li>
<li>post            👉 성공/실패 후의 후처리 작업 정의</li>
</ul>
<hr>
<h2 id="🚨-aws-credentials-정의">🚨 AWS Credentials 정의</h2>
<h3 id="1-jenkins-관리---credentials">1) Jenkins 관리 -&gt; Credentials</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/1068e7a1-3f2a-4f9a-b136-dcd57086af7f/image.png" alt=""></p>
<h3 id="2-aws-credentials-생성">2) AWS Credentials 생성</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/133e7adc-a309-4798-8d49-3e90c318bb5e/image.png" alt=""></p>
<blockquote>
<ul>
<li>AWS Access Key Id  : 공개적으로 식별 가능한 키</li>
</ul>
</blockquote>
<ul>
<li>AWS Secret Access Key : 해당 Access Key ID에 대응하는 비밀번호 같은 비밀 키</li>
</ul>
<p>※  젠킨스 파이프라인 ECR Registry &amp; ECS Deploy 해당 Stage 에서 사용 예정</p>
<p>ex) withAWS(credentials: &#39;{AWS Credentials ID}&#39;, region: {AWS REGION}</p>
<hr>
<h4 id="-설명하기-앞서서-해당-글에-stage-view는-그림과-같다">※ 설명하기 앞서서 해당 글에 Stage View는 그림과 같다.</h4>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/bef31dae-6c2a-48e2-bf8c-a4db35c82e3d/image.png" alt=""></p>
<pre><code>pipeline {
  agent any

  environment {
    BITBUCKET_CREDENTIALS  = &#39;bc2fa053-e540-48bc-b7f7-b504430a63c9&#39;                   // Bitbucket 자격 증명 ID
    AWS_REGION             = &#39;ap-northeast-2&#39;                                        // AWS 리전
    ECR_URI                = &#39;998251115309.dkr.ecr.ap-northeast-2.amazonaws.com&#39;   // ECR URI
    ECR_REPOSITORY_NAME    = &#39;test&#39;                                                 // ECR 리포지토리 이름
    ECS_CLUSTER_NAME       = &#39;test-cluster&#39;                                      // ECS 클러스터 이름
    ECS_SERVICE_NAME       = &#39;test-service&#39;                                      // ECS 서비스 이름
    ECR_IMAGE_TAG          = &#39;latest&#39;                                          // 이미지 태그
    SLACK_CREDENTIALS      = &#39;slack&#39;                                          // Slack 자격 증명 ID
    SLACK_CHANNEL          = &#39;#test-notification&#39;                             // Slack 채널
    COMMIT_FILE            = &quot;previous_commit_hash.txt&quot;                        // 커밋 해시 저장 파일
    PREVIOUS_COMMIT        = &#39;&#39;                                               // 이전 커밋 해시 (동적 할당용)
    CURRENT_COMMIT         = &#39;&#39;                                           // 현재 커밋 해시 (동적 할당용)
  }

  stages {

    stage(&#39;Checkout Bitbucket&#39;) {
      steps {
        git branch: &#39;test&#39;,
            credentialsId: BITBUCKET_CREDENTIALS,
            url: &#39;{브랜치 주소}&#39;
      }
    }

    stage(&#39;Write Dockerfile&#39;) {
      steps {
        script {
          // 동적으로 Dockerfile 작성
          writeFile file: &#39;Dockerfile&#39;, text: &#39;&#39;&#39;
            FROM amazoncorretto:21
            ARG JAR_FILE=./build/libs/test-0.0.1-SNAPSHOT.jar
            COPY ${JAR_FILE} app.jar
            ENV SPRING_PROFILES_ACTIVE=dev
            ENTRYPOINT [&quot;java&quot;, &quot;-Xms3g&quot;, &quot;-Xmx3g&quot;, &quot;-XX:ActiveProcessorCount=2&quot;, &quot;-XX:MaxRAMPercentage=75.0&quot;, &quot;-XX:InitialRAMPercentage=75.0&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]
          &#39;&#39;&#39;
        }
      }
    }

    stage(&#39;Prepare Slack Notification&#39;) {
      steps {
        script {
          // 이전 커밋 해시 확인 (파일에서 읽거나 git에서 가져옴)
          if (fileExists(COMMIT_FILE)) {
            PREVIOUS_COMMIT = readFile(COMMIT_FILE).trim()
          } else {
            PREVIOUS_COMMIT = sh(
              script: &quot;git rev-parse HEAD~1&quot;,
              returnStdout: true
            ).trim()
          }

          // 현재 커밋 해시 가져오기
          CURRENT_COMMIT = sh(
            script: &quot;git rev-parse HEAD&quot;,
            returnStdout: true
          ).trim()
        }
      }
    }

    stage(&#39;Slack Build Notification&#39;) {
      steps {
        script {
          // 빌드 시작 알림
          slackSend(
            color: &quot;#439FE0&quot;,
            channel: SLACK_CHANNEL,
            message: &quot;*프로젝트 이름 : ${env.JOB_NAME}*\n*빌드번호 : ${env.BUILD_NUMBER}*\n*빌드실행 :jenkins_pepe_1:*&quot;,
            tokenCredentialId: SLACK_CREDENTIALS
          )
        }
      }
    }

    stage(&#39;Build Gradle&#39;) {
      steps {
        // Gradle 빌드 실행
        sh &#39;chmod +x ./gradlew&#39;
        sh &#39;./gradlew clean build -Dorg.gradle.java.home=/usr/lib/jvm/jdk-21&#39;
      }
    }

    stage(&#39;ECR Registry &amp; ECS Deploy&#39;) {
      steps {
        // AWS 인증 및 배포 작업
        withAWS(credentials: &#39;aws-credentials-id&#39;, region: AWS_REGION) {
          sh &#39;&#39;&#39;
            aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_URI}

            docker build -t ${ECR_REPOSITORY_NAME} .
            docker tag ${ECR_REPOSITORY_NAME}:${ECR_IMAGE_TAG} ${ECR_URI}/${ECR_REPOSITORY_NAME}:${ECR_IMAGE_TAG}
            docker push ${ECR_URI}/${ECR_REPOSITORY_NAME}:${ECR_IMAGE_TAG}

            docker rmi ${ECR_URI}/${ECR_REPOSITORY_NAME}:${ECR_IMAGE_TAG}
            docker rmi ${ECR_REPOSITORY_NAME}:${ECR_IMAGE_TAG}

            aws ecs update-service --cluster ${ECS_CLUSTER_NAME} --service ${ECS_SERVICE_NAME} --force-new-deployment
          &#39;&#39;&#39;
        }
      }
    }
  }

  post {
    success {
      script {
        // 변경된 커밋 로그 추출
        def newCommits = sh(
          script: &quot;git log ${PREVIOUS_COMMIT}..${CURRENT_COMMIT} --pretty=format:&#39;%s [%an]&#39;&quot;,
          returnStdout: true
        ).trim()

        // 최신 커밋 해시 저장
        writeFile file: COMMIT_FILE, text: CURRENT_COMMIT

        // Slack 알림 - 빌드 성공
        def message = newCommits ?
          newCommits.split(&#39;\n&#39;).collect { it + &quot;  :jenkins_pepe_2:&quot; }.join(&#39;\n&#39;) :
          &quot;*반영된 커밋 없음*&quot;

        slackSend(
          color: &quot;#36a64f&quot;,
          channel: SLACK_CHANNEL,
          message: &quot;*빌드성공 : ${env.JOB_NAME}*\n*빌드번호 : ${env.BUILD_NUMBER}*\n*반영된 커밋 목록*\n${message}&quot;,
          tokenCredentialId: SLACK_CREDENTIALS
        )
      }
    }

    aborted {
      slackSend(
        color: &quot;#ffcc00&quot;,
        channel: SLACK_CHANNEL,
        message: &quot;*Build Aborted: ${env.JOB_NAME} - ${env.BUILD_NUMBER}*&quot;,
        tokenCredentialId: SLACK_CREDENTIALS
      )
    }

    unstable {
      slackSend(
        color: &quot;#ffcc00&quot;,
        channel: SLACK_CHANNEL,
        message: &quot;*Build Unstable: ${env.JOB_NAME} - ${env.BUILD_NUMBER}*&quot;,
        tokenCredentialId: SLACK_CREDENTIALS
      )
    }

    failure {
      script {
        // 실패한 빌드의 커밋 로그 추출
        def newCommits = sh(
          script: &quot;git log ${PREVIOUS_COMMIT}..${CURRENT_COMMIT} --pretty=format:&#39;%s [%an]&#39;&quot;,
          returnStdout: true
        ).trim()

        // 커밋 해시 저장
        writeFile file: COMMIT_FILE, text: CURRENT_COMMIT

        // Slack 알림 - 빌드 실패
        def message = newCommits ?
          newCommits.split(&#39;\n&#39;).collect { it + &quot;  :pepe2:&quot; }.join(&#39;\n&#39;) :
          &quot;*반영된 커밋 없음*&quot;

        slackSend(
          color: &quot;#ff0000&quot;,
          channel: SLACK_CHANNEL,
          message: &quot;*빌드실패 : ${env.JOB_NAME}*\n*빌드번호 : ${env.BUILD_NUMBER}*\n*반영된 커밋 목록*\n${message}&quot;,
          tokenCredentialId: SLACK_CREDENTIALS
        )
      }
    }
  }
}</code></pre><hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[🤖 Jenkins FreeStyle 활용한 CI/CD 구현 및  Webhook]]></title>
            <link>https://velog.io/@dev-hsl-960221/Jenkins-FreeStyle-Project-%ED%99%9C%EC%9A%A9%ED%95%9C-CICD-%EA%B5%AC%ED%98%84-%EB%B0%8F-Webhook</link>
            <guid>https://velog.io/@dev-hsl-960221/Jenkins-FreeStyle-Project-%ED%99%9C%EC%9A%A9%ED%95%9C-CICD-%EA%B5%AC%ED%98%84-%EB%B0%8F-Webhook</guid>
            <pubDate>Tue, 18 Mar 2025 09:52:40 GMT</pubDate>
            <description><![CDATA[<h1 id="📌-워크로드-상황-정리">📌 워크로드 상황 정리</h1>
<p><strong>1. 특정 EC2 인스턴스 내 Docker로 Jenkins 컨테이너 운영</strong>
<strong>2. Jenkins에서 특정 Bitbucket Repository branch 를 Pull 받은 이후 Execute Shell을 활용하여 gradle clean build</strong>
<strong>3. 아티팩트(Jar)를 SSH를 통해 특정 호스트 내 특정 디렉토리로 전송</strong>
<strong>4. 쉘 스크립트와 DockerFile을 통해 ECR 이미지를 만들어주고 ECS 클러스터에 배포</strong>
<strong>5. 해당 브랜치에 반영된 Git Commit Log를 Slack Webhook으로 전송</strong></p>
<blockquote>
<p>해당 글에서는 Jenkins Images를 Pull 받고 Docker Container로 동작하는 부분은 제외 하도록 하겠다. Jenkins Home 까지 진입이 된 상태에서 설명된 글이기 때문에 Jenkins를 컨테이너로 동작하는 과정을 알고 싶다면 잘 정리 되어있고 보기 좋은 참조 문서들이 많기 때문에 다른 글을 참고하기 바란다.</p>
</blockquote>
<h1 id="1️⃣-bitbucket-app-passwrod-발급">1️⃣ Bitbucket App Passwrod 발급</h1>
<h3 id="1-create-app-password">1) Create App Password</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/ad1097ab-fe77-44ba-8330-fd2df72c8294/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/7238e811-aa35-4480-a9d1-22173987dada/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/f476a083-6ea9-4115-9361-11061d5696d7/image.png" alt=""></p>
<h3 id="2-permissions-지정">2) Permissions 지정</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/bc9095ec-82b6-4eeb-81fc-27f1dd7ea271/image.png" alt=""></p>
<blockquote>
<p>최소한의 권한은 소스코드 관리와 Webhook을 받기 위해 Repository, Webhook 접근권한이 필요하다.
※ 추가로 필요한 부분들은 체크 이후 사용하면 될 것 같다</p>
</blockquote>
<h3 id="3-app-password-발급">3) App Password 발급</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/080a3982-3aa6-4ea8-a0cf-cfb9abc6b3b0/image.png" alt=""></p>
<blockquote>
<p>*<em>🚨 고려 사항 : 발급 이후 딱 한 번만 확인이 가능하므로 별도의 저장소나 개인이 관리가 필요함 *</em></p>
</blockquote>
<h1 id="2️⃣-jenkins-bitbucket-credentials-생성">2️⃣ Jenkins Bitbucket Credentials 생성</h1>
<h3 id="1-jenkins-관리---crdentials">1) Jenkins 관리 -&gt; Crdentials</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/51a56c18-d90f-4a19-872a-56110eaaa350/image.png" alt=""></p>
<h3 id="2-add-credentials">2) Add Credentials</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/84d1b26e-89a8-4152-8a81-1c717071279c/image.png" alt=""></p>
<h3 id="3-create-credentials">3) Create Credentials</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/a7c802d4-7a39-4105-b26f-328c3d71f00b/image.png" alt=""></p>
<blockquote>
<ul>
<li><strong>Username</strong> : 형상관리 계정 이름을 나타내는게 대부분이다.</li>
</ul>
</blockquote>
<ul>
<li><strong>Password</strong> : <code>Bitbucket에서 생성된 App Password</code></li>
<li><strong>ID</strong> : Jekins 내에서 Credentials를 식별하는 식별자</li>
<li><strong>Description</strong> : 해당 Credentials를 대한 간단한 설명</li>
</ul>
<h1 id="3️⃣-publish-over-ssh-server-설정">3️⃣ Publish over SSH Server 설정</h1>
<h3 id="1-publish-over-ssh-설치">1) Publish Over SSH 설치</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/f14e9e28-1309-42c5-b149-c5badeaf2628/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/7ef4293d-a4e7-4a38-8445-714aa1164502/image.png" alt=""></p>
<blockquote>
<p>설치가 안 된 경우 Available pligins 에 들어가서 플러그인을 설치하면 된다. </p>
</blockquote>
<h3 id="2-jenkins-관리---system---ssh-설정">2) Jenkins 관리 -&gt; System -&gt; SSH 설정</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/fb93ca6d-dabf-4ef9-aa20-f0916d9f74a6/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/e62026d7-8c94-4a27-bc71-058d8ca566c0/image.png" alt=""></p>
<blockquote>
<ul>
<li><strong>Name</strong> : SSH 서버 별칭</li>
</ul>
</blockquote>
<ul>
<li><strong>Hostname</strong> : SSH 서버 호스트 주소</li>
<li><strong>Username</strong> : SSH 서버 아이디</li>
<li><strong>Remote Directory</strong> : SSH 서버 Remote 경로 위치</li>
<li>*<em>Passphrase / Password *</em> : SSH 서버 비밀번호
<code>🚨 ※ SSH Password 설정은 sshd_config 파일안에 PasswordAuthenticateion 상태 값을 yes 로 변경</code></li>
</ul>
<h1 id="4️⃣-slack-notification-설정">4️⃣ Slack Notification 설정</h1>
<h3 id="1-slack-jenkins-ci-플러그인-추가-및-토큰-생성">1) Slack Jenkins CI 플러그인 추가 및 토큰 생성</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/1d17e355-bb06-4e19-bc40-8c008d0b5721/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/1050ce4a-7507-4bb4-b756-56f4bf749e16/image.png" alt=""></p>
<blockquote>
<p>**🚨 토큰값은 Jekins Slack Credential 설정시 사용하기 때문에 기억해두기!!</p>
</blockquote>
<h3 id="2-slack-notification-plugin-설치">2) Slack Notification Plugin 설치</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/20474384-4a6f-41d3-b4de-ec6ba971dfd1/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/5b47054c-e793-4cad-b534-cafb881f0913/image.png" alt=""></p>
<blockquote>
<p>설치가 안 된 경우 Available pligins 에 들어가서 플러그인을 설치하면 된다. </p>
</blockquote>
<h3 id="3-slack-credential-설정">3) Slack Credential 설정</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/511063c0-ed23-4ec3-9e26-535fc82c38c5/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/bec90ab2-ef76-45b0-ab41-009a47f34b30/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/6f05fcd0-b908-42da-9148-0b1f9cf2d8e0/image.png" alt=""></p>
<blockquote>
<ul>
<li><strong>Secret</strong> : 1번에서 만든 Slack Jenkins CI 토큰</li>
</ul>
</blockquote>
<ul>
<li><strong>ID</strong> : 채널 ID 또는 고유 ID 를 지정</li>
<li><strong>Description</strong> : 해당 Credential 설명</li>
</ul>
<h3 id="4-jenkins-관리---system---slack-설정">4) Jenkins 관리 -&gt; System -&gt; Slack 설정</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/f4710d26-67b1-4c31-80f7-42aeb78a980a/image.png" alt=""></p>
<blockquote>
<ul>
<li><strong>Workspace</strong> : Slack 워크스페이스 이름</li>
</ul>
</blockquote>
<ul>
<li><strong>Credential</strong> : Jenkins가 Slack API를 사용하기 위한 인증 정보 <code>(토큰)</code> / 3번에서 만든 Credential 적용</li>
<li>*<em>Default channel / member id *</em> : 기본적으로 메시지가 전송될 Slack 채널 <code>(#채널명)</code> 또는 사용자 <code>(@사용자명)</code></li>
</ul>
<h1 id="5️⃣-쉘-스크립트-및-dockerfile-작성">5️⃣ 쉘 스크립트 및 DockerFile 작성</h1>
<h3 id="1-ssh-remote-directory-경로-안에-dockerfile-생성">1) SSH Remote Directory 경로 안에 DockerFile 생성</h3>
<pre><code># Amazon에서 제공하는 Corretto OpenJDK 11 이미지를 기본 이미지로 사용합니다.
FROM amazoncorretto:11

# 빌드시 전달받을 jar 파일의 경로를 정의합니다. 기본값은 현재 디렉토리의 weather-1.0.0-BUILD-SNAPSHOT.jar 파일입니다.
ARG JAR_FILE=./weather-1.0.0-BUILD-SNAPSHOT.jar

# 전달받은 jar 파일을 컨테이너 내부에 app.jar라는 이름으로 복사합니다.
COPY ${JAR_FILE} app.jar

# Spring 프로필을 &#39;dev&#39; 환경으로 활성화합니다.
ENV SPRING_PROFILES_ACTIVE=dev

# 컨테이너 내부의 시간대를 한국 시간으로 설정합니다.
ENV TZ=Asia/Seoul

# 컨테이너가 시작될 때 실행될 명령어입니다.
# 최소 힙 메모리 512MB, 최대 힙 메모리 2048MB로 JVM을 실행하며, app.jar를 실행합니다.
ENTRYPOINT [&quot;java&quot;, &quot;-Xms512m&quot;, &quot;-Xmx2048m&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</code></pre><blockquote>
<ul>
<li><strong>FROM</strong> : Docker 이미지를 만들 때 기반 이미지(Base image) 를 지정할 때 사용</li>
</ul>
</blockquote>
<ul>
<li><strong>ARG</strong> : Docker 이미지 빌드 과정 중에 사용할 수 있는 인자(argument)를 설정</li>
<li><strong>COPY</strong> : 로컬에 있는 파일 또는 디렉터리를 이미지 내부로 복사</li>
<li><strong>ENV</strong> : 컨테이너 내부에서 사용할 환경 변수를 설정</li>
<li><strong>ENTRYPOINT</strong> : Docker 컨테이너가 실행될 때 기본적으로 실행되는 명령어를 지정</li>
</ul>
<hr>
<h3 id="2-ssh-remote-directory-경로-안에-쉘-스크립트-생성---vi-편집기-">2) SSH Remote Directory 경로 안에 쉘 스크립트 생성 ( ※ VI 편집기 )</h3>
<h4 id="🚩-1단계-aws-ecr-로그인-인증">🚩 1단계: AWS ECR 로그인 (인증)</h4>
<h4 id="🚩-2단계-docker-이미지-빌드하기">🚩 2단계: Docker 이미지 빌드하기</h4>
<h4 id="🚩-3단계-docker-이미지에-ecr-레지스트리-태그-붙이기">🚩 3단계: Docker 이미지에 ECR 레지스트리 태그 붙이기</h4>
<h4 id="🚩-4단계-ecr로-docker-이미지-푸시하기-업로드">🚩 4단계: ECR로 Docker 이미지 푸시하기 (업로드)</h4>
<h4 id="🚩-5단계-로컬-이미지-삭제하기-정리">🚩 5단계: 로컬 이미지 삭제하기 (정리)</h4>
<h4 id="🚩-6단계-aws-ecs-서비스-재배포-이미지-업데이트-적용">🚩 6단계: AWS ECS 서비스 재배포 (이미지 업데이트 적용)</h4>
<pre><code>#!/bin/bash

# AWS ECR 로그인 (도커 클라이언트 인증)
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 998251115309.dkr.ecr.ap-northeast-2.amazonaws.com

# Docker 이미지 빌드 (로컬 Dockerfile 사용)
docker build -t dev-weather /home/admin/weather-dev

# Docker 이미지 태그 지정 (ECR 레포지토리에 맞게 변경)
docker tag dev-weather:latest 998251115309.dkr.ecr.ap-northeast-2.amazonaws.com/dev-weather:latest

# AWS ECR로 Docker 이미지 업로드
docker push 998251115309.dkr.ecr.ap-northeast-2.amazonaws.com/dev-weather:latest

# 로컬 Docker 이미지 삭제 (디스크 공간 확보)
docker rmi 998251115309.dkr.ecr.ap-northeast-2.amazonaws.com/dev-weather:latest
docker rmi dev-weather:latest

# AWS ECS 서비스 강제 재배포 (새로운 이미지 적용)
aws ecs update-service --cluster dev-weather-cluster --service dev-weather-service --force-new-deployment
</code></pre><h1 id="6️⃣-jenkins-freestyle-project-구성">6️⃣ Jenkins FreeStyle Project 구성</h1>
<h2 id="1-freestyle-project-생성">1) FreeStyle Project 생성</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/8d05a2ca-fba2-47da-885c-795ff03f61c0/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/5af83731-3b85-4fac-aa4a-225fcb66bfad/image.png" alt=""></p>
<h2 id="2-freestyle-project-구성">2) FreeStyle Project 구성</h2>
<h3 id="1-git-소스코드-관리">1. Git 소스코드 관리</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/0c51d802-8d14-4d54-acad-34d87ad5696a/image.png" alt=""></p>
<blockquote>
<ul>
<li><strong>Repository URL</strong> : Git Repository URL</li>
</ul>
</blockquote>
<ul>
<li><p><strong>Credentials</strong> : Jenkins에서 등록한 Bitbucket Credentials 설정</p>
</li>
<li><p><strong>Branch Specifier</strong> : 브랜치명</p>
<h5 id="git-trigger를-제외한-이유-❓">Git Trigger를 제외한 이유 ❓</h5>
<ul>
<li><h5 id="불필요한-빌드-방지">불필요한 빌드 방지</h5>
</li>
<li><h5 id="의도적인-수동-제어">의도적인 수동 제어</h5>
</li>
</ul>
</li>
</ul>
<p><strong><em><code>🚨 꺼진불도 다시보자!!!!!</code></em></strong></p>
<hr>
<h3 id="2-build-steps">2. Build Steps</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/67117470-d143-4a81-8ec0-1a5d7531fe8c/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/1163f8de-41ed-4da8-b78f-1598f6e3e127/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/37763323-e3ce-419b-9ea9-80c54b79c7a9/image.png" alt=""></p>
<h4 id="1-chmod-x-gradlew">1. <code>chmod +x ./gradlew</code></h4>
<pre><code>1) 이 명령어는 현재 디렉토리에 있는 gradlew 파일을 실행 가능(executable)하게 만드는 명령입니다.
2) chmod 명령어는 파일의 접근 권한(permission)을 변경하는 명령어입니다.
3) +x 옵션은 파일을 실행할 수 있는 권한을 부여하는 의미이며, 주로 스크립트나 실행파일을 실행하기 위해 설정합니다.
4) ./gradlew는 Gradle Wrapper로, 프로젝트에서 정의한 Gradle 버전을 자동으로 다운로드하고 빌드합니다.</code></pre><p> <strong><em>🚨 즉, 이 명령어가 없으면 ./gradlew 실행 시 Permission denied 에러가 발생할 수 있습니다.</em></strong></p>
<h4 id="2-gradlew-clean-build">2. <code>./gradlew clean build</code></h4>
<pre><code>1) 이 명령어는 Gradle 프로젝트를 빌드하는 과정입니다.
2) clean은 이전 빌드에서 생성된 모든 빌드 결과물을 삭제하는 작업입니다.
3) 주로 이전 빌드로 인해 발생할 수 있는 충돌이나 문제를 방지하기 위해 사용합니다.
4) build는 프로젝트의 소스를 컴파일하고, 테스트를 수행한 후 빌드 결과물(JAR/WAR 등)을 생성합니다.</code></pre><hr>
<h3 id="3-빌드-후-조치">3. 빌드 후 조치</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/453a189e-c44a-405f-bd2c-a9f402f6d0bd/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/347f5b46-1839-49aa-8336-b54013b86dc8/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/1552c420-801b-4322-8f6a-56de025b03ea/image.png" alt=""></p>
<blockquote>
<ul>
<li><strong>Name</strong> : Jenkins System에서 설정한 SSH</li>
</ul>
</blockquote>
<ul>
<li><p><strong>Source files</strong> : 빌드된 결과물(artifact)에 대한 경로 위치 </p>
</li>
<li><p><strong>Remove prefix</strong> : 접두사로 지정된 경로를 제거한 후 나머지 경로만 사용</p>
</li>
<li><p><strong>Remote directory</strong> : 전송된 아티팩트를 대상 서버 원하는 경로</p>
</li>
<li><p><strong>Exec command</strong> : 아티팩트를 원격 서버에 전송한 후 실행할 명령어</p>
</li>
<li><p>** 쉘 스크립트에 배포에 대한 명령어를 정의해놨으니 쉘 스크립트를 실행!!!!! ***</p>
</li>
</ul>
<hr>
<h3 id="4-slack-webhook">4. Slack Webhook</h3>
<p> <img src="https://velog.velcdn.com/images/dev-hsl-960221/post/4c020074-6005-46a8-ae5f-16e1338579fc/image.png" alt=""></p>
<blockquote>
<ul>
<li><strong>Notification message includes</strong> : Jenkins가 Slack으로 보낼 알림 메시지에 포함할 항목들을 선택할 수 있도록 해주는 설정입니다.</li>
</ul>
</blockquote>
<ul>
<li><strong>Credential</strong> : Jenkins System에서 설정한 Slack Credentials</li>
</ul>
<h1 id="🔹-결론">🔹 결론</h1>
<h3 id="jenkins에서-pipelinejob-as-code-방식이-더-많이-사용되고-권장되고-있지만-pipeline-대비-freestyle-장점을-적어보겠다">Jenkins에서 Pipeline(Job as Code) 방식이 더 많이 사용되고 권장되고 있지만 Pipeline 대비 Freestyle 장점을 적어보겠다.</h3>
<p> ⭐ 별도의 스크립트 작성 없이 플러그인을 쉽고 간편하게 추가 및 사용 가능
 ⭐ 코드 없이 직관적인 GUI 기반의 관리 
 ⭐ 빠르고 쉬운 설정이 가능하다. 즉, 구성 속도가 빠르다.
 ⭐ 러닝커브가 낮기 때문에 신규 사용자도 진입 장벽이 매우 낮은 장점이 있다.  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔔 Bitbucket - Slack Webhook Integration]]></title>
            <link>https://velog.io/@dev-hsl-960221/Bitbucket-Slack-Webhook-Integration</link>
            <guid>https://velog.io/@dev-hsl-960221/Bitbucket-Slack-Webhook-Integration</guid>
            <pubDate>Tue, 11 Mar 2025 08:45:37 GMT</pubDate>
            <description><![CDATA[<h1 id="🔹-integration이란">🔹 Integration이란</h1>
<blockquote>
<p>단어 그대로 <strong>통합</strong>이다. 서로 다른 서비스나 소프트웨어를 하나의 시스템처럼 동작하게 만드는 것이다. 쉽게 말하자면 서로 다른 독립적인 도구나 서비스가 정보를 주고받으면서 함께 동작하도록 연결하는 것</p>
</blockquote>
<h1 id="🔹-integration-하는-이유">🔹 Integration 하는 이유</h1>
<blockquote>
<p>협업(co-work)의 중요성은 항상 소통이 중요하다고 생각한다. 하지만 바쁘거나 다른 업무로 인한 인터럽트가 발생하면 우리는 놓치는 부분들이 많이 발생한다. 그렇기 때문에 Integration을 통한 즉각적인 정보 공유를 할 수 있고 어느 정도 말을 하지 않아도 누가 어떤 업무를 진행 중인지 파악을 할 수가 있다. 수동적인 작업이 줄어듦과 동시에 효율성이 증가한다.</p>
</blockquote>
<hr>
<h4 id="개발팀-중에서-be-부서는-현재-bitbucket-형상-관리를-통한-버전관리-소스코드-관리-변경-관리를-진행하고-있기-때문에-integration을-구현하는-방법중-하나인-webhook을-통해-이벤트를-slack으로-전달하는-프로세스를-설명하는-글입니다">개발팀 중에서 BE 부서는 현재 Bitbucket 형상 관리를 통한 버전관리, 소스코드 관리, 변경 관리를 진행하고 있기 때문에 Integration을 구현하는 방법중 하나인 Webhook을 통해 이벤트를 Slack으로 전달하는 프로세스를 설명하는 글입니다.</h4>
<h2 id="1️⃣-repository---settings">1️⃣ Repository -&gt; Settings</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/4576e09a-904a-4233-92be-538112c2bc3e/image.png" alt=""></p>
<blockquote>
<p>※ 만약 사이드바에 &quot;Repository settings&quot; 부분이 보이지가 않는다면 해당 레포지토리의 접근 권한을 부여 받아야함
Admin : 레포지토리 설정 관리 가능</p>
</blockquote>
<h2 id="2️⃣-slack---settings">2️⃣ Slack -&gt; Settings</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/8b899e2d-4ff8-4911-b6da-e02b66bd4ea1/image.png" alt=""></p>
<h2 id="3️⃣-add-new-workspace--add-subscription">3️⃣ Add New Workspace / Add Subscription</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/0f07a685-d912-4eb8-88b7-c63f2e59e0d0/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-hsl-960221/post/c4af5b2e-55fa-4c76-a287-5b7f05a198f4/image.png" alt=""></p>
<blockquote>
<p>Webhook을 보내기위한 Workspace와 Channel을 지정할수가있다. 본인은 이미 해당 bitbucket-notification이라는 채널에 Webhook을 보내고 있기 때문에 비활성화로 보이고 있다.</p>
</blockquote>
<h2 id="4️⃣-branch-및-notifications-지정">4️⃣ Branch 및 Notifications 지정</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/2cb4ebc0-2520-4cd3-ba97-06be82c43490/image.png" alt=""></p>
<blockquote>
<p>특정 브랜치마다 Notification 종류를 다르게 가져갈 수 있다. 본인은 main을 제외한 모든 브랜치에 대해서는
Pull Request, Branch, Commits 관련한 모든 노티를 적용 main 브랜치에 대해서는 Deploy 관련한 노티만 적용</p>
</blockquote>
<h2 id="5️⃣-slack-webhook">5️⃣ Slack Webhook</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/3cdb9fde-1f0b-4e89-bb3e-5458a616ea24/image.png" alt=""></p>
<h1 id="🔹-결론">🔹 결론</h1>
<p>⭐ Webhook을 통한 트래킹이 가능해지고 History가 남게 되므로 추적과 관찰이 가능
⭐ 즉각적인 업무 공유가 가능해지고 어느 정도 누군가의 업무 파악이 가능</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🗂️ yaml 파일을 Profile Group을 적용하여 목적에 맞게 분리]]></title>
            <link>https://velog.io/@dev-hsl-960221/yaml-%ED%8C%8C%EC%9D%BC%EC%9D%84-Profile-Group%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%98%EC%97%AC-%EB%AA%A9%EC%A0%81%EC%97%90-%EB%A7%9E%EA%B2%8C-%EB%B6%84%EB%A6%AC</link>
            <guid>https://velog.io/@dev-hsl-960221/yaml-%ED%8C%8C%EC%9D%BC%EC%9D%84-Profile-Group%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%98%EC%97%AC-%EB%AA%A9%EC%A0%81%EC%97%90-%EB%A7%9E%EA%B2%8C-%EB%B6%84%EB%A6%AC</guid>
            <pubDate>Tue, 11 Mar 2025 00:55:28 GMT</pubDate>
            <description><![CDATA[<h1 id="🔹-profile-group이란">🔹 Profile Group이란</h1>
<blockquote>
<p>간단하게 한 문장으로 정리하자면 <strong>여러 개의 프로파일을 그룹으로 묶어서 함께 적용할 수 있도록 하는 기능</strong> 이다.
즉, 특정 프로파일을 활성화하면 연관된 프로파일들을 자동으로 포함할 수 있도록 도와줍니다.</p>
</blockquote>
<h1 id="🔹-왜-profile-group을-사용했을까">🔹 왜 Profile Group을 사용했을까</h1>
<blockquote>
<p>application.yml 파일 안에 모든 환경별 설정을 계속해서 넣었을 때 유지보수나 가독성이 현저히 떨어지는 부분이 발생함
또 환경 설정 파일이 비대화 및 복잡도가 커지다보니 중복 설정이 발생하기도 했다. 이를 해결하기 위한 고민을 했던 것 같다. 더 유연한 프로파일 구성을 하는 방법이 있는지.... 🤔🤔</p>
</blockquote>
<hr>
<h2 id="✅-기존-방식--as-is-">✅ 기존 방식 ( AS-IS )</h2>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/2abb2488-ebcc-44fa-9bcf-025d29a944fe/image.png" alt=""></p>
<blockquote>
<p>application.yml 파일 안에 모든 설정값을 넣어서 관리하기 시작했다. 이때 내 생각은 설정값이 지나치게 길어지는 것보다 파일이 많아지는 게 오히려 개발 관점에서 불편함이 발생할 거라고 생각했었다.</p>
</blockquote>
<h2 id="✅-개선-이후-방식--to-be-">✅ 개선 이후 방식 ( TO-BE )</h2>
<h3 id="1-applicationyml을-목적에-맞게-분리">1. application.yml을 목적에 맞게 분리</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/eb278b2a-f045-4669-b267-84cf2405db59/image.png" alt=""></p>
<h3 id="2-profile-group-적용">2. Profile Group 적용</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/a81abca0-0dc8-4a20-af9a-b705f60e885d/image.png" alt=""></p>
<h3 id="3-run-application-edit-configuration-active-profiles-설정">3. Run Application Edit Configuration Active Profiles 설정</h3>
<p><img src="https://velog.velcdn.com/images/dev-hsl-960221/post/b65f5b1c-9620-49bf-83bb-2601fd5ccc4c/image.png" alt=""></p>
<blockquote>
<p>목적에 맞게 프로파일을 구분지어 관리하기 시작했다. 공통부분들은 묶어서 관리하였고 배포 환경별 설정들을 쉽게 조합하기가 쉬워졌다. </p>
</blockquote>
<h1 id="🔹-결론">🔹 결론</h1>
<p>⭐ Profile Group을 사용하면 중복 설정을 줄이고 가독성과 유지보수가 용이해진다.
⭐ 환경별 설정을 쉽게 관리할 수가 있고 더 유연한 프로파일 구성이 가능해진다.</p>
]]></description>
        </item>
    </channel>
</rss>