<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>gm-15.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Fri, 07 Aug 2026 12:05:18 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>gm-15.log</title>
            <url>https://velog.velcdn.com/images/gm-15/profile/dfc67a66-8a0f-4c40-9225-5f8fa4f9fb28/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. gm-15.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/gm-15" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[한국어 AI 뉴스 RAG는 왜 원하는 기사를 못 찾아올까]]></title>
            <link>https://velog.io/@gm-15/%ED%95%9C%EA%B5%AD%EC%96%B4-AI-%EB%89%B4%EC%8A%A4-RAG%EB%8A%94-%EC%99%9C-%EC%9B%90%ED%95%98%EB%8A%94-%EA%B8%B0%EC%82%AC%EB%A5%BC-%EB%AA%BB-%EC%B0%BE%EC%95%84%EC%98%AC%EA%B9%8C</link>
            <guid>https://velog.io/@gm-15/%ED%95%9C%EA%B5%AD%EC%96%B4-AI-%EB%89%B4%EC%8A%A4-RAG%EB%8A%94-%EC%99%9C-%EC%9B%90%ED%95%98%EB%8A%94-%EA%B8%B0%EC%82%AC%EB%A5%BC-%EB%AA%BB-%EC%B0%BE%EC%95%84%EC%98%AC%EA%B9%8C</guid>
            <pubDate>Fri, 07 Aug 2026 12:05:18 GMT</pubDate>
            <description><![CDATA[<h1 id="한국어-ai-뉴스-rag는-왜-원하는-기사를-못-찾아올까">한국어 AI 뉴스 RAG는 왜 원하는 기사를 못 찾아올까</h1>
<blockquote>
<p>검색, 재정렬, 생성을 분리해서 측정한 팀 연구 기록입니다. AI가 답을 못 만드는 것이 아니라, 원하는 기사를 제대로 찾지 못하는 것이 진짜 문제였습니다. 이번 연구는 그 원인이 어디에 있는지를 하나씩 분리해 확인한 과정입니다.</p>
</blockquote>
<hr>
<h2 id="ai는-답을-못-만든-것이-아니라-기사를-못-찾고-있었다">AI는 답을 못 만든 것이 아니라, 기사를 못 찾고 있었다</h2>
<p>AI 산업 뉴스를 매일 확인해야 하는 실무자는 하루 평균 약 1.5시간을 뉴스 검색과 정리에 사용합니다. 자동으로 뉴스를 요약해 주는 서비스는 많지만, 막상 사용해 보면 원하는 기사를 가져오지 못하는 경우가 적지 않습니다.</p>
<p>예를 들어 &quot;AI 반도체&quot;를 검색했는데 AI PC 기사가 나오거나, &quot;최근 LLM 동향&quot;을 물었는데 오래된 뉴스가 검색되기도 합니다. 생성된 답변이 틀린 것이 아니라, 애초에 검색된 근거가 잘못된 경우였습니다.</p>
<p>처음에는 생성 모델을 바꾸면 해결될 것이라고 생각했습니다. 하지만 실제 사례를 분석할수록 의문이 생겼습니다.</p>
<p><strong>정말 생성 모델이 문제일까?</strong></p>
<p>우리의 가설은 조금 달랐습니다.</p>
<blockquote>
<p>생성보다 먼저 검색(Retrieval)이 병목일 가능성이 있다.</p>
</blockquote>
<p>이 가설을 확인하기 위해 이번 연구는 검색, 재정렬(Reranker), 생성 단계를 하나의 파이프라인으로 보지 않고 각각 독립적으로 측정하기로 했습니다.</p>
<hr>
<h2 id="먼저-정답지를-의심했다">먼저 정답지를 의심했다</h2>
<p>검색 성능을 평가하려면 먼저 정답이 있어야 합니다.</p>
<p>생각보다 많은 RAG 프로젝트가 이 부분을 간단하게 넘깁니다. 질문 몇 개를 만들고 모델이 잘 찾는지만 확인합니다. 하지만 정답 자체가 애매하면 모델이 틀린 것인지, 평가셋이 잘못된 것인지 구분할 수 없습니다.</p>
<p>그래서 가장 먼저 평가셋부터 설계했습니다.</p>
<p>뉴스 데이터는 제가 백엔드로 운영·고도화해 온 뉴스 분석 시스템 <strong>INSK</strong>에서 수집한 코퍼스를 사용했습니다. Naver, AI Times, The Guru에서 수집한 기사 가운데 URL 중복 제거와 제목 Jaccard 유사도를 거친 <strong>616개</strong>의 기사를 대상으로 했습니다. INSK가 기사를 &quot;쌓는&quot; 시스템이라면, 이 연구는 그 위에서 기사를 &quot;찾는&quot; 문제를 다뤘습니다.</p>
<p>이후 AI 산업 부서 실무자를 가정해 질문 50개를 작성했습니다. 작성이 끝난 뒤에는 각 질문마다 실제 정답 기사가 코퍼스 안에 존재하는지 하나씩 직접 검증했습니다.</p>
<p>그 결과는 세 가지로 나뉘었습니다.</p>
<ul>
<li>정답이 명확한 질문 21개</li>
<li>현실적인 질문이지만 코퍼스에 정답이 없는 질문 6개</li>
<li>도메인 밖 질문 17개</li>
</ul>
<p>도메인 밖 질문은 제거했습니다.</p>
<p>반면 정답이 없는 질문은 버리지 않았습니다. 오히려 운영 환경에서는 &quot;답이 없는 질문&quot;이 더 중요하다고 판단했습니다. 존재하지 않는 정보를 그럴듯하게 만들어 내는 것보다, <strong>모른다고 말할 수 있는 능력</strong> 역시 RAG가 갖춰야 할 품질이기 때문입니다.</p>
<p>그래서 최종 평가셋은 다음과 같이 구성했습니다.</p>
<ul>
<li>Strict QA 7개</li>
<li>Trend QA 14개</li>
<li>Negative QA 6개</li>
</ul>
<p>총 27개의 평가셋으로 연구를 진행했습니다.</p>
<hr>
<h2 id="검색-재정렬-생성을-따로-측정했다">검색, 재정렬, 생성을 따로 측정했다</h2>
<p>이번 연구의 핵심은 하나였습니다.</p>
<p><strong>전체 RAG를 평가하지 않는다. 검색과 재정렬, 생성을 각각 따로 본다.</strong></p>
<p>파이프라인은 다음과 같이 구성했습니다.</p>
<pre><code>Retrieval
    ↓
Reranker
    ↓
Temporal Weighting (보조)
    ↓
Generation
    ↓
Evaluation</code></pre><p>검색은 BM25와 임베딩 기반 검색을 비교했고, 재정렬은 Cross Encoder를 파인튜닝하여 사용했습니다. 뉴스는 최신성이 중요하기 때문에, 발행일이 오래될수록 가중치를 낮추는 시간 가중치(exp(−λ·경과일))도 보조 장치로 더했습니다. 생성은 GPT-4o-mini를 이용했고, 평가는 RAGAS를 사용했습니다.</p>
<p>그리고 단계마다 서로 다른 지표를 사용했습니다.</p>
<ul>
<li>검색: Recall@5</li>
<li>재정렬: MRR, nDCG</li>
<li>생성: Answer Similarity, Faithfulness, Context Precision</li>
</ul>
<p>이렇게 해야 어느 단계가 병목인지 구분할 수 있기 때문입니다.</p>
<hr>
<h2 id="검색-모델-다섯-개를-비교했다">검색 모델 다섯 개를 비교했다</h2>
<p>먼저 Retrieval 성능부터 비교했습니다. 비교 대상은 다음 다섯 가지였습니다.</p>
<ul>
<li>BM25</li>
<li>OpenAI text-embedding-3-small</li>
<li>BGE-M3</li>
<li>ko-sroberta</li>
<li>Hybrid(RRF)</li>
</ul>
<p>Recall@5 결과는 다음과 같았습니다.</p>
<table>
<thead>
<tr>
<th>검색 방식</th>
<th align="right">Recall@5</th>
</tr>
</thead>
<tbody><tr>
<td>Hybrid(RRF)</td>
<td align="right"><strong>0.417</strong></td>
</tr>
<tr>
<td>BGE-M3</td>
<td align="right">0.401</td>
</tr>
<tr>
<td>ko-sroberta</td>
<td align="right">0.353</td>
</tr>
<tr>
<td>BM25</td>
<td align="right">0.306</td>
</tr>
<tr>
<td>OpenAI Embedding</td>
<td align="right">0.206</td>
</tr>
</tbody></table>
<p>가장 놀라웠던 결과는 범용 유료 모델인 OpenAI 임베딩이 가장 낮은 성능을 보였다는 점이었습니다. 범용 임베딩 모델이 항상 더 좋은 결과를 만드는 것은 아니었습니다.</p>
<p>질문 유형별로 보면 차이는 더 분명했습니다. 예를 들어 &quot;삼성전자&quot; 같은 한국어 기업명이 포함된 질문에서는 ko-sroberta가 <strong>0.688</strong>을 기록한 반면, BM25는 <strong>0.06</strong>으로 사실상 검색에 실패했습니다. 같은 질문에서 모델 간 10배 이상의 차이가 났습니다. 영문 기술 용어(NVIDIA 등)에서는 BGE-M3가 안정적이었고, 다양한 질문 유형을 평균적으로 가장 잘 처리한 것은 Hybrid 검색이었습니다.</p>
<p>이번 실험을 통해 얻은 결론은 단순했습니다.</p>
<p><strong>좋은 모델보다, 데이터와 도메인에 맞는 모델이 더 중요하다.</strong></p>
<hr>
<h2 id="reranker는-좋아졌지만-천장을-넘지는-못했다">Reranker는 좋아졌지만 천장을 넘지는 못했다</h2>
<p>검색 결과를 더 개선하기 위해 Cross Encoder 기반 Reranker를 학습했습니다.</p>
<p>정답과 매우 비슷하지만 실제 정답은 아닌 Hard Negative를 모아 질문-정답, 질문-오답 쌍을 구성했고, 총 972개의 학습 데이터를 만들었습니다.</p>
<p>학습 후 결과는 분명했습니다.</p>
<ul>
<li>MRR: 0.43 → <strong>0.81</strong></li>
<li>nDCG: 0.344 → <strong>0.601</strong></li>
</ul>
<p>순위 품질은 크게 좋아졌습니다. 실제로 1차 검색 상위 10개 안에 있던 정답을, Reranker가 상위 5개로 정확히 끌어올려 Recall@5가 <strong>0.536으로 유지</strong>됐습니다.</p>
<p>그런데 이상한 점이 있었습니다.</p>
<p>Recall@5는 더 이상 올라가지 않았습니다. <strong>3회 학습부터 지표가 천장에 닿아, 10회·30회로 늘려도 소수점까지 동일했습니다(Recall@5 0.536, MRR 0.810, nDCG 0.601). 1회 학습만 그보다 낮았습니다(Recall@5 0.512, MRR 0.738, nDCG 0.550).</strong></p>
<p>원인을 분석해 보니 이유는 단순했습니다.</p>
<p>Reranker는 1차 검색이 가져온 후보 안에서만 순서를 다시 정할 수 있습니다. 처음 검색이 정답을 가져오지 못했다면, 아무리 좋은 Reranker도 존재하지 않는 정답을 만들 수는 없습니다.</p>
<p>즉, Reranker는 이미 1차 검색이 만든 상한 안에서만 동작하고 있었습니다. 이번 실험은 성능 향상보다 더 중요한 사실을 보여주었습니다.</p>
<p><strong>재정렬은 검색의 한계를 넘을 수 없다.</strong></p>
<hr>
<h2 id="생성-모델의-문제가-아니었다">생성 모델의 문제가 아니었다</h2>
<p>마지막으로 생성 품질을 분석했습니다.</p>
<p>RAGAS 결과는 전반적으로 답변 유사도는 높았고(약 0.86), Faithfulness(약 0.61)와 Context Precision(약 0.68)은 상대적으로 낮았습니다.</p>
<p>처음에는 생성 모델의 환각을 의심했습니다. 하지만 실패 사례를 하나씩 확인해 보니 대부분은 환각이 아니었습니다.</p>
<p>검색이 관련 없는 기사를 가져오자, 모델은 답을 지어내지 않고 &quot;관련 정보를 찾을 수 없습니다&quot;라고 응답하고 있었습니다. 회피(abstention)가 많았던 것입니다.</p>
<p>회피 사례 11건을 다시 분석했습니다.</p>
<ul>
<li>검색 문제 5건</li>
<li>생성 문제 2건</li>
<li>복합 원인 4건</li>
</ul>
<p>가장 많은 원인은 검색이었습니다. 생성 모델을 아무리 교체해도 검색이 잘못되면 답변 역시 좋아질 수 없었습니다.</p>
<hr>
<h2 id="그래서-병목은-검색이었다">그래서 병목은 검색이었다</h2>
<p>이번 연구에서 가장 크게 배운 것은 성능보다 측정 방법이었습니다.</p>
<p>처음에는 더 좋은 생성 모델을 찾으면 해결될 것이라고 생각했습니다. 하지만 검색, 재정렬, 생성을 분리해서 측정해 보니 <strong>전체 성능의 상한은 이미 검색 단계에서 결정</strong>되고 있었습니다.</p>
<p>구체적으로, 1차 검색 상위 10개의 Recall이 약 <strong>0.54</strong>였고, 이 값이 전체 성능의 천장으로 작용했습니다. 재정렬은 검색이 가져온 후보 안에서만 동작했고, 생성은 검색된 문서를 기반으로만 답할 수 있었습니다. 실패 사례 분석에서도 회피 11건 중 5건이 검색 책임으로, 같은 결론을 가리켰습니다.</p>
<p>결국 RAG 성능을 높이기 위해 가장 먼저 개선해야 할 대상은 생성 모델이 아니라 검색 단계였습니다.</p>
<p>앞으로의 과제도 자연스럽게 정리되었습니다.</p>
<ul>
<li>Reranker 후보 수 확대(상위 10개 → 50개)</li>
<li>본문 단위 청크 검색</li>
<li>BGE-M3 Sparse·Multi-vector 활용</li>
<li>질문 특성 기반 검색 모델 라우팅</li>
</ul>
<hr>
<h2 id="내가-맡은-역할-그리고-insk로-이어진-것">내가 맡은 역할, 그리고 INSK로 이어진 것</h2>
<p>이번 연구는 4명이 함께 진행한 팀 연구였습니다.</p>
<p>저는 연구 전반의 설계를 담당하며 다음 영역을 직접 수행했습니다.</p>
<ul>
<li>INSK 운영 데이터를 활용한 뉴스 코퍼스 구축</li>
<li>QA 평가셋 설계 및 구축</li>
<li>Strict, Trend, Negative QA 평가 체계 설계</li>
<li>Retrieval, Reranker, Generation을 분리한 평가 구조 설계</li>
<li>Streamlit 기반 데모 및 결과 통합</li>
</ul>
<p>Cross Encoder 파인튜닝과 RAGAS 측정은 팀원이 구현했고, 저는 연구 방향을 설계하고 리뷰하며 전체 결과를 통합했습니다.</p>
<p>가장 크게 얻은 것은 모델 하나의 성능보다, <strong>문제를 어디에서 측정해야 하는지를 보는 관점</strong>이었습니다. 좋은 RAG는 더 뛰어난 생성 모델을 붙이는 문제가 아니라, 검색과 재정렬, 생성 가운데 어디가 병목인지 먼저 분리해서 확인하는 것에서 시작한다는 사실을 이번 연구를 통해 배웠습니다.</p>
<p>그리고 이 &quot;표면이 아니라 한 단계 아래를 분리해서 측정한다&quot;는 관점은, 제가 INSK를 운영하고 다시 설계할 때도 그대로 이어졌습니다. 다만 이 RAG 연구는 INSK에서 데이터를 가져와 진행한 별개의 연구 자산이며, INSK 본체(운영 백엔드)와는 구분되는 작업입니다.</p>
<hr>
<blockquote>
<p>관련 글: INSK 운영 백엔드를 다시 설계한 이야기는 <a href="https://velog.io/@gm-15/INSKv4-%EC%9A%B4%EC%98%81-%EA%B0%80%EB%8A%A5%ED%95%9C-AI-%EB%89%B4%EC%8A%A4-%EC%B6%94%EC%B2%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%EB%8B%A4%EC%8B%9C-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0">INSK_v4 - 운영 가능한 AI 뉴스 추천 시스템으로 다시 설계하기</a>에서 다룹니다.</p>
</blockquote>
<p>깃허브 링크 : <a href="https://github.com/gm-15/INSK-trend-forecast">https://github.com/gm-15/INSK-trend-forecast</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[INSK_v4 - 운영 가능한 AI 뉴스 추천 시스템으로 다시 설계하기]]></title>
            <link>https://velog.io/@gm-15/INSKv4-%EC%9A%B4%EC%98%81-%EA%B0%80%EB%8A%A5%ED%95%9C-AI-%EB%89%B4%EC%8A%A4-%EC%B6%94%EC%B2%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%EB%8B%A4%EC%8B%9C-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@gm-15/INSKv4-%EC%9A%B4%EC%98%81-%EA%B0%80%EB%8A%A5%ED%95%9C-AI-%EB%89%B4%EC%8A%A4-%EC%B6%94%EC%B2%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%EB%8B%A4%EC%8B%9C-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 07 Aug 2026 11:05:03 GMT</pubDate>
            <description><![CDATA[<h1 id="insk-v4---운영-가능한-ai-뉴스-추천-시스템으로-다시-설계하기">INSK v4 - 운영 가능한 AI 뉴스 추천 시스템으로 다시 설계하기</h1>
<blockquote>
<p>INSK 시리즈의 마지막 글.
프로젝트를 다시 만들며 바뀐 것은 판단 기준이었다.</p>
</blockquote>
<hr>
<h2 id="프로젝트-소개">프로젝트 소개</h2>
<p>INSK는 AI를 활용해 뉴스를 수집, 분석하고, 부서별 관심사에 맞는 뉴스를 추천하는 플랫폼이다.
<img src="https://velog.velcdn.com/images/gm-15/post/21735018-e8d2-49b9-b850-7ddd7ce4f1d8/image.png" alt=""></p>
<p>v1은 Make 기반 프로토타입으로,
v2는 Python 기반으로 발전했다.
둘 다 SK mySUNI 써니C에서 팀으로 만들었고,
v3부터는 혼자 Spring Boot 기반으로 제품화했다.</p>
<p>이번 v4에서는 기능을 더하는 대신,
운영 관점의 코드 리뷰와 그 과정에서 발견한 문제를 하나씩 다시 설계했다.</p>
<hr>
<h2 id="이전-글">이전 글</h2>
<ul>
<li><a href="%EB%A7%81%ED%81%AC">SK mySUNI 써니C 후기 </a></li>
<li><a href="https://velog.io/@gm-15/INSK-%EA%B0%9C%EB%B0%9C%ED%8E%B81%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85%EC%9D%84-%EB%B2%84%EB%A6%AC%EA%B3%A0-%EC%B2%98%EC%9D%8C%EC%9C%BC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EB%A7%8C%EB%93%A4%EA%B2%A0%EB%8B%A4%EA%B3%A0-%EA%B2%B0%EC%A0%95%ED%95%9C-%EC%88%9C%EA%B0%84">개발편 ① — INSK 개발편1(프로토타입을 버리고, 처음으로 ‘서비스’를 만들겠다고 결정한 순간)
</a></li>
<li><a href="https://velog.io/@gm-15/INSK-%EA%B0%9C%EB%B0%9C%ED%8E%B8-2">개발편 ② — INSK 개발편2
</a></li>
<li><a href="https://velog.io/@gm-15/INSK-%EB%B0%B0%ED%8F%AC%ED%8E%B8">배포편 ① — INSK 배포편
</a></li>
<li><a href="https://velog.io/@gm-15/INSK-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85%ED%8E%B8">트러블슈팅편
</a></li>
</ul>
<p><a href="https://github.com/gm-15/INSK">깃허브 링크 </a>: <a href="https://github.com/gm-15/INSK">https://github.com/gm-15/INSK</a></p>
<hr>
<h2 id="이-글을-읽기-전에">이 글을 읽기 전에</h2>
<p>이 글은 &quot;어떻게 구현했는가&quot;보다
&quot;왜 그렇게 다시 설계했는가&quot;에 집중한다.</p>
<p>모든 내용은 실제 구현과 측정 결과를 기준으로 작성했음을 알린다.</p>
<hr>
<h2 id="part-1-기능이-돌아가는-것과-신뢰할-수-있는-것은-달랐다">PART 1. 기능이 돌아가는 것과, 신뢰할 수 있는 것은 달랐다</h2>
<p>프로젝트를 처음 만들기 시작했을 때 가장 큰 관심사는 단순했다.
&quot;원하는 기능이 제대로 동작하는가?&quot;</p>
<p>뉴스를 수집하고, AI가 분석하고, 사용자에게 결과를 보여주는 것. 그 흐름이 끊기지 않고 돌아가는지가 가장 중요한 기준이었다.</p>
<p>v1과 v2는 그런 프로젝트였다. 빠르게 만들고, 빠르게 검증하고, 가능성을 확인하는 것이 목표였다.
하지만 실제 서비스를 만든다는 것은 전혀 다른 문제였다.</p>
<p>Spring Boot로 구조를 다시 설계하고, Next.js와 MySQL을 도입해 계층을 분리하면서 비로소 기능보다 구조를 먼저 고민하기 시작했다. 그 과정에서 자연스럽게 하나의 질문이 생겼다.
&quot;이 코드가 지금은 잘 동작하지만, 운영 환경에서도 계속 믿고 사용할 수 있을까?&quot;</p>
<hr>
<p>v3를 배포하고 운영하던 중, 현직자로부터 코드 리뷰를 받을 기회가 있었다. 트랜잭션 범위, 중복 처리, 캐시 전략, 재시도 방식 등 여러 관점에서 피드백을 받았고, 이 리뷰는 INSK v4를 다시 설계하게 된 직접적인 계기가 되었다.</p>
<p>당시에는 리뷰에서 지적된 항목들을 하나씩 개선하는 것이 가장 중요한 과제라고 생각했다.
그런데 프로젝트를 다시 들여다보던 중, 이상한 현상을 하나 발견했다.
추천 결과가 어딘가 이상했다.</p>
<p>예외는 발생하지 않았다. 로그도 정상적으로 남았다. API도 200 OK를 반환했다.
서비스는 아무 문제 없이 동작하는 것처럼 보였다.</p>
<p>하지만 추천 점수를 직접 확인해 보니, 모든 부서의 추천 점수가 0.0으로 계산되고 있었다.</p>
<hr>
<p>원인은 거대한 알고리즘이 아니었다.</p>
<p>실서비스 경로에 남아 있던 16줄짜리 placeholder 임베딩 구현 하나였다.
실제 기사 임베딩은 1536차원으로 저장되어 있었지만, placeholder 구현은 256차원 벡터를 반환하고 있었다. 코사인 유사도 계산 과정에서 차원이 맞지 않았고, 예외는 내부에서 처리되면서 추천 점수를 0.0으로 반환하고 있었다.</p>
<ul>
<li>서버는 정상이라고 말했고,</li>
<li>로그도 정상이라고 말했고,</li>
<li>API도 성공이라고 응답했지만,</li>
</ul>
<p>정작 사용자가 보는 추천 결과는 처음부터 잘못 계산되고 있었던 것이다.</p>
<hr>
<p>이 사건은 단순한 버그 수정으로 끝나지 않았다.
나는 처음으로 <strong>&quot;서비스가 동작하는 것&quot;</strong>과 <strong>&quot;서비스를 신뢰할 수 있는 것&quot;</strong>은 완전히 다른 문제라는 사실을 체감했다.
리뷰가 준 과제들을 하나씩 개선해 가는 과정에서, 정작 나를 가장 크게 바꾼 것은 리뷰에 없던 이 문제였다.</p>
<p>그 이후 내 관심사는 더이상 새로운 기능을 얼마나 많이 만드는지에 머무르지 않았다.
겉으로는 정상처럼 보이지만 잘못된 결과를 조용히 내보내는 시스템(silent failure)을 어떻게 막을 것인가, 그 질문이 더 중요해졌다.</p>
<p>기능이 동작하는 것과 결과를 신뢰할 수 있는 것은 전혀 다른 문제였기 때문에, 이 질문은 v4를 다시 설계하는 내내 나를 따라다녔다.</p>
<p>INSK v4는 새로운 기능을 추가한 프로젝트가 아니다.
기능이 돌아가는 시스템을, 신뢰할 수 있는 시스템으로 바꾸는 과정이었다.</p>
<hr>
<h2 id="part-2-가장-위험했던-버그는-예외를-만들지-않았다">PART 2. 가장 위험했던 버그는 예외를 만들지 않았다</h2>
<p>처음 추천 결과가 이상하다고 느꼈을 때도 시스템은 아무 문제 없이 동작하고 있었다.
크롤링과 OpenAI API는 정상적으로 응답했고 데이터베이스에도 데이터가 저장되었다.
REST API 역시 200 OK를 반환했다. 로그에도 특별한 오류는 없었다.
그래서 평소처럼 서비스를 사용한다면 아무도 이상하다고 생각하지 않았을 것이다.</p>
<p>하지만 추천 결과를 하나씩 확인하던 중 이상한 점을 발견했다.</p>
<hr>
<p>모든 부서의 추천 점수가 0.0이었다.</p>
<p>처음에는 추천 로직 자체를 의심했다.
프롬프트가 잘못된 것은 아닐까. 기사 분류 기준이 바뀐 것은 아닐까. 코사인 유사도 계산식이 잘못된 것은 아닐까.
그래서 실제 추천 점수를 직접 출력하며 하나씩 확인하기 시작했다.</p>
<p>그러자 예상과 전혀 다른 사실이 드러났다.</p>
<hr>
<p>추천 알고리즘과 OpenAI에는 문제가 없었고 기사 데이터도 정상적으로 저장되고 있었다.
문제는 훨씬 단순한 곳에 있었다.</p>
<p>문제는 바로, 실서비스 경로에 남아 있던 16줄짜리 placeholder 임베딩 구현이었다.
프로젝트 초기에 실제 임베딩 연동 전까지 임시로 사용하기 위해 만들어 두었던 구현이었기에, 256차원 벡터를 반환했다.
하지만 실제 기사 임베딩은 OpenAI의 1536차원 벡터였다.
두 벡터를 그대로 코사인 유사도 계산에 전달하면 차원이 맞지 않아 문제가 생긴다.</p>
<p>정상이라면 여기서 예외가 발생했어야 한다.
그런데 내부 예외 처리 때문에 이 오류는 밖으로 드러나지 않았다.
예외는 처리되었고, 추천 점수는 0.0으로 반환되었으며, 서비스는 계속 정상처럼 동작했다.
결국 추천 시스템 전체가 잘못된 결과를 만들고 있었지만, 시스템 어디에서도 위험 신호는 발생하지 않았다.
<img src="https://velog.velcdn.com/images/gm-15/post/9c054a64-2435-4efd-8bc1-cda8ffe15618/image.png" alt=""></p>
<hr>
<p>이 경험은 내 판단 기준을 완전히 바꾸었다.
이전까지는 장애를 예외(Exception) 중심으로 생각했다.
500 에러가 발생하거나, API 호출이 실패하거나, 로그에 Stack Trace가 찍히는 상황이 장애라고 생각했다.</p>
<p>하지만 실제 운영에서는 훨씬 위험한 문제가 존재했다.
아무런 예외 없이 잘못된 결과를 계속 만들어내는 시스템.
이런 문제는 서버 상태만 봐서는 절대 발견할 수 없다.</p>
<p>CPU도 정상, 메모리도 정상, API도 성공, 로그도 문제가 없다.
그렇기에 오직 결과를 검증하는 사람만 이상함을 발견할 수 있다.
이 버그를 수정하며 깨달은 것은 하나였다.</p>
<p>가장 위험한 장애는 시스템을 멈추는 장애가 아니라, <strong>아무 신호 없이 잘못된 결과를 계속 내보내는 장애</strong>라는 것.</p>
<p>이후부터 나는 기능이 성공했는지보다 <strong>결과가 정말 신뢰할 수 있는지</strong>를 먼저 확인하기 시작했다.</p>
<hr>
<h2 id="part-3-비용은-모델이-아니라-판단-순서에서-결정되었다">PART 3. 비용은 모델이 아니라 판단 순서에서 결정되었다</h2>
<p>Silent Failure를 수정한 뒤에도 한 가지 고민은 남아 있었다.
추천 결과는 신뢰할 수 있게 되었지만, 파이프라인을 다시 살펴보니 또 다른 병목이 보였다.</p>
<p>처음에는 OpenAI 호출 비용이 가장 큰 문제라고 생각했다.
그래서 더 저렴한 모델을 쓰거나, 프롬프트를 줄이는 방향부터 고민했다.
하지만 실제 파이프라인을 따라가 보니 문제는 모델이 아니라 신규 기사 여부를 판단하는 과정 자체에 있었다.</p>
<p>v3에서의 과정을 들여다보면, 먼저 URL을 확인해 이미 수집한 기사를 걸러냈다.
하지만 URL이 다른 기사라면 다음 단계에서 본문 전체를 임베딩해 기존 기사들과 비교했다.
중복 여부를 확인하기 위해 매 기사마다 새로운 임베딩을 생성했고, 기존 임베딩도 모두 메모리로 가져와 비교했다.</p>
<p>즉, &quot;새 기사인가?&quot;를 판단하는 일부터 이미 비용이 큰 작업이었다.
새로운 기사가 아닐 수도 있는데, 유료 임베딩을 호출하고, 기존 임베딩을 모두 읽어와 비교한 뒤에야, 비로소 다음 단계로 넘어갈 수 있었다.</p>
<p>현직자 코드 리뷰에서도 바로 이 구조가 지적되었다.</p>
<hr>
<p>중복 여부를 확인하기 위해 전체 임베딩을 메모리로 불러오는 방식은 데이터가 늘어날수록 메모리 사용량과 응답 시간이 함께 증가할 수 있었다.</p>
<p>v4에서는 이 <strong>판단 순서를</strong> 처음부터 다시 설계했다.
가장 먼저 확인하는 것은 기존과 같이 URL이다. 이미 수집한 URL이라면 즉시 종료한다.
URL이 처음 보는 기사라면 다음으로 제목을 비교한다. 제목 Jaccard 유사도를 이용해 거의 같은 기사인지 먼저 확인한다.</p>
<p>여기까지는 모두 데이터베이스 조회와 문자열 비교만 수행한다. 비용은 사실상 0원이다.</p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/98ab8ea5-afc4-4fb2-8528-33ce27134866/image.png" alt=""></p>
<p>이 구조에서 가장 중요한 변화는 GPT를 마지막에 둔 것이 아니다.</p>
<p>&#39;유료 판단이 필요한 기사&#39;만 다음 단계로 보내도록 만든 것이다.
<strong>비싼 판단을 하기 전에, 더 저렴한 방법으로 해결할 수 있는지</strong>를 먼저 확인한다.</p>
<p>이 원칙 하나만으로도 대부분의 중복 기사는 초기에 걸러지고, 불필요한 유료 호출도 함께 줄일 수 있었다.</p>
<hr>
<p>벡터 기반 ANN 중복 제거도 같은 관점에서 검토했다.
검색 규모가 커질수록 선형 탐색보다 ANN이 유리하기 때문이다.</p>
<p>하지만 현재 INSK의 데이터 규모에서는 URL과 제목 비교만으로도 충분한 효과를 얻을 수 있었고, 벡터 기반 중복 제거를 추가해도 비용 대비 이득이 크지 않았다.
그래서 ANN은 구조만 설계하고 실제 중복 제거 단계에는 적용하지 않았다. 대신 이후 추천 검색 구조를 개선하는 방향으로 활용하기로 결정했다.</p>
<p>이 경험을 통해 비용을 바라보는 기준도 달라졌다.</p>
<p>예전에는 더 저렴한 AI 모델을 찾는 것이 비용 최적화라고 생각했다.
하지만 이제는 비싼 판단이 정말 필요한 상황인지 먼저 판별하는 것이 더 중요한 설계라는 사실을 배웠다.</p>
<p>좋은 시스템은 가장 뛰어난 모델을 가장 많이 사용하는 시스템이 아니라, 가장 비싼 판단을 가장 늦게 수행하는 시스템이었다.</p>
<hr>
<h2 id="part-4-트랜잭션은-길수록-안전한-것이-아니었다">PART 4. 트랜잭션은 길수록 안전한 것이 아니었다</h2>
<p>프로젝트를 처음 설계했을 때는 하나의 요청을 하나의 트랜잭션으로 처리하는 것이 자연스럽다고 생각했다.
기사를 수집하고, AI 분석을 수행하고, 점수를 계산한 뒤, 데이터베이스에 저장하는 일련의 과정을 하나의 흐름으로 바라봤기 때문이다.</p>
<p>중간에 실패하면 모두 롤백되기 때문에 처음에는 이 방식이 더 안전하다고 생각했다.
하지만 v4를 설계하면서 트랜잭션을 다시 바라보게 되었다.</p>
<hr>
<p>가장 큰 이유는 AI 호출이었다.
LLM 응답 시간은 데이터베이스보다 훨씬 길고, 네트워크 상황에 따라 지연도 달라진다.</p>
<p>그런 작업까지 트랜잭션 안에서 수행한다면 데이터베이스 커넥션은 아무 일도 하지 못한 채 기다리게 된다.
그리고 이 문제는 요청이 몰릴수록 커진다. 외부 호출을 기다리는 커넥션이 쌓이면, 커넥션 풀이 고갈되어 정작 짧은 저장 쿼리조차 자리를 얻지 못하게 된다.</p>
<p>트랜잭션은 데이터베이스의 일관성을 보호하기 위한 장치다. 외부 API를 기다리기 위한 장치가 아니다.</p>
<hr>
<p>그래서 v4에서는 작업을 다시 분리했다.</p>
<p>AI 분석과 같이 시간이 오래 걸리는 외부 작업은 트랜잭션 밖에서 먼저 수행하고, 실제로 데이터를 저장하는 순간에만 트랜잭션을 시작하도록 구조를 변경했다.</p>
<p>이를 위해 저장 책임을 별도의 <code>ArticlePersistenceService</code>로 분리했다. 이 서비스만 <code>@Transactional</code>을 가지고 실제 데이터 저장을 담당한다.
<img src="https://velog.velcdn.com/images/gm-15/post/f2739f06-0e69-4422-bbf3-b405f776b5dc/image.png" alt=""><img src="https://velog.velcdn.com/images/gm-15/post/e5a39dba-fe1b-42eb-9d06-1c8aa5102650/image.png" alt=""></p>
<p>덕분에 트랜잭션이 열려 있는 시간은 데이터를 저장하는 짧은 구간으로 제한되었다.</p>
<p>이 차이는 재현 가능한 벤치마크로 확인했다.
커넥션 풀을 3개로 제한하고, 500ms가 걸리는 외부 I/O를 9개의 요청이 동시에 수행하는 상황을 만들었다.
외부 호출을 트랜잭션 안에서 기다리게 하면 커넥션이 풀에 막혀 전체가 1,544ms가 걸렸지만, 외부 호출을 트랜잭션 밖으로 빼자 509ms로, 약 3배 차이가 났다.</p>
<p>다만 이 수치는 실제 OpenAI 호출이 아니라, 외부 I/O 대기를 sleep으로 모사한 벤치마크다.
커넥션을 붙잡고 기다리느냐, 놓아주느냐라는 구조적 차이가 동시성 상황에서 어떻게 드러나는지를 확인하기 위한 측정이었다.</p>
<hr>
<p>여기서 중요한 것은 속도가 아니라 운영 안정성이었다.</p>
<p>외부 I/O를 기다리는 동안 데이터베이스 커넥션을 붙잡지 않게 만드는 것, 그래서 요청이 몰려도 커넥션 풀이 고갈되지 않게 하는 것이 핵심이었다.</p>
<p>책임을 경계로 나눈 것은 그 목적을 이루기 위한 수단이었고, 그 과정에서 코드 역시 훨씬 단순해졌다.
이 과정에서 Spring 트랜잭션의 동작 방식도 다시 이해하게 되었다.</p>
<p><code>@Transactional</code>은 메서드에 붙는 단순한 애너테이션이 아니라 프록시를 통해 동작한다. 같은 클래스 내부에서 메서드를 호출하면 새로운 트랜잭션이 열리지 않는다.
그래서 저장 로직을 별도의 서비스로 분리한 것은 단순한 코드 정리가 아니라, Spring이 의도한 방식대로 트랜잭션 경계를 만들기 위한 선택이었다.</p>
<hr>
<p>이 경험 이후 트랜잭션을 바라보는 기준도 달라졌다.</p>
<p>예전에는 &quot;얼마나 넓게 묶을 것인가&quot;를 고민했지만, 이제는 &quot;데이터베이스를 언제부터 언제까지 보호해야 하는가&quot;를 먼저 고민하게 됐다.</p>
<p>트랜잭션의 목적은 가능한 많은 코드를 감싸는 것이 아니라, 데이터를 변경하는 가장 짧은 순간만 책임지는 것이라는 사실을 프로젝트를 통해 배울 수 있었다.</p>
<hr>
<h2 id="part-5-실패를-없애는-대신-실패를-관리하기-시작했다">PART 5. 실패를 없애는 대신, 실패를 관리하기 시작했다</h2>
<p>v3를 만들 때의 목표는 단순했다. 가능하면 실패하지 않는 시스템을 만드는 것이었다.</p>
<p>예외가 발생하면 재시도하고, 성공할 때까지 다시 실행하면 된다고 생각했다. 실패는 해결해야 하는 문제였고, 결국 없어져야 하는 대상이라고 여겼다.</p>
<p>하지만 운영을 생각하기 시작하면서 관점이 조금씩 달라졌다.
외부 AI API는 내가 통제할 수 없는 시스템이다. 일시적인 네트워크 장애가 생길 수도 있고, 응답 시간이 갑자기 길어질 수도 있으며, 일시적으로 호출이 실패할 수도 있다.
그런데 모든 실패를 즉시 해결하려고 하면 오히려 시스템 전체가 함께 흔들린다.</p>
<p>그때부터 질문을 조금씩 다르게 해봤다.
&quot;실패를 어떻게 없앨 것인가?&quot;가 아니라 &quot;실패를 어떻게 격리하고 다시 처리할 것인가?&quot;</p>
<hr>
<p>분석 요청 하나에도 이미 여러 단계의 방어가 들어 있다.
우선 지수 백오프 기반 재시도를 최대 5회 수행하고, 그것마저 실패하면 폴백 모델로 한 번 더 분석을 시도한다. FAILED 상태는 첫 번째 실패가 아니라, 이런 재시도와 폴백이 모두 실패했을 때만 남는 최후의 결과다.</p>
<p>그렇다면 그 이후에는 무엇을 해야 할까?</p>
<p>많은 시스템은 Kafka 같은 메시지 브로커를 이용해 DLQ를 구성한다. 나도 처음에는 그런 구조를 떠올렸다.
하지만 INSK는 하루 수십 건 규모의 뉴스 분석 시스템이었다. 이 규모에서 Kafka를 추가하면 브로커 운영과 장애 관리가 오히려 더 큰 부담이 된다.</p>
<p>그래서 DLQ라는 패턴은 유지하되, 구현은 가장 단순한 형태를 선택했다. 기사의 분석 상태와 재시도 횟수를 MySQL에 저장하고, 스케줄러가 주기적으로 실패한 데이터를 다시 처리하도록 설계했다.</p>
<p>즉, DLQ는 Kafka가 아니라 상태(State)로 구현할 수도 있다는 판단이었다.
분석에 실패했다고 해서 기사를 버리지는 않았다. 실패한 데이터는 FAILED 상태로 데이터베이스에 남겨 두고, 일정 시간이 지난 뒤 다시 분석하도록 만들었다.</p>
<p>재처리 역시 무한히 반복하지 않았다. 실패 횟수를 기록하고, 일정 횟수 이상 계속 실패하는 데이터는 DEAD 상태로 전환하여 더 이상 유료 AI 호출을 반복하지 않도록 했다.</p>
<p>반대로 재처리에 성공하면 COMPLETED 상태로 변경되어 정상 파이프라인으로 다시 합류한다.</p>
<pre><code>분석 요청
(재시도 최대 5회 + 폴백 모델)
          │
     ┌────┴────┐
     │         │
   성공       실패
     │         │
COMPLETED   FAILED
               │
      6시간마다 재처리
               ▼
            재분석
        ┌────┴────┐
        │         │
      성공       실패
        │         │
 COMPLETED   retry_count++
                 │
      retry_count ≥ 3
                 │
               DEAD</code></pre><p>중요했던 것은 실패 자체가 아니었다. 실패가 발생했을 때 데이터가 사라지지 않는 것이 더 중요했다.
실패한 기사는 데이터베이스에 그대로 남아 있기 때문에 나중에 원인을 확인할 수 있고, API 장애가 복구된 뒤 다시 처리할 수도 있다.
반대로 계속 실패하는 데이터는 영구 격리하여 같은 요청에 비용을 반복해서 쓰지 않도록 했다.</p>
<hr>
<p>이 구조를 만들면서 실패를 바라보는 기준도 달라졌다.
예전에는 성공한 요청만 관심 대상으로 바라봤지만, 이제는 실패도 하나의 상태(State)라고 생각한다.
성공과 실패를 같은 흐름 안에서 관리할 수 있어야 운영이 가능하고, 그래야 장애가 생겨도 시스템은 계속 앞으로 진행된다.</p>
<p>실패는 시스템의 반대말이 아니다. 관리되지 않는 실패가 진짜 장애다.</p>
<hr>
<h2 id="part-6-캐시는-속도였지만-인메모리는-인스턴스마다-반쪽이었다">PART 6. 캐시는 속도였지만, 인메모리는 인스턴스마다 반쪽이었다</h2>
<p>추천 API는 생각보다 자주 호출됐다.</p>
<p>사용자가 같은 부서의 추천 결과를 반복해서 조회해도, 서버는 매번 같은 계산을 처음부터 다시 수행했다. 기사 임베딩을 읽고, 유사도를 계산하고, 추천 점수를 다시 만드는 과정이 계속 반복됐다.</p>
<p>기능은 맞지만 같은 결과를 계속 계산하는 것은 비용이었다.</p>
<hr>
<p>처음에는 Spring의 인메모리 캐시만으로도 충분할 것이라 생각했다. 하지만 운영 환경을 생각해보니 문제가 있었다.</p>
<p>인메모리 캐시는 애플리케이션 인스턴스마다 따로 존재한다.
서버가 한 대라면 문제가 없지만, 여러 인스턴스로 확장되면 A 서버에서 계산한 결과를 B 서버는 알지 못한다. 결국 같은 데이터를 인스턴스마다 다시 계산하게 되고, 캐시 효율은 급격히 떨어진다.</p>
<p>속도 문제처럼 보였지만, 실제로는 분산 환경에서는 캐시를 함께 볼 수 있어야 한다는 구조의 문제였다.
그래서 캐시를 Redis로 옮겼다.</p>
<pre><code>Client
   │
   ▼
Spring Cache (@Cacheable)
   │
   ├── Cache HIT
   │      │
   │      ▼
   │   Redis
   │      │
   │      ▼
   │   즉시 반환
   │
   └── Cache MISS
          │
          ▼
   추천 계산 수행
          │
          ▼
   Redis 저장 (TTL 10분)
          │
          ▼
        응답</code></pre><p>Redis를 적용한 뒤 벤치마크에서는 추천 API 응답 시간이 3,950ms에서 21.5ms로 감소했다.</p>
<p>같은 추천 결과를 다시 계산하지 않아도 되었고, 여러 인스턴스가 동일한 캐시를 공유할 수 있게 되면서 분산 환경에서도 같은 효과를 얻을 수 있었다.</p>
<p>이 장에서 중요한 것은 184배라는 숫자가 아니다. 더 중요한 변화는 캐시를 바라보는 기준이었다.
예전에는 캐시를 &quot;빠르게 만드는 기술&quot;이라고만 생각했다.</p>
<p>하지만 이번 경험을 통해 캐시는 여러 서버가 같은 계산 결과를 공유하기 위한 분산 구조라는 점을 이해하게 되었다. 속도는 그 결과일 뿐이었다.</p>
<hr>
<p>캐시는 계산을 줄이는 기술이 아니라, 여러 인스턴스가 같은 결과를 공유하기 위한 분산 구조였다.</p>
<hr>
<h2 id="part-7-검색-엔진이-아니라-검색-구조를-바꾸는-것이었다">PART 7. 검색 엔진이 아니라, 검색 구조를 바꾸는 것이었다</h2>
<p>추천 기능을 다시 설계하면서 마지막으로 남은 병목은 검색 구조였다.</p>
<p>Redis 캐시를 적용해 조회 속도는 크게 개선했지만, 캐시가 항상 존재하는 것은 아니었다. 캐시가 비워졌거나 새로운 데이터가 들어오면 결국 다시 추천을 계산해야 했다.</p>
<p>그 계산은 여전히 모든 임베딩을 메모리로 읽어 하나씩 비교하는 brute-force 방식이었다.</p>
<pre><code>모든 임베딩 조회
        ↓
Java에서 순차 비교
        ↓
Cosine Similarity 계산
        ↓
Top5 선정</code></pre><p>데이터가 수백 건 수준일 때는 문제가 크지 않았다. 하지만 데이터가 계속 쌓인다면 추천 시간 역시 선형으로 증가한다. 문제는 지금이 아니라, 앞으로였다.</p>
<p>처음에는 단순히 더 빠른 코드를 작성하면 된다고 생각했다. 하지만 구조를 다시 살펴보니 병목은 코드가 아니라 검색 방식 자체에 있었다.</p>
<hr>
<p>벡터 검색을 어떻게 처리할지 여러 방식을 비교했다.</p>
<ul>
<li>Redis Stack 벡터 검색</li>
<li>애플리케이션 내부에 직접 HNSW(hnswlib)를 두는 방식</li>
<li>Qdrant</li>
</ul>
<p>Redis Stack은 이미 Redis를 사용하고 있었기 때문에 자연스러운 선택처럼 보였다. 하지만 캐시와 벡터 검색은 목적이 달랐다.</p>
<p>캐시는 빠르게 재사용하기 위한 저장소이고, 벡터 검색은 근사 최근접 탐색(ANN)을 수행하는 검색 엔진이다. 같은 Redis 위에 올릴 수 있다는 이유만으로 두 역할을 하나로 묶는 것은 오히려 각자의 목적을 흐릴 수 있다고 판단했다.</p>
<p>애플리케이션 내부에 HNSW를 직접 두는 방법도 검토했다. 새로운 인프라가 필요 없다는 장점은 있었지만, 인덱스가 JVM 내부에 존재하기 때문에 애플리케이션이 재시작되면 다시 구축해야 하고, 여러 인스턴스가 하나의 인덱스를 공유하기도 어렵다.</p>
<p>이미 Redis를 통해 &quot;공유되어야 하는 데이터는 애플리케이션 밖으로&quot;라는 방향을 선택한 상황에서, 검색만 다시 애플리케이션 안으로 가져오는 것은 일관된 설계가 아니었다.</p>
<p>그래서 검색은 검색에 맞는 도구인 Qdrant에 맡기기로 했다.
중요했던 것은 새로운 기술을 사용하는 것이 아니라, 검색을 검색 엔진에게 맡기는 것이었다.</p>
<hr>
<p>Qdrant 적용 후 추천 계산 시간은 <strong>3,950ms → 1,616ms (약 2.4배)</strong> 로 줄었다.
<img src="https://velog.velcdn.com/images/gm-15/post/fca9a032-7e3e-49dd-bbd2-871cc834c993/image.png" alt=""></p>
<p>처음에는 숫자가 기대보다 작게 느껴졌다. 하지만 프로파일링 결과는 오히려 설계를 더 명확하게 설명해 주었다.</p>
<p>추천 시간 대부분은 여전히 OpenAI 임베딩 생성 요청이 차지하고 있었고, Qdrant가 제거한 것은</p>
<ul>
<li>전체 임베딩 순회</li>
<li>JSON 역직렬화</li>
<li>Java에서 수행하던 코사인 계산</li>
</ul>
<p>이었다.</p>
<p>즉, 검색 구조는 개선됐지만 외부 API 호출 시간은 그대로 남아 있었던 것이다. 그래서 2.4배라는 결과는 실패가 아니라, 병목을 정확하게 제거했다는 증거였다.</p>
<p>이 벤치마크는 JMeter가 아니라 실제 추천 API를 호출하는 방식(curl)으로 측정했다. 또한 당시 데이터 규모는 약 320~600건 수준이었다.</p>
<p>지금 규모에서는 절대적인 차이가 크지 않지만, 데이터가 계속 증가하면 brute-force는 O(N)으로 증가하고 ANN은 훨씬 완만하게 증가한다. 내가 바꾼 것은 현재 속도가 아니라 미래의 증가 곡선이었다.</p>
<hr>
<p>이전에는 &quot;더 빠른 알고리즘을 만들자&quot;가 우선순위였지만, 
이 경험 이후로는 &quot;이 데이터를 가장 잘 다룰 수 있는 구조는 무엇인가?&quot;라는 질문이 우선순위가 되었다.</p>
<p>캐시는 Redis가 잘하고, 벡터 검색은 Qdrant가 잘한다. 각 기술이 가장 잘하는 역할에 집중하게 하는 것이 전체 시스템을 가장 단순하게 만든다는 사실을 배웠다.</p>
<p>기술 이름보다 역할을 먼저 생각하는 것이 중요하다.</p>
<p>검색 엔진을 쓰고 싶어서 Qdrant를 선택한 것이 아니라, 검색이라는 문제를 가장 잘 해결하는 구조가 Qdrant였기 때문에 선택한 것이다.</p>
<hr>
<h2 id="part-8-기능보다-중요한-것은-판단-기준이었다">PART 8. 기능보다 중요한 것은 판단 기준이었다</h2>
<p><img src="https://velog.velcdn.com/images/gm-15/post/6c477066-aef9-4b2f-b7fc-411c83fb439d/image.png" alt=""></p>
<p>INSK v4를 돌아보면 가장 크게 달라진 것은 기술 스택이 아니다.</p>
<p>Spring Boot를 사용했고, MySQL을 사용했고, OpenAI API를 사용했다. 새로운 프레임워크를 도입해서 프로젝트가 달라진 것은 아니었다.</p>
<p>정말 달라진 것은 문제를 바라보는 기준이었다.</p>
<p>처음 프로젝트를 만들 때는 기능을 구현하는 것이 목표였다. 추천이 나오면 성공이고, AI가 분석하면 성공이며, API가 응답하면 성공이라고 생각했다.</p>
<p>하지만 프로젝트를 운영하고 개선하는 과정에서, 같은 기능이라도 전혀 다른 질문을 던지게 되었다.</p>
<table>
<thead>
<tr>
<th>문제</th>
<th>처음의 질문</th>
<th>바뀐 질문</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>외부 API 동안 DB 커넥션을 붙잡고 있지는 않나?</td>
</tr>
<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>
<p>이 질문들이 하나씩 쌓이면서 INSK v4의 설계가 만들어졌다.</p>
<hr>
<p>가장 기억에 남는 사건은 16줄짜리 placeholder 임베딩 구현이었다.</p>
<p>API도 정상이었고, 로그도 정상이었고, 예외도 없었다. 그런데 추천 결과만 틀렸다.
그 사건 이후 나는 시스템을 보는 기준이 완전히 달라졌다. 장애는 서버가 죽는 것만이 아니었다. 가장 위험한 장애는 정상처럼 보이면서 잘못된 결과를 계속 내보내는 것이었다.
그래서 이후에는 기능을 만드는 것보다 결과를 검증하는 방법을 먼저 고민하게 되었다.</p>
<p>현직자 코드 리뷰도 내 판단 기준을 크게 바꾸었다.
트랜잭션 범위, 중복 처리, 캐시, 재시도. 운영 관점의 피드백을 하나씩 반영하면서 알게 된 것은 하나였다.
좋은 설계는 새로운 기술을 많이 사용하는 것이 아니라, 문제에 맞는 가장 작은 해결책을 선택하는 것이다.</p>
<hr>
<p>이번 프로젝트에서 구현하지 않은 것도 분명히 남아 있다.</p>
<ul>
<li>Redis는 로컬 환경에서만 검증했고 ElastiCache에는 아직 배포하지 않았다.</li>
<li>Redis 장애 시 Graceful Degradation도 아직 구현하지 않았다.</li>
<li>벡터 기반 중복 제거(ANN dedup)는 설계만 마쳤고 아직 적용하지 않았다.</li>
</ul>
<p>이 프로젝트를 하며 가장 크게 배운 것은 기술 자체가 아니었다.
문제가 생기면 먼저 병목을 찾고, 측정으로 확인하고, 가장 작은 해결책을 선택하고, 그리고 구현한 것만 이야기한다.
이 네 가지 기준은 INSK를 만들며 얻은 가장 큰 자산이다.</p>
<p>앞으로 어떤 서비스를 만들더라도 새로운 기술보다 먼저 이 질문들을 던질 것이다.
&quot;이 문제의 본질은 무엇인가.&quot;
그리고 그 문제를 해결하는 데 가장 적합한 방법이 무엇인지부터 생각하는 개발자가 되고 싶다.</p>
<hr>
<h2 id="epilogue-이-프로젝트가-내-대표작중-하나인-이유">Epilogue. 이 프로젝트가 내 대표작중 하나인 이유</h2>
<p>INSK는 SK mySUNI 써니C 4기에서 시작됐다.</p>
<p>그때의 INSK는 Make와 Streamlit으로 가능성을 확인하는 작은 프로토타입이었다. 이후 팀 프로젝트로 Python 기반의 v2를 만들었고, 현직자에게 한 문장을 들었다.</p>
<blockquote>
<p>&quot;돌아가는 것과 서비스는 다르다.&quot;</p>
</blockquote>
<p>당시에는 그 말을 완전히 이해하지 못했다. 기능이 동작했고, 원하는 결과도 나왔다. 그래서 충분히 잘 만들었다고 생각했다.</p>
<p>써니C가 끝난 뒤에도 그 말은 계속 머릿속에 남아 있었다.</p>
<p>그래서 프로젝트를 다시 처음부터 만들었다. Spring Boot와 Next.js, MySQL로 구조를 다시 설계했고, AWS에 배포하며 제품 형태로 발전시켰다. 그 과정이 v3였다.</p>
<p>하지만 배포까지 끝내고 나서도 깨달았다. 서비스를 만드는 일은 배포가 끝이 아니었다.</p>
<p>다시 현직자 코드 리뷰를 받으며 운영 관점에서 부족한 점을 하나씩 마주했고, 그 과정에서 리뷰에도 없던 문제를 직접 발견하기도 했다. 기능은 정상인데 결과는 틀릴 수 있었고, 빠른 코드보다 오래 버틸 구조가 더 중요했다. 그 경험들이 쌓이며 v4가 만들어졌다.</p>
<p>돌이켜보면 v1부터 v4까지 바뀐 것은 기술 스택이 아니었다.</p>
<p>프로토타입을 만들던 시기에는 &quot;동작하는가&quot;를 먼저 생각했고, 제품을 만들던 시기에는 &quot;사용할 수 있는가&quot;를 고민했다. 그리고 지금은 &quot;계속 신뢰할 수 있는가&quot;를 먼저 묻게 되었다.</p>
<p>같은 프로젝트를 여러 번 다시 만든 이유도 여기에 있다. 새로운 기능을 추가하기 위해서가 아니라, 예전의 내가 내렸던 판단을 다시 검토하고, 더 나은 기준으로 고쳐 나가기 위해서였다.</p>
<p>그래서 INSK는 내게 기능을 보여주는 프로젝트가 아니다.</p>
<p>어떤 문제를 만나면 무엇부터 의심했고, 어떤 이유로 설계를 바꾸었으며, 무엇을 구현했고 무엇은 아직 구현하지 않았는지를 남긴 기록이다. 결과보다 판단의 과정을 설명할 수 있다는 점에서, 이 프로젝트는 지금의 나를 가장 잘 보여주는 대표작이 되었다.</p>
<p>앞으로 사용하는 기술은 계속 달라질 것이다. 하지만 문제를 이해하고, 근거를 확인한 뒤, 가장 작은 해결책을 선택하는 태도만큼은 이 프로젝트를 시작했을 때보다 더 오래 남아 있을 것이라고 믿는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI EXPO KOREA 2026 & AWS Summit Seoul 2026 후기]]></title>
            <link>https://velog.io/@gm-15/AI-EXPO-KOREA-2026-AWS-Summit-Seoul-2026-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@gm-15/AI-EXPO-KOREA-2026-AWS-Summit-Seoul-2026-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Fri, 07 Aug 2026 05:49:44 GMT</pubDate>
            <description><![CDATA[<h1 id="ai를-보러간-두-곳이지만-보인-건-운영-시스템">AI를 보러간 두 곳이지만, 보인 건 운영 시스템</h1>
<h3 id="ai-expo-korea-2026--aws-summit-seoul-2026-후기">AI EXPO KOREA 2026 &amp; AWS Summit Seoul 2026 후기</h3>
<p>2026년 5월, AI 기술을 보러 두 곳을 방문했다.</p>
<p>그런데 가장 오래 기억에 남은 건 AI 모델이 아니라, 그 AI를 운영하는 시스템이었다.</p>
<br>

<h2 id="why">Why?</h2>
<p>올해 상반기는 AI 이야기로 가득했다.</p>
<p>새로운 모델이 계속 등장했고, 기업들도 앞다투어 AI 서비스를 발표했다.</p>
<p>나 역시 INSK 프로젝트를 진행하며 LLM을 활용하고 있었기 때문에 자연스럽게 궁금해졌다.</p>
<blockquote>
<p>기업들은 AI를 어디까지 활용하고 있을까?</p>
</blockquote>
<p>그리고 더 궁금했던 건 하나였다.</p>
<blockquote>
<p>AI를 적용시키는 것 이후에는 무엇을 고민할까?</p>
</blockquote>
<p>이 질문을 가지고 5월에는 AI EXPO KOREA, 그리고 AWS Summit Seoul을 다녀왔다.</p>
<p>행사를 모두 보고 돌아온 지금 가장 크게 남은 생각은 하나다.</p>
<p><strong>AI를 만드는 것과 AI를 운영하는 것은 전혀 다른 문제였다.</strong></p>
<br>

<h2 id="ai-expo에서-본-것은-ai보다-ai를-쓰기-위한-환경이었다">AI EXPO에서 본 것은 &#39;AI&#39;보다 &#39;AI를 쓰기 위한 환경&#39;이었다</h2>
<p><img src="https://velog.velcdn.com/images/gm-15/post/10691e28-9763-4d45-94e3-fc67dee5870b/image.jpg" alt=""></p>
<p>AI EXPO는 과 동기들과 방문했다.
행사장은 생각했던 것보다 훨씬 컸다.</p>
<p>사람도 많았고 부스도 정말 많았다.</p>
<p>모든 부스를 다 보기에는 시간이 부족했기 때문에 미리 관심 있는 기업들을 정리해 우선순위를 정하고 돌아다녔다.</p>
<p>내가 집중해서 본 분야는 클라우드와 운영, 그리고 데이터를 안정적으로 활용하는 서비스였다.</p>
<p>물론 다양한 AI 서비스도 많았다.
<img src="https://velog.velcdn.com/images/gm-15/post/a8b1c010-4e8c-44d5-af1d-8833838313cc/image.jpg" alt=""><img src="https://velog.velcdn.com/images/gm-15/post/4300071b-5b93-48c7-a917-029e03b4153e/image.jpg" alt=""><img src="https://velog.velcdn.com/images/gm-15/post/33fb1324-14c6-45ba-9f4b-6fb24b0b75f0/image.jpg" alt=""></p>
<ul>
<li>FaceMe처럼 얼굴을 인식해 성별과 나이를 추정하는 서비스</li>
<li>사내 문서를 검색하는 엔터프라이즈 AI</li>
<li>검증된 오픈소스 모델을 API 형태로 제공하는 플랫폼</li>
<li>AI 기반 테스트 설계</li>
<li>온프레미스 AI 코딩 어시스턴트</li>
</ul>
<p>처음에는 새로운 AI 기술이 많이 나와 있을 거라고 생각했다.</p>
<p>그런데 한 바퀴를 돌고 나니 오히려 다른 점이 눈에 들어왔다.</p>
<ul>
<li>GPU를 여러 사용자에게 세밀하게 나누어 제공하는 서비스</li>
<li>원클릭으로 프라이빗 클라우드를 구성해 주는 서비스</li>
<li>AI 인프라를 대신 관리해 주는 서비스</li>
</ul>
<p>그때 처음 들었던 생각은 이거였다.</p>
<blockquote>
<p>AI보다 GPU를 효율적으로 쓰는 것이 더 중요한 시대가 왔구나.</p>
</blockquote>
<p>좋은 모델은 이미 많다.</p>
<p>하지만 비용이 너무 크면 서비스에서는 사용할 수 없다.</p>
<p>결국 기업들은 새로운 AI를 만드는 것보다 한정된 자원을 어떻게 더 효율적으로 운영할 것인지를 함께 고민하고 있었다.</p>
<p>반면 학생 입장에서는 조금 아쉬운 점도 있었다.</p>
<p>대부분 B2B 중심 서비스였기 때문에 직접 사용해 보기에는 비용이나 접근성이 높은 편이었다.</p>
<p>그래도 현재 AI 산업이 어디에 투자하고 있는지는 충분히 느낄 수 있었다.</p>
<br>

<h2 id="aws-summit에서는-ai보다-운영-이야기가-더-기억에-남았다">AWS Summit에서는 AI보다 운영 이야기가 더 기억에 남았다</h2>
<p>AWS Summit은 CloudWave의 클마카세 팀원들과 함께 다녀왔다.
<img src="https://velog.velcdn.com/images/gm-15/post/d77e3458-a2a5-4d8f-8166-bf858b90669b/image.jpg" alt=""></p>
<p>행사 전에도 &quot;AWS는 AI를 어떻게 바라볼까?&quot;라는 기대를 가지고 있었는데, 막상 가장 오래 기억에 남은 것은 모델 소개가 아니었다.</p>
<p>운영이었다.</p>
<p>가장 인상 깊었던 세션은 <strong>&#39;에이전트 성능 평가와 개선: 개발부터 운영까지&#39;</strong> 였다.
<img src="https://velog.velcdn.com/images/gm-15/post/e63c79aa-1a27-46c9-b12d-1fa07e4718e1/image.jpg" alt=""></p>
<p>발표에서는 에이전트 AI의 특징으로</p>
<ul>
<li>설계상 비결정론적이라는 점</li>
<li>잘못된 도구와 파라미터를 선택할 수 있다는 점</li>
<li>&#39;더 좋아졌는가&#39;를 정량적으로 평가하기 어렵다는 점</li>
<li>자동 회귀(Regression) 문제가 발생할 수 있다는 점
<img src="https://velog.velcdn.com/images/gm-15/post/5bfc3047-566b-416a-a053-131a2179b38f/image.jpg" alt="">
을 이야기했다.</li>
</ul>
<p>흥미로웠던 건 발표의 중심이 모델 성능이 아니었다는 것이다.</p>
<p>오히려</p>
<blockquote>
<p>에이전트가 목표를 달성했는가?
올바른 도구를 선택했는가?
답변은 적절했는가?</p>
</blockquote>
<p>를 계속 검증해야 한다는 이야기가 중심이었다.
<img src="https://velog.velcdn.com/images/gm-15/post/cbbdba4e-b8b0-4d7b-bbc6-05812cb10e5f/image.jpg" alt=""><img src="https://velog.velcdn.com/images/gm-15/post/1569ec6e-ea60-46ec-b3ec-bb1eedc50b10/image.jpg" alt=""></p>
<p>온디맨드 평가, 온라인 평가, 사용자 시뮬레이터, 코드 기반 Evaluator까지.</p>
<p>AI도 결국 운영되는 시스템이라면, 지속적으로 평가하고 검증하는 체계가 반드시 필요하다는 메시지였다.</p>
<p>또 다른 세션에서는 Samsung Account의 AI SecOps와 AIOps 사례도 소개됐다.<img src="https://velog.velcdn.com/images/gm-15/post/d9aa19c0-687d-42a7-952f-ec60f6360824/image.jpg" alt=""><img src="https://velog.velcdn.com/images/gm-15/post/b2ff86f0-fcc2-4484-a9ed-9aacdc4e2647/image.jpg" alt=""></p>
<p>장애가 발생하면 AI가 로그를 분석하고, 원인을 추적하고, 대응 방안까지 제안하는 구조였다.</p>
<p>AI가 개발을 돕는 수준을 넘어 배포, 운영, 모니터링, 장애 대응까지 조금씩 들어오고 있다는 점이 인상 깊었다.</p>
<p>동시에 AI를 활용하는 만큼 AI 자체를 향한 보안 위협도 함께 커지고 있다는 점도 다시 한번 느낄 수 있었다.</p>
<br>

<h2 id="결국-두-행사가-말하고-있던-것은-같았다">결국 두 행사가 말하고 있던 것은 같았다</h2>
<p>처음에는</p>
<ul>
<li>AI EXPO는 AI 서비스</li>
<li>AWS Summit은 클라우드 행사</li>
</ul>
<p>라고 생각했다.</p>
<p>그런데 둘을 모두 다녀오고 나니 같은 이야기를 하고 있었다.</p>
<p><strong>AI EXPO는</strong> AI를 서비스로 만들기 위한 환경을 보여줬고,</p>
<p><strong>AWS Summit은</strong> 그 AI를 안정적으로 운영하는 방법을 이야기하고 있었다.</p>
<p>행사는 달랐지만 결론은 같았다.</p>
<p>AI도 결국 서비스였다.</p>
<p>그리고 서비스라면 운영이 따라온다.</p>
<br>

<h2 id="그래서-나는-무엇을-가져왔을까">그래서 나는 무엇을 가져왔을까?</h2>
<p>이번 행사를 다녀오며 새로운 기술 하나를 배운 것은 아니다.</p>
<p>오히려 질문이 하나 더 생겼다.</p>
<blockquote>
<p>AI를 어디에 붙일까? 가 아니라
AI를 어떻게 운영할까?</p>
</blockquote>
<p>다녀온 뒤, INSK 프로젝트에서도 이 생각을 적용했다.</p>
<p>LLM을 더 많이 호출하는 것이 아니라,</p>
<ul>
<li>언제 호출해야 하는지</li>
<li>중복 호출은 어떻게 줄일 것인지</li>
<li>비용은 어떻게 관리할 것인지</li>
<li>결과를 어떻게 신뢰할 것인지</li>
</ul>
<p>이런 질문을 먼저 하게 됐다.</p>
<p>돌아보면 이전 프로젝트에서는 <a href="https://velog.io/@gm-15/%EC%99%9C-Kafka%EC%98%80%EC%9D%84%EA%B9%8C-%EC%9E%A5%EC%95%A0%EB%A5%BC-%EA%B2%AC%EB%94%94%EB%8A%94-%EA%B5%AC%EC%A1%B0%EB%A5%BC-%EA%B3%A0%EB%AF%BC%ED%95%9C-3%EA%B0%80%EC%A7%80-%EC%A7%88%EB%AC%B8">Kafka나 Circuit Breaker</a>처럼 장애를 견디는 구조를 고민했다면, 이제는 한 걸음 더 나아가 AI를 어떻게 신뢰하고 운영할 것인가를 고민하게 됐다.</p>
<p>기술은 바뀌었지만 기준은 크게 달라지지 않았다.
여전히 내가 중요하게 생각하는 것은 서비스가 계속 동작하도록 만드는 것이다.</p>
<p>AI를 만드는 것과 AI를 운영하는 것은 다르다.
그리고 백엔드 엔지니어에게 중요한 것은 새로운 기술을 가장 먼저 사용하는 사람이 아니라,
그 기술을 <strong>안정적으로 운영하고 신뢰할 수 있는 시스템</strong>으로 만드는 사람이라는 생각이 더 확고해졌다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[역할은 나눴지만 기준은 나누지 않았다 - 무작위로 모인 6명이 한 달만에 만든  Clmakase]]></title>
            <link>https://velog.io/@gm-15/%EC%97%AD%ED%95%A0%EC%9D%80-%EB%82%98%EB%88%B4%EC%A7%80%EB%A7%8C-%EA%B8%B0%EC%A4%80%EC%9D%80-%EB%82%98%EB%88%84%EC%A7%80-%EC%95%8A%EC%95%98%EB%8B%A4-%EB%AC%B4%EC%9E%91%EC%9C%84%EB%A1%9C-%EB%AA%A8%EC%9D%B8-6%EB%AA%85%EC%9D%B4-%ED%95%9C-%EB%8B%AC%EB%A7%8C%EC%97%90-%EB%A7%8C%EB%93%A0-Clmakase</link>
            <guid>https://velog.io/@gm-15/%EC%97%AD%ED%95%A0%EC%9D%80-%EB%82%98%EB%88%B4%EC%A7%80%EB%A7%8C-%EA%B8%B0%EC%A4%80%EC%9D%80-%EB%82%98%EB%88%84%EC%A7%80-%EC%95%8A%EC%95%98%EB%8B%A4-%EB%AC%B4%EC%9E%91%EC%9C%84%EB%A1%9C-%EB%AA%A8%EC%9D%B8-6%EB%AA%85%EC%9D%B4-%ED%95%9C-%EB%8B%AC%EB%A7%8C%EC%97%90-%EB%A7%8C%EB%93%A0-Clmakase</guid>
            <pubDate>Wed, 15 Jul 2026 06:25:58 GMT</pubDate>
            <description><![CDATA[<p>지난 세 편은 기술 이야기였다. <a href="https://velog.io/@gm-15/15%EB%A7%8C-VU%EB%A5%BC-%EB%B2%84%ED%8B%B0%EA%B8%B0%EA%B9%8C%EC%A7%80-%EB%82%B4%EA%B0%80-%EC%84%B8-%EB%B2%88-%ED%8B%80%EB%A0%B8%EB%8D%98-%EC%9D%B4%EC%9C%A0">1편</a>에서는 부하 테스트 중 깨진 직관을, <a href="https://velog.io/@gm-15/%EB%B9%8C%EB%93%9C%EB%8A%94-%EC%84%B1%EA%B3%B5-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%8A%94-%EC%8B%A4%ED%8C%A8-%EB%B0%B0%ED%8F%AC%EC%97%90%EC%84%9C-%EB%A7%8C%EB%82%9C-3%EA%B0%80%EC%A7%80-%ED%95%A8%EC%A0%95">2편</a>에서는 배포 과정에서 만난 함정을, <a href="https://velog.io/@gm-15/%EC%99%9C-Kafka%EC%98%80%EC%9D%84%EA%B9%8C-%EC%9E%A5%EC%95%A0%EB%A5%BC-%EA%B2%AC%EB%94%94%EB%8A%94-%EA%B5%AC%EC%A1%B0%EB%A5%BC-%EA%B3%A0%EB%AF%BC%ED%95%9C-3%EA%B0%80%EC%A7%80-%EC%A7%88%EB%AC%B8">3편</a>에서는 장애를 견디는 구조를 고른 기준을 정리했다.</p>
<p>이번 글은 그 기술을 함께 만든 팀에 관한 이야기다.</p>
<p>프로젝트가 끝날 무렵 강사와 심사위원으로부터 &quot;팀워크가 가장 좋은 팀&quot;이라는 평가를 들었다. 무작위로 모인 6명이었고, 기간도 2월 4일부터 27일까지 한 달이 채 되지 않았다. 돌아보면 특별한 사람들이 모여서라기보다, 기술적인 판단을 함께 맞춰 가는 방식을 만들었던 것이 가장 컸다고 생각한다.</p>
<h1 id="무작위로-모인-6명이-한-달-동안-한-일">무작위로 모인 6명이 한 달 동안 한 일</h1>
<p>프로젝트가 시작된 첫날, 우리는 서로의 열정을 느낄 수 있었다.</p>
<p>우리는 12월부터 매일 8시간씩 공부해 온 기술을 프로젝트에 제대로 적용해 보고 싶었다. 배운 것뿐 아니라 새로운 기술도 가능하다면 프로젝트에 녹여 보고 싶었다. 각자 잘하는 분야는 달랐지만, 자기 파트만 완성하는 프로젝트로 끝내고 싶지는 않았다.</p>
<p>팀은 백엔드, 보안, AI, DR, 네트워크, 모니터링 등으로 역할이 나뉘어 있었다. 나는 팀장을 맡아 백엔드와 CI/CD, 컨테이너, 부하 테스트를 담당했다. 솔직히 내 파트만 따라가기에도 벅찬 일정이었다. 하지만 각자 자기 분야만 알고 있는 상태로는 마지막에 결과물을 하나로 연결하기 어렵다고 생각했다.</p>
<p>그래서 팀이 같은 기준으로 판단할 수 있는 환경부터 만들기로 했다.</p>
<hr>
<h2 id="①-모르는-걸-편하게-물을-수-있는-분위기를-만들었다">① 모르는 걸 편하게 물을 수 있는 분위기를 만들었다</h2>
<p>팀장을 맡고 가장 먼저 한 일은 내 역할을 다시 정하는 것이었다.</p>
<p>직전에 읽은 『최고의 팀은 무엇이 다른가』에서 가장 인상 깊었던 내용은 &#39;좋은 팀은 안전한 분위기에서 시작된다&#39;는 것이었다. 그래서 기술적으로 팀을 이끄는 사람보다, 누구나 질문할 수 있는 분위기를 만드는 사람이 되기로 했다.</p>
<p>보안도, DR도 내가 더 잘 아는 분야는 아니었다. 내가 맡지 않은 영역은 그 팀원의 판단을 믿었다.</p>
<p>대신 회의에서는 먼저 모른다고 말했다.</p>
<blockquote>
<p>&quot;그 부분은 잘 모르니 설명해 달라.&quot;</p>
</blockquote>
<p>팀장이 먼저 모른다고 말하자 다른 팀원들도 모르는 걸 숨기지 않았다. 질문하는 게 자연스러워졌고, 의견도 훨씬 편하게 나왔다.</p>
<p>잘 드러나지 않는 작업도 의식적으로 짚었다. 화면에 보이지 않는 설정이나 운영 작업일수록 가치가 쉽게 묻히기 때문이다. 작은 기여라도 먼저 칭찬했고, 그 분위기는 자연스럽게 팀 전체로 이어졌다. 서로의 작업을 인정하고 질문하는 문화가 자리 잡으면서 다른 사람의 작업에도 관심을 갖기 시작했다.</p>
<p>지금 생각하면 칭찬 자체가 목적은 아니었다. 서로의 생각을 편하게 설명하고, 모르는 걸 거리낌 없이 물을 수 있는 환경을 만드는 방법이었다.</p>
<hr>
<h2 id="②-서로에게-설명하게-했다">② 서로에게 설명하게 했다</h2>
<p>매일 아침 회의 시간을 만들었다.</p>
<p>규칙은 하나였다. 어제 한 일을, 그 분야를 모르는 사람도 이해할 수 있게 설명하는 것. 설명이 끝나면 바로 질문을 받았다.</p>
<p>처음에는 진행 상황을 공유하기 위한 회의라고 생각했다. 그런데 시간이 지날수록 더 큰 변화가 생겼다.</p>
<p><strong>첫째, 자기 파트를 더 깊이 이해하게 됐다.</strong>
사전 지식이 없는 사람에게 설명하려면 내가 먼저 정확히 이해하고 있어야 했다. 설명이 막히는 곳이 곧 내가 제대로 이해하지 못한 곳이었다.</p>
<p><strong>둘째, 자신감이 생겼다.</strong>
&quot;이 부분은 내가 제일 잘 안다&quot;는 감각이 생기자 자연스럽게 책임감도 함께 따라왔다.</p>
<p><strong>셋째, 기술적인 판단의 기준이 맞춰지기 시작했다.</strong>
회의 중 조금이라도 궁금한 점이 생기면 바로 질문하고 함께 고민했다. 왜 그런 선택을 했는지 서로 묻고 답하다 보니 점점 같은 기준으로 판단하게 됐다.</p>
<p>지금 돌아보면 3편에서 정리했던 &quot;왜 Kafka를 선택했는가&quot;도 블로그에서 처음 정리한 생각이 아니었다. 이미 이 시간에 여러 번 설명하고 질문을 받으면서 다듬어진 판단이었다.</p>
<p>덕분에 나중에 각자 만든 결과물을 합치거나 새로운 기능을 붙일 때도 시행착오가 눈에 띄게 줄었다. 같은 기준으로 판단하고 있었기 때문이다.</p>
<hr>
<h2 id="③-기준은-문서로-남겼다">③ 기준은 문서로 남겼다</h2>
<p>설명으로 끝나면 그 순간에만 기억한다. 그래서 기술적인 기준은 모두 문서로 남겼다.</p>
<p>각자 만든 결과물은 Terraform 16개 모듈로 하나의 인프라 코드에 통합했다. 누가 무엇을 바꾸면 어디에 영향을 주는지 한곳에서 확인할 수 있도록 만들었다.</p>
<p>부하 테스트에서 확인한 운영 기준도 마찬가지였다. 1편에서 다뤘던 Aurora 커넥션 문제를 해결하면서 세운 기준은 팀 공통 체크리스트에 추가했다.</p>
<pre><code>maxReplicas × Connection Pool ≤ DB max_connections</code></pre><p>누군가 나중에 스케일 정책을 변경하더라도 같은 실수를 반복하지 않도록 하기 위해서였다.</p>
<p>기준은 말보다 문서로 남겼을 때 오래 남는다.</p>
<hr>
<h1 id="마치며">마치며</h1>
<p>결과만 놓고 보면 15만 VU 부하 테스트에서 5xx 오류 없이 테스트를 마쳤고, 팀은 최종 2등으로 프로젝트를 마무리했다. 그러나 그것보다 더 중요한 게 있었다.</p>
<p>앞선 세 편에서 다룬 기술적인 판단들도 결국 혼자 내린 결정은 아니었다. 3편에서 Circuit Breaker를 다시 검토하게 된 것도 팀원의 질문에서 시작됐다. &quot;왜 이 도구를 선택했는가&quot;를 함께 고민하면서 기존 구조를 다시 살펴봤고, 3-Broker와 DLT 구조를 별도로 구현하게 됐다.</p>
<p>그 질문이 나올 수 있었던 것도, 그 질문을 자연스럽게 받아들일 수 있었던 것도 매일 서로의 선택을 설명하고 근거를 묻는 시간이 있었기 때문이라고 생각한다.</p>
<p>지금 돌아보면 우리가 매일 공유했던 건 진행 상황이 아니라 판단의 기준이었다.</p>
<p><strong>역할을 나누는 것과, 같은 기준으로 결정하는 것은 다르다.</strong></p>
<br>

<p>한 달은 짧았고, 이 방식이 더 큰 조직에서도 그대로 통할지는 확신할 수 없다. 다만 무작위로 모인 사람들이 한 달 만에 같은 방향을 보게 되는 건 우연만으로 만들어지는 일이 아니라는 건 분명히 배웠다.</p>
<p>돌아보면 이번 프로젝트에서 가장 크게 배운 건 Kubernetes나 Kafka를 다루는 방법이 아니었다.</p>
<p>증상보다 원인을 먼저 보는 것,
성공했다는 표시보다 실제 동작을 확인하는 것,
도구보다 시스템에 필요한 특성이 무엇인지 생각하는 것,
그리고 역할보다 판단의 기준을 함께 맞추는 것.</p>
<p>네 편에 걸쳐 정리한 이야기는 결국 이 네 가지를 배우는 과정이었다.</p>
<p>CJ OliveNetworks CloudWave 후기부터 이어진 Clmakase 프로젝트 이야기는 여기서 마무리한다.</p>
<p>긴 글을 읽어 주신 모든 분께 감사드린다.</p>
<p>후기 - <a href="https://velog.io/@gm-15/CJ-OliveNetworks-CloudWave-7%EA%B8%B0-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0">[CJ OliveNetworks CloudWave 7기] 수료 후기
</a>1편 - <a href="https://velog.io/@gm-15/15%EB%A7%8C-VU%EB%A5%BC-%EB%B2%84%ED%8B%B0%EA%B8%B0%EA%B9%8C%EC%A7%80-%EB%82%B4%EA%B0%80-%EC%84%B8-%EB%B2%88-%ED%8B%80%EB%A0%B8%EB%8D%98-%EC%9D%B4%EC%9C%A0">스케일아웃하면 나아질 줄 알았다 - 15만 VU 부하 테스트에서 깨진 3가지 직관
</a>2편 - <a href="https://velog.io/@gm-15/%EB%B9%8C%EB%93%9C%EB%8A%94-%EC%84%B1%EA%B3%B5-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%8A%94-%EC%8B%A4%ED%8C%A8-%EB%B0%B0%ED%8F%AC%EC%97%90%EC%84%9C-%EB%A7%8C%EB%82%9C-3%EA%B0%80%EC%A7%80-%ED%95%A8%EC%A0%95">빌드는 성공, 서비스는 실패 - 배포에서 만난 3가지 함정
</a>3편 - <a href="https://velog.io/@gm-15/%EC%99%9C-Kafka%EC%98%80%EC%9D%84%EA%B9%8C-%EC%9E%A5%EC%95%A0%EB%A5%BC-%EA%B2%AC%EB%94%94%EB%8A%94-%EA%B5%AC%EC%A1%B0%EB%A5%BC-%EA%B3%A0%EB%AF%BC%ED%95%9C-3%EA%B0%80%EC%A7%80-%EC%A7%88%EB%AC%B8">왜 Kafka였을까? 장애를 견디는 구조를 고민한 3가지 질문
</a></p>
<p>-Clmakase 깃허브 링크
<a href="https://github.com/gm-15/Clmakase">https://github.com/gm-15/Clmakase</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 Kafka였을까? 장애를 견디는 구조를 고민한 3가지 질문]]></title>
            <link>https://velog.io/@gm-15/%EC%99%9C-Kafka%EC%98%80%EC%9D%84%EA%B9%8C-%EC%9E%A5%EC%95%A0%EB%A5%BC-%EA%B2%AC%EB%94%94%EB%8A%94-%EA%B5%AC%EC%A1%B0%EB%A5%BC-%EA%B3%A0%EB%AF%BC%ED%95%9C-3%EA%B0%80%EC%A7%80-%EC%A7%88%EB%AC%B8</link>
            <guid>https://velog.io/@gm-15/%EC%99%9C-Kafka%EC%98%80%EC%9D%84%EA%B9%8C-%EC%9E%A5%EC%95%A0%EB%A5%BC-%EA%B2%AC%EB%94%94%EB%8A%94-%EA%B5%AC%EC%A1%B0%EB%A5%BC-%EA%B3%A0%EB%AF%BC%ED%95%9C-3%EA%B0%80%EC%A7%80-%EC%A7%88%EB%AC%B8</guid>
            <pubDate>Wed, 15 Jul 2026 05:16:18 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@gm-15/15%EB%A7%8C-VU%EB%A5%BC-%EB%B2%84%ED%8B%B0%EA%B8%B0%EA%B9%8C%EC%A7%80-%EB%82%B4%EA%B0%80-%EC%84%B8-%EB%B2%88-%ED%8B%80%EB%A0%B8%EB%8D%98-%EC%9D%B4%EC%9C%A0">스케일아웃하면 나아질 줄 알았다 - 15만 VU 부하 테스트에서 깨진 3가지 직관</a>에서 브로커 장애에 대비한 두 버전의 결과만 비교했다. 이번에는 그 결과에 이르기까지 어떤 기준으로 구조를 선택했는지를 정리해 보려고 한다.</p>
<h1 id="장애를-견디는-구조를-고르며-마주한-3가지-질문">장애를 견디는 구조를 고르며 마주한 3가지 질문</h1>
<p>올영 세일처럼 짧은 시간에 대규모의 트래픽이 몰리는 시스템에서, 나는 세 가지 질문을 순서대로 마주했다.</p>
<hr>
<h2 id="①-왜-kafka였나">① 왜 Kafka였나</h2>
<p>대기열 진입과 주문 이벤트를 비동기로 처리하기로 했을 때 후보는 SQS, RabbitMQ, Kafka였다.</p>
<p>기준을 정하기 전에 이 도메인에서 무엇이 중요한지부터 생각했다. 
이 프로젝트가 가정한 상황은 올영 세일처럼 정해진 시각에 트래픽이 몰리는 이벤트다. 세일은 짧고 물량은 한정돼 있다. 그 순간에 처리되지 못한 주문은 나중에 살려내도 의미가 없다. 고객은 구매하지 못했고, 그 경험은 다음 세일에 다시 찾지 않을 이유로 이어질 수 있다. 트래픽이 몰리는 순간의 장애는 그 장애만으로 끝나지 않는다.</p>
<p>더 큰 문제는 실패 사실조차 알아채지 못하는 경우다. 이벤트 하나가 유실되면 화면에는 성공으로 보이지만 실제 데이터는 남지 않는다. 고객은 주문이 완료됐다고 생각하지만 운영자는 문제가 발생한 사실조차 바로 알기 어렵다.</p>
<p>반면 대기열 진입은 실패하더라도 &#39;다시 시도&#39;라는 대안이 있다. 물론 타격은 없지 않지만, 같은 실패라도 서비스에 미치는 영향의 차이가 있다는 것을 알 수 있었다.
<br></p>
<p>그래서 기술을 고르는 기준도 하나로 정리됐다.</p>
<p>실패했을 때, 그 실패를 어떻게 처리할 수 있는가.</p>
<p>실패에도 종류가 있다. 네트워크가 잠시 불안정한 경우와 DB 커넥션이 부족한 경우, 인프라 자체에 장애가 발생한 경우는 기다려야 하는 시간도 다르다. 한 번 재시도하고 포기하는 구조로는 이런 상황을 모두 처리할 수 없었다. 원인에 따라 재시도 단계를 나누고, 끝내 처리하지 못한 이벤트도 버리지 않고 남겨 두었다가 원인을 해결한 뒤 다시 처리할 수 있어야 했다.</p>
<p>그리고 실패한 이벤트 하나 때문에 뒤에 있는 정상 주문까지 함께 멈춰서도 안 됐다. 같은 큐에서 계속 재시도하면 뒤에 있는 메시지까지 함께 대기하게 된다. Kafka의 @RetryableTopic은 실패한 메시지를 별도의 재시도 토픽으로 이동시키기 때문에, 정상적인 주문은 계속 처리하면서 실패한 이벤트만 따로 재시도할 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/fc65912b-4e3b-41d4-92f1-a02680c0f284/image.png" alt=""></p>
<pre><code>order-events
  │ 실패
  ├─ retry-0  (1초)   네트워크 지터
  ├─ retry-1  (5초)   DB 커넥션 부족
  ├─ retry-2  (30초)  인프라 장애
  └─ order-events.DLT  격리 보관 → 원인 확인 후 재처리</code></pre><p>SQS에도 재시도와 DLQ는 있다. 다만 이번 프로젝트에서는 실패 원인별로 재시도 단계를 나누고 각각을 독립적으로 관리하는 구조가 필요했고, 그 요구사항에는 Kafka가 더 잘 맞았다.</p>
<p>(여기서 말하는 주문은 주문 생성과 재고 차감까지다. 실제 결제 연동까지 간 건 아니라 유실의 무게는 실제 서비스와 다르다. 다만 사용자가 본 결과와 데이터가 어긋난다는 점은 같다.)</p>
<hr>
<h2 id="②-왜-전부-kafka를-쓰지-않았나">② 왜 전부 Kafka를 쓰지 않았나</h2>
<p>Kafka를 쓴 후로 자연스럽게 &#39;대기열도 전부 Kafka로 보내면 되지 않을까&#39;하는 생각이 들었다.</p>
<p>그런데 대기열의 연산들은 성격이 서로 달랐다.</p>
<ul>
<li><strong>대기열 진입(쓰기)</strong> : 세일이 열리는 순간 요청이 한꺼번에 몰린다. 이걸 그대로 Redis에 쓰면 순간 부하가 그대로 전달된다.</li>
<li><strong>순번 조회(읽기)</strong> : 사용자는 &quot;내 앞에 몇 명인지&quot;를 지금 알아야 한다. Redis의 <code>ZRANK</code>는 바로 답을 준다. 이걸 Kafka로 보내면 요청과 응답이라는 패턴 자체가 성립하지 않는다.</li>
<li><strong>대기열 처리, 완료</strong> : 스케줄러가 도는 관리 작업이라 폭주가 없고, 즉시 반영돼야 한다.</li>
</ul>
<p>그래서 쓰기가 몰리는 구간만 Kafka로 완충하고, 읽기와 관리 작업은 Redis 직접 호출로 남겼다.</p>
<p>물론 Tradeoff도 있었다. 진입 요청에 응답하는 시점에는 아직 Redis에 순번이 기록되기 전이다. 그래서 첫 응답의 순번은 추정값이고, 이후 조회로 확정된다. 짧은 지연을 받아들이는 대신 폭주 구간을 지킨 셈이다.</p>
<p>비동기는 장점만 있는 도구가 아니었다. 필요한 구간에만 쓰고, 그 대가를 받아들일 수 있는 곳에만 써야 하는 도구였다.</p>
<hr>
<h2 id="③-circuit-breaker가-맞는-도구였을까">③ Circuit Breaker가 맞는 도구였을까</h2>
<p>Kafka가 흔들릴 때를 대비해 Circuit Breaker를 넣었다. Kafka 전송 실패율이 50%를 넘으면 회로를 열고, Redis에 직접 기록하는 우회로로 넘어간다. 30초 뒤 다시 Kafka를 시도해 보고 정상이면 원래 경로로 돌아온다.</p>
<p>그런데 팀원의 지적이 있었다.</p>
<blockquote>
<p>&quot;Circuit Breaker는 원래 동기 호출에서 장애가 번지는 걸 막는 도구다. Kafka 같은 비동기 파이프라인은 Kafka 자체가 완충이라, 브로커를 여러 대로 두고 DLQ로 처리하는 게 정석이다. 그리고 우회로로 넣은 데이터는 나중에 Kafka가 복구됐을 때 중복될 수 있다.&quot;</p>
</blockquote>
<p>이에 Circuit Breaker에 대해 자세히 찾아봤고, 두 가지를 알 수 있었다.</p>
<p>첫째, 용도가 달랐다. 원래 Circuit Breaker는 A가 응답하지 않을 때 A를 호출하는 B까지 무너지는 걸 막는 도구다. 내가 쓴 건 &quot;Kafka가 안 되면 Redis로 간다&quot;는 경로 전환이었다. 동작은 하지만 그 패턴이 원래 풀려던 문제와는 달랐다.</p>
<p>둘째, 중복은 이 경로에선 생기지 않았다. 대기열에 넣는 값은 <code>sessionId:productId:token</code> 형태다. 우회로로 넣는 값과, 나중에 Kafka가 복구돼 컨슈머가 처리하는 값은 같은 메시지라 값이 동일하다. Redis Sorted Set은 같은 멤버를 다시 넣으면 새로 추가하지 않고 점수만 갱신한다. 그래서 이 경로의 중복은 발생하지 않았다. (다만 사용자가 요청을 두 번 보내면 토큰이 달라 별개 항목이 된다. 그건 다른 층위의 문제다.)</p>
<p>기술의 용도를 바라보는 시선을 갖게 해준 지적이 감사했다. 이후로 3-Broker 클러스터에 복제와 DLT를 얹은 구조를 따로 만들었고, 최종 부하 테스트에 배포했다. Circuit Breaker가 들어간 버전은 비교 대상으로 남았다. <a href="https://velog.io/@gm-15/15%EB%A7%8C-VU%EB%A5%BC-%EB%B2%84%ED%8B%B0%EA%B8%B0%EA%B9%8C%EC%A7%80-%EB%82%B4%EA%B0%80-%EC%84%B8-%EB%B2%88-%ED%8B%80%EB%A0%B8%EB%8D%98-%EC%9D%B4%EC%9C%A0">스케일아웃하면 나아질 줄 알았다 - 15만 VU 부하 테스트에서 깨진 3가지 직관</a>에서 보여준 브로커 장애 비교가 바로 그 두 버전이다.</p>
<blockquote>
<p><strong>동작한다는 것과, 맞는 도구를 쓰고 있다는 것은 다르다.</strong></p>
</blockquote>
<hr>
<h1 id="마치며">마치며</h1>
<p>세 질문을 돌아보니 공통점이 있었다. 셋 다 &quot;어떤 기술이 더 좋은가&quot;를 묻는 질문은 아니었다.</p>
<p>Kafka를 선택한 이유는 Kafka 자체가 좋아서가 아니었다. 이 시스템에서는 실패를 단계별로 나누어 처리하고, 끝내 처리하지 못한 이벤트도 다시 살릴 수 있는 구조가 더 중요했다. 전부 Kafka로 보내지 않은 것도 마찬가지였다. 비동기가 항상 좋은 게 아니라, 요청이 몰리는 구간에서만 필요한 선택이었다. Circuit Breaker를 다시 찾아본 이유도 동작 여부를 확인하기 위해서가 아니라, 이 상황에 정말 적절한 도구인지 다시 판단해 보기 위해서였다.</p>
<p>앞선 두 편에서는 증상이 나타난 곳과 원인이 있는 곳은 다를 수 있다는 것, 설정이 있다고 해서 시스템이 의도대로 동작하는 것은 아니라는 것을 배웠다. 이번에는 그보다 한 단계 앞선 걸 배웠다.</p>
<p>기술보다 먼저 정해야 하는 건, 시스템이 실패를 어떻게 받아들일 것인가였다.</p>
<p>물론 이 구조가 실제 운영 환경에서 검증된 것은 아니다. 또 여기서 내린 선택이 모든 상황에 맞는 정답이라고 생각하지도 않는다. 다만 이번 프로젝트를 거치며 새로운 기술을 바라볼 때, 기술 자체보다 이 시스템에 정말 필요한 특성이 무엇인지부터 먼저 생각하게 됐다.</p>
<br>

<p>후기 - <a href="https://velog.io/@gm-15/CJ-OliveNetworks-CloudWave-7%EA%B8%B0-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0">[CJ OliveNetworks CloudWave 7기] 수료 후기
</a>1편 - <a href="https://velog.io/@gm-15/15%EB%A7%8C-VU%EB%A5%BC-%EB%B2%84%ED%8B%B0%EA%B8%B0%EA%B9%8C%EC%A7%80-%EB%82%B4%EA%B0%80-%EC%84%B8-%EB%B2%88-%ED%8B%80%EB%A0%B8%EB%8D%98-%EC%9D%B4%EC%9C%A0">스케일아웃하면 나아질 줄 알았다 - 15만 VU 부하 테스트에서 깨진 3가지 직관
</a>2편 - <a href="https://velog.io/@gm-15/%EB%B9%8C%EB%93%9C%EB%8A%94-%EC%84%B1%EA%B3%B5-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%8A%94-%EC%8B%A4%ED%8C%A8-%EB%B0%B0%ED%8F%AC%EC%97%90%EC%84%9C-%EB%A7%8C%EB%82%9C-3%EA%B0%80%EC%A7%80-%ED%95%A8%EC%A0%95">빌드는 성공, 서비스는 실패 - 배포에서 만난 3가지 함정
</a>4편 - <a href="https://velog.io/@gm-15/%EC%97%AD%ED%95%A0%EC%9D%80-%EB%82%98%EB%88%B4%EC%A7%80%EB%A7%8C-%EA%B8%B0%EC%A4%80%EC%9D%80-%EB%82%98%EB%88%84%EC%A7%80-%EC%95%8A%EC%95%98%EB%8B%A4-%EB%AC%B4%EC%9E%91%EC%9C%84%EB%A1%9C-%EB%AA%A8%EC%9D%B8-6%EB%AA%85%EC%9D%B4-%ED%95%9C-%EB%8B%AC%EB%A7%8C%EC%97%90-%EB%A7%8C%EB%93%A0-Clmakase">역할은 나눴지만 기준은 나누지 않았다 - 무작위로 모인 6명이 한 달만에 만든 Clmakase</a></p>
<p>-Clmakase 깃허브 링크
<a href="https://github.com/gm-15/Clmakase">https://github.com/gm-15/Clmakase</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[빌드는 성공, 서비스는 실패 - 배포에서 만난 3가지 함정]]></title>
            <link>https://velog.io/@gm-15/%EB%B9%8C%EB%93%9C%EB%8A%94-%EC%84%B1%EA%B3%B5-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%8A%94-%EC%8B%A4%ED%8C%A8-%EB%B0%B0%ED%8F%AC%EC%97%90%EC%84%9C-%EB%A7%8C%EB%82%9C-3%EA%B0%80%EC%A7%80-%ED%95%A8%EC%A0%95</link>
            <guid>https://velog.io/@gm-15/%EB%B9%8C%EB%93%9C%EB%8A%94-%EC%84%B1%EA%B3%B5-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%8A%94-%EC%8B%A4%ED%8C%A8-%EB%B0%B0%ED%8F%AC%EC%97%90%EC%84%9C-%EB%A7%8C%EB%82%9C-3%EA%B0%80%EC%A7%80-%ED%95%A8%EC%A0%95</guid>
            <pubDate>Wed, 15 Jul 2026 03:57:25 GMT</pubDate>
            <description><![CDATA[<p>지난 <a href="https://velog.io/@gm-15/15%EB%A7%8C-VU%EB%A5%BC-%EB%B2%84%ED%8B%B0%EA%B8%B0%EA%B9%8C%EC%A7%80-%EB%82%B4%EA%B0%80-%EC%84%B8-%EB%B2%88-%ED%8B%80%EB%A0%B8%EB%8D%98-%EC%9D%B4%EC%9C%A0">스케일아웃하면 나아질 줄 알았다 - 15만 VU 부하 테스트에서 깨진 3가지 직관</a>에서는 부하 테스트 중 만난 세 가지 문제를 다뤘다. 그때 배운 건 증상이 나타난 계층과 실제 원인이 있는 계층은 다를 수 있다는 것이었다.</p>
<h1 id="배포에서-만난-3가지-함정">배포에서 만난 3가지 함정</h1>
<p>이번 글은 그보다 앞선 단계, 배포하고 연결하는 과정에서 만난 문제들이다. 이번 문제들 역시 하나의 공통점으로 바라볼 수 있었다. 
&#39;모든 신호는 정상처럼 보였지만, 실제 서비스는 의도대로 동작하지 않았다.&#39;</p>
<p>여기서 다루는 내용은 내가 맡았던 CI/CD와 컨테이너, 스케일링 영역에서 직접 겪은 이야기다.</p>
<hr>
<h2 id="①-배포는-성공인데-이미지가-안-바뀐다">① 배포는 성공인데 이미지가 안 바뀐다</h2>
<p>코드를 수정하고 배포했다. 그런데 화면이 그대로였다. 모든 과정은 성공했다고 나타났기에 더  이상했다.</p>
<ul>
<li>GitLab CI : <code>test → build → scan</code> 모두 성공</li>
<li>ArgoCD : 최신 커밋과 작성자 정상 반영</li>
<li>하지만 실제 Pod는 이전 이미지를 그대로 실행 중</li>
</ul>
<p>처음에는 배포가 늦게 반영되는 줄 알았다. 더불어 캐시 문제도 의심했다. 하지만 원인은 훨씬 단순했다.</p>
<p>이미지 태그를 계속 <code>latest</code>로 사용하고 있던 것이 문제였다. ArgoCD는 Git 저장소의 매니페스트(YAML)가 변경되었는지를 기준으로 배포를 수행한다. 그런데 태그가 항상 <code>latest</code>라면 코드가 바뀌어도 YAML의 내용은 그대로다. ArgoCD 입장에서는 변경 사항이 없는 것이었다.</p>
<blockquote>
<p>ArgoCD에서 최신 커밋을 가리키는 것과, 실제 새로운 이미지를 실행하는 것은 다른 문제였다.</p>
</blockquote>
<p>해결은 태그 전략을 바꾸는 것이었다. 이미지 태그를 커밋 SHA로 변경하고, CI의 deploy 단계에서 Deployment YAML의 태그를 자동으로 교체하도록 수정했다.</p>
<pre><code class="language-bash">sed -i &quot;s|oliveyoung-api:.*|oliveyoung-api:${CI_COMMIT_SHORT_SHA}|g&quot; k8s/deployment.yaml
git commit -m &quot;ci: update image tag to ${CI_COMMIT_SHORT_SHA} [skip ci]&quot;
git push origin main</code></pre>
<p><code>[skip ci]</code>를 함께 사용해 배포 커밋이 다시 CI를 실행하는 무한 루프도 방지했다. 이후에는 커밋이 바뀔 때마다 이미지 태그도 함께 변경되고, ArgoCD가 이를 감지해 정상적으로 롤링 배포를 수행했다.</p>
<p>이 경험 이후로는 빌드 성공보다 실제 실행 중인 이미지가 바뀌었는지 먼저 확인하는 과정을 꼭 거쳤다.</p>
<hr>
<h2 id="②-요청-몇-개에-pod가-최대까지-늘어났다">② 요청 몇 개에 Pod가 최대까지 늘어났다</h2>
<p>스케일링에서도 비슷한 일이 있었다. 설정은 분명 적용했는데 동작이 이상했다.</p>
<p>처음에는 스케일링이 실제로 동작하는지 확인하려고 KEDA의 queryValue를 10으로 낮게 잡아 뒀다. 그대로 두고 부하 테스트에 들어갔는데, 그 값이 왜 위험한지는 몰랐다.</p>
<p>부하를 올리자 요청이 조금만 들어와도 Pod가 최대 개수까지 늘어났다가 다시 줄어드는 현상이 계속 반복됐다. 스케일 아웃과 스케일 인이 반복되는 thrashing 상태였다.</p>
<p>문제는 queryValue를 민감도를 조절하는 값 정도로 이해하고 있었다는 점이었다. 이 값은 사실 파드 하나가 감당할 목표치다. KEDA는 현재 지표 ÷ queryValue로 필요한 파드 수를 계산한다. 즉 값이 작을수록 같은 트래픽에도 필요한 파드 수가 폭증한다. <code>queryValue: 10</code>이면 요청 100건만 들어와도 파드 10개가 필요하다는 계산이 나오는 것이다.</p>
<p>그제야 이 값이 감으로 정할 수 있는 값이 아니라는 걸 알았다. 목표 트래픽과 파드당 처리량에서 역산해야 하는 값이었다.</p>
<pre><code>목표 160 RPS × 60초 ÷ 파드당 처리량(6) ≈ 1600
queryValue : 10 → 1600</code></pre><p>여기에 minReplicaCount도 1에서 2로 변경했다. 트래픽이 거의 없을 때 Pod가 계속 생성, 삭제되는 현상도 함께 줄일 수 있었다.</p>
<p>이 경험 이후에는 스케일링 설정을 바꿀 때도 먼저 운영 목표를 계산하고, 그 결과를 기준으로 임계값을 결정하게 됐다.</p>
<hr>
<h2 id="③-두-컨트롤러가-같은-deployment를-동시에-제어했다">③ 두 컨트롤러가 같은 Deployment를 동시에 제어했다</h2>
<p>마지막은 컨트롤러 충돌이었다.</p>
<p>KEDA는 HPA를 대체하는 도구가 아니다. KEDA를 적용하면 내부적으로 HPA를 자동 생성하고, 그 위에서 동작한다. 이 구조를 모른 채 HPA를 하나 더 직접 만들어 뒀더니, 두 개의 HPA가 같은 Deployment의 Replica 수를 서로 다르게 조정하려 했다. 각 컨트롤러는 자기 기준으로는 정상 동작하고 있었지만, 전체 시스템은 계속 충돌이 생겼다. 결국 수동으로 만든 HPA를 제거하고 KEDA가 생성한 HPA 하나만 남겨 해결했다.</p>
<p>그런데 같은 문제가 한 번 더 나타났다. 이번에는 컨트롤러가 아니라 Deployment 자체였다.</p>
<pre><code class="language-yaml">replicas: 2</code></pre>
<p>replicas는 원하는 파드 수를 선언적으로 고정하는 필드다. 반면 오토스케일러는 그 값을 런타임에 계속 바꾸는 주체다. 둘 다 있으면 같은 필드의 소유권이 둘로 갈린다. Deployment는 2로 되돌리려 하고, KEDA는 부하에 맞춰 바꾸려 했다. 추후 replicas 필드를 제거해 파드 수를 조정하는 주체를 KEDA 하나로 통일하고 나서야 안정됐다.</p>
<p>두 번 모두 원인은 같았다. 어느 쪽도 잘못 동작한 것은 아니었다. 문제는 같은 대상을 제어하는 주체가 둘이었다는 것이었다.</p>
<blockquote>
<p><strong>스케일링을 제어하는 주체는 하나여야 한다.</strong></p>
</blockquote>
<hr>
<h1 id="마치며">마치며</h1>
<p>1편에서는 증상이 나타난 곳과 실제 원인이 있는 곳은 다를 수 있다는 걸 배웠다. 이번에는 조금 다른 걸 배웠다.</p>
<p><strong>설정이 존재한다는 것과 시스템이 의도대로 동작하는 것은 다르다.</strong></p>
<p>CI는 성공했고, ArgoCD는 최신 커밋을 가리켰고, KEDA도 정상적으로 설치되어 있었고, HPA 역시 오류 없이 실행되고 있었다. 하지만 실제 서비스는 내가 기대한 대로 움직이지 않았다.</p>
<p>지금은 성공했다는 표시보다, 실제로 원하는 상태가 만들어졌는지를 먼저 확인한다. 배포가 성공했다면 이미지가 정말 교체되었는지, 스케일링을 설정했다면 목표 트래픽에 맞는 값인지, 오토스케일러를 적용했다면 제어 주체가 하나인지부터 확인한다.</p>
<p>그 외에도 DB Secret을 만들면서 쉘 특수문자로 값이 변질됐던 삽질처럼 작은 문제들도 있었지만, 이번 글에서는 같은 성격의 사례는 제외했다.</p>
<br>
다음 글에서는 왜 Kafka를 선택했고, 어떤 고민 끝에 지금의 구조를 택했는지를 정리해 보려고 한다.

<br>

<p>후기 - <a href="https://velog.io/@gm-15/CJ-OliveNetworks-CloudWave-7%EA%B8%B0-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0">[CJ OliveNetworks CloudWave 7기] 수료 후기
</a>1편 - <a href="https://velog.io/@gm-15/15%EB%A7%8C-VU%EB%A5%BC-%EB%B2%84%ED%8B%B0%EA%B8%B0%EA%B9%8C%EC%A7%80-%EB%82%B4%EA%B0%80-%EC%84%B8-%EB%B2%88-%ED%8B%80%EB%A0%B8%EB%8D%98-%EC%9D%B4%EC%9C%A0">스케일아웃하면 나아질 줄 알았다 - 15만 VU 부하 테스트에서 깨진 3가지 직관
</a>3편 - <a href="https://velog.io/@gm-15/%EC%99%9C-Kafka%EC%98%80%EC%9D%84%EA%B9%8C-%EC%9E%A5%EC%95%A0%EB%A5%BC-%EA%B2%AC%EB%94%94%EB%8A%94-%EA%B5%AC%EC%A1%B0%EB%A5%BC-%EA%B3%A0%EB%AF%BC%ED%95%9C-3%EA%B0%80%EC%A7%80-%EC%A7%88%EB%AC%B8">왜 Kafka였을까? 장애를 견디는 구조를 고민한 3가지 질문
</a>4편 - <a href="https://velog.io/@gm-15/%EC%97%AD%ED%95%A0%EC%9D%80-%EB%82%98%EB%88%B4%EC%A7%80%EB%A7%8C-%EA%B8%B0%EC%A4%80%EC%9D%80-%EB%82%98%EB%88%84%EC%A7%80-%EC%95%8A%EC%95%98%EB%8B%A4-%EB%AC%B4%EC%9E%91%EC%9C%84%EB%A1%9C-%EB%AA%A8%EC%9D%B8-6%EB%AA%85%EC%9D%B4-%ED%95%9C-%EB%8B%AC%EB%A7%8C%EC%97%90-%EB%A7%8C%EB%93%A0-Clmakase">역할은 나눴지만 기준은 나누지 않았다 - 무작위로 모인 6명이 한 달만에 만든 Clmakase</a></p>
<p>-Clmakase 깃허브 링크
<a href="https://github.com/gm-15/Clmakase">https://github.com/gm-15/Clmakase</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스케일아웃하면 나아질 줄 알았다 - 15만 VU 부하 테스트에서 깨진 3가지 직관]]></title>
            <link>https://velog.io/@gm-15/15%EB%A7%8C-VU%EB%A5%BC-%EB%B2%84%ED%8B%B0%EA%B8%B0%EA%B9%8C%EC%A7%80-%EB%82%B4%EA%B0%80-%EC%84%B8-%EB%B2%88-%ED%8B%80%EB%A0%B8%EB%8D%98-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@gm-15/15%EB%A7%8C-VU%EB%A5%BC-%EB%B2%84%ED%8B%B0%EA%B8%B0%EA%B9%8C%EC%A7%80-%EB%82%B4%EA%B0%80-%EC%84%B8-%EB%B2%88-%ED%8B%80%EB%A0%B8%EB%8D%98-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Mon, 13 Jul 2026 17:56:24 GMT</pubDate>
            <description><![CDATA[<p>지난 <a href="https://velog.io/@gm-15/CJ-OliveNetworks-CloudWave-7%EA%B8%B0-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0">CloudWave 7기 수료 후기</a>에서 &quot;다음 글에서는 수치와 실패, 그리고 문제를 해결했던 과정을 정리해 보겠다&quot;고 적었다. 
이번 글은 그 후속편이다.
<br></p>
<p>우리 팀은 최종 2등이라는 결과로 프로젝트를 마무리했다.</p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/65d15f32-ba41-400f-b313-d114499e0b33/image.png" alt=""></p>
<p>최종 부하 테스트(2026.02.26, k6 분산 테스트, 20분)에서는 다음 결과를 확인했다.</p>
<ul>
<li>150,000 VU</li>
<li>56,300 hits/s</li>
<li>총 4.65M 요청</li>
<li>성공률 100% (5xx 0건)</li>
<li>P99 ≤ 180ms</li>
</ul>
<p>나는 6인 팀 프로젝트에서 팀장을 맡아 백엔드, CI/CD, 컨테이너 트랙과 부하 테스트를 담당했다. 보안, AI, DR, 네트워크는 각각 다른 팀원이 담당하였다.</p>
<p>프로젝트 기간은 2/4~2/27일로 한 달도 채 되지 않았다. 각자 맡은 영역만 따라가기에도 빠듯한 일정이었지만, 우리는 매일 아침 진행 상황을 공유하고 서로의 설계를 설명하는 시간을 만들었다. 덕분에 장애가 발생했을 때도 문제를 함께 이해하고 빠르게 원인을 좁혀갈 수 있었다. [팀워크 글 링크 예정]</p>
<h1 id="15만-vu-부하-테스트에서-깨진-3가지-직관">15만 VU 부하 테스트에서 깨진 3가지 직관</h1>
<p>하지만 모든 것이 끝나고 난 뒤, 팀워크와 더불어 오래 기억에 남은 건, 개발 과정 중 당연하게 여겼던 세 가지가 모두 틀렸다는 사실이었다.</p>
<ul>
<li>스케일아웃하면 성능은 자연스럽게 올라갈 줄 알았다.</li>
<li>Karpenter만 설치하면 먼저 실행할 노드 없이도 알아서 생성될 줄 알았다.</li>
<li>Pod가 OOMKilled 되면 애플리케이션 문제일 줄 알았다.</li>
</ul>
<p>세 가지 모두 틀렸다.</p>
<p>이 글에서는 내가 직접 구현하고 문제를 해결했던 백엔드와 인프라 이야기에 집중해 보겠다.</p>
<hr>
<h2 id="①-스케일아웃하면-성능이-올라갈-줄-알았다">① 스케일아웃하면 성능이 올라갈 줄 알았다</h2>
<p>부하 테스트를 시작하자 예상하지 못한 문제가 나타났다.</p>
<p>앞서, 트래픽이 늘어나면 Pod를 더 생성하도록 KEDA를 설정해 뒀다. 그런데 Pod 수가 늘어날수록 5xx 응답이 함께 증가했다. 분산시키려고 스케일아웃을 했는데 오히려 장애가 늘어난 상황이다.</p>
<p>처음에는 애플리케이션 로직을 의심했다. 하지만 로그를 확인해 보니 같은 메시지가 계속 반복됐다.</p>
<pre><code>Too many connections</code></pre><p>같은 시점 CloudWatch에서는 Aurora <code>DatabaseConnections</code> 경보가 발생하고 있었다.</p>
<blockquote>
<p>🖼️ ** CloudWatch <code>DatabaseConnections 72 / 90 (80%)</code> 경보**
<img src="https://velog.velcdn.com/images/gm-15/post/b87c54f9-66c3-44d8-b3ce-b6b6ed050fc6/image.png" alt="">  Aurora DatabaseConnections가 최대치에 가까워지고 있음을 확인</p>
</blockquote>
<p>Aurora(<code>db.t3.medium</code>)의 최대 커넥션 수는 약 90개였다. 반면 Spring Boot는 Pod 하나가 생성될 때 HikariCP Connection Pool만큼 DB 커넥션을 확보한다. 기본값이 10이고 Pod가 최대 100개까지 늘어난다면, 전체 커넥션은 이렇게 계산된다.</p>
<pre><code>maxReplicas × HikariCP.poolSize ≤ Aurora.maxConnections
100 × 10 = 1000   (Aurora 최대 ≈ 90)</code></pre><p>Aurora가 감당할 수 있는 커넥션 수보다 훨씬 많았다. 그제야 원인이 보였다.</p>
<blockquote>
<p><strong>증상은 EKS에서 나타났지만, 원인은 Spring Boot의 Connection Pool 설정에 있었다.</strong></p>
</blockquote>
<p>이 경험 이후부터는 장애가 발생하면 증상이 나타난 계층과 실제 원인이 있는 계층이 같은지부터 확인하는 습관을 가졌다.</p>
<br>

<h3 id="pool을-절반으로-줄였는데도-이상했다">Pool을 절반으로 줄였는데도 이상했다</h3>
<p>가장 먼저 HikariCP Pool Size를 10에서 5로 줄였다. 그 결과 5xx는 사라졌고 부하 테스트도 정상적으로 통과했다. 그런데 다시 계산해 보면 이상한 점이 있었다.</p>
<pre><code>100 × 5 = 500   (여전히 90 초과)</code></pre><p>Pool Size를 절반으로 줄였는데도 계산상 최대 커넥션은 여전히 Aurora 한도를 훨씬 넘고 있었다. 즉, Pool Size를 줄인 것이 근본적인 해결은 아니었다.</p>
<p>HikariCP의 Pool Size는 최대값일 뿐이고, 모든 Pod가 동시에 Pool을 끝까지 사용하는 상황은 거의 발생하지 않는다. 이번 변경은 현재 트래픽에서 여유를 확보한 수준에 가까웠다.</p>
<p>그래서 여기서 끝내지 않았다. 함께 적용한 내용은 다음과 같다.</p>
<ul>
<li>HikariCP Pool Size 축소</li>
<li>KEDA 최소 Replica 조정으로 급격한 확장 방지</li>
<li>Cron Warm-up으로 Pod 미리 준비</li>
</ul>
<p>Aurora 자체를 상위 인스턴스로 변경하는 것은 근본적인 방법이지만, 프로젝트 범위를 고려해 후속 과제로 남겼다. 그리고 팀에서는 스케일 정책을 수정하기 전에 반드시 아래 식부터 확인하기로 했다.</p>
<pre><code>maxReplicas × Connection Pool ≤ DB max_connections</code></pre><br>
예전에는 Pod 개수만 확인했다. 그러나 지금은 Pod 뒤에 있는 데이터베이스까지 함께 본다. 스케일아웃은 애플리케이션만 늘리는 작업이 아니라, DB가 감당할 수 있는 한계까지 함께 계산해야 하는 정책이라는 걸 이번에 직접 확인했다.

<hr>
<h2 id="②-karpenter도-먼저-실행될-노드가-필요했다">② Karpenter도 먼저 실행될 노드가 필요했다</h2>
<p>노드 프로비저닝 과정에서도 예상과 다른 문제가 발생했다.</p>
<p>기존 Managed Node Group에서 Karpenter 기반으로 변경을 결정했었다. Managed Node Group에서 노드 생성 실패가 반복됐고, 인스턴스 선택을 자동화하면서 Spot 인스턴스를 활용할 수 있는 Karpenter가 더 낫다고 판단했기 때문이다.</p>
<p>하지만 문제가 발생했다. 자세히 들여다보니 Pod는 계속 Pending 상태였고 새 노드도 생성되지 않았다.</p>
<p>처음에는 IAM 권한을 의심했다. 그래서 권한을 하나씩 확인하고 수정했지만 상황은 여전히 그대로였다. 조금 더 확인해 보니 원인은 전혀 다른 곳에 있었다.</p>
<p>Kubernetes 시스템 컴포넌트도 결국 실행될 노드가 필요하다. CoreDNS도 노드가 있어야 실행되고, Karpenter 역시 노드 위에서 동작한다. 그런데 정작 노드를 생성해야 하는 Karpenter가 먼저 실행되지 못하면 새로운 노드도 만들어질 수 없다.</p>
<p>노드를 만드는 프로그램도 먼저 실행될 노드가 필요했다.</p>
<p>전형적인 &#39;닭이 먼저냐 달걀이 먼저냐(chicken-and-egg)&#39; 문제였다.</p>
<pre><code>Worker Node 없음
        │
        ▼
CoreDNS / Karpenter 모두 Pending
        │
        ▼
노드를 생성할 Karpenter가 실행되지 못함
        │
        ▼
새 Worker Node 생성 불가

────────────────────────────

Fargate에서 CoreDNS·Karpenter 먼저 실행
        │
        ▼
Karpenter 정상 동작
        │
        ▼
Worker Node 생성</code></pre><p>해결 방법은 시스템 컴포넌트의 실행 위치를 분리하는 것이었다. Karpenter와 CoreDNS를 Fargate에서 먼저 실행하도록 구성해 워커 노드 의존성을 제거했다. 이후 Karpenter가 정상적으로 동작하면서 필요한 워커 노드를 생성할 수 있었다.</p>
<p>이 과정에서 IAM 권한 누락, CRD 충돌 등 여러 연쇄 오류도 함께 발생했다. 같은 문제를 반복하지 않기 위해 원인과 해결 과정을 ADR 문서로 정리했다. 명시적으로 정리한 주요 오류는 8건이며, SG 중복·CIDR·ENI 같은 연관 이슈까지 포함한 전체 과정은 <a href="https://github.com/gm-15/Clmakase/blob/main/docs/adr/karpenter-migration-errors.md">ADR</a>에 기록해 두었다.</p>
<p>이 경험 이후부터는 새로운 컴포넌트를 도입할 때 기능보다 먼저 &quot;이 컴포넌트는 무엇에 의존해 실행되는가&quot;를 확인하는 습관이 생겼다.</p>
<hr>
<h2 id="③-pod가-죽으면-내-애플리케이션-문제일-줄-알았다">③ Pod가 죽으면 내 애플리케이션 문제일 줄 알았다</h2>
<p>부하 테스트 과정에서 또 다른 문제가 발생했다. 1, 2차 테스트에서 여러 Pod가 OOMKilled로 종료됐다. 당연히 애플리케이션 메모리 사용량 문제라고 생각해 Heap 설정부터 확인했다.</p>
<p>하지만 애플리케이션 컨테이너의 메모리 사용량은 크게 증가하지 않았다. OOMKilled가 발생한 컨테이너를 확인해 보니 원인은 애플리케이션이 아니라 Istio 사이드카(istio-proxy)였다.</p>
<br>
서비스 메시를 적용하면서 모든 요청은 Istio Proxy를 거쳐 전달되고 있었다. 평소에는 문제가 없었지만 15만 VU 수준에서는 사이드카의 메모리 사용량이 크게 늘었고, 기존 256Mi limit으로는 부족했다.

<p>처음에는 limit만 올리면 해결될 거라고 생각했지만 그렇지 않았다. request와 limit의 역할이 다르다는 점을 다시 확인했다. request는 스케줄링 기준이고, limit은 실제 사용할 수 있는 최대 메모리다. 결국 request와 limit을 분리하고, limit을 단계적으로 조정했다.</p>
<pre><code>256Mi → 2Gi → 10Gi</code></pre><p>수정 이후 3차 부하 테스트에서 OOMKilled 0건을 확인했다. (1·2차에서는 분명히 여러 Pod가 죽었고, 세 번째 반복 끝에 잡은 결과다.)</p>
<p>서비스 메시를 쓰는 환경에서는 사이드카도 하나의 애플리케이션처럼 관리해야 한다는 걸 직접 경험했다.</p>
<hr>
<h2 id="세-문제의-공통점">세 문제의 공통점</h2>
<p>돌아보면 세 문제는 전혀 다른 장애처럼 보였다. Aurora는 데이터베이스, Karpenter는 Kubernetes, Istio는 서비스 메시 문제였다. 하지만 원인을 찾는 과정은 모두 같았다. 처음엔 증상이 나타난 곳을 원인이라고 생각했지만, 실제 원인은 항상 다른 계층에 있었다.</p>
<ul>
<li>5xx는 EKS에서 보였지만, 원인은 Spring HikariCP 설정이었다.</li>
<li>노드 생성 실패는 Karpenter에서 보였지만, 원인은 초기 실행 구조였다.</li>
<li>Pod 종료는 Kubernetes에서 보였지만, 원인은 Istio 사이드카였다.</li>
</ul>
<p>이번 프로젝트에서 가장 크게 달라진 건 장애를 바라보는 방식이었다. 예전에는 오류가 발생한 위치를 먼저 봤다. 이제는 오류가 발생한 위치와 실제 원인이 있는 위치가 다를 수 있다는 것을 먼저 떠올린다.</p>
<hr>
<hr>
<h2 id="만약-브로커가-죽으면-어떻게-될까">만약 브로커가 죽으면 어떻게 될까?</h2>
<p>앞의 세 가지는 모두 문제가 생긴 뒤 원인을 찾은 이야기였다. 반대로 이 이야기는 문제가 생기기 전에 먼저 던졌던 질문이다.</p>
<p>주문 이벤트를 Kafka로 처리하기 시작하면서 가장 먼저 든 생각은 하나였다.</p>
<blockquote>
<p>&quot;브로커 한 대가 죽으면 어떻게 될까?&quot;</p>
</blockquote>
<p>실제 서비스 환경을 그대로 재현할 수는 없어서, 로컬에서 브로커 하나를 종료(<code>docker stop kafka</code>)한 뒤 같은 요청을 보내며 동작을 확인했다. 결과는 예상보다 차이가 컸다.</p>
<ul>
<li>대비하지 않은 버전(A): P95 3,137ms, 일부 메시지 유실</li>
<li>대비한 버전(C): P95 436ms, 메시지 유실 없음</li>
</ul>
<blockquote>
<p>🖼️ <strong>Kafka Version A / C 비교</strong><img src="https://velog.velcdn.com/images/gm-15/post/51351736-4c56-40de-a274-573d71e3387a/image.png" alt="">  P95 3,137ms → 436ms, 데이터 유실 여부</p>
</blockquote>
<p>Version C에는 다음 구성을 적용했다.</p>
<ul>
<li>3-Broker StatefulSet</li>
<li>Replication Factor = 3</li>
<li><code>min.insync.replicas = 2</code></li>
<li><code>@RetryableTopic</code> (1초 → 5초 → 30초 단계적 재시도)</li>
<li>Dead Letter Topic(DLT)</li>
</ul>
<p>왜 SQS가 아니라 Kafka였냐는 질문도 많이 받았다. 이번 프로젝트에서는 실패 원인에 따라 재시도 단계를 세분화하고, DLT에 저장한 뒤 다시 처리하는 흐름이 더 중요했다. 그 목적에는 Kafka가 더 적합하다고 판단했다.</p>
<p>이 경험 이후에는 장애를 없애려 하기보다,</p>
<blockquote>
<p><strong>실패가 발생하더라도 관측되고, 다시 처리될 수 있는 구조를 만드는 것이 더 중요하다.</strong></p>
</blockquote>
<p>는 걸 배웠다.</p>
<hr>
<h2 id="직관이-잘못된-사례---트리거를-많이-걸수록-좋은-줄-알았다">+직관이 잘못된 사례  — 트리거를 많이 걸수록 좋은 줄 알았다</h2>
<p>부하 테스트를 진행하면서 KEDA의 스케일링 기준도 계속 조정했다. 처음에는 Datadog RPS와 Kafka Consumer Lag를 함께 사용하면 더 정확하게 스케일링할 수 있을 거라고 생각했다.</p>
<p>하지만 실제 테스트에서는 예상하지 못한 문제가 발생했다. Consumer Group이 완전히 등록되기 전에 KEDA가 Lag를 조회하면서 race condition이 발생했고, 결국 스케일링 자체가 멈춰 버렸다.</p>
<p>원인을 찾은 뒤에는 오히려 구성을 단순하게 바꿨다. Kafka Lag를 제거하고 Datadog RPS 하나만 스케일링 기준으로 사용했다. 내부 상태에 크게 영향을 받는 지표보다, 안정적으로 수집되는 외부 지표가 실제 운영에서 더 예측 가능하다고 판단했기 때문이다.</p>
<p>이 경험 이후에는 지표를 많이 쓰는 것보다 신뢰할 수 있는 지표를 쓰는 것이 더 중요하다는 기준이 생겼다.</p>
<hr>
<h2 id="실패를-보이게-만드는-것도-설계라고-생각했다">실패를 보이게 만드는 것도 설계라고 생각했다</h2>
<p>앞의 문제들을 해결할 수 있었던 건 문제를 눈으로 확인할 수 있었기 때문이다.</p>
<p>Kafka 재시도와 DLT 흐름에는 Micrometer 커스텀 카운터를 추가했다. 재시도가 어느 단계에서 발생했는지, DLT에는 몇 건이 들어갔는지 각 단계를 구분해서 확인할 수 있도록 구성했다. 애플리케이션 관측은 Datadog APM으로 통합했다. (CloudWatch, Grafana, Loki, Tempo 등 전체 모니터링 스택은 모니터링 트랙 팀원들이 담당했다.)</p>
<p>다만 여기에도 한계가 있었다. 각 카운터가 실제 운영 환경에서 어떤 패턴으로 증가하는지까지는 아직 충분히 관찰하지 못했다. 이 부분은 다음 사이클에서 확인할 과제로 남겨 뒀다.</p>
<hr>
<h1 id="마치며">마치며</h1>
<p>CloudWave에 지원했을 때 세운 목표는 &quot;인프라를 이해하는 백엔드 개발자가 되고 싶다&quot;였다. 프로젝트를 마친 지금도 그 목표가 달성됐다고 말할 수는 없다. 대규모의 실사용자를 유치하는 서비스를 운영해 본 경험도 아직 없고, 보안, DR처럼 내가 맡지 않은 영역을 깊게 다뤄 보지도 못했다.</p>
<p>하지만 이번 프로젝트를 하면서 문제를 바라보는 기준은 분명히 달라졌다. 예전에는 서버가 느려지면 코드부터, Pod가 죽으면 애플리케이션부터 의심했다. 지금은 먼저 &quot;이 문제가 정말 여기에서 시작된 게 맞을까&quot;를 생각하게 됐다.</p>
<p>Aurora에서는 데이터베이스를, Karpenter에서는 부트스트랩 구조를, Istio에서는 사이드카를, Kafka에서는 장애 이후의 흐름을 먼저 확인했다. 이번 프로젝트에서 가장 크게 바뀐 건 새로운 기술을 써 본 것이 아니라, 원인을 찾는 순서였다.</p>
<p>앞으로도 새로운 기술을 적용하기 전에 왜 이 구조를 선택하는지, 어떤 장애가 생길 수 있는지, 문제가 생기면 어디부터 확인해야 하는지를 먼저 고민하는 개발자가 되고 싶다.</p>
<p><strong>증상이 나타난 곳과 원인이 발생한 곳은 다를 수 있다.</strong></p>
<p>이번 프로젝트를 마치며 가장 인상 깊었던 말이다.</p>
<p>부하 테스트 결과와 측정 데이터는 아래 저장소에서 확인할 수 있다.</p>
<ul>
<li><a href="https://github.com/gm-15/Clmakase/tree/main/evidence/load-test-2026-02-26">Datadog Dashboard / k6 결과 / 부하 테스트 Evidence</a></li>
<li><a href="https://github.com/gm-15/Clmakase/blob/main/docs/adr/karpenter-migration-errors.md">ADR 문서 (Karpenter Migration, Troubleshooting)</a></li>
</ul>
<p>긴 글 읽어 주셔서 감사하다. 😊 다음 글부터는 배포 과정에서 만난 트러블슈팅, 기술 선택의 고민, 그리고 팀을 운영했던 이야기를 차례대로 정리해 보려고 한다.</p>
<br>

<p>후기 - <a href="https://velog.io/@gm-15/CJ-OliveNetworks-CloudWave-7%EA%B8%B0-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0">[CJ OliveNetworks CloudWave 7기] 수료 후기
</a>2편 - <a href="https://velog.io/@gm-15/%EB%B9%8C%EB%93%9C%EB%8A%94-%EC%84%B1%EA%B3%B5-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%8A%94-%EC%8B%A4%ED%8C%A8-%EB%B0%B0%ED%8F%AC%EC%97%90%EC%84%9C-%EB%A7%8C%EB%82%9C-3%EA%B0%80%EC%A7%80-%ED%95%A8%EC%A0%95">빌드는 성공, 서비스는 실패 - 배포에서 만난 3가지 함정
</a>3편 - <a href="https://velog.io/@gm-15/%EC%99%9C-Kafka%EC%98%80%EC%9D%84%EA%B9%8C-%EC%9E%A5%EC%95%A0%EB%A5%BC-%EA%B2%AC%EB%94%94%EB%8A%94-%EA%B5%AC%EC%A1%B0%EB%A5%BC-%EA%B3%A0%EB%AF%BC%ED%95%9C-3%EA%B0%80%EC%A7%80-%EC%A7%88%EB%AC%B8">왜 Kafka였을까? 장애를 견디는 구조를 고민한 3가지 질문
</a>4편 - <a href="https://velog.io/@gm-15/%EC%97%AD%ED%95%A0%EC%9D%80-%EB%82%98%EB%88%B4%EC%A7%80%EB%A7%8C-%EA%B8%B0%EC%A4%80%EC%9D%80-%EB%82%98%EB%88%84%EC%A7%80-%EC%95%8A%EC%95%98%EB%8B%A4-%EB%AC%B4%EC%9E%91%EC%9C%84%EB%A1%9C-%EB%AA%A8%EC%9D%B8-6%EB%AA%85%EC%9D%B4-%ED%95%9C-%EB%8B%AC%EB%A7%8C%EC%97%90-%EB%A7%8C%EB%93%A0-Clmakase">역할은 나눴지만 기준은 나누지 않았다 - 무작위로 모인 6명이 한 달만에 만든 Clmakase</a></p>
<p>-Clmakase 깃허브 링크
<a href="https://github.com/gm-15/Clmakase">https://github.com/gm-15/Clmakase</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Race Condition 해결 전략]]></title>
            <link>https://velog.io/@gm-15/Race-Condition-%ED%95%B4%EA%B2%B0-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@gm-15/Race-Condition-%ED%95%B4%EA%B2%B0-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Tue, 24 Mar 2026 08:42:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>[병렬프로그래밍]에서 멀티스레드를 다루던 도중 레이스 컨디션 상황에 대한 언급이 나왔다.
마침 [냉장GOAT]에서 레이스 컨디션을 메인으로 다루었고, [ParkingMate]에서도 이중 예약 방지를 위해 동일한 문제를 다룬 적이 있어 이번 기회에 자세히 정리하고자 한다.</p>
</blockquote>
<hr>
<h2 id="race-condition이란">Race Condition이란?</h2>
<p>두 개 이상의 스레드가 <strong>같은 자원에 동시에 접근</strong>할 때 실행 순서에 따라 결과가 달라지는 현상이다.</p>
<p>각 스레드의 코드는 틀리지 않았다. 다만 실행 순서가 보장되지 않아서 생기는 문제다.</p>
<p>고전적인 예시가 있다.</p>
<pre><code class="language-java">// 재고가 1개 남은 상황
if (stock &gt; 0) {       // 스레드 A: 통과
                       // 스레드 B: 통과 (동시에!)
    stock--;           // 스레드 A: stock = 0
    stock--;           // 스레드 B: stock = -1 💥
}</code></pre>
<p>두 스레드 모두 <code>stock &gt; 0</code>을 확인하고 통과한다.
결과는 재고 -1. 현실에서는 결제는 됐는데 상품이 없는 상황이다.</p>
<hr>
<h2 id="어디서-터지나">어디서 터지나?</h2>
<p>레이스 컨디션은 <strong>(확인 → 행동) 사이에 틈이 생길 때</strong> 발생한다.</p>
<p>냉장GOAT에서는 여러 직원이 동시에 재고를 차감하는 상황이었고,
ParkingMate에서는 같은 주차 공간에 두 명이 동시에 예약을 시도하는 상황이었다.</p>
<p>둘 다 본질적으로 <strong>공유 자원에 대한 비원자적 연산</strong>에 관한 문제이다.</p>
<hr>
<h2 id="해결-전략-4계층">해결 전략 4계층</h2>
<p>레이스 컨디션 해결법은 적용 범위에 따라 4가지 계층으로 나눌 수 있다.</p>
<h3 id="1-애플리케이션-레벨-jvm-내부">1. 애플리케이션 레벨 (JVM 내부)</h3>
<p>단일 서버, 스레드 간 경합을 제어한다.</p>
<p><strong>synchronized</strong></p>
<pre><code class="language-java">public synchronized void decreaseStock(Long id, int quantity) {
    // 한 번에 하나의 스레드만 진입
}</code></pre>
<p>메서드나 블록에 synchronized 키워드를 붙여 한 번에 하나의 스레드만 접근하게 한다.</p>
<p>메서드 단위로 걸면 간단하지만, JVM 하나 안에서만 유효하다.
서버가 2대가 되는 순간 무용지물이다.
<br>
<strong>Atomic 클래스 (CAS)</strong></p>
<pre><code class="language-java">AtomicInteger stock = new AtomicInteger(10);
stock.decrementAndGet(); // CPU 명령어 수준의 원자적 연산</code></pre>
<p>AtomicInteger 등은 하드웨어 수준의 Compare-And-Swap 알고리즘을 사용하여 락(Lock) 없이 원자성을 보장한다. 성능이 좋다.</p>
<p>단, 주의할 점이 있다. <code>decrementAndGet()</code> 자체는 원자적이다. 문제는 아래처럼 확인과 행동을 분리할 때다.</p>
<pre><code class="language-java">if (stock.get() &gt; 0) {        // CAS 1번째
    stock.decrementAndGet();   // CAS 2번째 ← 사이에 다른 스레드가 끼어들 수 있음
}</code></pre>
<p>각 연산은 원자적이지만 <strong>CAS와 CAS 사이는 원자적이지 않다.</strong>
&quot;재고 확인 후 차감 + 주문 생성&quot;처럼 복합 연산의 원자성은 보장하지 못한다.</p>
<br>

<h3 id="2-db-레벨-rdb">2. DB 레벨 (RDB)</h3>
<p>가장 확실하게 정합성을 보장할 수 있는 계층이다.</p>
<p><strong>낙관적 락 (Optimistic Lock)</strong></p>
<p>충돌이 드물다는 가정 하에 동작한다.</p>
<pre><code class="language-java">@Version
private Long version;</code></pre>
<p>수정 시점에 내가 읽었던 버전과 현재 버전을 비교한다.
버전이 다르면 <code>OptimisticLockException</code> 발생 → 애플리케이션에서 재시도.</p>
<p>DB 락을 잡지 않아서 가볍다.
단, <strong>충돌이 잦아질수록 재시도가 폭발적으로 늘어나</strong> 오히려 DB 부하가 커진다.
<br>
<strong>비관적 락 (Pessimistic Lock)</strong></p>
<p>충돌이 잦다는 가정 하에 동작한다.</p>
<pre><code class="language-java">@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional&lt;Stock&gt; findByProductId(Long productId);</code></pre>
<p><code>SELECT FOR UPDATE</code>로 읽는 순간부터 해당 row를 선점한다.
정합성은 강하게 보장되지만, 락 점유 시간 = DB 커넥션 점유 시간이다.
트래픽이 몰리면 HikariCP 커넥션 풀 고갈로 이어진다.</p>
<p>냉장GOAT에서 <code>@Transactional</code>을 <code>processOrder</code>에서 제거한 이유가 바로 이것이다.
트랜잭션이 길어질수록 커넥션을 오래 붙잡게 되고, 결국 전체가 막히게 된다.</p>
<p><em><strong>ParkingMate 적용 사례</strong></em>
ParkingMate에서는 같은 문제를 다른 방향으로 풀었다. 
락 범위를 줄이는 대신, 트랜잭션 안에 묶여 있던 알림 처리를 Transactional Outbox로 통째로 분리했다. 
예약 트랜잭션(T1)은 booking INSERT + outbox_event INSERT만 처리하고 커밋한다. 
알림은 별도 스케줄러(T2)가 나중에 처리하도록 분리했다.</p>
<p>실측 결과 평균 응답시간은 37ms → 36ms로 큰 변동이 없었지만, Hikari 커넥션 점유 표준편차가 5.1ms → 2.1ms로 줄었다. 
절대값보다 분산 안정성이 핵심이었던 사례이다.</p>
<br>

<h3 id="3-분산-락-레벨-다중-서버">3. 분산 락 레벨 (다중 서버)</h3>
<p>서버가 여러 대로 늘어나면 JVM 레벨 락은 의미가 없다.
각 서버의 <code>synchronized</code>는 서로를 모른다.</p>
<p><strong>Redis (Redisson)</strong></p>
<pre><code class="language-java">RLock lock = redissonClient.getLock(&quot;stock:lock:&quot; + productId);
boolean acquired = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!acquired) throw new IllegalStateException(&quot;락 획득 실패&quot;);
try {
    // 임계 구역
} finally {
    lock.unlock();
}</code></pre>
<p>Redisson의 <code>RLock</code>은 내부적으로 두 가지를 함께 사용한다.</p>
<p>Lua 스크립트로 &quot;키 확인 → 세팅 → TTL 설정&quot; 세 단계를 원자적으로 묶는다. Redis는 Lua 스크립트를 실행하는 동안 다른 명령을 일절 받지 않기 때문에 중간에 끼어드는 것이 불가능하다.</p>
<p>락을 못 얻은 스레드는 Pub/Sub 채널을 구독하고 대기한다. 락이 해제되면 Redis가 해당 채널에 알림을 보내고, 대기 중이던 스레드가 그때 재시도한다. 계속 폴링하지 않으니 Redis 부하가 낮다.</p>
<p>냉장GOAT에서 직접 적용한 방식이기도 하다.
<br>
주의할 점은 Redis 자체가 단일 장애점(SPOF)이 된다는 것.
이를 보완하기 위한 방법으로 <strong>Redlock</strong> 알고리즘이 있다. Redis 노드를 홀수 개(보통 5개)로 독립적으로 운영하고, 과반수(3개 이상)에서 락 획득에 성공하면 유효한 락으로 인정하는 방식이다. 노드 하나가 죽어도 나머지로 과반수를 충족할 수 있다.</p>
<p>다만 Redlock의 안전성에 대한 논쟁이 있다. Kleppmann은 GC pause 중 TTL이 만료되면 두 클라이언트가 동시에 락을 가질 수 있다고 지적했고, Redis 창시자 antirez는 이는 분산 시스템 전반의 문제이지 Redlock만의 문제가 아니라고 반박했다. 완벽한 분산 락은 없다는 것이 현재까지의 결론이다.</p>
<p>냉장GOAT에서는 Resilience4j 서킷 브레이커로 Redis 장애 시 fallback을 처리했다.
<br><br><strong>MySQL Named Lock</strong></p>
<pre><code class="language-sql">SELECT GET_LOCK(&#39;order:lock:1&#39;, 3);
-- 임계 구역
SELECT RELEASE_LOCK(&#39;order:lock:1&#39;);</code></pre>
<p>별도 인프라 없이 DB만으로 분산 락을 구현할 수 있다.
단, Named Lock은 커넥션 단위로 락을 관리하기 때문에 트랜잭션용 커넥션과 락용 커넥션을 반드시 분리해야 한다. 결국 커넥션 풀을 두 배로 운영해야 하는 복잡도가 생긴다. Redis는 DB 커넥션과 완전히 독립된 시스템이라 이 문제가 없다. 프로덕션에서 Named Lock보다 Redis를 선택하는 이유다.</p>
<br>

<h3 id="4-아키텍처-레벨-queue">4. 아키텍처 레벨 (Queue)</h3>
<p>락 자체를 없애버리는 방식이다.</p>
<pre><code>요청 → Kafka → 단일 Consumer → 순차 처리</code></pre><p>모든 요청을 큐에 쌓고, 소비자 하나가 순서대로 처리하면 동시성 문제가 원천 차단된다.
대규모 트래픽에서 처리량과 정합성을 동시에 잡는 가장 강력한 방법이다.</p>
<p>단, Consumer가 1개면 처리량이 병목이 된다.
실전에서는 <strong>파티셔닝 + 파티션당 1 Consumer</strong> 구조로 처리량도 확보한다.</p>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>선택</th>
</tr>
</thead>
<tbody><tr>
<td>단일 서버 + 저빈도 충돌</td>
<td>Optimistic Lock</td>
</tr>
<tr>
<td>단일 서버 + 고빈도 충돌</td>
<td>Pessimistic Lock</td>
</tr>
<tr>
<td>다중 서버 + 실시간 정합성</td>
<td>Redis 분산 락</td>
</tr>
<tr>
<td>다중 서버 + 대용량 트래픽</td>
<td>Kafka 순차 처리</td>
</tr>
</tbody></table>
<br>

<p>레이스 컨디션 해결의 핵심은 <strong>&quot;어떤 락을 쓸지&quot;가 아니라 &quot;지금 어떤 상황인지를 먼저 정의하는 것&quot;</strong>이다.</p>
<p>충돌 빈도, 서버 수, 트래픽 규모.
이 세 가지에 따라 적절한 답이 달라진다.</p>
<p>2편에서는 낙관적/비관적 락의 트레이드오프를 더 깊게 파고, 냉장GOAT에 실제로 적용한 구조를 함께 다룰 예정이다.</p>
<hr>
<p>참고 : CAS(<a href="https://steady-coding.tistory.com/568#google_vignette">https://steady-coding.tistory.com/568#google_vignette</a>)
<br></p>
<p>위에서 언급한 ParkingMate 실측 과정</p>
<p>측정 방법 :
BookingConnectionTimingTest.java에서 직접 측정. 
@SpringBootTest + @ActiveProfiles(&quot;mysqltest&quot;)로 실제 MySQL(Docker, localhost:3307)에 연결하고, TransactionTemplate으로 트랜잭션을 직접 제어하면서 System.currentTimeMillis()로 커넥션 획득부터 반납까지의 시간을 10회 반복 측정.</p>
<p>Before(알림을 T1에 포함): [32, 37, 51, 37, 38, 34, 38, 36, 35, 39] → 평균 37ms, 표준편차 5.1ms
After(Outbox 분리): [36, 40, 34, 38, 38, 38, 34, 37, 36, 33] → 평균 36ms, 표준편차 2.1ms</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CJ OliveNetworks CloudWave 7기] 수료 후기]]></title>
            <link>https://velog.io/@gm-15/CJ-OliveNetworks-CloudWave-7%EA%B8%B0-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@gm-15/CJ-OliveNetworks-CloudWave-7%EA%B8%B0-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sat, 21 Mar 2026 18:16:37 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>인프라 이해에 대한 필요성을 느껴
네트워크, 도커, 쿠버네티스, 클라우드 등을 학습하고 
대규모 트래픽 실무형 프로젝트를 진행하며 
팀워크와 엔지니어적 사고를 배울 수 있었던 CloudWave 수료 후기.</p>
</blockquote>
<h2 id="why">why?</h2>
<p>이전 프로젝트(INSK_V3 배포 등)를 겪으며
&quot;서버가 죽으면 내 백엔드 코드는 동작할 수 없다&quot;는 것을 알 수 있었다.</p>
<p>배포하는 과정에서 익숙치 않은 AWS, CI/CD 등을 시도하며 많은 어려움을 겪었지만, 동시에 내가 만든 코드를 클라우드에 띄워 실제 운영을 할 수 있다는 것이 신선한 충격으로 다가왔다.</p>
<p>코더의 역할이 점점 줄어가는 현 추세에
단순 코딩을 넘어서 SDLC를 이해하고 풀 사이클을 통제할 수 있는 능력이 중요하다고 느꼈고,
운영과 유지보수 더 나아가 <strong>&#39;인프라를 통제할 줄 아는 백엔드 엔지니어&#39;</strong>가 되고 싶었다. </p>
<br>
앞선 프로젝트로 인프라에 대한 지식이 매우 부족하다는 것을 느꼈기 때문에 체계적인 학습이 필요했다.
적절한 학습 방법을 찾던 도중 SNS에서 CloudWave 모집에 관한 글을 볼 수 있었다.

<p><img src="https://velog.velcdn.com/images/gm-15/post/231f2e72-1746-430e-a0e5-55cdd1447628/image.png" alt=""></p>
<p>대기업에서 여러 해 진행했던 부트캠프라 체계적인 과정 및 지원이 확실했다.
심지어 교육혜택 중 CJ올리브네트웍스 서류 가점, CJ올리브네트웍스 현직자 특강 및 멘토링 지원이 포함되어 있어 추후 취업 준비에도 큰 도움이 될 것 같았다.</p>
<p>사실 제일 맘에 들었던 건 AWS 기반 프로젝트 진행과 실무형 프로젝트를 통한 취업 포트폴리오 확보였다. 
비용 걱정 없이 다양한 AWS 서비스들을 사용해보고 이를 활용해 실무형 프로젝트를 하게 된다면 인프라에 대한 전반적인 이해는 물론이고 기존에 목표했던 백엔드 엔지니어적 역량을 채울 수 있을 것 같았다.</p>
<hr>
<h2 id="지원-과정">지원 과정</h2>
<p>지원 절차는 다음과 같았다. 자세히 언급한 후기들이 많기 때문에 가볍게 넘어가겠다.</p>
<ul>
<li>지원서 접수 </li>
<li>[1차] 테스트 </li>
<li>[2차] 면접</li>
</ul>
<p>면접을 대비하여
AWS 배포 과정에서 <strong>502 에러와 DB 연결 실패, GitHub Actions의 레이스 컨디션 문제</strong>를 
<strong>Nginx 로그 분석과 RDS 보안 그룹 설정, CI/CD 파이프라인 개선</strong>으로 해결한 경험을 준비해갔다.</p>
<p>전체적으로 전문적인 역량보다 이 활동에 참여할 열정과 의지를 더 중요하게 봤던 것 같다.
면접에서도 인천 송도에서 이뤄지는 대면 과정인데 집이 먼 경우 어떻게 통학할지를 물어보셨다.
(나를 비롯한 여러 수강생들은 근처에서 자취를 하며 수강했다.)</p>
<hr>
<h2 id="활동">활동</h2>
<blockquote>
<p>주관 : CJ OliveNetworks
활동명 : 클라우드 웨이브(CLOUD WAVE) 시즌 7
기간 : 25.12.15~26.02.27</p>
</blockquote>
<p>활동은 크게 3가지로 나누어졌다.</p>
<ol>
<li>교과 과정 교육</li>
<li>현직자 특강 및 멘토링 </li>
<li>실무 프로젝트 및 발표</li>
</ol>
<br>

<h3 id="1-교과-과정-교육기본-80h-심화-192h">1. 교과 과정 교육(기본 80H, 심화 192H)</h3>
<p>교과 과정은 약 7주 간의 과정으로 구성됐다.</p>
<p>1주차 : 네트워크 
2주차 : 리눅스
3주차 : 도커
4주차 : 쿠버네티스
5주차 : 퍼블릭 클라우드
6주차 : Ansible
7주차 : Terraform</p>
<p>짧은 기간동안 많은 과정들을 이해하고 습득하기 위해 정말 열심히 공부했다.
하루 8시간 동안 진행되는 교육이 쉽진 않았지만 힘든만큼 얻을 것들이 많아보여 철저히 학습했다.</p>
<p>1~2 과목마다 다른 강사님(전문 강사, 타 기업 현직자)분들이 오셔서 강의를 해주셨다.
이론학습과 실습을 통한 밀도 높은 교육에 지식적으로도 도움이 되었지만, 교과목이 아닌 <strong>실무 팁, 사고체계</strong> 또한 큰 도움이 되었다.</p>
<p>개인적으로는, A가 B보다 더 최신 버전이고 좋은 기술이더라도 무조건 A를 선택하지는 않는다는 것에 놀랐다. <strong>회사에 맞는 기술이 B라면 B를 선택해야 한다.</strong> 회사를 다니지 않아 이런 관점은 생각해보지 못했는데 실무에 계신 현직자분들이 강의를 해주셔서 이런 관점도 얻을 수 있어 유익했다.
(그 외에도 
항상 <strong>대체제</strong>, <strong>Tradeoff를</strong> 생각하기
장애가 났을 때, 어떤 일이 벌어지고 어떤 방안이 있을지를 계속 생각해기 등등)</p>
<p>이론적인 부분과 더 생각해보면 좋을 사고들을 분리해서 Notion에 전부 기록했고 예습, 복습에 유용하게 사용했다. 과정이 종료된 이후에도 한 번씩 복습하며 잊지 않으려고 노력했다.
<img src="https://velog.velcdn.com/images/gm-15/post/bcff541f-9fdd-481c-b614-f93c5d403180/image.png" alt=""></p>
<p>중간중간 쉬는 시간에는 밖으로 나가 클라우드 센터와 주변 건물들을 보며 바람을 쐬곤 했다.
찬 바람에 기분까지 환기되어 다음 수업에 집중할 수 있었다!
<img src="https://velog.velcdn.com/images/gm-15/post/66d18185-7cc3-43e0-8512-13a8789b3646/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/a82ae952-09f9-4c66-95ad-677ae9f3a9cf/image.png" alt=""></p>
<p>또, 송도IDC 센터 견학을 할 기회가 있었다. 실제 <strong>대규모 인프라의 물리적 계층과 철저한 보안 시스템</strong>을 직접 확인할 수 있어 재밌었다. 특히 서버 랙과 쿨링 설비를 실물로 접할 수 있었던 게 인상 깊다. 하드웨어 가용성과 인프라 관리의 중요성에 CJ OliveNetworks가 얼마만큼 투자하는 지 알 수 있었다.
<img src="https://velog.velcdn.com/images/gm-15/post/30f3f099-3f12-47a8-b6fe-207ffab6754c/image.png" alt=""></p>
<br>

<h3 id="2-현직자-특강-및-멘토링">2. 현직자 특강 및 멘토링</h3>
<p>특강은 기대했던 것보다 훨씬 많이 이루어졌다. (거의 20회정도)
CJ OliveNetworks 현직자분들부터 외부 클라우드, 개발 현직자분들까지 다양한 분야의 전문가분들이 오셔서 강의를 해주셨다. <strong>현직 업무와 트렌드, 그리고 신입으로서 중요한 자세</strong>들을 배울 수 있어 유익했다. 최근 몇 년 사이에 취업하신 강사님도 계셔서 클라우드/백엔드 분야 취업에 관련된 팁들도 받을 수 있어 좋았다. 
<br></p>
<h3 id="3-실무-프로젝트-및-발표">3. 실무 프로젝트 및 발표</h3>
<p>프로젝트는 약 3주간 진행되었다. 
한 조에 6명씩 배정되었고, 현직 멘토님 한 분과 매칭이 되어 올리브영과 CGV 중 한 기업의 문제 상황과 관련된 프로젝트를 진행해야 했다.</p>
<p>교육받을 때부터 느낀 것이 있었는데, Cloud Wave에 모인 수강생들의 열의가 대단했다. 
대부분의 외부 프로젝트들에선 시간이 안 맞는다거나, 개인적으로 할 일이 있다거나, 갖가지 이유로 연락이 안되는 팀원들이 많았다. 하지만 7주간 하루 8시간 동안 같이 공부하며 버틴 수강생들이 모여서 그런지 설 명절이 중간에 끼어있었음에도 팀 프로젝트에서의 단합이 아주 잘 됐다.</p>
<p>특히 우리 팀은 수 차례 Cloud Wave를 맡으신 강사님과 CJ임원이신 심사위원분들께도 <strong>팀워크적으로 큰 칭찬</strong>을 받은 팀이었다.
(팀워크와 프로젝트에 관한 자세한 내용은 추후 포스팅 예정)
<img src="https://velog.velcdn.com/images/gm-15/post/f922db9f-e6df-48c4-ae87-400425feaeba/image.png" alt=""></p>
<br>

<p>우리 팀은 &quot;<strong>올영세일 대응 AWS 인프라 구축</strong>&quot;을 주제로 삼았다. 시스템 신뢰성, 보안, AI를 차별점으로 설계하고 15만 동시 접속 환경에서의 부하 테스트를 통해 대규모 트래픽 감당을 검증해냈다.</p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/de192ef9-b54c-4fab-a24b-e92b759903c6/image.png" alt="">
<img src="https://velog.velcdn.com/images/gm-15/post/965e1933-41aa-446f-be45-3f9543b861cb/image.png" alt=""></p>
<hr>
<h2 id="마치며">마치며</h2>
<p>10주간의 교육 기간이 정말 쏜살같이 지나갔다.</p>
<p>솔직히 말하면, 첫 주 수업을 마치고 남은 9주간의 일정이 정말 막막했었다.
알고 있다고 생각했던 네트워크도 낯설었고, 처음보는 쿠버네티스와 IaC 도구들은 더 낯설었다. 모니터만 바라보며 10주동안 모든 것을 쏟아내는 경험은 정말 흔치 않은 경험이었다.</p>
<p>그런데 프로젝트를 비롯한 모든 활동이 끝나고 나서 다시 돌아보니, 처음엔 막막했던 개념들과 버그들이 이제는 이해가 되고 말로 설명할 수 있다는 것을 알게 되어 정말 뿌듯했다.</p>
<p>Kafka 브로커가 죽었을 때 데이터가 유실되지 않으려면 어떻게 설계해야 하는지, 15만 명이 동시에 몰렸을 때 서버가 버티려면 Pod이 몇 초 안에 몇 개 떠야 하는지, Aurora 연결이 왜 갑자기 터졌는지를 직접 부딪히며 알게 됐다. 코드만으로는 알 수 없는 <strong>인프라 레벨에서 장애를 막는 경험이었다.</strong></p>
<p>같이 밤새 Slack하고, 의도대로 작동하지 않아 에러 로그를 들여다보고 Pod와 Node를 체크하며 모니터링 수치가 잘 나오는지 확인하던 팀원들 덕분에 힘들다는 것도 잘 느끼지 못한채 즐겁게 프로젝트에 열중할 수 있었던 것 같다. 
<br>
이 과정을 통해 얻고 싶던 목표가 
풀 사이클 통제를 위해 운영과 유지보수 그리고 더 나아가 &#39;<strong>인프라를 통제할 줄 아는 백엔드 엔지니어</strong>&#39;가 되자는 것이었다. </p>
<p>수료를 하고 난 지금, 나는 그 방향으로 한 걸음 왔다는 것을 분명히 느낄 수 있다. 
<br></p>
<p>팀워크를 비롯해, 15만 VU를 감당했던 인프라 설계와 그 과정에서 마주친 트러블슈팅들은 다음 포스팅에서 구체적으로 풀어볼 예정이다. 수치도 있고, 실패도 있고, 그걸 고치는 과정도 있다. </p>
<p>긴 글 읽어 주신 분들께 감사하다는 말씀 드리고 싶다.😊
이어지는 글은 아래 첨부하겠다.</p>
<br>

<p>1편 - <a href="https://velog.io/@gm-15/15%EB%A7%8C-VU%EB%A5%BC-%EB%B2%84%ED%8B%B0%EA%B8%B0%EA%B9%8C%EC%A7%80-%EB%82%B4%EA%B0%80-%EC%84%B8-%EB%B2%88-%ED%8B%80%EB%A0%B8%EB%8D%98-%EC%9D%B4%EC%9C%A0">스케일아웃하면 나아질 줄 알았다 - 15만 VU 부하 테스트에서 깨진 3가지 직관
</a>2편 - <a href="https://velog.io/@gm-15/%EB%B9%8C%EB%93%9C%EB%8A%94-%EC%84%B1%EA%B3%B5-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%8A%94-%EC%8B%A4%ED%8C%A8-%EB%B0%B0%ED%8F%AC%EC%97%90%EC%84%9C-%EB%A7%8C%EB%82%9C-3%EA%B0%80%EC%A7%80-%ED%95%A8%EC%A0%95">빌드는 성공, 서비스는 실패 - 배포에서 만난 3가지 함정
</a>3편 - <a href="https://velog.io/@gm-15/%EC%99%9C-Kafka%EC%98%80%EC%9D%84%EA%B9%8C-%EC%9E%A5%EC%95%A0%EB%A5%BC-%EA%B2%AC%EB%94%94%EB%8A%94-%EA%B5%AC%EC%A1%B0%EB%A5%BC-%EA%B3%A0%EB%AF%BC%ED%95%9C-3%EA%B0%80%EC%A7%80-%EC%A7%88%EB%AC%B8">왜 Kafka였을까? 장애를 견디는 구조를 고민한 3가지 질문
</a>4편 - <a href="https://velog.io/@gm-15/%EC%97%AD%ED%95%A0%EC%9D%80-%EB%82%98%EB%88%B4%EC%A7%80%EB%A7%8C-%EA%B8%B0%EC%A4%80%EC%9D%80-%EB%82%98%EB%88%84%EC%A7%80-%EC%95%8A%EC%95%98%EB%8B%A4-%EB%AC%B4%EC%9E%91%EC%9C%84%EB%A1%9C-%EB%AA%A8%EC%9D%B8-6%EB%AA%85%EC%9D%B4-%ED%95%9C-%EB%8B%AC%EB%A7%8C%EC%97%90-%EB%A7%8C%EB%93%A0-Clmakase">역할은 나눴지만 기준은 나누지 않았다 - 무작위로 모인 6명이 한 달만에 만든 Clmakase</a></p>
<p>-Clmakase 깃허브 링크
<a href="https://github.com/gm-15/Clmakase">https://github.com/gm-15/Clmakase</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[나만의 Claude Code 사용법]]></title>
            <link>https://velog.io/@gm-15/Claude-Code-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@gm-15/Claude-Code-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Thu, 19 Mar 2026 08:00:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프로젝트 개발 중 Claude 계정 3개를 돌려쓰다가, 
토큰이 아니라 내 사용 방법에 문제가 있지 않을까 의문을 가지게 되었고,
갖가지 방법들을 시도하여 성공적으로 Claude를 다루게 된 과정에 대한 기록.</p>
</blockquote>
<hr>
<h2 id="claude-code">Claude Code?</h2>
<p>여러 프로젝트를 다루며 LLM을 통해 개발을 하게 되었다.
처음엔 무료 사용량이 어느 정도 제공되고 범용성이 좋은 ChatGPT, 대학생 전용 1년 무료인 Gemini를 통해 개발을 시작했다.</p>
<p><strong>간단한 규모</strong>에서는 코드 짜달라고 하면 짜주고, 에러를 붙여넣으면 고쳐주는 ChatGPT, Gemini가 편리했다.</p>
<p>그런데 프로젝트 규모가 조금씩 커지게 되고, 파일이 여러 개로 쪼개지기 시작하며 
&quot;파일 전체 복사해서 붙여넣기 → 수정 요청 → 다시 복사해서 내 파일에 덮어쓰기&quot;를 반복하는 과정에서 <strong>개발의 비효율성</strong>을 느끼기 시작했다.
<br></p>
<p>그러다 코딩에 좋다고 소문이 난 Cursor를 접하게 되었다. 
그리고 그 결과는 충격적이었다. 
몇 주 동안 질질 끌던 작업들을 체험판 <strong>단 3일</strong> 만에 끝낼 수 있었다.
IDE에 LLM이 통합되어, 파일 복붙 없이 코드베이스 전체를 이해한 채로 작업이 가능하다는 게 얼마나 효율적인지 느낀 뒤로 LLM이 통합된 개발 도구에 더 관심이 생겼다.</p>
<br>
본격적으로 CLI 기반 LLM을 사용하고 싶었다.
구독에는 돈이 들기 때문에 내 돈을 합당하게 가져갈 이 분야 최고의 도구를 찾아나섰다. 
그리고 코딩 분야에서는 Claude Code가 최고라는 소문을 들을 수 있었다.
문득 Cursor와의 차이가 무엇인지 궁금해졌다.

<table>
<thead>
<tr>
<th>항목</th>
<th>Cursor</th>
<th>Claude Code</th>
</tr>
</thead>
<tbody><tr>
<td>실행 환경</td>
<td>VS Code 기반 IDE</td>
<td>터미널 (CLI)</td>
</tr>
<tr>
<td>편집기 종속</td>
<td>O (VS Code 전용)</td>
<td>X (편집기 무관)</td>
</tr>
<tr>
<td>컨텍스트 제어</td>
<td>자동 (불투명)</td>
<td>수동 (직접 지정)</td>
</tr>
<tr>
<td>프로젝트 지시 관리</td>
<td><code>.cursorrules</code></td>
<td><code>CLAUDE.md</code></td>
</tr>
<tr>
<td>서브에이전트</td>
<td>미지원</td>
<td>지원</td>
</tr>
<tr>
<td>외부 도구 연결</td>
<td>제한적</td>
<td>MCP 프로토콜</td>
</tr>
<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>
<p>정리하자면,
Cursor는 IDE에 종속되는 구조였다. 
VS Code 기반이라 다른 편집기와 함께 쓰기 불편했다. 
(자바 기반의 프로젝트를 할 때, IntelliJ를 사용했기 때문에 두 앱을 번갈아 사용해야 했다.)</p>
<p>반면,
Claude Code는 터미널에서 돌아가니까 편집기를 가리지 않았고, 내가 컨텍스트에 무엇을 넣을지 직접 결정할 수 있었다.
_(물론 더 편리한 부분과 차이점이 있었지만 이미 Claude를 글쓰기나 코딩에 이용하며 신뢰를 가진 상태였기 때문에 가볍게 살펴본 뒤 막연하게 Claude Code 또한 좋겠다고 생각하고 넘어갔다.) _</p>
<hr>
<h2 id="토큰-문제">토큰 문제</h2>
<p>그렇게 Claude Code를 쓰기 시작했고, 개발 속도는 확실히 올라갔다.
마침 CJ 올리브네트웍스 부트캠프인 CloudWave에서 프로젝트를 시작하게 되어서 개발에 유용하게 사용할 수 있었다.</p>
<p>그런데 문제가 생겼다. 
<strong>토큰이 내 예상보다 빠르게 닳았다.</strong>
처음엔 &quot;프로젝트를 시작하면서 처음부터 제작하기 때문에 단순히 많이 드나보다&quot; 하고 넘겼는데, 한도 초과가 반복되고, 토큰 재사용 시간을 기다리는게 일상이 되었다.</p>
<p>프로젝트 마감이 가까워지면서는, 다른 작업을 하고 있는 팀원의 계정 2개를 빌려 총 3개의 계정으로 토큰을 한계까지 돌려써가면서 작업했다.
A 계정 한도 차면 B로, B 차면 C로. 당시엔 그게 최선인줄로 알았다.</p>
<p>토큰이 너무 적다고 불평하던 와중, 문득 이런 생각이 들었다.
<strong>토큰이 문제가 아니라, 내 사용 방법이 문제 아닐까?</strong></p>
<hr>
<h2 id="원인부터-찾았다">원인부터 찾았다</h2>
<p>프로젝트 마감까지 시간은 촉박했기에, 막연하게 &quot;덜 써야겠다&quot;는 결론은 내릴 수 없었다. 토큰이 왜 이렇게 빨리 닳는지 원인을 먼저 파악해야 했다.</p>
<p>그리고 몇 가지 원인을 찾을 수 있었다.</p>
<p><strong>첫째, 긴 대화가 누적되면 이전 대화 전체가 매 요청마다 컨텍스트로 들어간다.</strong> 
채팅창을 닫지 않고 계속 이어가면, Claude는 처음부터 지금까지의 모든 대화를 매번 읽고 답한다. 대화가 길어질수록 요청 한 번에 소모되는 토큰이 기하급수적으로 늘어난다.</p>
<p><strong>둘째, CLAUDE.md에 수백 줄짜리 프롬프트를 때려박으면 매 세션마다 전부 로드된다.</strong> 
정성껏 작성한 지시사항이 오히려 독이 된다. 쓸 때마다 전부 토큰으로 소모되는 구조였다.</p>
<p><strong>셋째, 계획 없이 구현을 요청하면 수정 반복이 발생한다.</strong> 
&quot;이렇게 만들어줘 → 아 이건 아닌데 → 저렇게 바꿔줘 → 다시&quot;를 반복하면 토큰이 2~3배로 낭비된다.</p>
<p><strong>넷째, 필요 없는 파일까지 컨텍스트에 포함된다.</strong> 
관련 없는 파일을 통째로 넘기면 Claude는 그걸 전부 읽는다.</p>
<p>토큰을 많이 쓴 게 아니라, 비효율적으로 쓰고 있었던 거였다.</p>
<hr>
<h2 id="claude-code는-이미-에이전트-프레임워크다">Claude Code는 이미 에이전트 프레임워크다</h2>
<p>원인을 파악하고 나서 해결책을 찾는 과정에서 중요한 사실을 하나 깨달았다.</p>
<p>많은 사람들이 Claude Code 위에 또 다른 제어 로직을 얹으려 한다. 복잡한 프롬프트 체인을 만들거나, 외부 스크립트로 동작을 제어하거나. 그런데 이건 이중 레이어 구조로 오히려 비효율적이다. <strong>Claude Code 자체가 이미 에이전트 프레임워크로 설계되어 있기 때문이다.</strong></p>
<p>네이티브 기능만 제대로 써도 충분하다.</p>
<table>
<thead>
<tr>
<th>기능</th>
<th>설명</th>
<th>토큰 절약 효과</th>
</tr>
</thead>
<tbody><tr>
<td>Progressive Disclosure</td>
<td>필요한 스킬만 그때그때 로드</td>
<td>불필요한 컨텍스트 차단</td>
</tr>
<tr>
<td>Context Forking</td>
<td>서브에이전트 실행 시 컨텍스트 격리</td>
<td>메인 컨텍스트 오염 방지</td>
</tr>
<tr>
<td>네이티브 구조</td>
<td><code>.claude/agents·skills·rules/</code></td>
<td>Anthropic 업데이트 즉시 수혜</td>
</tr>
</tbody></table>
<p>커스텀 레이어를 쌓는 대신, 이 구조를 따르는 것만으로도 강력한 에이전트 운영이 가능하다.</p>
<hr>
<h2 id="토큰을-아끼는-워크플로우">토큰을 아끼는 워크플로우</h2>
<p>그리고 핵심적인 관점 전환이 있었다.</p>
<blockquote>
<p>&quot;코드를 잘 쓰게 만드는 것이 아니라, 코드를 쓰기 전에 무엇을 써야 하는지를 확실하게 만드는 것&quot;</p>
</blockquote>
<p>토큰 낭비의 근본 원인은 잘못된 방향으로 구현한 뒤 수정을 반복하는 것이다. 이 사이클을 끊으면 토큰은 자연히 줄어든다. 그래서 5단계 워크플로우를 정착시켰다.</p>
<p><strong>Step 1 - Research</strong></p>
<pre><code>이 [폴더]를 깊이 읽고 세부 사항 파악 후 research.md에 작성해라</code></pre><p>채팅으로 주고받지 않고 파일로 저장한다. 컨텍스트가 오염되지 않는다. &quot;깊이&quot;, &quot;세부 사항&quot; 키워드가 없으면 Claude는 고수준 요약으로 끝낸다. 사전 리서치가 잘못된 구현으로 인한 수정 반복 사이클을 차단한다.</p>
<p><strong>Step 2 - Plan</strong></p>
<pre><code>research.md 바탕으로 plan.md에 구현 계획 작성해라. 아직 구현하지 마.</code></pre><p>plan.md에는 접근 방식, 코드 스니펫, 파일 경로, 트레이드오프, 체크리스트가 들어간다. Claude Code 내장 플랜 모드 대신 MD 파일을 쓰는 이유는 에디터에서 직접 편집할 수 있기 때문이다. 의사결정을 이 단계에서 완료하면 구현 중 방향 전환이 없다.</p>
<p><strong>Step 3 - Review</strong></p>
<p>plan.md를 에디터에서 열고 직접 주석을 단다.</p>
<pre><code class="language-markdown">&lt;!-- PUT이 아니라 PATCH여야 함 --&gt;
&lt;!-- 캐싱 불필요, 제거 --&gt;
&lt;!-- 이 함수 시그니처 변경 불가 --&gt;</code></pre>
<p>피드백 후에는 반드시 이렇게 요청한다.</p>
<pre><code>메모 반영해서 업데이트해라. 아직 구현하지 마.</code></pre><p><strong>&quot;아직 구현하지 마&quot;는 필수 문구다.</strong> 없으면 Claude는 즉시 구현을 시작한다.</p>
<p><strong>Step 4 - Implement</strong></p>
<pre><code>plan.md대로 전부 구현해라.
완료 시 체크리스트 체크. 매 단계 타입 체크 실행.</code></pre><p>이 단계에서 개발자의 역할은 설계자(Architect)에서 감독자(Supervisor)로 바뀐다. 이미 확정된 계획을 실행하는 것뿐이라 토큰 낭비 없는 직선 경로로 끝난다.</p>
<p><strong>Step 5 - Refine</strong></p>
<p>UI 오차가 있으면 말 대신 스크린샷을 넘긴다. 말로 설명하다 생기는 오해를 없애준다. 방향이 완전히 틀어졌다 싶으면 <code>git reset</code> 후 재시작한다. 반복되어 덧칠되는 수정을 반복하는 것보다 토큰도 결과물 퀄리티도 낫다.</p>
<hr>
<h2 id="스킬-시스템으로-반복을-없앴다">스킬 시스템으로 반복을 없앴다</h2>
<p>워크플로우를 정착시키고 나서 한 가지가 더 거슬렸다. 반복되는 작업마다 같은 설명을 매번 해야 한다는 것.</p>
<p>그래서 스킬 시스템을 구축했다.</p>
<p>스킬은 특정 상황에서만 로드되는 전문 매뉴얼이다. 
평소엔 컨텍스트에 없다가, 관련 요청이 들어올 때만 동적으로 로드된다.</p>
<pre><code>평소          → 메모리에 없음 (토큰 0 소비)
관련 요청 감지 → description 키워드 매칭 → 자동 로드</code></pre><p>직접 구축한 구조는 이렇다.</p>
<pre><code>~/.claude/skills/skill-creator/
├── SKILL.md              ← 트리거 + 핵심 요약
└── references/
    └── skill_creator.md  ← 상세 가이드 (필요 시만 로드)</code></pre><p>&quot;스킬 만들어줘&quot;, &quot;스킬 검토해줘&quot;, &quot;스킬이 안 불러와져&quot; 같은 말이 나오면 자동으로 로드된다. 
반복 작업 패턴을 스킬로 등록하면 매번 설명할 필요가 없어진다.</p>
<p><strong>스킬은 <a href="https://www.linkedin.com/posts/keunyoung0924_claude-ai-ugcPost-7417740403829002241-k04O?utm_source=social_share_send&amp;utm_medium=member_desktop_web&amp;rcm=ACoAAFc-SPIBnIGMnQDG__SHmZdpp0MeL6juZtY">해당 게시글</a>을 참고하였습니다.</strong></p>
<hr>
<h2 id="최종-셋업">최종 셋업</h2>
<p>결국 정착한 파일 구조는 이렇다.</p>
<pre><code>~/.claude/
├── rules/
│   └── guidelines.md          ← 매 세션 자동 로드 (2.7KB, 61줄)
└── skills/
    └── skill-creator/
        ├── SKILL.md
        └── references/
            └── skill_creator.md

프로젝트별 작업 파일:
├── research.md                ← 코드베이스 분석 결과
├── plan.md                    ← 구현 계획 + 체크리스트
└── skill_creator.md           ← 스킬 제작 상세 전략</code></pre><p><code>guidelines.md</code>는 매 세션마다 자동으로 로드되어 프롬프트 품질을 실시간으로 진단한다.</p>
<table>
<thead>
<tr>
<th>감지 패턴</th>
<th>문제</th>
<th>대응</th>
</tr>
</thead>
<tbody><tr>
<td>계획 없이 구현 요청</td>
<td>잘못된 방향 위험</td>
<td>Step 1~3 먼저 제안</td>
</tr>
<tr>
<td>&quot;더 좋게 해줘&quot;</td>
<td>기준 없음</td>
<td>구체적 기준 요구</td>
</tr>
<tr>
<td>범위 과대 요청</td>
<td>누락·오류</td>
<td>분리 제안</td>
</tr>
<tr>
<td>CLAUDE.md 긴 프롬프트</td>
<td>토큰 낭비</td>
<td>네이티브 구조 유도</td>
</tr>
</tbody></table>
<hr>
<h2 id="결론">결론</h2>
<p>위 과정을 거친  뒤, 계정 3개로 돌려써야 겨우 가능했던 작업들을 계정 1개로 감당할 수 있었다. 
또, plan.md 파일을 만들고 구현하기 시작하니까 구현 후 수정 반복이 거의 사라졌다. 같은 작업에 드는 대화 턴 수가 눈에 띄게 줄은 것을 체감할 수 있었다.</p>
<br>

<p>결과적으로, 3가지로 정리할 수 있었다.</p>
<p><strong>확실한 구조가 토큰을 아끼는 방법이다.</strong> 
잘못된 구현 후 수정 반복이 토큰의 가장 큰 낭비다. Research → Plan → Review가 이 사이클을 차단한다.</p>
<p><strong>네이티브를 이기려 하지 마라.</strong> 
Claude Code는 이미 프레임워크다. 그 위에 중복 레이어를 쌓는 건 역효과다. 공식 구조를 따를수록 Anthropic 업데이트 혜택을 즉시 받는다.</p>
<p><strong>방법론이 도구보다 오래간다.</strong> 
모델이 바뀌어도 &quot;계획 → 검증 → 실행&quot;의 구조는 어떤 LLM에도 그대로 적용된다. GPT-5가 나오든 Gemini Ultra가 나오든 상관없다.</p>
<br>

<p>이번 과정을 통해
<strong>도구를 탓하기 전에 도구를 어떻게 쓰고 있는지를 먼저 봐야 한다</strong>는 것을 깨달았다.</p>
<p>좋은 개발자는 코드를 많이 쓰는 사람이 아니라, 잘못된 코드를 쓰지 않도록 사고하는 사람이라고 생각한다.
나는 도구를 소비하는 개발자가 아니라, 문제와 구조를 설계하는 엔지니어가 되기 위해 노력할 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[INSK 트러블슈팅편]]></title>
            <link>https://velog.io/@gm-15/INSK-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85%ED%8E%B8</link>
            <guid>https://velog.io/@gm-15/INSK-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85%ED%8E%B8</guid>
            <pubDate>Sun, 18 Jan 2026 06:04:21 GMT</pubDate>
            <description><![CDATA[<h2 id="운영-환경에서-처음으로-전제가-깨지기-시작했다">운영 환경에서 처음으로 전제가 깨지기 시작했다</h2>
<p>이 문서는 INSK를 운영 환경에 올리면서 실제로 겪은 문제들을 정리한 기록이다.
해결 방법을 정리하기보다는, <strong>왜 이런 문제가 발생했는지</strong>,
그리고 <strong>이 문제가 어떤 설계 전제 위에서 발생했는지</strong>를 남기는 것이 목적이다.</p>
<hr>
<h2 id="1-배포는-성공했지만-서비스는-열리지-않았다-502-bad-gateway">1. 배포는 성공했지만 서비스는 열리지 않았다 (502 Bad Gateway)</h2>
<pre><code>- 상황
  - CI/CD 파이프라인은 정상 종료
  - 브라우저 접근 시 `Nginx 502 Bad Gateway` 응답
  - 서버는 살아 있었지만 API는 응답하지 않았다

- 최초 판단
  - 애플리케이션 포트 매핑 오류
  - Spring Boot 프로세스 비정상 종료 가능성

- 확인 과정
  - Nginx access / error 로그 확인
  - EC2 내 애플리케이션 프로세스 상태 확인
  - Spring Boot 기동 로그 확인

- 실제 원인
  - Spring Boot가 기동 단계에서 DB 연결 실패
  - RDS 보안 그룹 인바운드에 EC2 보안 그룹이 허용되지 않음

- 해결
  - RDS 보안 그룹에 EC2 보안 그룹 ID 추가
  - DB 연결 정상화 후 애플리케이션 기동 확인

- 설계상 문제
  - 애플리케이션 오류로 단정하고 네트워크 계층을 후순위로 판단
  - “코드가 문제일 가능성이 가장 크다”는 개발자 편향이 작동</code></pre><hr>
<h2 id="2-배치-작업이-조용히-멈춰-있었다-scheduled">2. 배치 작업이 조용히 멈춰 있었다 (@Scheduled)</h2>
<pre><code>- 상황
  - 서비스는 정상 동작
  - 하지만 특정 날짜 이후 뉴스 데이터가 수집되지 않음
  - 모니터링이나 알림은 없었음

- 최초 판단
  - 외부 뉴스 API 응답 지연
  - 데이터 수집 대상 부족

- 확인 과정
  - 배치 로그 직접 확인
  - 특정 스케줄 작업 이후 로그가 더 이상 남지 않음

- 실제 원인
  - 배치 로직 중 예외 발생
  - 예외가 스케줄 스레드를 종료시켰고 이후 재실행되지 않음

- 해결
  - 예외 처리 보완
  - 실패 로그 추가

- 설계상 문제
  - 배치 작업이 항상 정상 동작한다는 전제
  - 실패를 정상 시나리오로 다루지 않음
  - 재시도, 상태 관리, 알림이 없는 구조</code></pre><hr>
<h2 id="3-캐시-적용-이후-데이터-정합성-문제-redis">3. 캐시 적용 이후 데이터 정합성 문제 (Redis)</h2>
<pre><code>- 상황
  - 기사 점수 수정 이후에도 화면에는 이전 점수가 노출됨
  - 새로고침을 반복해야 값이 갱신됨

- 최초 판단
  - 프론트엔드 캐시 문제
  - API 응답 캐싱 문제

- 확인 과정
  - Redis 키 확인
  - 캐시 TTL 확인
  - API 호출 시 캐시 히트 여부 확인

- 실제 원인
  - 캐시 무효화 기준이 API 요청 단위로만 설계됨
  - 배치 작업에서 변경된 데이터에 대한 캐시 무효화가 누락됨

- 해결
  - TTL 단축
  - 일부 키 수동 삭제

- 설계상 문제
  - TTL을 해결책으로 착각
  - 캐시를 성능 도구로만 인식하고 일관성 리스크를 고려하지 않음</code></pre><hr>
<h2 id="4-llm-호출-실패로-전체-파이프라인-중단">4. LLM 호출 실패로 전체 파이프라인 중단</h2>
<pre><code>- 상황
  - 일부 뉴스가 저장되지 않음
  - 특정 날짜 데이터가 통째로 누락

- 최초 판단
  - 뉴스 API 수집 문제
  - 데이터 파싱 오류

- 확인 과정
  - LLM 호출 로그 확인
  - OpenAI API 실패 응답 확인

- 실제 원인
  - 요약 / 분류 / 점수 계산을 하나의 흐름으로 처리
  - LLM 호출 실패 시 이후 로직 전체 중단

- 해결
  - 실패 로그 추가
  - 부분 결과 저장

- 설계상 문제
  - 외부 API를 내부 트랜잭션처럼 다룸
  - 실패를 예외 케이스로만 취급
  - 외부 의존성은 항상 실패할 수 있다는 전제 부재</code></pre><hr>
<h2 id="5-cicd-배포-이후-코드가-갱신되지-않은-문제">5. CI/CD 배포 이후 코드가 갱신되지 않은 문제</h2>
<pre><code>- 상황
  - GitHub Actions 성공
  - 하지만 실제 서비스는 이전 코드 상태

- 최초 판단
  - 배포 지연
  - 서버 캐시 문제

- 확인 과정
  - Docker 이미지 태그 확인
  - 배포 시점과 이미지 빌드 시점 비교

- 실제 원인
  - Docker 이미지에 latest 태그 사용
  - 이미지 Push / Pull 타이밍 간 레이스 컨디션 발생

- 해결
  - Git Commit Hash 기반 이미지 태그 사용
  - 배포 대상 이미지 명시

- 설계상 문제
  - CI/CD에서 불변성(Immutability) 개념 미적용
  - 배포 결과를 “신뢰”하고 검증하지 않음</code></pre><hr>
<h2 id="6-개발-관점-트러블슈팅에서-느낀-한계">6. 개발 관점 트러블슈팅에서 느낀 한계</h2>
<p>이 문제들의 공통점은 분명했다.</p>
<ul>
<li><p>대부분의 문제는 코드 품질 문제가 아니었다</p>
</li>
<li><p>설계 시 암묵적으로 깔아둔 전제가 깨진 결과였다</p>
</li>
<li><p>실패를 고려하지 않은 구조는, 운영 환경에서 반드시 흔들렸다</p>
</li>
</ul>
<p>이 시점에서 INSK는
“기능이 완성된 서비스”가 아니라
<strong>운영을 통해 설계 한계를 드러낸 상태</strong>에 가까웠다.</p>
<hr>
<h2 id="이-트러블슈팅편의-의미">이 트러블슈팅편의 의미</h2>
<p>이 문서는 문제 해결 자랑을 위한 기록이 아니다.
오히려 반대로, 내가 어떤 판단을 너무 쉽게 했는지를 드러내는 문서다.</p>
<p>그리고 이 기록들이 쌓이면서
다음 질문으로 자연스럽게 이어졌다.</p>
<blockquote>
<p>“이 문제들을 계속 땜질로 해결하는 게 맞는가,
아니면 구조 자체를 다시 봐야 하는가?”</p>
</blockquote>
<p>이 질문에 대한 답이
다음 INSK 개발편 3의 출발점이 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[INSK 배포편]]></title>
            <link>https://velog.io/@gm-15/INSK-%EB%B0%B0%ED%8F%AC%ED%8E%B8</link>
            <guid>https://velog.io/@gm-15/INSK-%EB%B0%B0%ED%8F%AC%ED%8E%B8</guid>
            <pubDate>Sun, 18 Jan 2026 05:56:51 GMT</pubDate>
            <description><![CDATA[<h3 id="처음으로-운영-환경을-전제로-코드를-올린-순간">“처음으로 운영 환경을 전제로 코드를 올린 순간”</h3>
<p>INSK를 배포하기로 결정했을 때, 목표는 명확했다.
“서비스처럼 보이는 결과물”이 아니라, 운영 환경에서 실제로 돌아가는 구조를 만드는 것이었다.</p>
<p>개발 단계에서는 로컬 환경에서 모든 기능이 정상 동작했다.
하지만 그 상태로는 이 프로젝트가 어디까지 갈 수 있는지 판단할 수 없었다.
그래서 비교적 이른 시점에 배포를 먼저 경험해보기로 했다.</p>
<hr>
<h3 id="배포-구조-요약">배포 구조 요약</h3>
<p>INSK의 배포 구조</p>
<pre><code>- Compute

     AWS EC2 단일 인스턴스

- Database

    AWS RDS (MySQL)

- Cache

    Redis

- Web / Reverse Proxy

    Nginx

- Application

    Spring Boot (API + 배치 작업)

- 배포 방식

    Docker 기반 컨테이너화

    GitHub Actions 기반 CI/CD</code></pre><p>배치 작업은 <code>@Scheduled</code> 기반으로 애플리케이션 내부에서 함께 실행되도록 구성했다.
API 서버와 배치 서버를 분리하지 않은 구조였다.</p>
<p>이 시점에서는 <strong>빠른 배포와 단순한 운영</strong>을 우선 기준으로 삼았다.</p>
<hr>
<h3 id="이-구조를-선택한-이유">이 구조를 선택한 이유</h3>
<p>당시의 판단 기준은 현실적이었다.</p>
<ul>
<li><p>하나의 인스턴스로도 충분한 트래픽 규모</p>
</li>
<li><p>비용을 통제할 수 있는 구조</p>
</li>
<li><p>배포 파이프라인을 직접 구성해보는 경험</p>
</li>
<li><p>API와 배치 로직을 한 흐름에서 관리</p>
</li>
</ul>
<p>특히 뉴스 수집·요약·분류 파이프라인을
“서비스 외부 작업”이 아니라 <strong>서비스 내부 책임</strong>으로 두고 싶었다.
그래서 배치 작업 역시 Spring 애플리케이션 안에 포함시켰다.</p>
<hr>
<h3 id="배포-구조에-깔려-있던-전제들">배포 구조에 깔려 있던 전제들</h3>
<p>배포를 마친 뒤에야
이 구조가 여러 전제 위에 서 있다는 걸 인식하게 됐다.</p>
<ul>
<li><p>서버는 쉽게 죽지 않는다</p>
</li>
<li><p>배치 작업은 대부분 정상 종료된다</p>
</li>
<li><p>LLM API 호출은 거의 실패하지 않는다</p>
</li>
<li><p>API 요청은 동시에 몰리지 않는다</p>
</li>
<li><p>Redis는 캐시일 뿐, 없어도 치명적이지 않다</p>
</li>
<li><p>배포 중 트래픽은 거의 없다</p>
</li>
</ul>
<p>이 전제들은 문서로 정리한 적도, 검증한 적도 없었다.
&#39;그럴 것이다&#39;라는 가정 위에서 구조를 쌓아 올린 것이었다.</p>
<hr>
<h3 id="배포-이후-처음-마주한-현실">배포 이후 처음 마주한 현실</h3>
<p>배포 자체는 비교적 수월하게 끝났다.
CI/CD 파이프라인도 정상적으로 동작했고, 서비스는 외부에서 접근 가능했다.</p>
<p>하지만 운영 환경에 올린 순간부터,
개발 환경에서는 느끼지 못했던 문제들이 하나씩 드러나기 시작했다.</p>
<ul>
<li><p>배포는 성공했지만 애플리케이션이 기동되지 않는 경우</p>
</li>
<li><p>배치 작업이 조용히 멈춰 있는 상태</p>
</li>
<li><p>캐시 적용 이후 데이터 정합성이 어긋나는 문제</p>
</li>
<li><p>외부 API 실패가 전체 흐름을 중단시키는 상황</p>
</li>
</ul>
<p>이 문제들은 대부분 코드 한 줄의 버그라기보다,
구조와 전제의 문제에 가까웠다.</p>
<hr>
<h3 id="배포를-통해-처음-체감한-한계">배포를 통해 처음 체감한 한계</h3>
<p>이 시점에서 분명해진 사실이 하나 있었다.</p>
<blockquote>
<p>이 구조는 ‘돌아가는 데에는 충분하지만’,
운영을 전제로 안전하다고 말하기에는 근거가 부족하다.</p>
</blockquote>
<p>배포를 통해 알게 된 건
“어떻게 배포했는가”보다
<strong>“무엇을 당연하다고 가정하고 배포했는가”</strong>였다.</p>
<p>이 구조는 장애를 전제로 설계되지 않았고,
실패를 정상 흐름으로 받아들이지도 않았다.</p>
<p>그리고 이 문제들은
단순한 리팩토링이나 설정 수정으로 해결될 성격이 아니었다.</p>
<hr>
<h3 id="배포편의-위치">배포편의 위치</h3>
<p>이 글은 배포 성공 사례를 정리하기 위한 기록이 아니다.
<strong>개발자 기준으로 세운 판단들이 운영 환경에서 어떻게 흔들리는지를 확인</strong>하기 위한 중간 정리다.</p>
<p>다음으로 정리할 트러블슈팅 아카이브는
이 구조에서 실제로 발생한 문제들을
사고 기록 형태로 남기는 문서가 될 것이다.</p>
<p>그 이후에,
이 전제들을 하나씩 부수는 재설계를 시작하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[INSK 개발편2]]></title>
            <link>https://velog.io/@gm-15/INSK-%EA%B0%9C%EB%B0%9C%ED%8E%B8-2</link>
            <guid>https://velog.io/@gm-15/INSK-%EA%B0%9C%EB%B0%9C%ED%8E%B8-2</guid>
            <pubDate>Fri, 16 Jan 2026 15:34:56 GMT</pubDate>
            <description><![CDATA[<h3 id="돌아간다는-사실이-더-이상-충분하지-않다고-느꼈을-때">“돌아간다”는 사실이, 더 이상 충분하지 않다고 느꼈을 때</h3>
<p>개발편 1까지의 INSK는 분명히 &#39;돌아가는&#39; 서비스였다.
인증이 있었고, 자동화된 뉴스 수집 파이프라인이 있었고, 배포와 캐시까지 얹혀 있었다. 겉으로 보기엔 부족함이 없어 보였다.</p>
<p>하지만 직접 쓰기 시작하면서, 그리고 다른 사람의 입장에서 다시 바라보면서 한 가지가 계속 걸렸다.
이 서비스는 ‘<strong>누구를 위한 서비스인가?</strong>’라는 질문에 답하지 못하고 있었다.</p>
<p>기능은 많았지만, 흐름은 없었다.
기술적으로 가능한 것들을 하나씩 붙여 나가다보니, 사용자가 실제로 무엇을 기대하는지는 뒤늦게 생각하고 있었다.</p>
<br>

<h3 id="기능-중심-개발의-한계가-드러난-순간">기능 중심 개발의 한계가 드러난 순간</h3>
<p>가장 먼저 느낀 문제는 진입 지점이었다.
로그인을 하고 나면 사용자는 <strong>무엇을 해야 하는지 바로 알기 어려웠다.</strong> 키워드, 기사, 점수, 추천 기능은 있었지만 &#39;왜 이게 나에게 필요한지&#39;가 드러나지 않았다.</p>
<p>그제서야
지금까지의 개발은 “구현할 수 있는 것”을 기준으로 쌓아 올린 구조였고,
“사용자가 어떤 순서로 이 서비스를 쓰는지”를 고려하지 않았다는 것을 깨닳았다.</p>
<p>그래서 방향을 바꿨다.
기존 구조 위에 기능을 더 얹는 대신, 사용자 기준으로 전부 다시 잘라보기로 했다.</p>
<br>

<hr>
<h3 id="다시-정의한-기준-사용자-흐름부터">다시 정의한 기준: 사용자 흐름부터</h3>
<p>개발편2에서 가장 먼저 한 일은 기능 추가가 아니었다.
“사용자가 처음 접속했을 때부터, 이 서비스를 떠날 때까지 어떤 흐름을 타야 하는가”를 다시 그리는 일이었다.</p>
<p>그 결과 몇 가지 원칙이 생겼다.</p>
<ul>
<li><p>인증은 단순한 보안 기능이 아니라, 개인화의 출발점이어야 한다</p>
</li>
<li><p>키워드는 설정 화면이 아니라, 이 서비스의 필터 엔진이어야 한다</p>
</li>
<li><p>뉴스 수집과 분석은 보여주는 기능이 아니라, 뒤에서 조용히 돌아가는 엔진이어야 한다</p>
</li>
<li><p>점수와 추천은 설명 없이도 납득 가능한 결과여야 한다</p>
</li>
</ul>
<p>이 기준에 맞지 않는 기능들은 과감하게 밀어냈고, 남길 기능들만 다시 설계했다.</p>
<br>

<h3 id="사용자-중심으로-다시-정리된-기능들">사용자 중심으로 다시 정리된 기능들</h3>
<p>이 과정에서 INSK는 v3.0 구조로 정리됐다.
중요한 건 “기능이 늘었다”가 아니라, 각 기능이 맡은 역할이 명확해졌다는 점이다.</p>
<p>사용자는 로그인과 동시에 자신의 부서와 키워드를 기준으로 개인화된 서비스를 이용할 수 있다. 키워드는 단순 입력 값으로 쓰이는 것은 아닌, 이후 모든 기사 필터링과 점수 계산의 기준이 되도록 했다. AI 기반 키워드 추천은 선택사항으로 두되, 승인 과정을 거쳐야만 실제 서비스에 반영되도록 했다. 무분별한 자동화는 오히려 신뢰를 해친다고 판단했기 때문이다.</p>
<p>뉴스 수집과 분석 파이프라인은 여전히 서비스의 핵심이지만, 더 이상 전면에 드러나지 않게 했다.
사용자가 “분석 버튼”을 누르지 않아도, 내부에서는 자동으로 요약·분류·중복 제거·점수 계산이 이루어진다. 이 기능은 보여주기 위한 기능에서, <strong>쓰이기 위한 기능</strong>으로 역할이 바뀌었다.</p>
<p>기사 조회 역시 마찬가지다.
목록과 상세 화면에서 사용자는 점수, 요약, 인사이트를 자연스럽게 소비할 뿐, 그 점수가 어떻게 계산됐는지를 알 필요는 없다. 좋아요/싫어요와 간단한 피드백만으로도 추천 결과에 영향을 줄 수 있도록 설계했다.</p>
<p>부서별 Top5 기능은 이 흐름의 결과물이다.
개별 사용자의 키워드를 넘어, 조직 단위로 <strong>“지금 이 부서가 봐야 할 뉴스”</strong>를 한 번에 보여주기 위한 장치였다. 기능을 추가했다기보다, 서비스의 목적을 한 줄로 요약한 화면에 가깝다.</p>
<br>

<hr>
<h3 id="기능-구현-이후-남아-있던-불안">기능 구현 이후, 남아 있던 불안</h3>
<p>여기까지 오고 나서야, INSK는 처음으로 “서비스처럼 보이는 형태”를 갖추게 됐다.</p>
<p>기능 구현을 모두 마친 뒤, 한동안은 이 구조를 그대로 두고 써봤다.
큰 문제 없이 돌아갔고, 원하는 결과도 나왔다.</p>
<p>LLM 호출 비용은 아직 감당 가능한 수준이었고, 트래픽도 크지 않았다.
그래서 지금 구조가 꽤 괜찮다고 생각했다.</p>
<p>이 시점까지는 &#39;여기서 더 손볼 이유가 있을까&#39; 라는 생각이 들어서 완성됐다고 판단했었다.</p>
<br>

<p>다만 그 판단이 실제 운영을 전제로 한 결론인지,
아니면 아직 문제가 드러나지 않았을 뿐인 상태인지는 구분하기 어려웠다.</p>
<p>잘 작동하는 구조였지만, 그 판단 기준이 전부 <strong>개발자 입장</strong>이었기 때문에 고려하지 못한 측면이 있을 것이라고 생각했다.</p>
<br>

<p>특히 불안했던 건, 중요한 결정 대부분이 경험이 아니라 <strong>가정 위</strong>에 서 있다는 점이었다.</p>
<p>요약과 분류는 이 정도 호출 빈도면 충분할 거라 생각했고, 
동시 요청 역시 나중에 오면 그때 고민하자며 넘겨왔다.
지금 당장은 아무 일도 일어나지 않았지만, 그게 구조가 안전하다는 증거는 아니었다.</p>
<p>이 상태로 더 기능을 얹는 건 의미 없다고 느꼈다.
다음 단계로 가려면, 무엇을 더 만들 것인지 보다
“지금까지 당연하게 둔 전제 중 무엇이 틀렸는가”를 먼저 확인해야 했다.</p>
<p>그래서 다시 외부의 시선을 요청했다.
기능 설명이 아니라, 구조 전체를 놓고 실제로 이 프로젝트를 굴릴 수 있겠는지를 묻고 싶었다.
SK 초청 시 만나뵙고 피드백을 받았던 개발자님께 다시 연락을 드렸다.</p>
<p><br><br>
그리고 며칠 후 돌아온 피드백은,
내가 안정적이라고 믿고 있던 이 구조가
운영 관점에서는 아직 검증되지 않은 선택들의 집합에 가깝다는 점을 정확히 짚고 있었다.</p>
<p>이제 다음 단계에서는 기능을 추가하지 않고,
이 구조를 가능하게 만든 전제부터 하나씩 부숴보려 한다.</p>
<p>개발편 3에서 이어진다.</p>
<p>INSK_V3
github : <a href="https://github.com/gm-15/INSK">https://github.com/gm-15/INSK</a>
시연영상 :  <a href="https://youtu.be/WlKGbvbxHik">https://youtu.be/WlKGbvbxHik</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[INSK 개발편1(프로토타입을 버리고, 처음으로 ‘서비스’를 만들겠다고 결정한 순간)]]></title>
            <link>https://velog.io/@gm-15/INSK-%EA%B0%9C%EB%B0%9C%ED%8E%B81%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85%EC%9D%84-%EB%B2%84%EB%A6%AC%EA%B3%A0-%EC%B2%98%EC%9D%8C%EC%9C%BC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EB%A7%8C%EB%93%A4%EA%B2%A0%EB%8B%A4%EA%B3%A0-%EA%B2%B0%EC%A0%95%ED%95%9C-%EC%88%9C%EA%B0%84</link>
            <guid>https://velog.io/@gm-15/INSK-%EA%B0%9C%EB%B0%9C%ED%8E%B81%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85%EC%9D%84-%EB%B2%84%EB%A6%AC%EA%B3%A0-%EC%B2%98%EC%9D%8C%EC%9C%BC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EB%A7%8C%EB%93%A4%EA%B2%A0%EB%8B%A4%EA%B3%A0-%EA%B2%B0%EC%A0%95%ED%95%9C-%EC%88%9C%EA%B0%84</guid>
            <pubDate>Fri, 16 Jan 2026 07:35:06 GMT</pubDate>
            <description><![CDATA[<h1 id="insk-개발편-1">INSK 개발편 1</h1>
<h3 id="프로토타입을-버리고-처음으로-서비스를-만들겠다고-결정한-순간">프로토타입을 버리고, 처음으로 ‘서비스’를 만들겠다고 결정한 순간</h3>
<blockquote>
<p><a href="https://velog.io/@gm-15/%EC%8D%A8%EB%8B%88C-4%EA%B8%B0-%ED%9B%84%EA%B8%B0">SK 미래역량 프로그램 mySUNI 써니C 4기 대외활동 후기</a>
써니C 4기 대외활동 이후 프로젝트 고도화한 과정</p>
</blockquote>
<p>INSK에 대한 컨셉과 뉴스를 수집하고 요약하고 분류하는 자동화 자체는 이미 다른 형태로 만들어본 상태였다. Make 기반 자동화, 그리고 Streamlit + Python으로 빠르게 결과를 확인하는 프로토타입까지는 충분히 의미가 있었다. 하지만 그 구조는 명확했다. 혼자 돌리는 자동화 실험에 가까웠고, 누군가 실제로 쓰는 서비스를 상정한 설계는 아니었다.
<img src="https://velog.velcdn.com/images/gm-15/post/3b2d6440-89fb-4d8c-b946-1755be800774/image.png" alt="Make"></p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/1d87bec2-7f0d-4496-8359-ae23f66f3470/image.png" alt="Streamlit_1"></p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/9970d15e-152c-481b-93b3-946d86fd1f77/image.png" alt="Streamlit_2">
<a href="https://github.com/gm-15/INSK_2">INSK_V2 </a><a href="https://github.com/gm-15/INSK_2">https://github.com/gm-15/INSK_2</a>
현업 개발자들과의 미팅 이후, 이 한계는 더 이상 외면할 수 없게 됐다.
“<strong>돌아가는 것</strong>”과 “<strong>서비스</strong>”는 완전히 다른 문제라는 지적이었고, 그 말에 동의할 수밖에 없었다. 그래서 결론을 내렸다. 기존 구조를 고도화하는 대신, 아예 엔터프라이즈 서비스를 흉내 내는 방향으로 다시 만들자고.</p>
<hr>
<h3 id="기술-스택을-다시-고른-이유">기술 스택을 다시 고른 이유</h3>
<p>Spring, React, MySQL을 아무 의미 없이 선택한 것은 아니었다.
솔직히 말하면 Spring을 선택한 가장 직접적인 이유는 방학 동안 김영한 강사의 스프링 강의를 기초부터 일반까지 한 번 훑어본 상태였기 때문이다. 개념과 실습을 그저 아는 것에서 끝내고 싶지 않았고, 실제 서비스 구조에서 이 선택이 어떤 부담과 이점을 주는지 직접 겪어보고 싶었다.</p>
<p>다만 단순한 학습 연장으로 보이지 않게 하기 위해 기준을 명확히 잡았다.</p>
<ul>
<li><p>계층 분리가 강제되는 구조</p>
</li>
<li><blockquote>
<p>기능이 늘어날수록 수정 지점이 한 사람에게 집중되는 상황을 피하기 위해</p>
</blockquote>
</li>
<li><p>인증, 트랜잭션, 보안이 기본 전제인 서버</p>
</li>
<li><blockquote>
<p>사용자 개념 추가에 따라, 감당해야하는 비용 체감을 위해</p>
</blockquote>
</li>
</ul>
<p>React 역시 마찬가지였다. 프론트엔드 자체를 깊게 파는 것이 목적은 아니었고, 백엔드 API가 실제 사용자 화면과 연결되는 지점까지만 책임지는 역할로 두었다. 이 프로젝트에서 중심은 명확히 백엔드였다.</p>
<h3 id="개인-프로젝트가-아니라-팀-프로젝트로-만들기로-한-이유">개인 프로젝트가 아니라, 팀 프로젝트로 만들기로 한 이유</h3>
<p>이 프로젝트는 혼자서도 만들 수 있었지만, 일부러 팀으로 진행했다.
이유는 단순하다. 실제 서비스 개발은 항상 협업 위에서 돌아가기 때문이다.</p>
<p>당시 팀원들은 대부분 실질적인 개발 경험이 거의 없는 상태였다. 그래서 자연스럽게 내가 맡은 역할은 기능 구현자라기보다 아키텍처와 일정의 총괄에 가까웠다. 프론트 / 백엔드 / DB / 배포로 역할을 나누고, 나는 백엔드와 배포를 직접 맡으면서 전체 기술 방향과 진도를 관리했다.</p>
<p>5주 단위로 계획을 쪼갰고, 매주 화상 회의를 통해 진행 상황을 공유했다. 말로 설명하는 방식은 최대한 줄이고, 문서로 기준을 남기는 방식을 택했다. 어떻게 하면 팀이 흔들리지 않을까 고민했다.</p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/74108880-8835-46c4-96a6-77affb87648a/image.png" alt=""></p>
<p>5주 계획은 일정이 아니라 사고 방식이었다.</p>
<p>1주차는 기능이 아니라 뼈대를 만드는 데 집중했다.
환경 설정, Git 협업 전략, DB 스키마, 기본 아키텍처를 이 시점에 확정했다. Controller–Service–Repository 구조를 먼저 잡았고, 첫 API는 회원가입이었다. 기능의 난이도보다 중요한 건, 이 구조로 앞으로 모든 기능을 밀어붙일 수 있는지였다.</p>
<p>2주차부터 사용자와 직접 맞닿는 기능을 만들기 시작했다.
인증과 개인화 키워드 CRUD. 이 시점에서 Spring Security, Validation, JWT를 얹으면서 “생각보다 귀찮고 복잡하다”는 걸 체감했다. 동시에, Python으로 빠르게 만들던 시절엔 느끼지 못했던 구조의 무게도 분명히 느껴졌다.</p>
<p>3주차는 프로젝트의 핵심 가치였다.
기존 Python 프로젝트에서 가장 중요했던 자동 뉴스 수집과 AI 분석 파이프라인을 Spring의 @Scheduled 기반 배치 작업으로 옮겼다. 이때부터 INSK는 단순 CRUD 프로젝트가 아니라, 엔진을 가진 서비스처럼 보이기 시작했다.</p>
<p>4주차에는 배포와 운영을 건드렸다.
Docker로 애플리케이션을 컨테이너화하고, GitHub Actions 기반 CI/CD를 구성해 AWS에 자동 배포했다. 중요한 API에는 Redis 캐시를 붙였다. JMeter 부하 테스트를 통해 캐시 적용 전(195ms)과 적용 후(70ms)의 평균 응답 속도를 측정하여 <strong>&quot;64.1% 성능 향상&quot;</strong>이라는 정량적 성과를 얻을 수 있었다. 성능 최적화라는 말을 처음으로 “경험”이라고 부를 수 있는 지점이었다. 이 부분의 고민은 이전에 따로 정리한 글로 대신한다.</p>
<p>5주차는 코드가 아니라 설명을 다듬는 시간이었다.
README를 단순한 사용법 문서가 아니라, 기술 선택의 근거를 설명하는 문서로 다시 썼다. 아키텍처 다이어그램을 정리하고, 예상 면접 질문을 뽑으면서 스스로에게 &quot;why?&quot;를 계속 물었다</p>
<hr>
<h3 id="aws-배포-구조">AWS 배포 구조</h3>
<p><img src="https://velog.velcdn.com/images/gm-15/post/1bfcbf38-dec4-41cc-a31c-ae030ed19a54/image.png" alt="">
우리의 초기 목표는 인프라보다 백엔드 설계와 데이터 흐름에 집중하자는 것이었기에,
직접 EC2 + ASG + ALB를 다 구성하지 않고도
배포, 롤백, 환경변수 관리, 헬스체크를 한 번에 처리하기 위해서 EB를 사용했다.</p>
<br>
앞단에는 ALB를 썼는데,

<p>/articles/{id}/feedbacks
/articles
/admin/*</p>
<p>이런 REST API들을 확장 가능 구조로 만들어서
HTTPS 종단이나 향후 다중 인스턴스를 대비하고, 헬스체크 기반 트래픽 분산을 챙기기 위한 것이었다.</p>
<br>

<p>가장 중요하게 둔 것은 RDS를 분리하는 것이었다.
EB 환경 종료가 데이터 삭제로 이어지면 안됐기에 환경 외부 자원인 RDS를 사용해 분리했다.
*<em>비용 때문에 환경을 내려도 데이터는 살아있다는게 좋았다.
*</em></p>
<br>

<p>결과적으로 이번 배포 구성은
Elastic Beanstalk 기반의 관리형 배포 환경 위에서
Spring Boot 백엔드를 운영하고,
데이터는 RDS로 분리해 환경 종료에도 안전하도록 설계한 것이다.</p>
<p>이후 피드백 기반 기능 확장과 재설계를 고려한 부분도 있었다.</p>
<hr>
<blockquote>
<p>INSK_V2 구현 화면</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/gm-15/post/d8bbf7f8-46aa-4caf-89bf-0b7d4073f957/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/4e3f5db9-82df-4c22-9474-eeb568ae3d73/image.png" alt=""></p>
<p>여기까지의 INSK는 분명히 “<strong>돌아가는 서비스</strong>”였다.</p>
<p>이미 “돌아가는 것과 서비스는 다르다” 는 지적을 인지한 상태였지만,
엔터프라이즈 서비스를 흉내내는 데 집중한 나머지
<strong>익숙하지 않은 기술 스택과 기능 구현 자체</strong>가 <strong>목적</strong>이 되어 있었다.</p>
<p>그 결과, 사용자 흐름과 서비스 관점에서 정말 중요한 질문들은
구현 뒤로 밀려나 있었다.</p>
<p>이걸 깨달았다는 사실 자체가, 다음 단계로 넘어갈 수 있었던 이유였다.
개발편 1은 여기까지다.</p>
<p>다음 편, INSK 개발편2에서는 구조를 전면 개편하면서 사용자 측면에서 어떻게 새로 구성했는지를 다룬다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[캐시를 고민하다 DevOps까지 생각하게 된 기록]]></title>
            <link>https://velog.io/@gm-15/Redis-vs-Memcashed</link>
            <guid>https://velog.io/@gm-15/Redis-vs-Memcashed</guid>
            <pubDate>Sun, 04 Jan 2026 14:36:40 GMT</pubDate>
            <description><![CDATA[<h2 id="캐시를-도입하면서-고민했던-기록">캐시를 도입하면서 고민했던 기록</h2>
<h3 id="redis-vs-memcached">Redis vs Memcached</h3>
<p>INSK를 만들면서 처음으로 “캐시”를 진지하게 고민하게 됐다.
데이터가 많아지고 기능이 늘어나면서, DB만으로는 점점 부담이 커졌기 때문이다.</p>
<p>그래서 자연스럽게 이런 질문이 생겼다.</p>
<blockquote>
<p>&quot;캐시를 쓰자. 그런데 어떤 캐시를 쓰지?&quot;</p>
</blockquote>
<p>Memcached? 아니면 Redis?
막연하게 “Redis가 더 유명하니까” 같은 기준이 대신,
<strong>우리 프로젝트의 특징과 미래 확장</strong>까지 함께 고려해보면서 정리해 보았다.</p>
<hr>
<h3 id="캐시는-왜-필요한가">캐시는 왜 필요한가</h3>
<p>웹 서비스 구조를 보면, 캐시는 결국 <strong>속도를 위한 계층</strong>이다.</p>
<p>요청이 들어올 때 항상 DB까지 내려가면 느려질 수밖에 없다.
자주 조회되지만 매 순간 최신일 필요는 없는 데이터라면, 메모리에 저장해 놓고 빠르게 꺼내 쓰는 것이 훨씬 효율적이다.</p>
<p>그래서 캐시는 보통 이렇게 이용된다.</p>
<blockquote>
<p>사용자 요청<br>
캐시에서 먼저 확인<br>
(없으면) DB 조회<br>
결과를 캐시에 저장</p>
</blockquote>
<p>이 단순한 구조 하나로 서비스 체감 속도는 크게 달라진다.</p>
<p>!캐시는 동일하거나 자주 반복되는 요청에 대해 DB 대신 빠르게 응답하기 위해 사용된다.
다만, 캐시는 한 번 저장된 값을 일정 시간 그대로 유지하기 때문에, 시점에 따라 값이 달라지는 데이터에는 만료 전략이나 무효화(캐시 삭제) 설계가 필요하다.</p>
<hr>
<h3 id="memcached">Memcached</h3>
<p>Memcached는 아주 단순한 캐시 서버다.</p>
<blockquote>
<p><strong>메모리 기반
key-value만 저장
영속성 없음
구조가 직관적이고 빠름</strong></p>
</blockquote>
<p>즉, 단순 조회 캐시에 정말 적합하다.</p>
<p>예를 들면,</p>
<blockquote>
<p><strong>메인 화면에 노출되는 데이터
자주 바뀌지 않는 목록
자주 조회되지만 중요도가 낮은 정보</strong></p>
</blockquote>
<p>우리 프로젝트인 INSK는 AI 기반 뉴스 트렌드 센싱을 하는 서비스이다.
뉴스 같은 경우, <strong>속도가 아주 중요하다</strong>거나 <strong>정확한 최신 데이터가 아니어도 된다</strong>면
Memcached만으로도 충분히 운영할 수 있다는 얘기가 많다.</p>
<p>실제로 맞는 말이다.</p>
<p>하지만, 곧바로 “우리 서비스에도 충분하다”라고 보기엔 애매했다.</p>
<hr>
<h3 id="조금-다른-방향의-도구-redis">조금 다른 방향의 도구 Redis</h3>
<p>Redis를 공부하고 프로젝트 &#39;INSK&#39;와 &#39;냉장Goat&#39;등에 적용하며 느낀 점은,
Redis가 단순 캐시라기보다 “메모리 기반 데이터 저장소”에 가깝다는 것이다.</p>
<p>문자열뿐 아니라 리스트, 집합, 정렬된 집합, 해시, 큐, 카운터 등
<strong>여러 구조</strong>를 다룰 수 있고,
필요하면 데이터 일부를 디스크에 남길 수도 있다.</p>
<p>그래서 세션 관리, 랭킹, 분산락, 메시지 큐 비슷한 역할까지 맡을 수 있다.</p>
<p>단순히 “빠르다”를 넘어서, <strong>서비스 전체 구조</strong>에 자연스럽게 녹일 수 있는 도구였다.</p>
<hr>
<h3 id="우리-프로젝트에서-실제로-고민했던-지점들">우리 프로젝트에서 실제로 고민했던 지점들</h3>
<p>INSK는 단순한 뉴스 열람 서비스가 아니다.
다음과 같은 기능을 갖춘 <strong>뉴스 트렌드 센싱 자동화 플랫폼</strong>이다.</p>
<blockquote>
<p>키워드 기반 자동 뉴스 수집
AI 요약과 인사이트 분석
중복 기사 판정
좋아요/싫어요 피드백
기사 점수 계산
부서별 TOP5 추천
PDF 보고서 생성</p>
</blockquote>
<p>이 흐름을 만들다 보니, 캐시가 단순 조회에서 끝나지 않았다.</p>
<p>점수 랭킹이 필요했고,
파이프라인 중복 실행을 막아야 했고,
나중에 세션/토큰 관리까지 고려해야 했다.</p>
<p>여기서 Memcached는 <strong>기능적으로 한계</strong>가 있었고,
Redis는 이미 잘 알려진 패턴이 많았다.</p>
<p>특히 점수와 랭킹이 결정적이었다.</p>
<p>기사 점수는 계속 변한다.
추천 결과도 계속 변한다.</p>
<p>이걸 모두 DB만으로 처리하려고 하면 부하가 크다.</p>
<p>Redis의 정렬된 집합 구조를 사용하면,</p>
<p>점수를 기준으로 정렬
상위 N개 즉시 조회</p>
<p>이런 기능을 자연스럽게 만들 수 있었다.</p>
<p>이 지점에서 “Redis가 필요하겠다”라는 그림이 어느 정도 확정됐다.</p>
<hr>
<h2 id="cloud-wave-이후">Cloud Wave 이후</h2>
<h3 id="redis-vs-kafka">Redis vs Kafka</h3>
<p>캐시를 고민하다 보니, 자연스럽게 “캐시로 감당하면 안 되는 데이터는 어디로 가야 하는가”라는 질문이 생겼다.</p>
<p>CJ올리브네트웍스 부트캠프 Cloud Wave 도커 강의에서 들은 내용이 특히 인상 깊었다.</p>
<p>Redis는 메모리 기반이라 빠르지만, 기본적으로 “사라져도 되는 데이터”에 어울린다.
Kafka는 디스크 기반이라 상대적으로 느리지만, 데이터를 다시 읽고 재처리할 수 있다.</p>
<p>결국 기준은 단순하다.</p>
<p>이 데이터가 사라져도 되는가?
아니면 반드시 남아 있어야 하는가?</p>
<p>캐시는 사라져도 큰 문제가 없어야 한다.
하지만 로그, 이벤트 흐름, 결제 내역 같은 것들은 사라지면 안 된다.</p>
<hr>
<h3 id="장애-상황">&#39;장애 상황&#39;</h3>
<p>Elastic Beanstalk 서버가 죽을 수도 있다.
문제는 여기서 끝이 아니다.</p>
<p>서버가 죽으면, 그 순간 무슨 일이 발생했는지 기록조차 남지 않는다.</p>
<p>그래서 실무에서는 애플리케이션 안에 로그를 두지 않고,
Kafka 같은 큐 시스템으로 로그를 외부에 모아 둔다.</p>
<p>서버가 죽어도 로그는 남는다.
재처리도 가능하다.</p>
<p>이걸 보며 알게 되었다.</p>
<p>“잘 동작하게 만드는 것”보다
“망가졌을 때 추적 가능하게 만드는 것”이 훨씬 중요할 때가 많다는 것.</p>
<hr>
<h3 id="trade-off">Trade-off</h3>
<p>빠를수록 좋은 것도 아니고,
안전할수록 좋은 것도 아니다.</p>
<p>비용, 운영 난이도, 유지보수, 확장성.</p>
<p>이 모든 걸 놓고 판단해야 한다.</p>
<p>상사가 1초마다 새 일을 던지면 결국 아무 것도 못하듯,
시스템도 한 곳에 책임을 몰아주면 결국 무너진다.</p>
<p>그래서 역할을 나누고, 필요하면 다른 계층으로 빼는 것이다.</p>
<hr>
<h3 id="컨테이너-볼륨-쿠버네티스까지">컨테이너, 볼륨, 쿠버네티스까지</h3>
<p>실습 중 네트워크 설정을 바꾸려고 컨테이너를 내렸다가 올렸는데
로그가 모두 사라진 경험이 있었다.</p>
<p>그때 볼륨의 중요성을 체감할 수 있었다.</p>
<p>컨테이너는 언제든 삭제될 수 있는 존재다.
데이터는 컨테이너 바깥에 두어야 한다.</p>
<p>쿠버네티스도 같은 맥락이었다.</p>
<p>장애가 나면 자동으로 다시 띄우고,
트래픽이 증가하면 자동으로 늘리고,
배포 과정에서 서비스가 멈추지 않게 제어한다.</p>
<p>이 모든 것이 결국 “살아남도록 설계하는 일”이었다.</p>
<hr>
<h3 id="프록시를-이용한-인증-분리">프록시를 이용한 인증 분리</h3>
<p>DB 비밀번호, API 키 같은 민감 정보는
애플리케이션이 직접 들고 있지 않도록 프록시에서 처리한다.</p>
<p>이렇게 되면,</p>
<p>키 공유 문제,
비밀번호 변경,
타팀 협업 이슈가 훨씬 단순해진다.</p>
<p><a href="https://docs.cloud.google.com/sql/docs/mysql/connect-auth-proxy?hl=ko">https://docs.cloud.google.com/sql/docs/mysql/connect-auth-proxy?hl=ko</a>
구글 Cloud SQL Proxy 문서를 보면서,
AWS는 방식이 다르지만 목적은 동일하다는 것도 이해됐다.</p>
<p>인증은 가능한 한 애플리케이션 바깥으로 빼는 게 중요하다.</p>
<hr>
<h3 id="이번-프로젝트를-통해-느낀-점">이번 프로젝트를 통해 느낀 점</h3>
<p>처음엔 그냥 “Redis vs Memcached” 정도의 고민이었다.</p>
<p>하지만 점점 커지더니 이렇게 확장되었다.</p>
<p>캐시 → 영속 메시지 큐 → 장애 상황 → 로그 수집 → 인프라 안정성 → 컨테이너와 쿠버네티스 → 프록시 인증 구조</p>
<p>도구 하나 선택하던 문제에서,
“시스템 전체를 어떻게 설계할 것인가”로 바뀌었다.</p>
<p>INSK 프로젝트에서 Redis를 쓰면서 배운 건 단순했다.</p>
<p>기술은 기능으로만 선택하면 안 되고,
“이 서비스가 앞으로 어떻게 커질지”까지 보고 선택해야 한다는 것.</p>
<p>그리고 장애, 데이터 유실, 확장, 운영이라는 현실적인 문제 앞에서
도구의 의미가 완전히 달라진다는 것.</p>
<p>개발이 단순 기능 구현에서,
엔지니어링으로 넘어가는 지점이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SK AI Dream Camp]와 [심층학습] 수강기]]></title>
            <link>https://velog.io/@gm-15/SK-AI-Dream-Camp%EA%B3%BC-%EC%8B%AC%EC%B8%B5%ED%95%99%EC%8A%B5</link>
            <guid>https://velog.io/@gm-15/SK-AI-Dream-Camp%EA%B3%BC-%EC%8B%AC%EC%B8%B5%ED%95%99%EC%8A%B5</guid>
            <pubDate>Tue, 30 Dec 2025 08:50:05 GMT</pubDate>
            <description><![CDATA[<p>SK AI Dream Camp &amp; 심층학습 수강기</p>
<p>데이터 분석과 머신러닝의 기초를 다졌던 SK AI Dream Camp,
그리고 이를 바탕으로 인공지능의 깊이를 더한 상명대학교 전공심화 심층학습.
<br>
두 과정을 통해</p>
<p>“데이터가 어떻게 가치 있는 정보가 되고,
실제 서비스에 적용되는지”</p>
<p>를 단계적으로 이해할 수 있었다. 이 글은 그 여정을 정리한 기록이다.</p>
<hr>
<h1 id="sk-ai-dream-camp">SK AI Dream Camp</h1>
<h2 id="지원동기">지원동기</h2>
<blockquote>
<p>구분 : 📋수료
항목 : SK AI Dream Camp (초급)
주관기관 : SK mySUNI
기간 : 25.07.01~08.14</p>
</blockquote>
<p>이번 여름, SK AI Dream Camp에 대한 공지가 올라왔다.
<img src="https://velog.velcdn.com/images/gm-15/post/68a5f2ea-a724-40e9-861d-dcfd46c935db/image.png" alt=""></p>
<p>파이썬은 교내 프로젝트와 알고리즘 코딩 테스트 등으로 많이 다뤄봤지만, 데이터 분석과 머신러닝은 아직 다룬 경험이 없어서 이 교육이 AI에 입문하는데 큰 도움이 될 것 같았다. 중,고급반은 실제 데이터를 다룰 수 있는 숙련도가 필요해서 아쉽지만 초급으로 시작하게 되었다.  </p>
<hr>
<h2 id="학습과정">학습과정</h2>
<p>지원 후 과목을 살펴보니</p>
<p>데이터 분석 -&gt; 머신러닝 -&gt; AI경연(SK텔레콤 LTE 기지국 장비의 성능 상태 예측)
으로 앞선 과목을 수강해야 다음 과목이 수강 가능한 방식이었다.</p>
<p>수강시작까지 기간이 조금 남아서 pandas, numpy, series, dataframe등에 관해 미리 학습했다. 
<img src="https://velog.velcdn.com/images/gm-15/post/c576899c-7820-4eb9-8260-932c2db09ab6/image.png" alt=""></p>
<hr>
<h3 id="데이터-분석">데이터 분석</h3>
<p>pandas numpy 기반으로 series, dataframe을 기본적인 파이썬으로 배우고 실습하는 과정이었다. 사전에 미리 학습했기에 크게 어려운 부분은 없었다.</p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/15c74cf0-ba85-46bb-8c68-51c17595d2ca/image.png" alt="">
이런식으로 문제가 주어지고 빈칸에 코드를 작성하고 실행해서 원하는 결과가 나오면 넘어가는 식이었다. </p>
<p>시각화에 대해서도 배웠다. 통계나 그래프를 위해 seaborn을 사용했다. matplotlib으로도 가능하지만 </p>
<ul>
<li>쉬운 사용성 (1줄 완성 그래프)</li>
<li>seaborn 에서만 제공되는 <strong>통계 기반 plot</strong></li>
<li>아름다운 스타일링</li>
<li>Pandas의 데이터프레임과 높은 호환성</li>
</ul>
<p>이런 특징이 있어 seaborn을 사용한다고 했다.</p>
<p>다양한 map이나 plot들에 대해서도 다뤄보았다.</p>
<hr>
<h3 id="머신러닝">머신러닝</h3>
<p>지도/비지도 학습과 같은 학습 방법, 회귀/분류나 군집/차원 축소 같은 문제 유형과 
머신러닝 모델들에 관해 배웠다.</p>
<p>특히 인상 깊었던 것은 단순히 모델을 돌리는 것이 아니라, 
데이터 준비 → 학습 → 평가 → 제출
까지 이어지는 End-to-End 프로세스를 정립했던 것이었다.</p>
<blockquote>
<p><strong>머신러닝 프로세스</strong></p>
<p>1) 결측치 처리
2) 데이터 전처리(모두 숫자형으로)
3) Feature Engineering
4) 주요 특성 선택 및 제거
5) 학습을 위해 데이터 분리(X,Y 분리)
6) 레이블에 따른 모델 선정
7) 모델 학습
8) 모델 평가
9) 예측 데이터 추론
10) 결과 제출</p>
</blockquote>
<p>보이는 문제에만 집중하는 그리디한 개발자와 전체 프로세스를 알고 설계하는 개발자는 다를 수 밖에 없겠다는 생각이 들었다.</p>
<p><br><br></p>
<p><strong>분류/회귀/군집</strong> 개념에 대해서도 명확하게 이해할 수 있었다.
<img src="https://velog.velcdn.com/images/gm-15/post/12406e4e-b2a5-4dd5-a3fb-6c37ba9cd723/image.png" alt=""></p>
<blockquote>
<p>분류 : 정답이 범주일 때
회귀 : 값이 연속일 때
군집 : 정답 없이 비슷한 것끼리 묶기</p>
</blockquote>
<p>학습 방법과 문제 유형, 그리고 머신 러닝 모델에 따라 머신러닝 문제 유형이 나뉜다는 것을 알 수 있었다.</p>
<p>또한 비지도 학습의 대표적인 알고리즘인 K-Means를 통해 정답(Label)이 없는 데이터들을 거리 기반으로 군집화(Clustering)하여 숨겨진 패턴을 찾는 방법도 실습해 볼 수 있었다.
<br>
<br></p>
<h2 id="학습-후">학습 후</h2>
<p>모든 과정이 마무리되고, 수료증을 발급 받았다.
<img src="https://velog.velcdn.com/images/gm-15/post/38904b16-6b5a-40cf-b0d2-362f1bdad60c/image.png" alt=""></p>
<p>수료증은 발급받았지만, 가볍게 기본 개념을 배운 느낌이라 그 다음이 없어서 아쉬웠다.</p>
<p>그러나 학교 전공과목에서 그 아쉬움을 달랠 수 있는 과목을 발견했다.
바로 전공 심화 과목인 &#39;심층학습&#39;이다.</p>
<br>
<br>
<br>
<br>


<h1 id="심층학습">심층학습</h1>
<p>SK AI Dream Camp가 머신러닝의 &#39;전체 숲&#39;을 보는 과정이었다면, 
심층학습은 그 숲속에 있는 &#39;거대한 나무&#39;들의 내부 구조를 파헤치는 시간이었다.</p>
<p>기존 머신러닝(ML)으로는 해결하기 어려웠던 비정형 데이터(이미지, 텍스트, 시계열) 처리를 위해, 심층 신경망(DNN)이 어떻게 발전해 왔는지 단계별로 학습했다.</p>
<hr>
<h3 id="cnn-합성곱-신경망">CNN (합성곱 신경망)</h3>
<p>기존 ML에서는 사람이 직접 특징을 설계해야 했다.</p>
<p>CNN은:</p>
<blockquote>
<p>Convolution + Pooling
을 통해 스스로 중요한 특징을 학습한다.</p>
</blockquote>
<p>Stride / Padding 등을 수식으로 계산하며,
LeNet-5, ResNet (Skip Connection으로 기울기 소실 해결)
같은 구조를 이해할 수 있었다.</p>
<p>이를 통해 Feature Engineering이 모델 내부로 들어왔다는 것을 알 수 있었다.
<br>
<img src="https://velog.velcdn.com/images/gm-15/post/0b770081-eaa8-456d-b47e-136929cab26a/image.png" alt=""></p>
<p>별개로, AlexNet의 핵심 3요소인 GPU, ReLu, Dropout에서 
Dropout의 역할이 과적합 방지인 이유가 기억에 남는다.
-서브 네트워크를 만들어서 앙상블 효과를 만들기 때문.
-뉴런을 확률적으로 끄기 때문에 뉴런 자체의 역할을 강건하게 함.</p>
<p>서브 네트워크 개념을 떠올리지는 못했었는데 교수님의 설명을 듣고 놀랐던 기억이 난다.</p>
<br>

<h3 id="시계열--nlp">시계열 &amp; NLP</h3>
<p>SK 캠프 경연 주제였던 &#39;시계열 예측&#39;을 심층학습에서는 훨씬 정교한 모델로 다루었다.</p>
<p>시계열 데이터를 처리하는 RNN은 데이터 길이가 길어지면 앞의 내용을 까먹는(장기 의존성 문제) 치명적인 단점이 있다. </p>
<p>이를 해결하기 위해 망각(Forget), 입력(Input), 출력(Output) 게이트를 도입한 LSTM 구조를 상세히 뜯어보았다.
<img src="https://velog.velcdn.com/images/gm-15/post/221ed780-4b66-450a-9a8c-b2ffd588343e/image.png" alt=""></p>
<p>Transformer는 현재 자연어 처리(NLP)의 표준이 된 모델이다. 
순차적으로 처리하는 RNN의 한계를 넘어, 
Self-Attention($Attention(Q, K, V)$) 메커니즘을 통해 문장 전체의 문맥을 한 번에 병렬로 파악하는 원리를 학습했다.</p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/f5753c40-3a2e-4262-a351-ea323ef05b83/image.png" alt=""></p>
<h3 id="생성-모델">생성 모델</h3>
<p>VAE: 확률 분포 기반 생성</p>
<p>GAN: 생성자와 판별자의 경쟁</p>
<p>단순 분류를 넘어서,
모델이 새로운 데이터를 <strong>창조</strong>할 수 있다는 점이 인상적이었다.</p>
<br>
<br>

<h1 id="sk-ai-dream-camp와-심층학습-수강-후">SK AI Dream Camp와 심층학습 수강 후</h1>
<p>Dream Camp를 먼저 듣고 심층학습을 배우니,</p>
<p>*<em>왜 이 기술이 필요한가?
*</em>
가 분명해졌다.</p>
<blockquote>
<p>🔗 연결 1 — Feature Engineering → 자동화
Dream Camp: 내가 직접 변수 고민
Deep Learning: 필터가 자동으로 특징 학습</p>
</blockquote>
<blockquote>
<p>🔗 연결 2 — 시계열 이해
Dream Camp: 회귀/통계 위주 접근
Deep Learning: RNN/LSTM/Attention으로 순서와 상태를 보존</p>
</blockquote>
<blockquote>
<p>🔗 연결 3 — Transfer Learning
Pre-trained 모델을 가져와 미세조정
현실적인 데이터 부족 문제 해결</p>
</blockquote>
<br>
<br>

<h1 id="마치며">마치며</h1>
<p>Dream Camp는 Workflow를,
심층학습은 엔진 내부 구조를 이해하게 해 주었다.</p>
<blockquote>
<p>*<em>데이터 파이프라인 이해
모델 구조와 연산 비용 감각
클라우드 환경에서 AI를 다루는 시야
*</em></p>
</blockquote>
<p>ML 프로세스를 직접 구현해 봄으로써, 데이터 수집부터 전처리, 학습, 추론으로 이어지는 MLOps 파이프라인의 구조를 이해하게 되었다.</p>
<p>또한 CNN, Transformer 등 거대 모델의 연산 비용과 구조를 이해함으로써, 향후 클라우드 환경에서 GPU 리소스 할당이나 모델 서빙(Serving) 아키텍처를 설계할 때 더 효율적인 의사결정을 할 수 있을 것이다.</p>
<br>

<p>앞으로는 
실제 서비스에 AI를 연동하고,
클라우드 환경에서 안정적으로 운영하는 경험을 쌓으며
AI를 이해하는 백엔드 엔지니어로 성장하고자 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SK 미래역량 프로그램 mySUNI 써니C 4기] 대외활동 후기]]></title>
            <link>https://velog.io/@gm-15/%EC%8D%A8%EB%8B%88C-4%EA%B8%B0-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@gm-15/%EC%8D%A8%EB%8B%88C-4%EA%B8%B0-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 22 Dec 2025 01:10:11 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>후기는 
<strong>써니C 프로젝트 1(make 및 대외활동 기록)
써니C 프로젝트 2(python과 스트림릿)
써니C 프로젝트 3(엔터프라이즈급 아키텍처 구성 및 개발)
써니C 프로젝트 4(실무자 피드백 기반 추가 개발)</strong>
으로 예정되어 있다.</p>
</blockquote>
<blockquote>
<p>주관 : SK mySUNI
활동명 : 써니C 4기 &#39;AI로 일하는 방식 혁신&#39; 프로젝트
기간: 25.07.21~08.21</p>
</blockquote>
<h2 id="지원동기">지원동기</h2>
<p>이번 여름, SK mySUNI에서 주관하는 SK의 대외활동 mySUNI 써니C 4기에 참여하게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/ad61f553-387f-420e-bbaf-4bac53069302/image.png" alt=""></p>
<p>보자마자 지원하기로 결정했다.
이유는 두 가지였다.</p>
<ol>
<li>써니C 지원 경험</li>
<li>AI </li>
</ol>
<p>작년 여름, 써니C에 3기에 지원했다가 최종에서 떨어졌었다.
비록 떨어졌지만 최종 단계까지 준비하면서 써니C 활동이 충분히 매력적이라는 것을 느낄 수 있었다. 실무 경험과 프로젝트가 매력적으로 보였기 때문이다.</p>
<p>그런데 이번엔 프로그램이 조금 달라져있었다. 기본적으로 기초 미래역량 교육과 실무 프로젝트는 동일했지만, 이번엔 <strong>AI</strong>가 주요 키워드로 들어가있었다. </p>
<blockquote>
<p>&lt;&#39;직무 X AI프로젝트&#39;를 통한 &#39;일 경험&#39;&gt;
&lt;&#39;AI Tool&#39;&gt;</p>
</blockquote>
<p>이제 대외활동까지 AI가 머리를 내밀기 시작했다.
백엔드 개발자, 클라우드 엔지니어가 되고 싶은 나에게 AI 사용 경험은 선택이 아닌 필수였기 때문에 이번 프로그램이 더욱 매력적으로 느껴졌다.</p>
<p>이전 지원 경험을 살려 이번 활동에 꼭 참가하고 싶다고 생각했다.</p>
<h2 id="지원과정">지원과정</h2>
<p>3기 때는 합격자에게만 교육이 제공되었지만 
4기 때는 지원 -&gt; 교육 -&gt; 합격순으로 진행되었다.</p>
<h3 id="사전학습">사전학습</h3>
<p>사전 학습 과목은 사진과 같았다. 
<img src="https://velog.velcdn.com/images/gm-15/post/79e56c08-fb2e-4f87-a10a-7e8bcba2f045/image.png" alt=""></p>
<p>지원서 제출 전까지 과목들을 이수해야 한다. 이수하면 이수증도 발급된다.
<img src="https://velog.velcdn.com/images/gm-15/post/b7bcf947-12f8-4aca-856b-57b891a71d91/image.png" alt=""></p>
<p>SK그룹원들에게 제공되는 강의라 그런지 강의 퀄리티는 전체적으로 높았다. 특히 <em>AI 리터러시 생성형 AI의 이해와 전망: 2025 AI Keywords</em> 과정은 들으면서 박수칠 정도로 유익했다. 김지현 CIO님이 강의를 해주셨는데 LMM, LAM, 온디바이스 AI, AI 에이전트에 대해 알기 쉽게 설명해주셔서 현재의 LLM 기술이 왜 중요한 지, 앞으로 어떤 게 중요한 지 단번에 알 수 있었다. 강의에서 다뤘던 ChatGPT의 플랫폼화가 글을 적는 지금 실제로 상용화됐는데 6개월이 전에 학습한 강의임에도 기억이 나서 기뻤다.</p>
<p>그 외에도 프롬프트 엔지니어링이나 문제해결 과목도 큰 도움이 되었다. 교육 이후 문제해결 방식을 이용해서 LLM 사용법을 나에게 적용하고 심지어는 남에게 알려주기도 했다.</p>
<p>지원서에 사전 학습 과정과 연관된 질문이 나오기 때문에 자신과 맞지 않는 과목이더라도 <strong>처음 학습 시 제대로 학습</strong>하는 걸 추천한다. 나중에 다른 학생들 얘기 들어보니 대충 들어서 여러 번 다시 듣느라 더 힘들었다고 했다.</p>
<h3 id="지원문항">지원문항</h3>
<p>지원 문항은 다음과 같았다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/gm-15/post/69b72487-80db-4a4c-9a56-50889874edfc/image.png" alt="">
지원 동기는 써니C 프로그램에서 자신에게 가장 필요한 부분을 살려서 작성하면 좋다.</p>
</blockquote>
<blockquote>
<p><img src="https://velog.velcdn.com/images/gm-15/post/67b80224-1be5-4d11-afc2-964dc6c5264d/image.png" alt="">
이 부분이 사전학습과 관련된 문항이다. 써니C는 &#39;일 경험&#39;을 중요하게 여기는 활동이다. 사는 곳도, 전공도 다른 사람들과 같이 프로젝트를 진행하려면 커뮤니케이션이 정말 중요하다. 일에서는 더 중요하다. 그렇기 때문에 사전학습부터 관련 교육을 제공하고 지원문항까지 작성하게 하는 것 같다.
나는 고등학교 때, 컴퓨터 동아리 폐부 위기를 마주하고 부장을 맡아 극복했던 과정을 작성했다. 대니얼 코일의 &#39;최고의 팀은 무엇이 다른가&#39;를 읽고 소속 신호의 중요성을 깨달아 &#39;존중&#39;을 바탕으로 새로운 부원들을 뽑고, 동아리의 전반적인 체계를 구성해나갔다. 결국 연말 교내 축제에서 수상까지 할 수 있었는데, 이 경험이 문항에서 요구하는 부분을 잘 보여줄 수 있을 것 같아 작성했다.</p>
</blockquote>
<blockquote>
<p><img src="https://velog.velcdn.com/images/gm-15/post/466919e7-e8c5-408e-8452-c0a0b57ac6e6/image.png" alt="">
이번 활동은 AI Tool을 사용하여 프로젝트를 진행하기 때문에 AI에 관한 문항이 있을 거라고 예상했는데 역시 있었다. 나는 AI기반 유휴 주차공간 매칭 서비스 ParkingMate를 구상하고 있었기 때문에 사전학습 과정과 엮어서 이렇게 작성했다.
<img src="https://velog.velcdn.com/images/gm-15/post/d8617943-eb66-4007-9f55-cec494f929c1/image.png" alt=""></p>
</blockquote>
<h2 id="ot">OT</h2>
<p>역시 대감집 대외활동이라 그런지 OT 장소가 엄청났다. OT는 워커힐 WAVEHILL에서 진행됐다. 원래 SK 임직원분들이 워크숍할 때 이용하시는 장소인데 우리를 위해 장소를 마련해주셨다..!
<img src="https://velog.velcdn.com/images/gm-15/post/a765e620-4258-48ec-bf9d-494649a65bec/image.jpg" alt="">
<img src="https://velog.velcdn.com/images/gm-15/post/5d9e85a8-ed55-4df7-941d-c54108929063/image.png" alt="">
오티가 시작되고 이수 기준, 프로그램 진행 계획, 4기 혜택 등의 소식들을 전해주셨다. 
<img src="https://velog.velcdn.com/images/gm-15/post/8f851496-3b3f-4feb-a1b5-a110557d0099/image.jpg" alt="">
출결 기준은 조금 엄격한 편이었다. 써니C 기간동안 매일 출석해야 되기 때문에 해당 기간에 다른 활동이나 일정과의 병행은 어려운 편이므로 참고 바란다.<br><img src="https://velog.velcdn.com/images/gm-15/post/607a83a5-9cdc-4b8a-8d24-1e21c4c1b577/image.jpg" alt="">
<img src="https://velog.velcdn.com/images/gm-15/post/67fe99b5-db64-4524-9da7-723235a20012/image.jpg" alt="">
프로젝트 진행 단계와 평가 기준도 안내되었는데, 해당 기준들은 프로젝트를 진행하고 발표 준비를 할 때 키 포인트로 삼았다.
<img src="https://velog.velcdn.com/images/gm-15/post/a64207da-e8bc-4a50-9d3b-aea3c9cc3840/image.jpg" alt="">
팀과 주제가 정해진 뒤로는 간단한 아이스 브레이킹 및 팀 이름, 팀 방향성을 잡는 시간을 가졌다. 사진 속 리스트의 강점 3가지를 골라 자신의 이름과 함께 소개하는 시간을 가졌다. 키워드가 꽤 세세하고 다양해서 팀원들의 성향을 파악하는데 도움이 되었다.</p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/b94321d1-7018-447c-9523-50276ce5ff6f/image.jpg" alt="">
<img src="https://velog.velcdn.com/images/gm-15/post/b9a8963b-0473-492a-9a37-7a6195ad7eed/image.jpg" alt="">
OT가 종료된 후, 4기 키트를 받았다. 우리 기수는 작은 캐리어와 에코백, 텀블러, 컵과 필기도구, 써니C 인형 등을 받았다. 작은 캐리어는 다뤄본 적이 없었는데 색감이 이뻐서 만족스러웠다.</p>
<h2 id="활동">활동</h2>
<p>프로젝트는 써니C측에서 Sk그룹의 현업자분들께 업무 중 생긴 불편한 점이나 어려운 부분을 미리 받아 주제로 선정하고 이를 우리가 AI Tool을 통해 해결해나가는 방식이었다.</p>
<p>주제를 제출해주신 멘토님과 한 팀씩 매칭되고 매주 방향성을 잡아가며 프로젝트를 제작하는 과정이다. 매주 멘토링과 AI 코칭이 진행되었다. AI 코치님들은 외부에서 오신 분들이었는데 한 분당 3팀 정도 코칭을 해주신 것 같다.</p>
<p>참고로 설문을 매 시간마다 진행하는데 이때 궁금한 점이나 프로젝트 진행 중 어려운 점을 적으면 바로 다음 시간에 반영을 해주신다. 프로젝트에 집중할 수 있게 빠른 피드백과 친절한 태도로 직원분들이 신경 써주셔서 활동 내내 관리받고있다는 느낌이 들었다.</p>
<h2 id="활동이후">활동이후</h2>
<p>프로젝트에 집중하며 매일 같이 회의, 활동을 하다보니 어느덧 8월이 훌쩍 지나가버렸다. 이번 활동에서는 지난 해커톤의 경험을 바탕으로 팀원들과의 소통과 유대를 신경쓰려고 노력했다. 팀원들의 전공이 컴퓨터쪽이 아니라서 개발 경험은 내가 제일 많은 편이었지만, 코딩 경험이 없더라도 소통이나 팀워크적 면에서 뛰어난 팀원들이 많아서 많은 것을 느끼고 배울 수 있었다. 아무래도 열심히 노력하는 친구들이 대외활동에 참여하다보니 모두가 강점을 하나씩은 가지고 있는 듯 했다.</p>
<p>활동 이후, Sk 현직자님이 추가적인 개발을 원하셨다. 대외활동에서 진행한 활동은 특정 AI Tool을 이용했기 때문에 비용 문제나 비효율적인 면이 존재했다. 그래서 1차적으로 MAKE로 제작한 우리 프로젝트를 Python+Streamlit으로 개발했다.</p>
<p><a href="https://youtu.be/YApunpY1TRc">MAKE로 제작한 프로젝트 시연 영상</a></p>
<p><img src="https://velog.velcdn.com/images/gm-15/post/4341e3bf-4029-49e3-9b27-fdbb3551fd27/image.png" alt=""></p>
<p>SK에 초청 받기도 했다. 기존 대외활동에서 진행한 우리 프로젝트의 구상단계, 문제점 해결 기준 등을 설명드리고 Python+Streamlit으로 개발한 INSK_V2 또한 시연했다. 감사하게도 Sk 개발자 두 분이 참석해주셔서 우리 프로젝트에 대해 많은 피드백과 조언을 주셨다. 엔터프라이즈급 프로젝트로의 발전을 위해 갖춰야 할 것들, 기술 스택들, 학업 조언등을 주셔서 앞으로의 계획을 쉽게 세울 수 있었다. </p>
<h2 id="마치며">마치며</h2>
<p>많은 대외활동을 참여해보지는 않았지만 써니C 활동은 확실히 알찬 활동이라는 것을 알 수 있었다. 성대한 OT와 엄청난 키트로 처음부터 동기부여를 얻고, 훌륭한 기초 강의를 시작으로 현업자분들의 실무 팁과 AI 팁들을 받으며 프로젝트를 진행하다보면 어느새 성장한 자신을 발견할 수 있을 것이다. 개인적으로는 추후 개발자님과의 미팅으로 써니C가 강조하는 &#39;일 경험&#39;까지 경험할 수 있어서 정말 만족스러웠다.</p>
<p>프로젝트에 관한 기술적인 부분은 이어지는 시리즈에서 포스팅할 계획이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[실리콘밸리 해외연수(2025.02)]]></title>
            <link>https://velog.io/@gm-15/%EC%8B%A4%EB%A6%AC%EC%BD%98%EB%B0%B8%EB%A6%AC-%ED%95%B4%EC%99%B8%EC%97%B0%EC%88%982025.02</link>
            <guid>https://velog.io/@gm-15/%EC%8B%A4%EB%A6%AC%EC%BD%98%EB%B0%B8%EB%A6%AC-%ED%95%B4%EC%99%B8%EC%97%B0%EC%88%982025.02</guid>
            <pubDate>Sat, 27 Sep 2025 10:50:34 GMT</pubDate>
            <description><![CDATA[<h1 id=""></h1>
<blockquote>
<p>기술의 성지, 실리콘밸리 엔지니어들의 생각과 철학</p>
</blockquote>
<h2 id="실리콘밸리">&#39;실리콘밸리?&#39;</h2>
<p>해외연수 이전, 나에게 실리콘밸리는 막연히 &#39;거대 IT 기업이 모여있는 IT업계의 성지&#39;였다. 하지만 일주일간의 해외연수가 끝나고 느낀 것은 사뭇 달랐다. 이곳은 단순히 테크 회사들이 모인 곳이 아니라, 세상의 문제를 기술로 해결하려는 사람들의 열정이 모여 폭발하는 용광로였다.</p>
<p>어디서도 듣기 힘든 실리콘밸리 현직 개발자들의 생생한 고민과 철학을 직접 들으며, &#39;좋은 개발자&#39;로 성장하기 위한 구체적인 이정표를 발견할 수 있었다. 이 글은 그 치열했던 일주일간의 배움과 성장에 대한 기록이다.</p>
<hr>
<h2 id="기업-탐방">기업 탐방</h2>
<h3 id="1-onehouse-스타트업의-속도와-기술적-깊이의-균형">1. Onehouse: 스타트업의 속도와 기술적 깊이의 균형</h3>
<p><img src="https://velog.velcdn.com/images/gm-15/post/1afc7fee-e688-4b47-a745-9fc384e8c500/image.jpg" alt=""></p>
<p><code>Apache Hudi</code> 기반 데이터 레이크하우스 Onehouse에서 이용균님은 <strong>&quot;필요한 것만 빠르게 학습하고 즉시 적용하는 실용성&quot;</strong>과 <strong>&quot;<code>LeetCode</code>, <code>CS 지식</code>의 탄탄한 기본기&quot;</strong>를 동시에 강조하셨다. 이를 통해 속도와 안정성, 두 마리 토끼를 모두 잡아야 하는 현대 백엔드 개발의 현실을 체감할 수 있었다.</p>
<h3 id="2-databricks-빅데이터-시대-본질에-집중하는-학습법">2. Databricks: 빅데이터 시대, 본질에 집중하는 학습법</h3>
<p><img src="https://velog.velcdn.com/images/gm-15/post/8c9acb1a-59aa-4426-beab-79dab0512a15/image.jpg" alt=""></p>
<p><strong>&quot;목표를 세워 부딪히며 만드는 경험&quot;</strong>과 &quot;프로그래밍 언어의 근본적인 특징 파악&quot;을 강조하신 이문수 개발자님의 조언은, 기술의 홍수 속에서 길을 잃지 않는 법이 되었다. 기술의 트렌드를 쫓기보다, 그 근본 원리를 이해하는 것이 얼마나 중요한지 깨닫게 되었다.</p>
<h3 id="3-google-대규모-시스템의-보이지-않는-디테일-성능-최적화">3. Google: 대규모 시스템의 보이지 않는 디테일, 성능 최적화</h3>
<p><img src="https://velog.velcdn.com/images/gm-15/post/5cb6558b-1f38-4845-9dcb-bd17c884bdde/image.jpg" alt=""></p>
<p><code>Android</code> 유튜브 앱 성능 관리 담당자 이재일님과의 대화는 충격이었다. </p>
<p>&quot;수많은 안드로이드 기기의 성능 편차로 인해 사용자들이 IOS로 이동한다. 사용자가 안드로이드도 쾌적하다고 느끼게 하기 위해 성능을 계속해서 안정시키는 중이다. 현재는 최적화를 할 수 있는 만큼 해서 더 성능을 향상시키려면 플랫폼쪽을 건드려야 해 플랫폼 부서와 마찰도 일어난다.&quot; </p>
<p><strong>기기 성능의 편차가 실질적인 사용자 이탈</strong>로 이어지는 상황에 대해 듣고, 백엔드 개발자에게 API 응답 속도 <code>1ms</code>를 줄이는 노력이 어떤 의미를 갖는지 생각해볼 수 있었다. 기술의 깊이는 보이지 않는 곳에서 발현된다는 것을 느꼈고, 백엔드의 매력을 실감할 수 있었다. <strong>추후 내가 진행할 프로젝트에서 이런 문제를 다뤄보고 싶다고 생각했다.</strong></p>
<h3 id="4-microsoft-창의적-활동이-미치는-영향">4. Microsoft: 창의적 활동이 미치는 영향</h3>
<p><img src="https://velog.velcdn.com/images/gm-15/post/332fe3f7-bee1-4ffa-8c6a-7885a3c6c48a/image.jpg" alt=""></p>
<p>심재은님의 안내에 따라 개발자들이 본업 외에도 창의적인 프로젝트를 수행하는 &quot;Garage&quot; 프로그램 현장을 방문할 수 있었다. 최신 장비 및 기술적 지원을 활용하여 단순한 연구 및 개발뿐만 아니라, 팀원들과 함께 보드게임을 즐기거나, 레이저 커팅, 3D 프린팅과 같은 다양한 기술을 시도해 볼 수 있었다. </p>
<p>개발자들이 샤워하다가 업무시간엔 풀리지 않던 문제를 무의식으로 풀곤 한다고 들은 적이 있다.
이곳에서 휴식을 취하며 다양한 활동들을 하다보면, 리프레시 되어 새로운 관점에서 문제를 해결할 수 있겠다는 생각이 들었다. 개발자들이 <strong>자율적이고 창의적으로 성장</strong>할 수 있도록 도와주는 프로그램 또한 중요하다는 것을 알 수 있었다.</p>
<h3 id="5-meta-선-빠른-개발-후-지속적-개선">5. Meta: 선 빠른 개발, 후 지속적 개선</h3>
<p>메타는 <strong>빠른 개발</strong>과 <strong>지속적인 개선</strong>을 중요하게 생각한다는 것을 장예원 개발자님을 통해 알 수 있었다. 
<img src="https://velog.velcdn.com/images/gm-15/post/4bed4b02-7e5f-4d9b-bf5f-fa8903ebcd00/image.jpg" alt=""></p>
<p>• 완벽한 코드보다는 빠른 개발 후 성능을 점진적으로 개선하는 방식
• 개발자가 주도적으로 프로젝트를 제안하고 실행하는 문화
• 본인의 업무량을 스스로 조정하고, 필요한 프로젝트를 맡아 진행</p>
<p>위와 같은 회사 문화를 통해 개발자의 <strong>주도적 능력</strong>과 <strong>MVP</strong>의 중요성을 알 수 있었다.</p>
<h3 id="6-intel-새로운-돌파구">6. Intel: 새로운 돌파구</h3>
<p><img src="https://velog.velcdn.com/images/gm-15/post/2f6aeb38-87ae-4ae7-aa48-3cb786c3c4e6/image.jpg" alt=""></p>
<p>CPU 내부 <code>NPU</code> 테스트를 담당하시는 강화석 개발자님과의 대화에서 무어의 법칙의 한계와 새로운 돌파구의 필요성을 들었다. </p>
<p>CPU의 성능이 1년에 2배씩 상승한다는 무어의 법칙은 최근 2년 주기로 변경되었고 이조차도 점점 한계를 보이고 있다고 한다. 10나노 이하의 초미세공정에 들어가면서 양자 터널효과가 발생해 물리적 크기만 줄이는 것에는 한계가 온 것이 그 이유다. 따라서 초전도체등의 다른 다양한 방식을 찾아야 한다는 것을 알 수 있었다.</p>
<p>또한 &quot;한 가지 특별한 강점과 다른 분야의 넓은 지식을 가진 <strong>T자형 인재</strong>&quot;를 선호한다는 조언을 들을 수 있었다. 신입 개발자에게 요구하는 능력이 점점 상향되는 상황에서 뾰족한 강점을 가진 T자형 인재가 되겠다는 생각을 했다.</p>
<hr>
<h2 id="기술을-넘어선-사람들의-이야기-개발자-간담회">기술을 넘어선 사람들의 이야기: 개발자 간담회</h2>
<p><img src="https://velog.velcdn.com/images/gm-15/post/13ad4647-7d5c-405e-8dfe-eb697c5c96b6/image.jpg" alt=""></p>
<p>이번 연수의 하이라이트는 단연 첫날 저녁의 개발자 간담회였다. 다양한 배경의 개발자들이 모여 기술에 대해 이야기하는 모습은 단순한 네트워킹을 넘어, 내가 앞으로 어떤 개발자가 되어야 하는지에 대한 깊은 영감을 주었다.</p>
<h3 id="순수한-열의">순수한 열의</h3>
<p>AI의 미래에 대해 서로의 의견을 존중하며 토론하고, 학생들의 질문 하나하나에 자신의 경험을 녹여 진심으로 답변해주는 시간을 가졌다. 계속된 토론에서 느낄 수 있었던 것은, 그들이 가지고 있는 <strong>&#39;순수한 열의&#39;</strong>였다. 다양한 나이대, 다양한 배경에서 다른 일을 하는 개발자들이었지만, 그 순수한 열의만큼은 모두가 같았다. 그 중엔 시각장애를 가진 개발자분도 계셨는데, 나레이션을 통해 코딩을 한다는 것을 듣고 그 열의가 대단하다고 느꼈다.</p>
<h3 id="관계의-중요성">관계의 중요성</h3>
<p>&quot;적을 적게 만들고, 진정성 있는 관계를 유지하라&quot;는 조언은 기술 커뮤니티 안에서 협업과 소통이 얼마나 중요한지 다시 한번 깨닫게 했다.</p>
<hr>
<h2 id="대학-탐방과-ai-시대의-생존-전략">대학 탐방과 AI 시대의 생존 전략</h2>
<p><img src="https://velog.velcdn.com/images/gm-15/post/44a2ee5e-21a3-4a2b-916a-8dddd3bb7c9f/image.jpg" alt=""></p>
<p><code>Stanford University</code>와 <code>San Jose State University</code>를 방문하며 교육의 진정한 가치와 능동적인 학습 태도의 중요성을 깨달았다. </p>
<p>앞선 여러 개발자와의 대화를 통해 &quot;실리콘밸리 시니어 개발자들도 AI 발전 속도에 위기감을 느낀다&quot;는 현실과 마주했었다. 그러나 역설적으로 AI 시대일수록 AI가 해결해주지 못하는 <strong>근본적인 문제 해결 능력, 즉 CS 기본기의 중요성</strong>이 더욱 부각된다는 것을 알게 되었고, 교육의 중요성과 능동적 학습 태도의 중요성을 체감할 수 있었다.</p>
<hr>
<h2 id="웨이모-탑승-체험-미래를-직접-경험하다">웨이모 탑승 체험: 미래를 직접 경험하다</h2>
<p><img src="https://velog.velcdn.com/images/gm-15/post/101c142f-5a07-442e-aba4-a7b1a4333c0a/image.jpg" alt=""></p>
<p>레벨 4 자율주행 기술을 직접 체험한 것은 깊은 기술적 성찰을 가져다주었다. 센서 데이터의 실시간 처리, 네트워크 지연 최소화, 안전성 확보라는 복합적 과제들이 어떻게 하나의 서비스로 통합되는지 몸소 체험할 수 있었다. 이 경험은 &quot;기술의 궁극적 목표는 사용자가 기술을 의식하지 않게 만드는 것&quot;이라는 깨달음이 되었다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>일주일간의 여정 끝에 실리콘밸리에서 발견한 것은 화려한 기술이나 거대한 사옥이 아니었다. 그것은 문제를 해결하려는 치열함, 기술에 대한 순수한 열정, 그리고 더 나은 세상을 만들고자 하는 개발자들의 진정성이었다.</p>
<p>특히 개발자 간담회에서 만난 분들의 열정은 마음속 깊은 곳에 새로운 불씨를 지펴주었다. 그들이 보여준 <strong>&quot;기술로 세상을 더 좋게 만들겠다&quot;</strong>는 신념을 나 또한 품고 싶다.</p>
<p>이 경험을 자양분 삼아, 끊임없이 배우고, 대담하게 도전하며, 언젠가는 나 또한 후배들에게 영감을 주는 개발자로 성장하고 싶다. 실리콘밸리에서 받은 그 뜨거운 열정을 개발자 여정의 영원한 동력으로 삼아, 기술로 사람들의 삶을 더 풍요롭게 만드는 개발자가 되겠다.</p>
<hr>
<h2 id="연수-후-7개월-영감을-프로젝트로-구체화하기">+연수 후 7개월: 영감을 프로젝트로 구체화하기</h2>
<p>실리콘밸리에서 돌아온 지 7개월이 지났다. 당시 느꼈던 뜨거운 열정과 영감은 막연한 다짐을 넘어, 구체적인 &#39;문제 해결&#39;을 위한 프로젝트 계획으로 발전했다. 연수에서 얻은 가장 큰 가르침인 <strong>&#39;기능 구현을 넘어, 근본적인 기술적 문제를 해결하는 엔지니어&#39;</strong>가 되기 위해 다음과 같은 프로젝트들을 준비하고 있다.</p>
<h3 id="1-parkingmate-공유-경제의-신뢰-문제를-해결하다">1. ParkingMate: 공유 경제의 &#39;신뢰&#39; 문제를 해결하다</h3>
<p>P2P 주차 공간 공유 플랫폼 <code>파킹메이트</code>는 공유 경제의 고질적인 &#39;신뢰&#39; 문제를 기술로 해결하는 데 집중한다. <code>Onehouse</code>와 <code>Google</code>에서 강조했던 <strong>대규모 트래픽 환경에서의 데이터 정합성</strong>의 중요성을 체감했기에, 이 프로젝트에서는 <strong>&#39;동시성 제어&#39;</strong>를 핵심 기술 목표로 삼았다. 여러 사용자가 특정 주차 공간을 동시에 예약하려 할 때 발생하는 <strong><code>Race Condition</code>을 <code>Locking</code> 메커니즘을 통해 해결</strong>하고, &#39;사진 증거 시스템&#39;을 도입하여 기술이 어떻게 사용자의 신뢰를 구축할 수 있는지 증명하고자 한다.</p>
<h3 id="2-냉장goat-단순-유틸리티를-실시간-협업-시스템으로">2. 냉장Goat: &#39;단순 유틸리티&#39;를 &#39;실시간 협업 시스템&#39;으로</h3>
<p>Google에서 1ms의 성능 개선이 사용자의 이탈을 막는다는 사실에 큰 충격을 받았던 것처럼, 이 프로젝트는 점심 피크타임에 수십 개의 POS 단말기에서 발생하는 동시 주문에도 단 하나의 오차 없이 재고 데이터의 정합성을 보장하는 것을 핵심 기술 목표로 삼는다. &#39;1인분 기준 실시간 재료 차감&#39; 기능은 Redis 분산 락을 이용한 동시성 제어 능력을 증명하는 핵심 과제이다.</p>
<p>나아가, 우리는 단순히 데이터를 기록하는 시스템을 넘어, &#39;AI 기반 스마트 발주 추천&#39; 기능을 통해 미래 수요를 예측하고 점주에게 최적의 의사결정을 돕는 &#39;능동적인 비즈니스 파트너&#39;를 구축하고자 한다. 이 과정은 Meta에서 배운 &quot;빠른 개발 후 점진적 개선&quot; 철학을 적용하여, 현실적인 &#39;모듈형 모놀리스&#39; 아키텍처 위에서 안정적으로 구현하고 고도화하는 경험의 장이 될 것이다.</p>
<h3 id="3-insk-v30-프로토타입을-실제-서비스로-고도화하다">3. INSK v3.0: &#39;프로토타입&#39;을 &#39;실제 서비스&#39;로 고도화하다</h3>
<p>&#39;AI 뉴스 트렌드 센싱 플랫폼&#39;은 SK 대외활동 써니C 4기에서 진행된 기존 <code>Python</code> 프로토타입을 실제 서비스 수준으로 고도화하는 프로젝트이다. <code>Meta</code>에서 배운 <strong>&quot;빠른 개발 후 점진적 개선&quot;</strong> 철학을 실제로 적용하는 사례다. 사용자의 피드백을 반영하여 확장성 없는 단일 사용자용 스크립트를, <strong><code>Java/Spring</code> 기반의 안정적인 백엔드와 <code>React</code> 기반의 인터랙티브 프론트엔드 아키텍처로 재설계</strong>한다. 이 과정을 통해 대규모 트래픽을 감당하고, 안정적인 <code>CI/CD</code> 파이프라인 위에서 지속적으로 발전하는 서비스를 만드는 경험을 쌓고자 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS와 함께하는 소중한 상명해커톤 참가 후기: AI 기반 학과 추천 서비스 '인상룩' 개발기]]></title>
            <link>https://velog.io/@gm-15/%EC%86%8C%EC%A4%91%ED%95%9C-%EC%83%81%EB%AA%85%ED%95%B4%EC%BB%A4%ED%86%A4-%EC%B0%B8%EA%B0%80-%ED%9B%84%EA%B8%B0-AI-%EA%B8%B0%EB%B0%98-%ED%95%99%EA%B3%BC-%EC%B6%94%EC%B2%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9D%B8%EC%83%81%EB%A3%A9-%EA%B0%9C%EB%B0%9C%EA%B8%B0</link>
            <guid>https://velog.io/@gm-15/%EC%86%8C%EC%A4%91%ED%95%9C-%EC%83%81%EB%AA%85%ED%95%B4%EC%BB%A4%ED%86%A4-%EC%B0%B8%EA%B0%80-%ED%9B%84%EA%B8%B0-AI-%EA%B8%B0%EB%B0%98-%ED%95%99%EA%B3%BC-%EC%B6%94%EC%B2%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9D%B8%EC%83%81%EB%A3%A9-%EA%B0%9C%EB%B0%9C%EA%B8%B0</guid>
            <pubDate>Sun, 31 Aug 2025 09:37:14 GMT</pubDate>
            <description><![CDATA[<h1 id="소중한-상명해커톤-참가-후기-ai-기반-학과-추천-서비스-인상룩-개발기">소중한 상명해커톤 참가 후기: AI 기반 학과 추천 서비스 &#39;인상룩&#39; 개발기</h1>
<blockquote>
<p><strong>&quot;기술이 아닌 협업과 문제해결의 경험&quot;</strong></p>
</blockquote>
<h2 id="해커톤-개요">해커톤 개요</h2>
<p><strong>주제</strong>: 기술기반 캠퍼스/GO 글로벌/융합/Life Long Learning을 주제로 한 AWS 인텔리전스 캠퍼스 해커톤<br><strong>개발 서비스</strong>: Slack/Canva/App-Web 등 Any Service<br><strong>결과</strong>: 최우수상 수상 🏆<br><strong>개발 기간</strong>: 24시간 (2024.08.30~08.31)</p>
<hr>
<h2 id="프로젝트-소개-인상룩">프로젝트 소개: &#39;인상룩&#39;</h2>
<p><strong>&quot;AI 관상 분석을 통한 무전공 신입생 맞춤 학과 추천 서비스&quot;</strong></p>
<h3 id="서비스-개요">서비스 개요</h3>
<ul>
<li><strong>타겟</strong>: 2025년 무전공으로 입학하는 신입생</li>
<li><strong>목적</strong>: 관상 분석을 통해 재미있고 직관적인 학과 탐색 경험 제공</li>
<li><strong>핵심 기능</strong>: 얼굴 이미지 → 관상 분석 → MBTI 변환 → 학과 추천 → 학과 사이트 연결</li>
</ul>
<h3 id="기술-스택-및-아키텍처">기술 스택 및 아키텍처</h3>
<ul>
<li><strong>얼굴 인식</strong>: DLIB 라이브러리 (오픈소스, Python 기반)</li>
<li><strong>분석 기준</strong>: 황금비율 대비 얼굴 특징 추출</li>
<li><strong>AI 활용</strong>: ChatGPT를 통한 학과별 MBTI 분류 데이터 생성</li>
<li><strong>프론트엔드</strong>: 사용자 친화적 웹 인터페이스</li>
</ul>
<hr>
<h2 id="개발-과정-24시간의-여정">개발 과정: 24시간의 여정</h2>
<h3 id="1단계-철저한-사전-준비의-중요성">1단계: 철저한 사전 준비의 중요성</h3>
<p><strong>&quot;준비된 자에게 기회는 온다&quot;</strong></p>
<p>첫 해커톤 참가라는 부담감에 사전 준비에 많은 시간을 투자했습니다.</p>
<ul>
<li><strong>AWS 사전 교육 수료</strong>: 클라우드 기반 개발 환경 이해</li>
<li><strong>아이디어 사전 검토</strong>: GPT, Gemini, Claude를 활용한 다각적 아이디어 발굴</li>
<li><strong>실제 데이터 수집</strong>: &#39;학교 맛집 찾기&#39; 아이디어 사전 준비로, 기숙사 공유 배달 카톡방에 접근하여 기숙사 배달 주문 데이터 2년치 확보 (22년 8월~24년)</li>
</ul>
<h3 id="2단계-팀-역학과-리더십의-도전">2단계: 팀 역학과 리더십의 도전</h3>
<p><strong>&quot;다양성 속에서 방향성을 찾아가는 과정&quot;</strong></p>
<p>4명의 해커톤 초참가자들로 구성된 팀에서 팀장 역할을 맡게 되었습니다.</p>
<p><strong>예상과 현실의 차이</strong>:</p>
<ul>
<li><strong>예상</strong>: 효율적이고 목표 지향적인 팀워크</li>
<li><strong>현실</strong>: 친목과 경험 중심의 편안한 분위기</li>
</ul>
<p><strong>리더십 학습 포인트</strong>:</p>
<ul>
<li>개인의 목표와 팀의 분위기 사이에서 균형점 찾기</li>
<li>준비한 아이디어를 팀원들과 공유하며 동기 부여하기</li>
<li>갑작스런 주제 변경(&#39;관상 파악하기&#39;)에 유연하게 대응하기</li>
</ul>
<h3 id="3단계-문제-정의와-솔루션-설계">3단계: 문제 정의와 솔루션 설계</h3>
<p><strong>&quot;엉뚱한 아이디어를 현실적인 서비스로 발전시키기&quot;</strong></p>
<p>팀원의 관상에 대한 흥미를 해커톤 주제와 연결시켜 <strong>&#39;관상 기반 학과 추천 서비스&#39;</strong>로 발전시켰습니다.</p>
<p><strong>아이디어 발전 과정</strong>:</p>
<pre><code>관상에 대한 흥미 
→ 관상 분석 프로그램 제작
→ 무전공 신입생 타겟 설정 
→ 재미있는 학과 탐색 경험 제공
→ 실용적인 학과 정보 연결</code></pre><p><strong>검증 과정</strong>: 중간 점검에서 심사위원들의 긍정적 피드백 확보</p>
<hr>
<h2 id="기술적-구현과-도전">기술적 구현과 도전</h2>
<h3 id="얼굴-인식-기술-선택">얼굴 인식 기술 선택</h3>
<p><strong>FACE++ vs DLIB 비교 분석</strong></p>
<ul>
<li><strong>FACE++</strong>: 우수한 성능, 유료 서비스</li>
<li><strong>DLIB</strong>: 충분한 성능, 오픈소스, 무료</li>
<li><strong>결정</strong>: 해커톤 환경과 예산을 고려해 DLIB 선택</li>
</ul>
<h3 id="핵심-알고리즘-설계">핵심 알고리즘 설계</h3>
<p><strong>5단계 처리 파이프라인</strong>:</p>
<ol>
<li><strong>얼굴 인식</strong>: DLIB를 통한 68개 특징점 추출</li>
<li><strong>특징 분석</strong>: 황금비율 대비 얼굴 특징 수치화</li>
<li><strong>성격 매핑</strong>: 얼굴 특징을 성격 지표로 변환</li>
<li><strong>MBTI 변환</strong>: 성격 지표를 MBTI 유형으로 분류</li>
<li><strong>학과 추천</strong>: MBTI 기반 최적 학과 매칭</li>
</ol>
<h3 id="데이터-설계의-창의성">데이터 설계의 창의성</h3>
<p><strong>학과별 MBTI 분류</strong>: ChatGPT를 활용하여 각 학과의 특성을 MBTI로 체계화</p>
<ul>
<li>객관적 기준 부재 문제를 창의적으로 해결</li>
<li>AI의 장점을 적극 활용한 데이터 생성</li>
</ul>
<hr>
<h2 id="프로젝트-성과-및-임팩트">프로젝트 성과 및 임팩트</h2>
<h3 id="🏆-수상-결과-최우수상">🏆 수상 결과: 최우수상</h3>
<ul>
<li><strong>심사 기준</strong>: 기술성, 창의성, 실용성, 발표력</li>
<li><strong>차별점</strong>: 타겟 명확화 + 재미있는 접근 + 실용적 연결</li>
</ul>
<h3 id="비즈니스-임팩트">비즈니스 임팩트</h3>
<ul>
<li><strong>사용자</strong>: 무전공 신입생의 학과 선택 고민 해결</li>
<li><strong>학교</strong>: 효과적인 학과 홍보 도구</li>
<li><strong>확장성</strong>: 타 대학교 적용 가능한 모델</li>
</ul>
<hr>
<h2 id="핵심-학습-포인트">핵심 학습 포인트</h2>
<h3 id="1-커뮤니케이션의-중요성">1. 커뮤니케이션의 중요성</h3>
<p><strong>&quot;기술보다 사람이 먼저다&quot;</strong></p>
<p>해커톤에서 가장 큰 도전은 기술이 아닌 팀원들과의 소통이었습니다.</p>
<ul>
<li><strong>다양한 성향의 팀원들과 공통 목표 설정하기</strong></li>
<li><strong>의견 충돌 시 건설적 방향으로 해결하기</strong></li>
<li><strong>각자의 강점을 파악하고 역할 분배하기</strong></li>
</ul>
<p><strong>실무 적용점</strong>: 실제 개발팀에서도 기술적 역량만큼 소통 능력이 중요함을 이해</p>
<h3 id="2-자체-개발-능력의-필요성-인식">2. 자체 개발 능력의 필요성 인식</h3>
<p><strong>&quot;LLM 의존도를 줄이고 근본적인 개발 실력을 키우자&quot;</strong></p>
<p>해커톤 과정에서 LLM에 과도하게 의존하는 자신을 발견했습니다.</p>
<ul>
<li><strong>문제 인식</strong>: 기본적인 알고리즘 구현에서도 AI 도구에 의존</li>
<li><strong>해결 방안</strong>: 자료구조, 알고리즘 등 CS 기초 과목 집중 이수</li>
<li><strong>후속 행동</strong>: 소프트웨어에 집중하고자 스마트정보통신공학과 → 소프트웨어학과 전과 결정, 알고리즘 실습 과목 A+ 달성</li>
</ul>
<p><strong>장기적 목표</strong>: AI 도구를 보조 수단으로 활용하되, 핵심 개발 능력은 자체적으로 확보</p>
<h3 id="3-프로젝트-관리와-시간-분배">3. 프로젝트 관리와 시간 분배</h3>
<p><strong>&quot;마라톤 같은 해커톤에서 배운 전체적 관점&quot;</strong></p>
<p>24시간이라는 제한된 시간에서 효율적인 작업 분배를 경험했습니다.</p>
<ul>
<li><strong>완벽주의의 함정</strong>: 한 단계에 모든 시간을 투자하는 위험성</li>
<li><strong>전체적 관점</strong>: 기획-개발-발표의 균형잡힌 시간 배분</li>
<li><strong>우선순위 설정</strong>: 핵심 기능 우선 구현 후 부가 기능 추가</li>
</ul>
<p><strong>실무 연관성</strong>: 실제 업무에서도 데드라인 내 전체 프로세스를 관리하는 능력의 중요성</p>
<hr>
<h2 id="이후-성장-계획">이후 성장 계획</h2>
<h3 id="단기-목표">단기 목표</h3>
<ul>
<li><strong>CS 기초 역량 강화</strong>: 자료구조, 알고리즘 심화 학습</li>
<li><strong>웹 개발 실력 향상</strong>: Spring Boot, React 등 실무 기술 습득</li>
<li><strong>포트폴리오 확장</strong>: 해커톤 경험을 바탕으로 한 개인 프로젝트 진행</li>
</ul>
<h3 id="장기-비전">장기 비전</h3>
<ul>
<li><strong>AI 서비스 개발자</strong>: 기술적 기반 위에서 AI를 활용하는 개발자</li>
<li><strong>팀 리더십</strong>: 다양한 팀 프로젝트 경험을 통한 리더십 역량 강화</li>
<li><strong>문제해결형 개발자</strong>: 단순 구현이 아닌 비즈니스 문제를 해결하는 개발자</li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>첫 해커톤 참가는 단순히 상을 받는 것을 넘어서 <strong>개발자로서의 방향성을 명확히 하는 계기</strong>가 되었습니다. </p>
<p>기술적 완성도보다는 <strong>문제 정의의 정확성</strong>과 <strong>팀워크의 중요성</strong>, 그리고 <strong>지속적인 학습의 필요성</strong>을 깊이 깨달았습니다.</p>
<p>앞으로도 이런 도전적인 경험들을 통해 성장해나가는 개발자가 되고 싶습니다.</p>
<hr>
<p><strong>🔗 관련 링크</strong></p>
<ul>
<li>[GitHub Repository][시연 영상][발표 자료]
<a href="https://github.com/gm-15/aws_smu_hackathon">https://github.com/gm-15/aws_smu_hackathon</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[복잡도, 점근적 복잡도, 검색]]></title>
            <link>https://velog.io/@gm-15/%EB%B3%B5%EC%9E%A1%EB%8F%84-%EC%A0%90%EA%B7%BC%EC%A0%81-%EB%B3%B5%EC%9E%A1%EB%8F%84-%EA%B2%80%EC%83%89</link>
            <guid>https://velog.io/@gm-15/%EB%B3%B5%EC%9E%A1%EB%8F%84-%EC%A0%90%EA%B7%BC%EC%A0%81-%EB%B3%B5%EC%9E%A1%EB%8F%84-%EA%B2%80%EC%83%89</guid>
            <pubDate>Fri, 20 Dec 2024 10:06:42 GMT</pubDate>
            <description><![CDATA[<p>** 똑같은 결과라 하더라도 성능이 더 좋은 알고리즘. 
 즉, <em>효율적인</em> 알고리즘을 짜는 것이 중요!**
<bar><bar></p>
<h3 id="복잡도complexity">복잡도(complexity)</h3>
<blockquote>
</blockquote>
<p>-시간(Time complexity) : 계산량
-공간(Space complexity) : 메모리</p>
<p>  계산량이 적은 
복잡도가 낮은 
효율성이 높은 
알고리즘을 짜야 함.
<bar><bar>
  <bar><bar>
    <bar><bar>
      <bar><bar>
        <bar><bar>
          <bar><bar></p>
<h3 id="점근적-복잡도">점근적 복잡도</h3>
<blockquote>
<p><strong>점근적 복잡도</strong> : (데이터의 양) n -&gt; ∞</p>
</blockquote>
<p>값이 늘어날 수록 최고차항의 차수가 중요함. 
우리는 점근적 복잡도에서 n이 무한대로 커진다는 것을 가정함.
ㄴ&gt; O(n)으로 쓰는 이유.</p>
<p>O(1) : 데이터의 양이 커져도 계산량이 증가하지 않는다. 항상 상수식.</p>
<p><bar><bar>
  <bar><bar>
    <bar><bar>
      <bar><bar>
        <bar><bar>
          <bar><bar>  </p>
<h3 id="재귀">재귀</h3>
<blockquote>
<p>재귀 = 자기호출(recursion)
<bar>
재귀적 구조 : 어떤 문제 안에 크기만 다를 뿐 성격이 똑같은 문제가 포함되어 있는 것.
예) factorial, 수열의 점화식</p>
</blockquote>
<p>각각의 함수 안에 그 함수 이름이 있어야 함.</p>
<p>$$n!$$</p>
<ul>
<li><p>$$n(n-1)(n-2)…$$
반복적 사고, 반목문적</p>
</li>
<li><p>$$n*(n-1)!$$
순환적, 재귀적, 귀납적 사고</p>
</li>
</ul>
<p><bar><bar>
  <bar><bar>
    <bar><bar>
      <bar><bar>
        <bar><bar>
          <bar><bar></p>
<h3 id="점근적-상한하한동일">점근적 상한/하한/동일</h3>
<blockquote>
<p>$$O(n)$$ - 점근적 상한(빅오)
$$Ჲ(n)$$ - 점근적 하한(빅오메가)
$$Θ(n)$$ - 점근적 동일(빅세타)</p>
</blockquote>
<p>$$O(n)$$ : 기껏해야(at most) $$g(n)$$의 비율로 증가하는 함수
    어떤 함수의 최대치, 천장값?
    예) $$3n^2, 7n^2-3n, nlogn+5n, 3n,….$$
    $$3n$$ 같은 경우 $$O(n^2)$$가 틀리진 않지만 권장x</p>
<p>$$Ჲ(n)$$ : 적어도(at least) $$g(n)$$의 비율로 증가하는 함수
     어떤 함수의 최소치, 바닥값?
    예) $$n^2, n^3, 2^n$$
    $$2^n$$의 경우 $$Ჲ(n^2)$$가 틀리지 않지만 권장x</p>
<p>$$Θ(n)$$ : $$g(n)$$의 비율로 증가하는 함수
    동일한 차수만 포함.
    예)$$n^2, 2n^2$$
    다른 차수는 틀림.
  <bar><bar>
  <bar><bar>
    <bar><bar>
      <bar><bar>
        <bar><bar>
          <bar><bar></p>
<h3 id="검색">검색</h3>
<p>계산 기준 -&gt; &#39;비교&#39;
| 최선의 경우 (Best case) | $B(n)$       | $O(1)$     |
| 최악의 경우 (Worst case) | $W(n)$       | $O(n)$     |
| 평균의 경우 (Average case) | $A(n)$       | $O(n)$     |</p>
<p>최악에 초점을 둠. </p>
<blockquote>
<p>-순차검색(sequential search) = $O(n)$
n개의 수에 원하는 값이 없다는 것은 n번만에 알 수 있다.</p>
</blockquote>
<blockquote>
<p>-이진검색(binary search)  실행 방법과 성능에 대한 충분한 이해 필수
전제조건 : 정렬된 데이터
$log_2(n)$</p>
</blockquote>
<p>처음과 끝값을 더하고 반으로 나눠 찾는 숫자가 그보다 크거나 작으면 해당 방향만 봐도 된다.</p>
<blockquote>
<p>&#39;이진검색알고리즘, binary search algorithm은 단순하지만 강력한 성능을 가지고 있는 알고리즘입니다.
<bar>
비록, 정렬된 데이터라는 전제 조건이 필요하지만, 전제 조건만 만족한다면 중간 값을 이용하는 방식을 통해, log2의 n번만에 원하는 값의 존재 유무와 위치를 알 수 있습니다.&#39;</p>
</blockquote>
<p>T(n) = T(1/2) + 1
.
.
.
.
Log n번 반복</p>
]]></description>
        </item>
    </channel>
</rss>