<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>banana-wuyu.log</title>
        <link>https://velog.io/</link>
        <description>Java/Kotlin Spring 개발자 황재명입니다.</description>
        <lastBuildDate>Sat, 18 Apr 2026 06:52:10 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>banana-wuyu.log</title>
            <url>https://velog.velcdn.com/images/banana-wuyu/profile/b704c4ed-d6fc-40ec-a0bc-19eedf8b875b/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. banana-wuyu.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/banana-wuyu" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Datadog → AI 수정 → GitLab MR 자동화]]></title>
            <link>https://velog.io/@banana-wuyu/Datadog-AI-%EC%88%98%EC%A0%95-GitLab-MR-%EC%9E%90%EB%8F%99%ED%99%94</link>
            <guid>https://velog.io/@banana-wuyu/Datadog-AI-%EC%88%98%EC%A0%95-GitLab-MR-%EC%9E%90%EB%8F%99%ED%99%94</guid>
            <pubDate>Sat, 18 Apr 2026 06:52:10 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기전-내가-한-작업과-ai가-한-작업">들어가기전 내가 한 작업과 AI가 한 작업</h1>
<h2 id="내가-한-작업">내가 한 작업</h2>
<ul>
<li>5개 인터페이스 분해, 의존성 방향, 계층 구조, 워크 플로우</li>
<li>수집 → 분류 → 백로그 → 수정 → MR의 파이프라인 설계</li>
<li>2단계 분류 게이트, 캐싱 전략, 메서드 단위 추출 등 기능과 목적 정의</li>
<li>&quot;수정 정확도를 높여라&quot;, &quot;토큰 비용을 줄여라&quot; 같은 목적을 제시하고 최신 연구를 찾아 적용하도록 지시</li>
<li>테스트 결과 분석 및 개선점 도출</li>
<li>자동화 범위 설정</li>
<li>목적 정리 및 이유</li>
</ul>
<h2 id="ai가-한-작업">AI가 한 작업</h2>
<ul>
<li>목적에 맞는 최신 연구/논문을 찾아서 설계에 반영 (Meta-Harness, 토큰 최적화 등)</li>
<li>설계 명세에 따라 인터페이스, 구현체, 비즈니스 로직, 테스트 코드 작성</li>
<li>스택트레이스를 보고 실제 운영 코드의 버그 수정</li>
<li>실패한 수정 시도를 분석해 다음 전략 제안 (Counterfactual Diagnosis)</li>
</ul>
<p>설계 문서를 작성하고, 테스트 결과를 분석하고, 아키텍처 방향을 잡는 것은 사람의 몫이다. AI는 그 설계대로 구현하고, 정해진 범위 안에서 코드를 수정하는 도구다.</p>
<hr>
<h1 id="왜-만들었는가">왜 만들었는가</h1>
<p><a href="https://velog.io/@banana-wuyu/Claude-Code%EB%A1%9C-%EA%B0%9C%EB%B0%9C-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EA%B0%9C%EC%84%A0%EC%8B%9C%EB%8F%84">이전에 Notion 백로그를 파싱해서 스펙과 프롬프트를 자동 생성하는 파이프라인을 만들어 봤다.</a> 결과는 실패였다. 트레이드오프나 비즈니스 로직이 요구사항대로 반영이 잘 안 됐고, human review 단계에서 결국 매번 수정 요청을 다시 해야 했다.</p>
<p>그 경험에서 하나 배운 게 있다. <strong>AI 자동화가 통하려면 &quot;정답이 하나인 영역&quot;이어야 한다.</strong></p>
<p>신규 기능 구현이나 비즈니스 로직 변경은 정답이 하나가 아니다. 왜 이 방향으로 만드는지, 어떤 트레이드오프가 있는지, 기획자와 개발자가 함께 고민해야 한다.</p>
<p>반면 서비스 운영 중 발생하는 오류(NullPointerException, NumberFormatException 등)와 N+1 쿼리 문제는 다르다. 문제가 명확하고, 테스트 케이스가 분명하다. 스택트레이스가 정확히 어디서 문제가 발생했는지 가리키고, 오류 메시지가 무엇이 잘못됐는지 알려준다. 수정 방향에 트레이드오프가 없다.</p>
<p>그런데 이 작업에도 개발자가 매번 같은 절차를 반복한다.</p>
<ol>
<li>Datadog 알림 확인 (슬랙 또는 이메일)</li>
<li>Error Tracking 화면에서 스택트레이스 열기</li>
<li>해당 코드 찾기 — IDE에서 클래스명 검색, 메서드 위치 특정</li>
<li>원인 분석 — 왜 NPE가 났는지, 어떤 경로에서 null이 들어왔는지</li>
<li>코드 수정 — Optional 처리, null 체크, @EntityGraph 추가 등</li>
<li>테스트 작성 또는 기존 테스트 수정</li>
<li>PR 작성 — 변경 사유, 스택트레이스, Datadog 링크 첨부</li>
<li>리뷰 요청 → 머지</li>
</ol>
<p>Datadog, Sentry 같은 관측 도구는 오류를 <strong>발견</strong>해 준다. 하지만 <strong>수정</strong>까지 해 주지는 않는다.</p>
<p>반대로 GitHub Copilot, Cursor 같은 AI 코딩 도구는 코드를 <strong>수정</strong>해 준다. 하지만 &quot;무엇을 수정해야 하는지&quot;를 사람이 판단해서 알려줘야 한다.</p>
<p><strong>발견과 수정 사이의 빈 공간</strong>을 메우는 파이프라인을 만들었다. 이전 시도에서 실패한 &quot;기능 구현 자동화&quot;가 아니라, <strong>문제가 명확한 오류 수정 자동화</strong>다.</p>
<h2 id="자동화-범위를-먼저-정했다">자동화 범위를 먼저 정했다</h2>
<p>모든 오류를 자동으로 고칠 수 있다는 뜻이 아니다. 자동화가 통하는 영역은 다음 세 조건을 동시에 만족하는 경우로 한정했다.</p>
<ol>
<li><strong>증거가 있다</strong> — 스택트레이스가 정확한 위치를 가리킨다</li>
<li><strong>수정 패턴이 정형화되어 있다</strong> — <code>Optional.get()</code> → <code>ifPresent()</code>, N+1 → <code>@EntityGraph</code> 등</li>
<li><strong>비즈니스 판단이 필요 없다</strong> — &quot;이 로직을 어떻게 바꿀 것인가&quot;가 아니라 &quot;이 버그를 어떻게 고칠 것인가&quot;의 문제다</li>
</ol>
<p>이 조건을 벗어나는 작업 — 신규 기능, 비즈니스 로직 변경, DB 스키마 수정 — 은 처음부터 자동화 범위에서 제외했다. 이전 시도에서 배운 교훈이다.</p>
<h2 id="ai에게-최신-연구를-찾아서-적용하라고-했다">AI에게 최신 연구를 찾아서 적용하라고 했다</h2>
<p>이 프로젝트는 AI(Claude Code)로 개발했다. 내가 한 것은 <strong>기능과 목적을 정의</strong>한 것이고, 구현은 AI가 했다.</p>
<p>구현을 시키면서 두 가지를 요구했다.</p>
<p>첫 번째는 <strong>하네스 엔지니어링 적용</strong>이다. 나는 하네스 엔지니어링의 기능과 목적 — AI 수정이 실패했을 때 같은 실수를 반복하지 않도록 매 시도마다 접근 방식을 개선하는 것 — 을 알고 있었고, 이걸 적용하라고 지시했다. AI가 <a href="https://arxiv.org/abs/2603.28052">Stanford IRIS Lab의 Meta-Harness 연구</a>를 참고해서 실패-학습 루프를 구현했다.</p>
<p>두 번째는 <strong>AI 토큰 비용 절감</strong>이다. 이쪽은 구체적인 방법론을 지정하지 않고, <strong>&quot;토큰 비용을 줄여라, 최신 연구와 논문을 찾아서 적용해라&quot;</strong>라고 지시했다. AI가 토큰 효율에 관한 연구들을 참고해서 메서드 단위 소스코드 추출, 캐싱 전략 등을 구현했다.</p>
<p>논문을 내가 직접 읽고 이해해서 설계에 반영한 것이 아니다. 내 역할은 <strong>기능과 목적을 정의</strong>하고, AI가 구현한 결과가 그 목적에 맞는지 검증하는 것이었다.</p>
<hr>
<h1 id="전체-워크플로우">전체 워크플로우</h1>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/e69f2f8a-843c-43cb-899d-edff4b197e0d/image.png" alt=""></p>
<p>Datadog에서 오류를 읽는 것부터 GitLab MR을 여는 것까지 사람 손이 닿지 않는다. 개발자가 개입하는 시점은 <strong>MR 검토 한 번뿐</strong>이다.</p>
<h3 id="①-datadog-rest-api로-오류와-n1을-수집한다">① Datadog REST API로 오류와 N+1을 수집한다</h3>
<ul>
<li>Error Tracking API로 NPE, SQL 예외 수집</li>
<li>Spans Analytics API로 N+1 쿼리 탐지</li>
<li>수집된 오류를 서비스별, 유형별로 정리</li>
</ul>
<h3 id="②-수정할-가치가-있는-이슈만-분류한다">② 수정할 가치가 있는 이슈만 분류한다</h3>
<p><strong>1차 — 규칙 필터</strong></p>
<ul>
<li>FE 서비스, HTTP 4xx, Business/Validation 예외 → 스킵</li>
<li>NPE/SQL → HIGH, N+1 → MEDIUM</li>
</ul>
<p><strong>2차 — AI 사전 검증</strong></p>
<ul>
<li>소스코드를 함께 보고 실제 코드 버그인지 판단</li>
<li><code>NEEDS_FIX</code> → 수정 진행 / <code>NOT_CODE_ISSUE</code>, <code>BUSINESS_EXCEPTION</code> → 스킵</li>
</ul>
<h3 id="③-notion-백로그를-발행한다">③ Notion 백로그를 발행한다</h3>
<ul>
<li>등록된 Notion DB 템플릿 속성을 읽어 포맷에 맞춰 title과 body만 채워서 생성</li>
<li>백로그 ID 발급</li>
</ul>
<h3 id="④-git-flow-브랜치를-생성한다">④ git-flow 브랜치를 생성한다</h3>
<ul>
<li><code>fix/{service}-{error_type}-{date}-{attempt}</code></li>
<li>예: <code>fix/order-service-NPE-20260418-1</code></li>
</ul>
<h3 id="⑤-로컬-프로젝트에서-ai가-코드를-수정한다">⑤ 로컬 프로젝트에서 AI가 코드를 수정한다</h3>
<ul>
<li>스택트레이스에서 메서드 단위 소스코드 추출</li>
<li>CLAUDE.md에서 프로젝트 코딩 컨벤션 로딩</li>
<li>Datadog MCP Server로 추가 트레이스/로그 탐색 (스택트레이스만으로 부족할 때)</li>
<li>AI 수정안 생성 → 커밋 → CI 파이프라인 실행</li>
<li>PASS → ⑥으로</li>
<li>FAIL → 실패 원인 분석 → 전략 변경 → 재시도 (Meta-Harness 루프, 최대 5회)</li>
</ul>
<h3 id="⑥-mr을-올린다">⑥ MR을 올린다</h3>
<ul>
<li>GitLab MR 자동 생성</li>
<li><strong>개발자가 MR을 검토하고 머지한다</strong> ← 유일한 사람 개입 지점</li>
</ul>
<hr>
<h1 id="아키텍처">아키텍처</h1>
<h2 id="인터페이스로-분리한-이유">인터페이스로 분리한 이유</h2>
<p>Datadog을 Sentry로, Notion을 Jira로, GitLab을 GitHub으로 바꿔도 비즈니스 로직은 안 바뀌어야 한다. 모든 외부 연동을 5개의 인터페이스(ABC)로 추상화했다.</p>
<table>
<thead>
<tr>
<th>인터페이스</th>
<th>역할</th>
<th>교체 가능 예시</th>
</tr>
</thead>
<tbody><tr>
<td>ErrorCollector</td>
<td>오류 수집</td>
<td>Datadog → Sentry, New Relic</td>
</tr>
<tr>
<td>AIAgent</td>
<td>AI 코드 수정 + 사전 검증 + 하네스 제안</td>
<td>Claude → GPT, Gemini</td>
</tr>
<tr>
<td>VCSClient</td>
<td>브랜치/커밋/MR/파이프라인</td>
<td>GitLab → GitHub</td>
</tr>
<tr>
<td>IssueTracker</td>
<td>백로그 등록/업데이트</td>
<td>Notion → Jira, Linear</td>
</tr>
<tr>
<td>HarnessStore</td>
<td>수정 시도 트레이스 저장</td>
<td>파일시스템 → DB</td>
</tr>
</tbody></table>
<p>예를 들어 <code>AIAgent</code>는 세 가지 메서드만 정의한다.</p>
<pre><code class="language-python">class AIAgent(ABC):
    @abstractmethod
    def fix_code(self, context: ErrorContext) -&gt; FixResult: ...

    @abstractmethod
    def propose_harness(self, traces: List[ExecutionTrace]) -&gt; Harness: ...

    @abstractmethod
    def validate_issue(self, context: ErrorContext) -&gt; ValidationResult: ...</code></pre>
<p>비즈니스 로직(<code>MetaHarnessLoop</code>, <code>Classifier</code> 등)은 이 인터페이스들에만 의존한다. 구체 구현체는 CLI 조립 시점에서 주입한다. 드라이런 모드에서는 <code>LocalVCSClient</code>를, 실전에서는 <code>GitLabClient</code>를 넣는데, 비즈니스 로직 쪽은 어떤 구현체가 들어왔는지 모른다.</p>
<h2 id="의존성-방향">의존성 방향</h2>
<ul>
<li><strong>CLI / Hook</strong> → FullRunner / StepRunner → <strong>비즈니스 로직</strong> → <strong>인터페이스</strong> ← 구현체</li>
</ul>
<p>상위 계층은 하위 인터페이스에만 의존한다. 구현체가 인터페이스를 구현할 뿐, 비즈니스 로직이 구현체를 직접 참조하는 경우는 없다.</p>
<h2 id="도메인-모델">도메인 모델</h2>
<p>시스템을 흐르는 데이터는 16개의 dataclass와 6개의 enum으로 정의했다. 흐름은 다음과 같다.</p>
<ul>
<li>Datadog API 응답 → <strong>ErrorEvent</strong> (서비스, 예외 클래스, 스택트레이스, 발생 횟수)</li>
<li>Classifier → <strong>Issue</strong> (ErrorEvent + 계층, 심각도, 레포 정보)</li>
<li>MetaHarnessLoop → <strong>ErrorContext</strong> (Issue + 메서드 단위 소스코드 + 이전 실패 전략 + 프로젝트 컨벤션)</li>
<li>AIAgent → <strong>FixResult</strong> (수정 성공 여부, 변경 파일, diff)</li>
</ul>
<p>각 단계에서 필요한 데이터만 담아서 넘긴다.</p>
<hr>
<h1 id="datadog-연동">Datadog 연동</h1>
<h2 id="오류-수집--error-tracking-api">오류 수집 — Error Tracking API</h2>
<p><a href="https://docs.datadoghq.com/error_tracking/">Datadog Error Tracking</a>은 운영 중 발생한 예외를 그룹화해 관리한다. <a href="https://docs.datadoghq.com/api/latest/error-tracking/">Error Tracking API</a>로 최근 7일 오류를 수집한다. 스택트레이스, 예외 클래스명, 발생 횟수, 최초 발생 시각이 함께 반환된다.</p>
<h2 id="n1-수집--spans-analytics-api">N+1 수집 — Spans Analytics API</h2>
<p><a href="https://docs.datadoghq.com/tracing/recommendations/">Datadog APM Recommendations</a>에서 N+1 패턴을 UI로 확인할 수 있지만, 해당 데이터는 공개 REST API로 직접 접근이 불가능하다.</p>
<p><a href="https://docs.datadoghq.com/api/latest/spans/">Spans API</a>로 동일한 데이터를 구성했다.</p>
<blockquote>
<p><strong>N+1 스코어 = count(DB 스팬) / cardinality(@trace_id)</strong>
트레이스당 평균 동일 쿼리 호출 횟수. 5 이상이면 N+1로 판정.</p>
</blockquote>
<hr>
<h1 id="2단계-분류--ai에게-다-넘기지-않는다">2단계 분류 — AI에게 다 넘기지 않는다</h1>
<p>AI에게 모든 오류를 넘기면 비용과 시간 모두 낭비된다. 수정할 가치가 있는 이슈만 골라내는 게 먼저다.</p>
<h2 id="1단계-규칙-필터">1단계: 규칙 필터</h2>
<pre><code class="language-python">class Classifier:
    def _classify_error(self, event: ErrorEvent) -&gt; Optional[Issue]:
        if service_info.get(&quot;layer&quot;) == &quot;FE&quot;:
            return None                          # FE → BE 코드 수정 불가
        if event.http_status in {400, 401, 403, 404, 422}:
            return None                          # 4xx → 클라이언트 문제
        if any(kw in event.exception_class for kw in (&quot;Business&quot;, &quot;Validation&quot;, &quot;Domain&quot;)):
            return None                          # 의도된 예외
        # NPE → HIGH, SQL → HIGH, N+1 → MEDIUM</code></pre>
<h2 id="2단계-ai-사전-검증">2단계: AI 사전 검증</h2>
<p>규칙을 통과한 이슈에 대해 AI가 소스코드를 함께 보고 최종 판단한다.</p>
<table>
<thead>
<tr>
<th>판정</th>
<th>의미</th>
<th>동작</th>
</tr>
</thead>
<tbody><tr>
<td><code>NEEDS_FIX</code></td>
<td>실제 코드 버그</td>
<td>수정 진행</td>
</tr>
<tr>
<td><code>ALREADY_FIXED</code></td>
<td>이미 고쳐진 코드</td>
<td>스킵</td>
</tr>
<tr>
<td><code>BUSINESS_EXCEPTION</code></td>
<td>의도된 예외 흐름</td>
<td>스킵</td>
</tr>
<tr>
<td><code>NOT_CODE_ISSUE</code></td>
<td>인프라/DB/설정 문제</td>
<td>스킵</td>
</tr>
<tr>
<td><code>NEEDS_REVIEW</code></td>
<td>AI 판단 불가</td>
<td>개발자에게 위임</td>
</tr>
</tbody></table>
<p>결과는 캐시에 저장된다. 스킵 판정은 7일, 수정 판정은 24시간 TTL로 분리했다. 수정 판정의 TTL이 짧은 이유는 코드가 바뀌면 판단도 바뀌어야 하기 때문이다.</p>
<hr>
<h1 id="meta-harness--실패할-때마다-접근을-바꾼다">Meta-Harness — 실패할 때마다 접근을 바꾼다</h1>
<p>AI 코드 수정의 가장 큰 과제는 정확도다.</p>
<p>단순 재시도(Naive Retry)는 동일한 실패를 반복한다. 사람이 프롬프트를 수정하면 새로운 실패 유형에 다시 사람이 개입해야 한다. Meta-Harness는 이 문제를 풀기 위해 <strong>AI에게 무엇을 줄 것인가(하네스)를 매 시도마다 개선</strong>한다.</p>
<h2 id="메서드-단위-추출">메서드 단위 추출</h2>
<p>파일 전체를 AI에게 주면 비용이 폭증하고, 관련 없는 메서드를 &quot;개선&quot; 대상으로 오인하는 문제가 있었다. 스택트레이스에서 클래스와 메서드명을 파싱하고, 메서드 경계를 특정한 뒤, 해당 블록만 잘라서 AI에게 넘긴다. 수정 결과가 돌아오면 원본의 해당 영역만 교체한다. 500줄짜리 파일에서 46줄만 뽑아서 넘기는 식이다.</p>
<h2 id="실패-학습-루프">실패-학습 루프</h2>
<ol>
<li>AI가 코드를 수정한다</li>
<li>CI 파이프라인을 돌린다</li>
<li>실패하면 — 어떤 컨텍스트로 접근했는지, 왜 실패했는지 트레이스를 저장한다</li>
<li>Counterfactual Diagnosis — 이전 시도의 어떤 결정이 실패를 유발했는지 AI가 분석한다</li>
<li>다음 시도의 전략을 갱신하고, 개선된 하네스로 재시도한다 (최대 5회)</li>
</ol>
<p>단순 재시도와 다른 점은 <strong>접근 방식 자체가 바뀐다</strong>는 것이다.</p>
<pre><code class="language-python">def _improve_context(self, issue, related_code) -&gt; ErrorContext:
    &quot;&quot;&quot;이전 실패를 분석해 개선된 컨텍스트를 만든다.&quot;&quot;&quot;
    previous_runs = self._store.load_runs()
    failed_traces = [r.execution_trace for r in previous_runs
                     if r.test_result == TestResult.FAIL]

    if failed_traces:
        improved = self._agent.propose_harness(failed_traces)
        # Counterfactual Diagnosis 결과를 다음 시도의 컨텍스트에 주입
        additional = &quot;\n\n&quot;.join([
            &quot;## Counterfactual Diagnosis — 이전 실패 분석&quot;,
            improved.description,
            &quot;## 다음 시도 구체적 전략\n&quot; + improved.code,
        ])

    return ErrorContext(issue=issue, related_code=related_code,
                        additional_context=additional)</code></pre>
<p>처음엔 메서드 단위로 수정하다가, 실패하면 호출부까지 범위를 넓히고, 그래도 안 되면 해당 패턴의 다른 사례를 참고해 전략을 바꾸는 것이 실제로 관찰됐다.</p>
<h2 id="rate-limit-페일오버">Rate Limit 페일오버</h2>
<p>Claude가 rate limit에 걸리면 자동으로 다음 에이전트(Cursor 계정 1 → 2 → 3)로 전환하는 라우터를 만들었다. 모든 에이전트가 소진되면 작업을 중단한다. 무한히 재시도하지 않는다.</p>
<hr>
<h1 id="실제-드라이런-결과">실제 드라이런 결과</h1>
<p>로컬 git 브랜치에만 커밋하는 드라이런 모드로 실제 운영 코드를 대상으로 검증했다.</p>
<h2 id="n1-수정">N+1 수정</h2>
<ul>
<li><strong>입력</strong> — <code>N+1 query detected: ExhibitionRepository.findAllByStatusCodeAndOperationTypeAndAutoType</code> (트레이스당 평균 8회 반복)</li>
<li><strong>AI 판단</strong> — Spring Data JPA 연관 엔티티를 lazy로 가져오면서 N+1 발생</li>
<li><strong>수정</strong> — 리포지토리 메서드에 <code>@EntityGraph(attributePaths = {...})</code> 추가. 수정 diff 2줄.</li>
</ul>
<h2 id="npe-수정">NPE 수정</h2>
<ul>
<li><strong>입력</strong> — <code>java.util.NoSuchElementException: No value present at Optional.get(...)</code></li>
<li><strong>수정</strong> — <code>optional.get()</code> → <code>optional.ifPresent(::method)</code> 리팩터링</li>
</ul>
<h2 id="수정-안-할-것도-정확히-걸러낸다">&quot;수정 안 할 것&quot;도 정확히 걸러낸다</h2>
<p><code>PriceCacheService.expire:22</code> 스택트레이스를 넣어보았다. <code>@AllArgsConstructor</code>로 의존성 주입을 받는 Spring Bean이라 <strong>코드 자체에 null 가능성이 없다</strong>. AI는 <code>NOT_CODE_ISSUE</code>로 판정하고, &quot;Spring 컨텍스트 초기화 실패나 빈 등록 누락 등 DI 설정 문제로 판단됩니다&quot;라고 사유를 남겼다.</p>
<p><strong>수정하지 않아야 할 것을 수정하지 않는 능력</strong>이 이 시스템의 신뢰성을 결정한다.</p>
<hr>
<h1 id="테스트">테스트</h1>
<h2 id="618개-테스트-phase별-검증">618개 테스트, Phase별 검증</h2>
<p>인터페이스 → 도메인 모델 → 구현체 → 비즈니스 로직 순서로 개발했고, 테스트도 같은 순서로 쌓았다. 각 Phase 테스트가 100% 통과해야 다음 Phase로 넘어갔다.</p>
<h3 id="phase-1--인터페이스-계약-45개">Phase 1 — 인터페이스 계약 (45개)</h3>
<p>5개 인터페이스에 대해 추상 클래스 직접 인스턴스화 불가, 불완전 구현 불가, 완전 구현 시 올바른 타입 반환을 검증한다.</p>
<h3 id="phase-2--도메인-모델-33개">Phase 2 — 도메인 모델 (33개)</h3>
<p>6개 enum 전수 검사, dataclass 필드 검증, 경계값(score 범위, occurrence_count 임계값) 검증.</p>
<h3 id="phase-3--구현체-137개">Phase 3 — 구현체 (137개)</h3>
<p>외부 API를 Mock으로 교체하고 각 구현체의 입출력을 검증한다. 가장 많은 테스트가 집중된 Phase다.</p>
<table>
<thead>
<tr>
<th>구현체</th>
<th>테스트 수</th>
<th>핵심 검증</th>
</tr>
</thead>
<tbody><tr>
<td>DatadogErrorCollector</td>
<td>17</td>
<td>API 응답 파싱, N+1 임계값, YAML 설정 주입</td>
</tr>
<tr>
<td>NotionIssueTracker</td>
<td>33</td>
<td>템플릿 복사, 속성 타입별 정규화(11개), 캐시 히트</td>
</tr>
<tr>
<td>ClaudeCodeAgent</td>
<td>19</td>
<td>JSON 파싱 성공/실패 fallback, 프로젝트 스킬 포함</td>
</tr>
<tr>
<td>GitLabClient</td>
<td>11</td>
<td>브랜치/MR/파이프라인 CRUD</td>
</tr>
<tr>
<td>LocalVCSClient</td>
<td>27</td>
<td>실제 git 명령어, 언어별 확장자 필터링</td>
</tr>
<tr>
<td>FilesystemHarnessStore</td>
<td>17</td>
<td>round-trip 보존, 최고 점수 선택, 손상 파일 스킵</td>
</tr>
<tr>
<td>AIAgentRouter</td>
<td>10</td>
<td>전체 소진 에러, fallback 전환</td>
</tr>
</tbody></table>
<h3 id="phase-4--비즈니스-로직-58개">Phase 4 — 비즈니스 로직 (58개)</h3>
<table>
<thead>
<tr>
<th>모듈</th>
<th>테스트 수</th>
<th>핵심 검증</th>
</tr>
</thead>
<tbody><tr>
<td>Classifier</td>
<td>16</td>
<td>NPE→HIGH, FE→스킵, 4xx→스킵, Business 키워드→스킵</td>
</tr>
<tr>
<td>MetaHarnessLoop</td>
<td>45</td>
<td>1차 성공, 재시도 후 성공, 최대 재시도 실패, Counterfactual</td>
</tr>
<tr>
<td>ValidationCache</td>
<td>14</td>
<td>핑거프린트 결정성, TTL 분기(7일/24시간), 만료 삭제</td>
</tr>
</tbody></table>
<h3 id="phase-5--유틸리티--스킬--runner-345개">Phase 5 — 유틸리티 + 스킬 + Runner (345개)</h3>
<table>
<thead>
<tr>
<th>모듈</th>
<th>테스트 수</th>
<th>핵심 검증</th>
</tr>
</thead>
<tbody><tr>
<td>StackTraceParser</td>
<td>19</td>
<td>Java/Python/JS/Kotlin 형식 파싱, 메서드 추출, merge</td>
</tr>
<tr>
<td>Language Detector</td>
<td>38</td>
<td>9개 언어 감지, APM→project 우선순위</td>
</tr>
<tr>
<td>Service Mapping</td>
<td>36</td>
<td>YAML 로딩, Datadog 쿼리 생성</td>
</tr>
<tr>
<td>Runner + CLI</td>
<td>141</td>
<td>파이프라인 상태 관리, 단계별 실행, 스케줄러, 캐시 신선도</td>
</tr>
<tr>
<td>Caches</td>
<td>21</td>
<td>수집 캐시 + 검증 캐시 TTL</td>
</tr>
</tbody></table>
<h3 id="통합-테스트--mock-없이-실제-api-호출">통합 테스트 — Mock 없이 실제 API 호출</h3>
<p>환경변수 미설정 시 자동 스킵. 실제 Datadog API, Notion API, Claude Code CLI를 호출한다.</p>
<table>
<thead>
<tr>
<th>테스트</th>
<th>검증</th>
</tr>
</thead>
<tbody><tr>
<td><code>test_collect_errors_real</code></td>
<td>Datadog Error Tracking API 실제 호출</td>
</tr>
<tr>
<td><code>test_collect_n1_issues_real</code></td>
<td>Datadog Spans Analytics API 실제 호출</td>
</tr>
<tr>
<td><code>test_create_and_update_backlog_real</code></td>
<td>Notion 페이지 생성 + 업데이트</td>
</tr>
<tr>
<td><code>test_fixes_npe</code></td>
<td>실제 NPE → Claude 호출 → 로컬 코드 수정 + git 커밋</td>
</tr>
<tr>
<td><code>test_fixes_n1</code></td>
<td>실제 N+1 → Claude 호출 → @EntityGraph 추가</td>
</tr>
</tbody></table>
<hr>
<h1 id="설계-결정">설계 결정</h1>
<h2 id="왜-파일시스템에-트레이스를-쌓는가">왜 파일시스템에 트레이스를 쌓는가</h2>
<p>대안은 DB 또는 벡터 저장소였다. 파일시스템을 선택한 이유는 세 가지다.</p>
<ol>
<li><strong>투명성</strong> — 어떤 시도가 왜 실패했는지 바로 볼 수 있다</li>
<li><strong>Meta-Harness 원 논문이 증명한 패턴</strong> — filesystem 자체가 AI 컨텍스트로 들어가는 설계</li>
<li><strong>교체 가능</strong> — HarnessStore 인터페이스 뒤에 있으므로 DB로 바꿔도 비즈니스 로직은 안 바뀐다</li>
</ol>
<h2 id="상태-관리--중간에-죽어도-처음부터-다시-하지-않는다">상태 관리 — 중간에 죽어도 처음부터 다시 하지 않는다</h2>
<p>각 단계 완료 시 결과를 파일로 저장한다. 네트워크 끊김이나 rate limit이 발생해도 직전 완료 지점부터 재개할 수 있다. CLI가 단계별 실행을 지원하는 것도 이 구조의 연장선이다.</p>
<h2 id="claude-code-cli--왜-api가-아닌-subprocess인가">Claude Code CLI — 왜 API가 아닌 subprocess인가</h2>
<ol>
<li><strong>사용자 계정 기반 인증</strong> — API 키를 별도 관리하지 않는다</li>
<li><strong>스킬 시스템 접근</strong> — CLI는 Claude Code의 스킬/도구 생태계와 연동된다</li>
<li><strong>프로세스 격리</strong> — AI의 상태가 메인 프로세스를 오염시키지 않는다</li>
</ol>
<p>토큰 사용량 측정이 세밀하지 않고, API 대비 호출 오버헤드가 크다. 하지만 운영 관점의 단순성이 훨씬 컸다.</p>
<h2 id="mcp-연동--ai가-외부-데이터에-직접-접근한다">MCP 연동 — AI가 외부 데이터에 직접 접근한다</h2>
<p><a href="https://docs.datadoghq.com/bits_ai/mcp_server/">Datadog MCP Server</a>를 Claude Code에 연결하면, AI가 코드를 수정하는 시점에 트레이스, 로그, 메트릭을 직접 조회할 수 있다. 파이프라인이 가져온 스택트레이스만으로 부족할 때, AI가 추가 맥락을 스스로 탐색하는 것이 가능해진다.</p>
<p>수집은 REST API로, 수정 시 탐색은 MCP로 — 역할이 분리되어 있다.</p>
<hr>
<h1 id="기술-스택">기술 스택</h1>
<table>
<thead>
<tr>
<th>구분</th>
<th>기술</th>
</tr>
</thead>
<tbody><tr>
<td>오류 수집</td>
<td><a href="https://docs.datadoghq.com/error_tracking/">Datadog Error Tracking API</a></td>
</tr>
<tr>
<td>N+1 탐지</td>
<td><a href="https://docs.datadoghq.com/api/latest/spans/">Datadog Spans Analytics API</a></td>
</tr>
<tr>
<td>MCP 연동</td>
<td><a href="https://docs.datadoghq.com/bits_ai/mcp_server/">Datadog MCP Server</a></td>
</tr>
<tr>
<td>백로그 관리</td>
<td><a href="https://developers.notion.com/">Notion API</a></td>
</tr>
<tr>
<td>AI 에이전트</td>
<td>Claude Code (<code>claude -p</code> CLI)</td>
</tr>
<tr>
<td>형상 관리</td>
<td>GitLab API / LocalVCSClient</td>
</tr>
<tr>
<td>언어</td>
<td>Python 3.11+</td>
</tr>
<tr>
<td>하네스 방식</td>
<td><a href="https://arxiv.org/abs/2603.28052">Meta-Harness (Stanford)</a></td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Claude Code로 개발 워크플로우 개선시도]]></title>
            <link>https://velog.io/@banana-wuyu/Claude-Code%EB%A1%9C-%EA%B0%9C%EB%B0%9C-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EA%B0%9C%EC%84%A0%EC%8B%9C%EB%8F%84</link>
            <guid>https://velog.io/@banana-wuyu/Claude-Code%EB%A1%9C-%EA%B0%9C%EB%B0%9C-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EA%B0%9C%EC%84%A0%EC%8B%9C%EB%8F%84</guid>
            <pubDate>Fri, 03 Apr 2026 07:40:22 GMT</pubDate>
            <description><![CDATA[<h1 id="claude-code로-개발-워크플로우-개선을-시도">Claude Code로 개발 워크플로우 개선을 시도</h1>
<p>Claude Code를 쓰기 시작하면서 자동화 못하나 고민을 했다.</p>
<p>그 답을 찾기 위해 두 가지 방식을 직접 만들어서 써봤다. 실제 사용하고 ai 한테 물어본 결론은 한쪽은 특정 상황에서만 의미 있었고, 나머지는 팀 단위에서나 진짜 가치가 있었다.</p>
<blockquote>
<p>질문 내용 
워크플로우 개선 시도
각 개선 시도에 대해 상세하게 분석하고 내가 말한 장점과 문제점에 대해 상세히 분석</p>
</blockquote>
<ol>
<li>코드가 있는경우 최대한 코드 전체를 분석한다.</li>
<li>엔지니어링 기법을 사용한경우 해당 엔지니어링 기법에 대해 조사를 최대한 자세하게 한다.</li>
<li>절대적으로 아래 시도 작업이 옳다는건 아니다. 실제 사실관계도 파악한다.</li>
<li>관련 혹은 ai관련 최신 논문, 연구결과 등도 같이 비교 분석 한다.</li>
<li>해당 모든 내용들을 문서로 정리한다.<blockquote>
<p>시도 작업</p>
</blockquote>
</li>
<li>노션, 피그마 링크를 ai 로 넘겨 interface, 설계, 프롬프트 생성해서 claude code 요청 - /Users/tpirates/workspace/workflow (local)
 1.1 장점<pre><code>  GUI 로 지원 노션, 피그마 링크로 좀 더 편하다.</code></pre> 1.2 문제점<pre><code>- 개발자가 직접 interface, 설계, 프롬프트 생성해서 하는게 더 명확하다.
- 실제 요구사항과 개발자가 원하는 방식을 맞추기 위해서는 많은 수정 요청을 한다.
- 수정 사항에 대해 매버 프롬프트로 다시 요청을 하는데 claude code로 하는거랑 뭐가 다른가.</code></pre><blockquote>
</blockquote>
</li>
<li>하네스엔지니어링
 2.1 장점<pre><code>  테스트로 좀 더 안정적인 개발 가능</code></pre> 2.2 문제점<pre><code> - 기존에 레거시 프로젝트의 경우 테스트가 없는 경우도 있는데 이거는 커버가 안됨
 - 실제 개발입장에서는 레거시 프로젝트를 유지보수 하는 경우가 많은데 해당 방식에 적용이 어려움</code></pre><blockquote>
</blockquote>
</li>
<li>ai skills interface 등록 - <a href="https://github.com/jaemyeong-hwnag/common-ai-skill">https://github.com/jaemyeong-hwnag/common-ai-skill</a>
 3.1 장점<pre><code> 기능만 명세하고 ai 가 직접 프로젝트에 맞는 구현을 개발</code></pre> 3.2 문제점<pre><code> - 이게 진짜 효용성이 있는지 잘 모르겠다.
 - 기능별로 너무 명확하게 한계가 있어 보인다.</code></pre></li>
</ol>
<hr>
<h2 id="시도한-것들">시도한 것들</h2>
<h3 id="시도-1-notionfigma-→-ai-→-spec-→-claude-code">시도 1. Notion/Figma → AI → Spec → Claude Code</h3>
<p>Notion 백로그 URL을 넣으면 코드 스캐닝, 스펙 생성, 프롬프트 생성까지 자동으로 처리하고 Claude Code를 실행하는 파이프라인을 만들었다.</p>
<p>흐름은 이렇다:</p>
<pre><code>Notion URL
→ 레포 코드 스캔 (메서드 시그니처만, 구현 코드 제외)
→ spec-draft (AI가 인터페이스 + 요구사항 초안)
→ [개발자가 아키텍처 결정 입력]
→ spec-refine
→ Claude Code 실행 프롬프트 생성
→ claude --dangerously-skip-permissions</code></pre><p>논문도 뒤졌다. Lost-in-the-Middle 방지를 위해 핵심 제약을 메시지 하단에 반복하고, Claude는 XML이 YAML보다 성능이 높다는 연구(arXiv:2411.10541)를 반영해 AI 내부 통신 포맷을 잡았다. LLM-as-Judge도 달았다.</p>
<p>꽤 공들였는데 — 결과가 좋지 않았다.</p>
<h3 id="시도-2-ai-skills-interface">시도 2. AI Skills Interface</h3>
<p>기능 명세만 작성하면 AI가 프로젝트에 맞는 구현을 알아서 한다는 개념이다.</p>
<p>핵심은 이 철학이다:</p>
<blockquote>
<p>&quot;Skills define <strong>what</strong> must be achieved, never <strong>how</strong>. You are the implementation: read the skill → inspect this project → fulfill the contract&quot;</p>
</blockquote>
<p>Interface-First Development를 AI에 적용한 버전으로 보면 된다. 구현 방법을 지시하는 게 아니라 계약을 정의하면 AI가 프로젝트 맥락에 맞게 해석해서 구현한다.</p>
<hr>
<h2 id="시도-1은-왜-실패했나">시도 1은 왜 실패했나</h2>
<p>실행 로그를 보면 솔직하게 드러난다.</p>
<pre><code>날짜: 2026-04-02 하루
실행 횟수: 30회
실행 시간: 약 3시간 42분
spec.md 상태: human_review (완료 안 됨)
ai_prompts.md: 비어 있음</code></pre><p>파이프라인이 human review 단계에서 멈췄다. spec-refine, 프롬프트 생성은 실행되지 않았다. 결국 직접 프롬프트를 써서 Claude Code를 30회 실행해 작업했다.</p>
<p>스펙 생성 레이어는 작동했지만 실제 작업에 쓰이지 않았다. 우회된 것이다.</p>
<h3 id="아키텍처-결정은-이미-머릿속에-있었다">아키텍처 결정은 이미 머릿속에 있었다</h3>
<p>워크플로우가 생성한 프롬프트의 제약 조건 섹션 내용:</p>
<pre><code>스케쥴러는 없고 일단 api로 비즈니스 로직만 구현
외부 메시지 발송은 core에 구현</code></pre><p>AI가 Notion에서 읽어온 게 아니라 개발자가 GUI에서 직접 타이핑한 것이다. Claude Code에 직접 쓰는 것과 차이가 없다.</p>
<p>Notion 백로그에는 비즈니스 언어로 쓰여 있었고, 어떤 패키지에 넣을지, 어떤 레이어에서 처리할지는 본인이 이미 알고 있었다. AI가 그걸 대신 결정해주는 게 아니라 본인이 입력한 것을 XML로 감싸줬을 뿐이다.</p>
<h3 id="런타임-오류는-스펙이-막지-못한다">런타임 오류는 스펙이 막지 못한다</h3>
<p>run_0의 첫 번째 오류:</p>
<pre><code>RSA Private Key가 없어서 loadPrivateKey() IOException 발생.
signing-key에 Webhook secret을 넣었는데 이건 RSA 키가 아님.</code></pre><p>이런 오류는 실행해봐야 안다. 스펙이 아무리 잘 만들어져도 잡을 수 없다. 30회 반복은 워크플로우 때문이 아니라 원래 일어날 수밖에 없는 디버깅 사이클이었다.</p>
<p>&quot;Claude Code에 직접 오류 던져줘, 고쳐줘&quot;가 똑같다.</p>
<h3 id="claude-code가-이미-탐색을-한다">Claude Code가 이미 탐색을 한다</h3>
<p>워크플로우 코드 스캐너가 하는 일:</p>
<ul>
<li>레포 파일 트리 추출</li>
<li>클래스/메서드 시그니처 추출</li>
<li>아키텍처 패턴 감지</li>
<li>XML로 압축해서 AI에 주입</li>
</ul>
<p>Claude Code의 Plan Mode에서 Claude Code가 직접 파일을 읽는 것이 중간 압축보다 정확하다. 시그니처만 추출하면 구현 맥락이 빠지지만, Claude Code는 실제 코드를 읽는다.</p>
<h3 id="mcp로-대체-가능">MCP로 대체 가능</h3>
<p>워크플로우가 Notion/Figma를 파싱해서 주입하는 작업:</p>
<pre><code class="language-bash">claude mcp add notion
claude mcp add figma</code></pre>
<p>이걸로 끝이다. Claude Code가 직접 Notion/Figma를 읽는다. 별도 서버, GUI, 파이프라인이 필요 없다.</p>
<hr>
<h2 id="그래도-의미-있는-경우가-있다">그래도 의미 있는 경우가 있다</h2>
<p>완전히 쓸모없는 건 아니다. 아래 조건이 맞을 때는 워크플로우 서버가 실제로 차별점이 있다:</p>
<ul>
<li>여러 레포(frontend + backend + infra)에 동시 반영해야 할 때</li>
<li>팀 전체가 같은 스펙을 공유해야 할 때</li>
<li>Notion 백로그가 수십 개고 각각 Claude Code 실행이 필요할 때</li>
</ul>
<p>병렬 실행, 실행 이력 영속화, 스펙 버전 관리 — 이 세 가지가 필요한 상황이면 의미가 있다. 레포 하나에 혼자 작업하는 경우라면 CLI 직접 쓰는 게 더 빠르다.</p>
<hr>
<h2 id="ai-skills-interface는-어땠나">AI Skills Interface는 어땠나</h2>
<p>레포: <a href="https://github.com/jaemyeong-hwnag/common-ai-skill">https://github.com/jaemyeong-hwnag/common-ai-skill</a></p>
<h3 id="장점-기능만-명세하면-ai가-프로젝트에-맞게-구현한다">장점: 기능만 명세하면 AI가 프로젝트에 맞게 구현한다</h3>
<p>기존에 AI에게 뭔가를 시킬 때 보통 이렇게 된다. &quot;이 파일에 이 메서드 추가해줘. 패턴은 이렇게 하고, 테스트는 이렇게 해줘.&quot; 구현 방법을 같이 설명한다.</p>
<p>스킬 방식은 반대다. &quot;이 기능이 있어야 한다&quot;는 계약만 정의하고, 어떻게 구현할지는 AI가 레포를 보고 판단하게 한다.</p>
<p>예를 들어 <code>hexagonal-development</code> 스킬은 이런 식이다:</p>
<pre><code>모든 신규 기능은 헥사고날 아키텍처를 따른다.
Port 인터페이스를 먼저 정의하고, Adapter가 구현한다.
도메인 로직은 외부 의존성에 대해 알지 못한다.</code></pre><p>이걸 한 번 등록해두면 AI가 코드를 추가할 때마다 프로젝트 구조를 스스로 파악해서 기존 패턴대로 맞춰준다. 매번 &quot;우리 프로젝트는 헥사고날이야, 포트 먼저 만들어야 해&quot;를 설명하지 않아도 된다.</p>
<p><code>delivery-workflow</code> 스킬은 구현 → 테스트 → 커버리지 확인 → 커밋 사이클 전체를 하나의 명세로 정의한다. 이것도 한 번 등록하면 &quot;구현하고 테스트까지&quot;를 별도로 말하지 않아도 된다.</p>
<p>팀에서 쓸 때 실용성이 있다. 새 팀원이 합류했을 때 &quot;이 스킬들 읽고 써&quot;가 되고, 구현 방식에 대한 반복 설명을 줄일 수 있다.</p>
<h3 id="문제점-1-효용성이-불확실하다">문제점 1: 효용성이 불확실하다</h3>
<p>쓰다 보면 이게 실제로 효과가 있는 건지 확신이 서지 않는다.</p>
<p>스킬이 마크다운 텍스트인 이상 AI가 해석하는 방식이 매번 다를 수 있다. 동일한 스킬 명세가 Claude 3에서 하던 것을 Claude 4에서 다르게 할 수 있다. 스킬 자체가 잘 작동하는지 테스트할 방법이 없다.</p>
<p>워크플로우 코드에서 내부 구현과 비교하면 차이가 분명하다. 내부에서 스킬은 Python 추상 클래스로 만들었다:</p>
<pre><code class="language-python">class Skill(ABC):
    @abstractmethod
    async def execute(self, input_: SkillInput) -&gt; SkillOutput:
        &quot;&quot;&quot;스킬 실행.&quot;&quot;&quot;</code></pre>
<p>타입 강제가 되고 단위 테스트를 붙일 수 있다. 반면 마크다운 명세는 AI가 읽고 해석하는 것이기 때문에 &quot;이 스킬이 정확히 실행됐다&quot;를 확인할 수단이 없다.</p>
<p>Chain-of-Thought 연구(Wei et al. 2022)에서도 비슷한 맥락이 있다. 같은 지시어도 모델 크기와 컨텍스트에 따라 일관성이 크게 달라진다. 스킬이 텍스트인 한 이 문제는 피하기 어렵다.</p>
<h3 id="문제점-2-프로젝트-구조를-가정한다">문제점 2: 프로젝트 구조를 가정한다</h3>
<p>스킬들이 Best Practice를 전제로 만들어져 있어서, 그 전제가 맞지 않는 프로젝트에서는 오히려 방해가 된다.</p>
<p><code>hexagonal-development</code> 스킬은 헥사고날 아키텍처를 가정한다. MVC 레거시 프로젝트에서 이 스킬을 쓰면 AI가 없던 레이어를 만들려고 한다.</p>
<p><code>coverage</code> 스킬은 80% 커버리지를 목표로 강제한다. 레거시에서 80%는 비현실적인 숫자다.</p>
<p><code>finalize</code> 스킬은 작업 후 자동으로 커밋까지 한다. 프로젝트마다 커밋 정책이 다른데 이게 충돌한다.</p>
<p>스킬이 Best Practice를 가정하는데, 실제 프로젝트는 Best Practice를 따르지 않는 경우가 많다. 그래서 기능별로 한계가 명확하다. 신규 프로젝트에서 아키텍처를 처음부터 잡는 경우에는 맞지만, 기존 프로젝트에 얹으려 하면 프로젝트마다 스킬을 별도로 만들어야 한다.</p>
<p>결국 혼자 쓰는 상황에서는 CLAUDE.md에 직접 프로젝트 컨벤션을 쓰는 것과 효과가 다르지 않다.</p>
<hr>
<h2 id="실제로-잘-작동한-것들">실제로 잘 작동한 것들</h2>
<h3 id="검증-기준을-프롬프트에-포함">검증 기준을 프롬프트에 포함</h3>
<p>Claude Code 공식 문서에서 &quot;single highest-leverage thing&quot;이라고 표현한 것이다.</p>
<pre><code># 나쁜 요청
&quot;알림 발송 기능 구현해줘&quot;

# 좋은 요청
&quot;POST /api/notify 구현해줘.
구현 후 curl -X POST http://localhost:8080/api/notify?date=2025-01-01 
실행해서 HTTP 200 나오면 성공. 테스트도 작성하고 실행해.&quot;</code></pre><p>Claude Code는 스스로 결과를 확인할 수 있을 때 훨씬 낫다.</p>
<h3 id="claudemd-한-번-쓰고-반복-활용">CLAUDE.md 한 번 쓰고 반복 활용</h3>
<p>매 실행마다 컨텍스트를 주입하는 것보다 레포 루트에 CLAUDE.md를 한 번 잘 써두는 게 낫다. 200줄 미만으로 유지하고, Claude가 코드에서 알 수 있는 것은 쓰지 않는다. 틀린 행동이 반복될 때만 추가한다.</p>
<pre><code class="language-markdown"># CLAUDE.md

## Architecture
- Hexagonal pattern (ports/adapters)
- 외부 API 호출은 core 패키지에서만

## Commands
- 빌드: ./gradlew build
- 테스트: ./gradlew test

## Do NOT
- git add -A 사용 금지
- .env 파일 수정 금지
- 기존 인터페이스 시그니처 변경 금지</code></pre>
<h3 id="레거시-프로젝트-접근법">레거시 프로젝트 접근법</h3>
<p>테스트 없는 레거시에 AI가 코드를 추가하면 검증 방법이 없다. 이 경우 Characterization Test를 먼저 요청한다:</p>
<pre><code>&quot;YourService.findByDate() 메서드의 현재 동작을 
 Characterization Test로 캡처해줘.
 실제 실행해서 현재 반환값을 확인하고, 
 그 값을 expected로 하는 테스트 작성.
 내 의도가 아니라 현재 코드가 실제로 하는 일을 테스트해야 함.&quot;</code></pre><p>Michael Feathers의 &quot;Working Effectively with Legacy Code&quot;에서 나온 패턴인데, AI 코딩에 그대로 쓸 수 있다.</p>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>권장 방식</th>
</tr>
</thead>
<tbody><tr>
<td>신규 기능 개발</td>
<td>CLAUDE.md + Claude Code CLI 직접</td>
</tr>
<tr>
<td>레거시 유지보수</td>
<td>Plan Mode + Characterization Test + 최소 변경</td>
</tr>
<tr>
<td>멀티 레포 대규모 작업</td>
<td>MCP 연동 또는 워크플로우 서버</td>
</tr>
<tr>
<td>공통</td>
<td>Human-in-the-Loop 필수 (AI solve rate 최대 62%)</td>
</tr>
</tbody></table>
<p>SWE-bench 기준으로 Claude 3.7 Sonnet이 ~62%를 해결한다. 38%는 항상 실패한다. 워크플로우가 좋든 나쁘든 AI가 완전히 틀릴 가능성은 항상 있다. 검증 단계와 사람 개입을 빼는 건 이 맥락에서 좋지 않다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Aurora MySQL History Pipeline with AWS Lambda]]></title>
            <link>https://velog.io/@banana-wuyu/Aurora-MySQL-Real-time-CDC-Pipeline-with-AWS-Lambda</link>
            <guid>https://velog.io/@banana-wuyu/Aurora-MySQL-Real-time-CDC-Pipeline-with-AWS-Lambda</guid>
            <pubDate>Thu, 30 Oct 2025 16:23:04 GMT</pubDate>
            <description><![CDATA[<h2 id="🎯-프로젝트-개요">🎯 프로젝트 개요</h2>
<h2 id="aurora-mysql의-binlog를-실시간으로-캡처하여-s3에-구조화된-데이터로-저장하는-서버리스-cdcchange-data-capture-파이프라인"><strong>Aurora MySQL의 binlog를 실시간으로 캡처하여 S3에 구조화된 데이터로 저장하는 서버리스 CDC(Change Data Capture) 파이프라인</strong></h2>
<h2 id="🏗️-아키텍처-설계">🏗️ 아키텍처 설계</h2>
<h3 id="1-다양한-cdc-구현-방식-vs-lambda-기반-직접-파싱">1. <strong>다양한 CDC 구현 방식 vs Lambda 기반 직접 파싱</strong></h3>
<h4 id="고려했던-대안-방식들">고려했던 대안 방식들</h4>
<p><strong>🎯 방식 ①: Debezium → MSK(Serverless) → S3(Iceberg) → Athena</strong></p>
<ul>
<li><strong>구성</strong>: Debezium 커넥터 → MSK Serverless → S3 Iceberg 테이블 → Athena</li>
<li><strong>장점</strong>: 완전한 CDC 솔루션, 다중 싱크 지원</li>
<li><strong>단점</strong>: 상시 비용 발생, 복잡한 운영</li>
</ul>
<p><strong>🎯 방식 ②: Aurora Native CDC → EventBridge Pipes → Lambda → S3(Iceberg)/Dynamo</strong></p>
<ul>
<li><strong>구성</strong>: Aurora CDC → EventBridge Pipes → Lambda → S3/DynamoDB</li>
<li><strong>장점</strong>: AWS 네이티브 서비스 활용</li>
<li><strong>단점</strong>: 중간 서비스 의존성, 비용 증가</li>
</ul>
<p><strong>🎯 방식 ③: CDC(DMS/Debezium) → EventBridge Pipes → DynamoDB</strong></p>
<ul>
<li><strong>구성</strong>: DMS/Debezium → EventBridge Pipes → DynamoDB</li>
<li><strong>장점</strong>: 단건 조회 최적화</li>
<li><strong>단점</strong>: 분석 기능 제한, DynamoDB 비용 폭증 위험</li>
</ul>
<p><strong>🎯 방식 ④: Debezium Server on Fargate(Spot) → S3(Iceberg) → Athena</strong></p>
<ul>
<li><strong>구성</strong>: Debezium Server → Fargate Spot → S3 Iceberg → Athena</li>
<li><strong>장점</strong>: MSK 없이 구현 가능</li>
<li><strong>단점</strong>: Fargate 상시 실행 비용</li>
</ul>
<h4 id="왜-lambda-기반-직접-binlog-파싱을-선택했는가">왜 Lambda 기반 직접 binlog 파싱을 선택했는가?</h4>
<p><strong>🤖 GPT-5 기반 비용 분석</strong>: 이 비교 분석은 <strong>GPT-5</strong>를 활용하여 5가지 CDC 구현 방식의 상세한 비용 계산과 의사결정 과정을 수행했습니다.</p>
<h5 id="월간-rds-cud-이벤트-현황-datadog-csv-기반">월간 RDS CUD 이벤트 현황 (Datadog CSV 기반)</h5>
<table>
<thead>
<tr>
<th>월</th>
<th>총 쿼리 수</th>
<th>INSERT</th>
<th>UPDATE</th>
<th>DELETE</th>
<th>CUD 합계</th>
</tr>
</thead>
<tbody><tr>
<td>2025-07</td>
<td>245,982,101</td>
<td>5,886,976</td>
<td>2,633,551</td>
<td>0</td>
<td>8,520,527</td>
</tr>
<tr>
<td>2025-08*</td>
<td>54,751,948</td>
<td>1,277,932</td>
<td>545,330</td>
<td>0</td>
<td>1,823,262</td>
</tr>
</tbody></table>
<blockquote>
<p>*2025-08 데이터는 수집 중 일부 기간만 포함됨</p>
</blockquote>
<p><strong>🎯 비용 효율성 비교 (월간 비용):</strong></p>
<pre><code>현재 구현 (Lambda 직접 파싱):     $30.58
방식 ④ (Fargate Spot):           $90    (+196%)
방식 ① (MSK Serverless):         $160   (+423%)
방식 ② (Aurora CDC + Pipes):     $260   (+751%)
방식 ③ (DMS + Dynamo):           $303   (+892%)

연간 절약 효과: $2,754-3,274 (70-90% 절약)</code></pre><p><strong>🎯 기술적 우위:</strong></p>
<ul>
<li><strong>완전한 제어</strong>: binlog 파싱부터 저장까지 모든 과정 제어</li>
<li><strong>운영 단순성</strong>: 최소 서비스 구성 (Lambda, DynamoDB, S3)</li>
<li><strong>에러 처리</strong>: 1236 에러 처리, S3 저장 실패 시 롤백 등 세밀한 제어</li>
</ul>
<p><strong>🎯 성능 최적화:</strong></p>
<ul>
<li><strong>파티션 프루닝</strong>: 쿼리 비용 절약</li>
<li><strong>체크포인트</strong>: 중간 체크포인트로 데이터 무결성 보장</li>
</ul>
<p><strong>🎯 확장성:</strong></p>
<ul>
<li><strong>서버리스</strong>: Lambda 자동 스케일링</li>
<li><strong>무제한 저장</strong>: S3 기반 무제한 확장</li>
<li><strong>선형적 비용</strong>: 사용량에 비례한 비용 증가</li>
</ul>
<h3 id="2-서버리스-우선-설계-serverless-first-architecture">2. <strong>서버리스 우선 설계 (Serverless-First Architecture)</strong></h3>
<h4 id="왜-서버리스로-설계했는가">왜 서버리스로 설계했는가?</h4>
<p><strong>🎯 비용 효율성</strong></p>
<ul>
<li><strong>사용량 기반 과금</strong>: 실제 데이터 변경이 있을 때만 비용 발생</li>
<li><strong>인프라 관리 불필요</strong>: 서버 프로비저닝, 패치, 모니터링 오버헤드 제거</li>
<li><strong>자동 스케일링</strong>: 데이터 볼륨에 따라 자동으로 리소스 조정</li>
</ul>
<p><strong>🎯 운영 단순화</strong></p>
<ul>
<li><strong>이벤트 기반 실행</strong>: CloudWatch Events로 주기적 실행 (1분마다)</li>
<li><strong>무상태 처리</strong>: 각 실행이 독립적이며 재시작 가능</li>
<li><strong>장애 격리</strong>: 개별 실행 실패가 전체 시스템에 영향 없음
<img src="https://velog.velcdn.com/images/banana-wuyu/post/09288236-830a-400a-ac02-ce69b737156d/image.png" alt=""></li>
</ul>
<pre><code class="language-mermaid">graph TB
    A[CloudWatch Events&lt;br/&gt;1분마다 트리거] --&gt; B[AWS Lambda&lt;br/&gt;aurora-cdc-parser]
    B --&gt; C[Aurora MySQL&lt;br/&gt;Binlog 스트림]
    B --&gt; D[DynamoDB&lt;br/&gt;체크포인트 관리]
    B --&gt; E[S3 Bucket&lt;br/&gt;JSONL + Parquet]

    style A fill:#ff9999
    style B fill:#99ccff
    style C fill:#99ff99
    style D fill:#ffcc99
    style E fill:#cc99ff</code></pre>
<h3 id="3-체크포인트-기반-재시작-메커니즘">3. <strong>체크포인트 기반 재시작 메커니즘</strong></h3>
<h4 id="왜-체크포인트-패턴을-선택했는가">왜 체크포인트 패턴을 선택했는가?</h4>
<p><strong>🎯 데이터 무결성 보장</strong></p>
<ul>
<li><strong>중간 체크포인트</strong>: 10건마다 진행 상황 저장으로 부분 실패 시에도 데이터 손실 방지</li>
<li><strong>최종 체크포인트</strong>: Lambda 실행 완료 시 최종 위치 저장</li>
<li><strong>자동 재시작</strong>: 장애 발생 시 마지막 체크포인트에서 자동 재개</li>
</ul>
<p><strong>🎯 처리 효율성</strong></p>
<ul>
<li><strong>증분 처리</strong>: 이미 처리된 데이터 재처리 방지</li>
<li><strong>병렬 실행 안전성</strong>: 동일한 binlog 위치에서 중복 처리 방지
<img src="https://velog.velcdn.com/images/banana-wuyu/post/4492928a-5dc5-4e2b-a6e0-1d30b4bd078f/image.png" alt=""></li>
</ul>
<pre><code class="language-mermaid">sequenceDiagram
    participant L as Lambda
    participant D as DynamoDB
    participant A as Aurora
    participant S as S3

    L-&gt;&gt;D: 체크포인트 조회
    D--&gt;&gt;L: 마지막 위치 반환
    L-&gt;&gt;A: Binlog 스트림 생성
    A--&gt;&gt;L: 이벤트 스트림

    loop 10건마다
        L-&gt;&gt;D: 중간 체크포인트 저장
        L-&gt;&gt;S: JSONL/Parquet 저장
    end

    L-&gt;&gt;D: 최종 체크포인트 저장</code></pre>
<hr>
<h2 id="🔄-핵심-처리-플로우">🔄 핵심 처리 플로우</h2>
<h3 id="1-binlog-스트림-처리-플로우">1. <strong>Binlog 스트림 처리 플로우</strong></h3>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/76af1d43-677a-4d45-9127-44280708024e/image.png" alt=""></p>
<pre><code class="language-mermaid">flowchart TD
    A[Lambda 시작] --&gt; B[환경 감지]
    B --&gt; C[DB 연결 및 설정 검증]
    C --&gt; D[체크포인트 조회]
    D --&gt; E[서버 ID 생성]
    E --&gt; F[Binlog 스트림 생성]

    F --&gt; G[이벤트 수집 루프]
    G --&gt; H{이벤트 존재?}
    H --&gt;|Yes| I[이벤트 변환]
    I --&gt; J[스키마 매핑]
    J --&gt; K[기본키 추출]
    K --&gt; L[10건마다 체크포인트]
    L --&gt; M[S3 저장]
    M --&gt; G

    H --&gt;|No| N[최종 체크포인트]
    N --&gt; O[Lambda 종료]

    style A fill:#ff9999
    style O fill:#99ff99</code></pre>
<h3 id="2-데이터-변환-파이프라인">2. <strong>데이터 변환 파이프라인</strong></h3>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/e9694f5c-ec36-41d0-9a4c-a4107cda6225/image.png" alt=""></p>
<pre><code class="language-mermaid">graph LR
    A[Raw Binlog Event] --&gt; B[이벤트 타입 분류]
    B --&gt; C[INSERT Event]
    B --&gt; D[UPDATE Event]
    B --&gt; E[DELETE Event]

    C --&gt; F[after_values 추출]
    D --&gt; G[before_values + after_values]
    E --&gt; H[before_values 추출]

    F --&gt; I[스키마 매핑]
    G --&gt; I
    H --&gt; I

    I --&gt; J[기본키 추출]
    J --&gt; K[구조화된 이벤트 데이터]
    K --&gt; L[JSONL 변환]
    K --&gt; M[Parquet 변환]

    style A fill:#ffcc99
    style K fill:#99ff99
    style L fill:#99ccff
    style M fill:#cc99ff</code></pre>
<hr>
<h2 id="🛠️-기술적-설계-결정사항">🛠️ 기술적 설계 결정사항</h2>
<h3 id="1-s3-저장-경로-설계-및-비용-최적화">1. <strong>S3 저장 경로 설계 및 비용 최적화</strong></h3>
<h4 id="parquet-경로-구조-athena-파티션-최적화">Parquet 경로 구조 (Athena 파티션 최적화)</h4>
<pre><code>s3://aurora-history-binlog/
└── env=dev/db=db-name/schema=schema-name/date=2025-01-15/
    └── PUSH_LOG_20250115_143022.parquet</code></pre><h4 id="왜-이렇게-설계했는가">왜 이렇게 설계했는가?</h4>
<p><strong>🎯 Parquet 경로</strong>: Athena 파티션 최적화</p>
<ul>
<li><strong>쿼리 성능</strong>: 파티션 프루닝으로 쿼리 성능 향상</li>
<li><strong>비용 절약</strong>: 필요한 파티션만 스캔하여 비용 절약```</li>
</ul>
<p><strong>파티션 키 구조:</strong></p>
<pre><code>1. env = &#39;prod&#39;           # 환경별 분리 (dev/stage/prod)
2. db = &#39;database_name&#39;        # 데이터베이스별 분리
3. schema = &#39;schema_name&#39;    # 스키마별 분리
4. date = &#39;2025-09-15&#39;    # 날짜별 분리 (가장 세밀한 파티션)</code></pre><h4 id="athena-쿼리-비용-최적화">Athena 쿼리 비용 최적화</h4>
<p><strong>파티션 프루닝 효과:</strong></p>
<pre><code class="language-sql">-- 비효율적인 쿼리 (전체 테이블 스캔)
SELECT * FROM binlog_event 
WHERE table_name = &#39;table_name&#39; AND pk_value = &#39;32811&#39;

-- 최적화된 쿼리 (파티션 프루닝)
SELECT * FROM binlog_event
WHERE env = &#39;prod&#39;                    -- 파티션 1: 환경
  AND database_name = &#39;database_name&#39;      -- 파티션 2: 데이터베이스
  AND schema_name = &#39;schema_name&#39;        -- 파티션 3: 스키마
  AND event_date BETWEEN &#39;2025-09-01&#39; AND &#39;2025-09-30&#39;  -- 파티션 4: 날짜
  AND table_name = &#39;table_name&#39;
  AND pk_value = &#39;32811&#39;</code></pre>
<pre><code>env=prod/db=database_name/schema=table_name/date=2025-09-01/  ← 스캔됨
env=prod/db=database_name/schema=table_name/date=2025-09-02/  ← 스캔됨
...
env=prod/db=database_name/schema=schema_name/date=2025-09-30/  ← 스캔됨
env=prod/db=database_name/schema=schema_name/date=2025-10-01/  ← 스캔 안됨 (범위 밖)
env=dev/db=database_name/schema=schema_name/date=2025-09-15/   ← 스캔 안됨 (환경 다름)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Datadog Summit Seoul - hands on]]></title>
            <link>https://velog.io/@banana-wuyu/Datadog-Summit-Seoul-hands-on</link>
            <guid>https://velog.io/@banana-wuyu/Datadog-Summit-Seoul-hands-on</guid>
            <pubDate>Fri, 24 Oct 2025 14:51:01 GMT</pubDate>
            <description><![CDATA[<h1 id="datadog-learn-sre-핸즈온-후기--slo-핵심-정리">Datadog Learn SRE 핸즈온 후기 &amp; SLO 핵심 정리</h1>
<blockquote>
<p>관련 링크: <a href="https://velog.io/@banana-wuyu/Datadog-Summit-Seoul">Datadog Summit Seoul 후기(velog)</a></p>
</blockquote>
<hr>
<h2 id="핸즈온-내용">핸즈온 내용</h2>
<p>Datadog Learn(무료) 코스에서 <strong>SLI를 정의</strong>하고 <strong>SLO를 설정</strong>한 뒤, SLO 위반 시 <strong>알림 전파와 담당자 자동 할당</strong>까지 구성했다.</p>
<ul>
<li><p><strong>SLI 정의</strong></p>
<ul>
<li><strong>APM 기반</strong>: HTTP 상태코드(2xx/5xx)를 기준으로 <strong>요청 성공률/오류율</strong> 측정</li>
<li><strong>RUM 기반</strong>: 실제 사용자 모니터링(RUM)의 <strong>페이지/리소스 지연시간(latency)</strong> 측정</li>
</ul>
</li>
<li><p><strong>SLO 구성</strong>: 위 SLI들을 대상으로 <strong>기간(예: 28일 롤링)</strong>, <strong>목표 비율(예: 성공률 99.9%)</strong>, <strong>대상(엔드포인트/서비스)</strong>를 명확히 설정</p>
</li>
<li><p><strong>알림/할당</strong>: SLO 위반 시 Datadog monitor 으로 알림을 보내고, 담당자에게 할당까지 할 수 있도록한다.</p>
</li>
</ul>
<h3 id="내가-느낀-점">내가 느낀 점</h3>
<ul>
<li>Log/APM을 통해 Slack 알림까지 보내는 부분은 실무에서 많이 사용해 봤지만, <strong>SLI/SLO로 기준을 두고 운영</strong>해 본 것은 처음이라 <strong>좋은 경험</strong>이었다.</li>
<li>이번 구성은 <strong>특정 지표에 임계값을 두고 도달 시 알림</strong>을 보내는 형태였기 때문에, <strong>기존 에러 알림은 병행</strong>해야 한다고 느꼈다.</li>
<li>특히 <strong>이벤트·연휴처럼 트래픽 변동이 큰 시기</strong>에 이런 체계를 갖춰 두면 <strong>더 빠르게 대응</strong>할 수 있을 것 같다.</li>
</ul>
<blockquote>
<p>아래부터는 핸즈온 중 다룬 <strong>이론 정리</strong>입니다.</p>
</blockquote>
<hr>
<h2 id="이론-정리">이론 정리</h2>
<h3 id="왜-sre-관점이-필요한가">왜 SRE 관점이 필요한가</h3>
<ul>
<li><p><strong>Ops 팀 확장성 문제</strong>: 서비스 규모·복잡도가 커질수록 운영 인력을 선형적으로 늘리는 방식은 한계가 있음.</p>
</li>
<li><p><strong>해결 우선순위</strong></p>
<ol>
<li><strong>자동화</strong>: 반복 작업, 배포 검증, 롤백, 런북/자가치유(auto-remediation)</li>
<li><strong>가시성 확보</strong>: 메트릭·로그·트레이스 상관분석으로 원인→영향을 빠르게 좁힘</li>
</ol>
</li>
</ul>
<h3 id="sli-service-level-indicator">SLI (Service Level Indicator)</h3>
<ul>
<li><p><strong>정의</strong>: 사용자가 체감하는 품질을 <strong>분자/분모가 명확한 수식</strong>으로 정의한 지표.</p>
</li>
<li><p><strong>예시</strong></p>
<ul>
<li><strong>지연시간(latency)</strong>: p95/p99 응답시간</li>
<li><strong>오류율(error rate)</strong>: 5xx 수 / 전체 요청 수</li>
<li><strong>성공률(success rate)</strong>: 2xx 수 / 전체 요청 수</li>
<li><strong>비즈니스 품질</strong>: 결제 성공 / 결제 시도 등</li>
</ul>
</li>
</ul>
<h3 id="slo-service-level-objective">SLO (Service Level Objective)</h3>
<ul>
<li><p><strong>정의</strong>: 특정 기간 동안 SLI가 만족해야 하는 <strong>목표</strong>.</p>
</li>
<li><p><strong>좋은 문장 예시</strong></p>
<ul>
<li>&quot;지난 <strong>28일 롤링 윈도우</strong> 기준, <strong>성공률 99.9% 이상</strong>&quot;</li>
<li>&quot;<strong>p95 응답시간 100ms 이하</strong> 요청이 <strong>28일 동안 99%</strong> 이상&quot;</li>
</ul>
</li>
</ul>
<blockquote>
<p>기간·대상·퍼센트가 빠지면 SLO로 불완전하다.</p>
</blockquote>
<h3 id="에러-버짓error-budget">에러 버짓(Error Budget)</h3>
<ul>
<li><strong>정의</strong>: (1 − SLO)만큼 허용되는 실패 여유. 예) SLO 99.9% → <strong>0.1%</strong>가 버짓.</li>
<li><strong>연간 허용 중단시간(감 잡기)</strong></li>
</ul>
<table>
<thead>
<tr>
<th>가용성</th>
<th>연간 허용 중단시간</th>
</tr>
</thead>
<tbody><tr>
<td>99.0%</td>
<td>3일 15시간 36분</td>
</tr>
<tr>
<td>99.5%</td>
<td>1일 19시간 48분</td>
</tr>
<tr>
<td>99.9%</td>
<td>8시간 45분 36초</td>
</tr>
<tr>
<td>99.95%</td>
<td>4시간 22분 48초</td>
</tr>
<tr>
<td>99.99%</td>
<td>52분 34초</td>
</tr>
<tr>
<td>99.999%</td>
<td>5분 15초</td>
</tr>
</tbody></table>
<ul>
<li><strong>운영 정책 예시</strong>: 버짓 소진 시 <strong>배포 일시 중단</strong>, 회고/개선 액션, 위험 실험 제한</li>
</ul>
<h3 id="임계값보다-버짓-소모율burn-rate-기반-알림">임계값보다 <strong>버짓 소모율(Burn Rate)</strong> 기반 알림</h3>
<ul>
<li><p><strong>공식</strong>: <code>Burn rate = (현재 오류율) / (1 − SLO)</code></p>
</li>
<li><p><strong>운영 팁</strong>: <strong>다중 윈도우</strong>로 빠른/느린 소모를 함께 감지</p>
<ul>
<li>예: <strong>1시간/6시간</strong> 두 창에서 기준 초과 시 알림</li>
<li>단발성 스파이크와 장기 악화를 모두 포착하고 <strong>알림 피로도</strong>를 낮춤</li>
</ul>
</li>
</ul>
<h3 id="골든-시그널golden-signals">골든 시그널(Golden Signals)</h3>
<ul>
<li><strong>Latency(지연시간)</strong>: p95/p99, 타임아웃 비율</li>
<li><strong>Errors(오류)</strong>: 5xx·애플리케이션 예외, 비즈니스 실패율</li>
<li><strong>Traffic(트래픽)</strong>: RPS/QPS, 동시 사용자, 메시지 처리량</li>
<li><strong>Saturation(포화도)</strong>: CPU·메모리·스레드풀·큐 길이·DB 커넥션 등 리소스 여유</li>
</ul>
<h3 id="실무-적용-체크리스트">실무 적용 체크리스트</h3>
<ul>
<li><strong>SLI 정의</strong>: 사용자 여정별로 <strong>분자/분모를 문장화</strong>하고 대시보드 메트릭과 <strong>1:1 매핑</strong></li>
<li><strong>SLO 문장화</strong>: <strong>기간·대상·퍼센트</strong>를 명시(엔드포인트/서비스/리전 단위)</li>
<li><strong>알림 설계</strong>: <strong>버짓 소모율 기반 다중 윈도우</strong>, 온콜 할당</li>
</ul>
<h3 id="마무리">마무리</h3>
<p>이번 핸즈온은 기존의 단순 에러 알림을 넘어, <strong>사용자 체감 품질을 수치로 정의(SLI) → 기간 목표(SLO) → 버짓 기반 운영</strong>으로 연결하는 <strong>SRE 운영의 뼈대</strong>를 체감하게 해 주었다. 이벤트·연휴 등 변동 구간에 특히 효과적이며, 팀의 <strong>확장 가능한 운영</strong>을 위한 기본기를 갖추는 데 큰 도움이 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Datadog Summit Seoul]]></title>
            <link>https://velog.io/@banana-wuyu/Datadog-Summit-Seoul</link>
            <guid>https://velog.io/@banana-wuyu/Datadog-Summit-Seoul</guid>
            <pubDate>Fri, 24 Oct 2025 13:33:57 GMT</pubDate>
            <description><![CDATA[<h1 id="2025-datadog-summit-seoul-후기">2025 Datadog Summit Seoul 후기</h1>
<p>AI와 Observability, 두 가지가 가장 큰 주제였다.</p>
<p>이중에서도 AI 관련 내용이 가장 크게 다뤄졌다. 요즘 대세인 AI에 맞춰 많은 내용에 AI가 포함되어 있었다. 실제 핸즈온에서도 &quot;작은 LLM 애플리케이션 개발부터 관측까지&quot;가 있었는데, 신청할 때 인원이 다 차 있어서 아쉽게도 참여하지 못했다.</p>
<h2 id="기조연설-및-외부-부스">기조연설 및 외부 부스</h2>
<p>기조연설과 외부 부스의 내용이 일맥상통했고, 주로 신규 서비스 소개가 중심이었다.</p>
<h3 id="ai-활용-서비스">AI 활용 서비스</h3>
<p>AI를 통해 이슈 트래킹부터 추정 서비스 트리 제공 등 다양한 기능을 제공하며, 실제 담당자에게 연락까지 이어지도록 되어 있었는데 이러한 기능을 외부 부스에서 시연하고 있어 어느 정도 확인할 수 있었다.</p>
<p>자세히는 AI가 자동으로 모니터링하고, Datadog 안에서 이전 이슈와 컨텍스트를 학습해 이를 기반으로 이슈 추정·정리, 오류가 발생한 서비스와 연관된 서비스 트리 등을 제공한다.</p>
<h3 id="클라우드-시큐리티"><a href="https://www.datadoghq.com/ko/dg/security/cloud-infrastructure-security">클라우드 시큐리티</a></h3>
<p>IDE에 MCP Server를 연동해 사용 중인 라이브러리/패키지의 보안 이슈 여부와 코드 레벨의 취약점을 확인해 준다.</p>
<p>AI 기반 로그 보안 탐지, ISMS, IdP 보안 취약점 관리 등 보안 관련 내용도 많았지만, AI 기반 내용이 특히 두드러졌다.</p>
<h2 id="아모레퍼시픽의-ai--통합-observability-혁신-사례">아모레퍼시픽의 AI + 통합 Observability 혁신 사례</h2>
<p>B2C용 AI 챗을 도입한 내용이 주였다. Datadog LLM 대시보드를 활용해 Observability를 확보했다.</p>
<p>보통 기업은 AI를 도입할 때 사내 서비스부터 차근차근 시작하지만, 여기는 곧바로 B2C AI 서비스를 오픈한 것이 특징이었다. 오픈 과정에서 발생한 이슈와 고려했던 점을 공유했는데, 이 부분이 매우 도움이 되었다.</p>
<h3 id="이슈">이슈</h3>
<p>AI 서비스를 운영하면서 발생한 문제들.</p>
<h4 id="품질---response-quality">품질 - Response Quality</h4>
<p>AI 특성상 비동기 요청이 많아 품질 이슈가 생기기 쉬운데, 속도, AI 답변 품질, 응답 실패 등이 주요 포인트였다. 각 케이스도 다른 API와 유사하게 LLM 대시보드를 활용해 개선했다고 한다.</p>
<h4 id="안전안정성">안전/안정성</h4>
<p>이 이슈는 실제 보안 문제뿐 아니라 프롬프트 인젝션, AI 사용 시 고객이 받는 답변의 적절성 문제도 포함된다고 했다. 정치·사회 등 민감 주제에 대해선 부적절한 표현이 포함되지 않도록 한 차례 검증한다고 한다. 또한 유사한 방식으로, 다른 기업과의 계약 기간이 만료되었음에도 해당 기업 관련 내용이 포함되는지 여부도 검증한다고 한다.</p>
<h4 id="비용">비용</h4>
<p>비용 측면에서는 토큰 비용이 주요 이슈였다. Datadog LLM 대시보드를 통해 토큰 사용량이 많은 스팬과 호출 빈도가 높은 스팬을 파악해 캐싱하는 방식을 사용했다고 한다.</p>
<h3 id="sre-hands-on">SRE Hands-on</h3>
<p><a href="https://velog.io/@banana-wuyu/Datadog-Summit-Seoul-hands-on">https://velog.io/@banana-wuyu/Datadog-Summit-Seoul-hands-on</a></p>
<h2 id="마무리">마무리</h2>
<h3 id="아쉬운-부분">아쉬운 부분</h3>
<ol>
<li>핸즈온과 다른 기업들의 세션이 각각 진행되어, 핸즈온 때문에 다른 기업 세션을 듣지 못한 게 매우 아쉽다. 핸즈온을 포기하고 세션을 들을걸 후회할 정도로 아쉬웠다. 오전에 아모레퍼시픽의 개발 경험을 듣고 인사이트를 많이 얻었는데, 다른 사례들도 들었으면 더 많은 인사이트를 얻을 수 있었을 텐데 아쉽다.</li>
<li>외부 행사는 인프런 행사가 처음이었고, 이번 Datadog Summit Seoul 2025가 두 번째라 사진을 많이 찍지 못한 게 아쉽다. 나중에 영상도 찾아봐야겠다.</li>
<li>내용을 충분히 정리하지 못했다.</li>
</ol>
<h3 id="좋았던-부분">좋았던 부분</h3>
<ol>
<li>들었던 다른 기업의 실제 후기가 매우 좋았다. 만약 AI 관련 서비스를 도입한다면, 여기서 얻은 인사이트들은 최소한 고려할 것 같다.</li>
<li>앞으로 다른 행사나 세션이 있으면 무엇을 들어야 하는지, 사진 촬영과 정리를 어떻게 해야 하는지 배웠다.</li>
</ol>
<h3 id="사진들">사진들</h3>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/ffe29ed0-5c0e-4d36-a1e5-39bad1112db1/image.png" alt="">
<img src="https://velog.velcdn.com/images/banana-wuyu/post/c4c5c3b0-1a11-408f-aaed-ee89136c710b/image.png" alt="">
<img src="https://velog.velcdn.com/images/banana-wuyu/post/2c04dae4-fd63-40b3-8c2f-28c108280d86/image.png" alt="">
<img src="https://velog.velcdn.com/images/banana-wuyu/post/e9d159c6-8738-4622-913d-2963d7b765b8/image.png" alt="">
<img src="https://velog.velcdn.com/images/banana-wuyu/post/5970c7ff-fd6a-4d62-9d12-47d6ddf2fe6a/image.png" alt="">
<img src="https://velog.velcdn.com/images/banana-wuyu/post/ff1f472d-5b70-4ed7-93bc-5286c90f038f/image.png" alt="">
<img src="https://velog.velcdn.com/images/banana-wuyu/post/df1d8527-6b4d-4b43-9b8b-4cdfe32615a3/image.png" alt="">
<img src="https://velog.velcdn.com/images/banana-wuyu/post/e2243505-355c-4a19-8ab1-e3dd274dea73/image.png" alt="">
<img src="https://velog.velcdn.com/images/banana-wuyu/post/ef30f789-68db-4db1-baa8-34c503f27d76/image.png" alt="">
<img src="https://velog.velcdn.com/images/banana-wuyu/post/58bb5093-d881-4559-94f8-6de3f8db0e3a/image.png" alt="">
<img src="https://velog.velcdn.com/images/banana-wuyu/post/5abceb52-fbf5-44f3-be04-b00083bcf119/image.png" alt="">
<img src="https://velog.velcdn.com/images/banana-wuyu/post/040ca845-8733-4133-9ce8-bc6b6ea5d0e1/image.png" alt="">
<img src="https://velog.velcdn.com/images/banana-wuyu/post/3ec2ed87-90f8-40ac-bfd9-fba40b32305d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RDS 감사 로그, DynamoDB와 SQS 기반 이력 시스템으로 옮기기]]></title>
            <link>https://velog.io/@banana-wuyu/RDS-%EA%B0%90%EC%82%AC-%EB%A1%9C%EA%B7%B8-DynamoDB%EC%99%80-SQS-%EA%B8%B0%EB%B0%98-%EC%9D%B4%EB%A0%A5-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%EC%98%AE%EA%B8%B0%EA%B8%B0</link>
            <guid>https://velog.io/@banana-wuyu/RDS-%EA%B0%90%EC%82%AC-%EB%A1%9C%EA%B7%B8-DynamoDB%EC%99%80-SQS-%EA%B8%B0%EB%B0%98-%EC%9D%B4%EB%A0%A5-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%EC%98%AE%EA%B8%B0%EA%B8%B0</guid>
            <pubDate>Wed, 30 Jul 2025 14:17:00 GMT</pubDate>
            <description><![CDATA[<h2 id="프로젝트-개요">프로젝트 개요</h2>
<ul>
<li>기존 RDS(MySQL) 환경에서는 여러 테이블의 히스토리성 데이터(감사 이력, 변경 로그 등)를 각각 별도의 테이블에 저장해 관리하고 있었습니다.</li>
<li><strong>감사 로그(히스토리 데이터)</strong>는 저장 빈도는 높고, 조회 빈도는 낮아 RDS의 저장/운영 비용과 성능 측면에서 비효율적이었습니다.</li>
<li>데이터 관리 일관성과 운영 효율성을 위해 <strong>DynamoDB 기반 이력 관리 시스템</strong>으로 구조를 개선했습니다.</li>
<li>SQS 및 AWS EventBridge 등 <strong>이벤트 기반 아키텍처</strong>를 적용하여, 다양한 모듈(관리자/유저 앱 등)과 멀티 컨테이너 환경에서도 서비스 무중단, 확장성을 보장할 수 있게 설계했습니다.</li>
</ul>
<hr>
<h2 id="기존-구조의-문제점">기존 구조의 문제점</h2>
<ol>
<li><strong>저장 및 운영 비용 증가</strong><ul>
<li>RDS는 읽기·쓰기 부하 모두 가격이 높고, 히스토리 테이블을 별도로 관리하면 스토리지/성능/백업 오버헤드가 큽니다.</li>
</ul>
</li>
<li><strong>I/O 부하 및 트랜잭션 병목</strong><ul>
<li>join이 많은 쿼리와 이력 insert가 동시에 발생하면, 메인 서비스 트랜잭션에도 영향을 주는 구조적 한계가 있습니다.</li>
</ul>
</li>
<li><strong>확장성 한계</strong><ul>
<li>모놀리식/멀티모듈 환경에서 여러 컨테이너가 동시에 이력 insert를 하면 RDS 커넥션 풀, 락, 확장성 이슈에 노출됩니다.</li>
</ul>
</li>
<li><strong>운영 관리의 복잡성</strong><ul>
<li>감사 테이블이 서비스별로 분산되어 있어 신규 테이블 추가, 이력 정책 변경, 마이그레이션 등 관리 포인트가 많아집니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="개선-방향-및-솔루션">개선 방향 및 솔루션</h2>
<ul>
<li><strong>비동기/이벤트 기반 파이프라인 도입</strong><ul>
<li>각 모듈에서 발생하는 이력 데이터는 컨테이너 내 <strong>메모리 큐(Heap)에 임시 저장</strong> 후,</li>
<li>일정 주기·사이즈마다 <strong>SQS로 배치 전송</strong>  </li>
<li>SQS에 저장된 메시지는 AWS Batch, EventBridge, Lambda 등을 활용해 비동기로 DynamoDB에 적재</li>
</ul>
</li>
<li><strong>SQS 선택 이유</strong><ul>
<li>감사/히스토리 데이터는 엄격한 순서 보장이 불필요</li>
<li>SQS는 저렴한 비용, 수평 확장성, AWS 서비스 연계성이 우수</li>
<li>RabbitMQ 등 타 메시지큐 대비 운영·모니터링 편의성(콘솔 UI/UX)도 장점</li>
</ul>
</li>
</ul>
<hr>
<h2 id="구현-상세">구현 상세</h2>
<h3 id="1-메모리-큐heap-적재--안전한-종료-처리">1. 메모리 큐(Heap) 적재 &amp; 안전한 종료 처리</h3>
<ul>
<li>컨테이너 내 각 인스턴스에서 이력 데이터를 <strong>Thread-safe 메모리 큐</strong>에 임시 저장</li>
<li>shutdown: graceful, timeout-per-shutdown-phase: 360s, @PreDestroy 등으로 <strong>서버 종료 시 안전하게 잔여 데이터 flush</strong>  <pre><code class="language-yaml">  server:
    shutdown: graceful
  spring:
    lifecycle:
      timeout-per-shutdown-phase: 360s</code></pre>
<pre><code class="language-java">  @PreDestroy
  public void flushQueueBeforeShutdown() {
      eventQueue.flushAll(); // 큐에 남은 데이터 안전하게 SQS로 전송
  }</code></pre>
</li>
</ul>
<h3 id="2-배치-전송-트리거">2. 배치 전송 트리거</h3>
<ul>
<li>일정 시간 간격(예: 10초), 또는 큐 크기가 특정 임계치 도달 시 SQS로 배치 전송  </li>
<li>전송 실패 시 재시도/예외 로깅 등 장애 대응 설계</li>
</ul>
<h3 id="3-sqs-→-dynamodb-비동기-적재">3. SQS → DynamoDB 비동기 적재</h3>
<ul>
<li>AWS Lambda, Batch, EventBridge로 SQS 메시지 폴링/수신</li>
<li>각 이력 데이터는 <strong>PK+SK 구조, GSI 인덱싱 등 확장 가능</strong>하게 설계  <ul>
<li>신규 테이블/로그 타입 추가 시 구조 변경 없이 확장 가능</li>
</ul>
</li>
</ul>
<hr>
<h2 id="아키텍처-및-데이터-모델-상세">아키텍처 및 데이터 모델 상세</h2>
<h3 id="시스템-아키텍처-흐름">시스템 아키텍처 흐름</h3>
<pre><code>[서비스] ─▶ [메모리 큐] ─▶ [SQS] ─▶ [Lambda/Batch] ─▶ [DynamoDB]
   ↑                                  │
(여러 인스턴스,                      │
  확장 가능)                        │
                                      ▼
                          [운영자/조회 서비스/분석 등]</code></pre><h3 id="dynamodb-단일-테이블-설계">DynamoDB 단일 테이블 설계</h3>
<ul>
<li><p><strong>PK(Partition Key):</strong> <code>{database}#{table}#{id}</code></p>
</li>
<li><p><strong>SK(Sort Key):</strong> <code>{timestamp}#{uuid}</code><br>  → 시간순 이력 정렬, 중복 방지, 범위 쿼리 지원</p>
</li>
<li><p><strong>GSI(Global Secondary Index):</strong>  </p>
<ul>
<li>예) <code>target_type_id-index</code>, <code>target_type_id_record_type-index</code> 등  </li>
<li>다양한 조회 패턴(타입별, 엔티티별, 날짜별 등)에 최적화</li>
</ul>
</li>
<li><p><strong>Map 구조 필드(<code>AuditingField</code>)</strong>  </p>
<ul>
<li>key: <code>&quot;tableName.fieldName&quot;</code></li>
<li>value: 필드명/값/사용자표시명 등  </li>
<li>신규 필드/테이블 추가에도 구조 변경 없이 확장</li>
</ul>
</li>
</ul>
<h4 id="dynamodb-감사-이력-item-예시">DynamoDB 감사 이력 Item 예시</h4>
<pre><code class="language-json">{
  &quot;pk&quot;: &quot;sampledb#sample_table#1001&quot;,
  &quot;sk&quot;: &quot;2024-07-30T10:15:20.123456#d24f2e67-xxxx-xxxx-xxxx-xxxxxx&quot;,
  &quot;actionType&quot;: &quot;UPDATE&quot;,
  &quot;targetType&quot;: &quot;sample_table&quot;,
  &quot;targetId&quot;: &quot;1001&quot;,
  &quot;fields&quot;: {
    &quot;sample_table.name&quot;: { &quot;name&quot;: &quot;이름&quot;, &quot;value&quot;: &quot;데모상품&quot;, &quot;value_name&quot;: &quot;데모상품&quot; },
    &quot;sample_table.status&quot;: { &quot;name&quot;: &quot;상태&quot;, &quot;value&quot;: &quot;ACTIVE&quot;, &quot;value_name&quot;: &quot;활성&quot; }
  },
  &quot;createdAt&quot;: &quot;2024-07-30T10:15:20.123456&quot;
  // ... 기타 조회/색인 필드 ...
}</code></pre>
<h4 id="java-데이터-모델-예시">Java 데이터 모델 예시</h4>
<pre><code class="language-java">public class AuditingField {
    private final Map&lt;String, AuditingFieldInfo&gt; fields = new ConcurrentHashMap&lt;&gt;();
    public static class AuditingFieldInfo {
        String name;       // 필드명
        String value;      // 실제 값
        String valueName;  // 사용자용 표시값
    }
}</code></pre>
<pre><code class="language-java">public class AuditingHistory {
    // PK, SK, actionType 등 주요 메타데이터
    private String pk;
    private String sk;
    private String actionType;
    // ... 생략 ...
    private AuditingField fields;
}</code></pre>
<pre><code class="language-java">public class AuditingFieldConverter implements AttributeConverter&lt;AuditingField&gt; {
    @Override
    public AttributeValue transformFrom(AuditingField input) {
        // Map&lt;String, AttributeValue&gt;로 변환
    }
    @Override
    public AuditingField transformTo(AttributeValue input) {
        // AttributeValue에서 Map 복원
    }
}</code></pre>
<hr>
<h2 id="주요-효과-및-개선점">주요 효과 및 개선점</h2>
<ul>
<li><strong>RDS 부하 감소 및 저장 비용 최적화</strong></li>
<li><strong>메인 트랜잭션 경량화, 장애 시 서비스 영향 최소화(Fail-safe)</strong></li>
<li><strong>운영 편의성</strong>: 테이블 추가/이력 정책 변경 시 구조 변경 최소화</li>
<li><strong>트래픽 급증, 컨테이너 수평 확장에도 무중단 운영</strong></li>
<li><strong>새로운 로그 타입/이력 테이블 추가도 구조적 유연성 보장</strong></li>
</ul>
<hr>
<h2 id="한-줄-요약">한 줄 요약</h2>
<blockquote>
<p>RDS에 분산 저장하던 감사 이력(로그) 데이터를 메모리 큐, SQS, DynamoDB 기반의 <strong>비동기 이벤트 파이프라인</strong>으로 이관해<br>저장/운영 효율, 시스템 확장성, 장애 내성을 크게 개선한 실전 사례입니다.</p>
</blockquote>
<hr>
<h2 id="참고심화-기술스택·운영-경험">참고/심화 (기술스택·운영 경험)</h2>
<ul>
<li><strong>Spring Boot, AWS SQS, Lambda, DynamoDB, Java</strong></li>
<li>Spring Batch, Virtual Thread, Hibernate Listener 등도 유연하게 조합 가능</li>
<li>실제 적용시 RDS 비용 절감, 서비스 latency 감소, 무중단 배포 및 장애 복구 경험 보유</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker Debian 10(buster) 및 11(bullseye) apt 오류 해결]]></title>
            <link>https://velog.io/@banana-wuyu/Docker-Debian-10buster-%EB%B0%8F-11bullseye-apt-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@banana-wuyu/Docker-Debian-10buster-%EB%B0%8F-11bullseye-apt-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Mon, 14 Jul 2025 09:09:45 GMT</pubDate>
            <description><![CDATA[<h1 id="docker에서-debian-버전별-apt-오류-해결-정리">Docker에서 Debian 버전별 apt 오류 해결 정리</h1>
<p>Dockerfile에서 <code>apt-get update &amp;&amp; apt-get install -y curl</code> 실행 중 아래와 같은 오류가 발생했다:</p>
<pre><code>E: The repository &#39;http://deb.debian.org/debian buster Release&#39; does not have a Release file.
E: The repository &#39;http://deb.debian.org/debian buster-updates Release&#39; does not have a Release file.
E: The repository &#39;http://security.debian.org/debian-security buster/updates Release&#39; does not have a Release file.</code></pre><hr>
<h2 id="1-debian-10-buster-레거시-환경에서-해결-방법">1. Debian 10 (buster) 레거시 환경에서 해결 방법</h2>
<p>Debian 10 (buster)은 2024년 기준으로 EOL(지원 종료)되어 공식 저장소에서 제거되었기 때문에, <code>apt-get update</code> 시 404 오류가 발생한다.</p>
<h3 id="해결-방법-아카이브-저장소로-변경">해결 방법: 아카이브 저장소로 변경</h3>
<p>Dockerfile에 아래 내용을 추가하여 저장소 URL을 archive 주소로 바꾼다:</p>
<pre><code class="language-dockerfile">RUN sed -i &#39;s|http://deb.debian.org/debian|http://archive.debian.org/debian|g&#39; /etc/apt/sources.list &amp;&amp; \
    sed -i &#39;s|http://security.debian.org/debian-security|http://archive.debian.org/debian-security|g&#39; /etc/apt/sources.list &amp;&amp; \
    apt-get update &amp;&amp; apt-get install -y curl</code></pre>
<hr>
<h2 id="2-debian-11-bullseye로-버전-변경-시-발생한-의존성-오류-해결">2. Debian 11 (bullseye)로 버전 변경 시 발생한 의존성 오류 해결</h2>
<p>기존 buster 이미지를 대체하여 아래와 같이 bullseye 기반의 openjdk 이미지를 사용한 경우:</p>
<pre><code class="language-dockerfile">FROM openjdk:11-jre-slim-bullseye</code></pre>
<p><code>apt-get install</code>이 일부 GUI/폰트 관련 라이브러리를 포함하지 않아 실행 중 오류 발생.</p>
<h3 id="해결-방법-필요한-라이브러리-명시적-설치">해결 방법: 필요한 라이브러리 명시적 설치</h3>
<pre><code class="language-dockerfile">RUN apt update &amp;&amp; apt install -y --no-install-recommends \
    curl \
    libfreetype6 \
    libfontconfig1 \
    libx11-6 \
    libxext6 \
    libxrender1 \
    &amp;&amp; rm -rf /var/lib/apt/lists/*</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI 자동완성을 사용을 멈춰보자]]></title>
            <link>https://velog.io/@banana-wuyu/AI-%EC%9E%90%EB%8F%99%EC%99%84%EC%84%B1%EC%9D%84-%EC%82%AC%EC%9A%A9%EC%9D%84-%EB%A9%88%EC%B6%B0%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@banana-wuyu/AI-%EC%9E%90%EB%8F%99%EC%99%84%EC%84%B1%EC%9D%84-%EC%82%AC%EC%9A%A9%EC%9D%84-%EB%A9%88%EC%B6%B0%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 03 Jul 2025 02:13:16 GMT</pubDate>
            <description><![CDATA[<h1 id="ai-사용-원칙-및-방향성">AI 사용 원칙 및 방향성</h1>
<h2 id="1-왜-이런-생각을-하게-되었는가">1. 왜 이런 생각을 하게 되었는가</h2>
<p>최근 <strong>코딩테스트</strong>를 준비하면서 아래와 같은 문제를 자주 느꼈다:</p>
<ul>
<li>문제를 보고도 <strong>접근 방식이 떠오르지 않음</strong></li>
<li>AI가 짜준 코드는 동작하지만, <strong>왜 그렇게 구현되는지 모름</strong></li>
<li>비슷한 문제를 다시 풀려고 하면 <strong>스스로 구현하지 못함</strong></li>
<li>기본 개념이 부족하니 <strong>디버깅도 어려움</strong></li>
</ul>
<p>이 과정에서 <strong>AI 자동완성을 그대로 사용하는 것이 내 실력을 갉아먹고 있다</strong>는 문제의식을 갖게 되었고, 학습 방향을 다시 잡기로 했다.</p>
<hr>
<h2 id="2-문제의-핵심">2. 문제의 핵심</h2>
<table>
<thead>
<tr>
<th>문제점</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>표면적인 이해</td>
<td>AI가 제공하는 코드는 결과 중심이라 중간 사고 과정이 생략됨</td>
</tr>
<tr>
<td>학습 효과 저하</td>
<td>스스로 개념을 고민하지 않고 정답을 복사하게 됨</td>
</tr>
<tr>
<td>재사용 불가능</td>
<td>나중에 유사한 문제를 만나도 스스로 해결하지 못함</td>
</tr>
<tr>
<td>디버깅 취약</td>
<td>코드가 망가지면 어디서 문제인지 감을 못 잡음</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-ai-사용-범위-제한">3. AI 사용 범위 제한</h2>
<p>효율적인 학습을 위해 AI 사용을 다음 범위로 제한한다:</p>
<table>
<thead>
<tr>
<th>✅ 허용 범위</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>코드 리뷰</strong></td>
<td>작성한 코드에 대한 개선점, 리팩토링, 시간복잡도 검토 요청</td>
</tr>
<tr>
<td><strong>정보 확인</strong></td>
<td>자료구조, 알고리즘, 라이브러리 등 개념적 정보 확인</td>
</tr>
<tr>
<td><strong>문서 정리</strong></td>
<td>기술 문서, 회의록, 문제 풀이 회고 등을 요약/정리</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-ai-사용-금지-영역">4. AI 사용 금지 영역</h2>
<table>
<thead>
<tr>
<th>⛔️ 금지 영역</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td><strong>코드 자동완성</strong></td>
<td>개념 이해 없이 자동으로 코드를 작성하게 되어 학습이 되지 않음</td>
</tr>
<tr>
<td><strong>전체 로직 구현 요청</strong></td>
<td>문제 해결 능력이 떨어지고 의존성이 높아짐</td>
</tr>
<tr>
<td><strong>템플릿 복사 요청</strong></td>
<td>뼈대부터 스스로 고민하는 경험을 놓치게 됨</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-실천-방법">5. 실천 방법</h2>
<ul>
<li>문제를 만나면 <strong>직접 설계</strong> → <strong>직접 코드 작성</strong></li>
<li>막히면 AI에게 <strong>개념 또는 구조 질문</strong>을 하여 보완</li>
<li>코딩 후에는 AI에게 <strong>코드 리뷰 및 개선 포인트 요청</strong></li>
<li>문제 풀이/회고는 정리해서 <strong>학습 로그 남기기</strong></li>
</ul>
<hr>
<h2 id="6-예시로-보는-올바른-사용">6. 예시로 보는 올바른 사용</h2>
<table>
<thead>
<tr>
<th>❌ 잘못된 사용</th>
<th>✅ 올바른 사용</th>
</tr>
</thead>
<tbody><tr>
<td>&quot;Spring Security 로그인 예제 코드 줘&quot;</td>
<td>&quot;내가 만든 로그인 설정 리뷰해줘&quot;</td>
</tr>
<tr>
<td>&quot;이 문제 풀어줘&quot;</td>
<td>&quot;내가 작성한 풀이 로직을 리뷰해줘&quot;</td>
</tr>
<tr>
<td>&quot;오류 고쳐줘&quot;</td>
<td>&quot;이 오류 메시지의 의미와 원인을 알려줘&quot;</td>
</tr>
</tbody></table>
<hr>
<h2 id="7-핵심-문장">7. 핵심 문장</h2>
<blockquote>
<p><strong>AI는 내 코드를 도와주는 리뷰어이지, 코드를 대신 짜주는 개발자는 아니다.</strong></p>
</blockquote>
<hr>
<h2 id="8-앞으로의-목표">8. 앞으로의 목표</h2>
<ul>
<li><strong>문제 해결 능력 강화</strong>  </li>
<li><strong>자료구조/알고리즘 개념 복습 및 정리</strong>  </li>
<li><strong>회고 중심의 학습 습관 정착</strong>  </li>
<li><strong>AI를 도구로 활용하되, 절대 의존하지 않기</strong></li>
</ul>
<h2 id="9-이-글도-ai가-정리했다">9. 이 글도 AI가 정리했다...</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin Coroutine FCM Push 처리 - Dispatchers.IO vs 커스텀 디스패처 정리]]></title>
            <link>https://velog.io/@banana-wuyu/Kotlin-Coroutine-FCM-Push-%EC%B2%98%EB%A6%AC-Dispatchers.IO-vs-%EC%BB%A4%EC%8A%A4%ED%85%80-%EB%94%94%EC%8A%A4%ED%8C%A8%EC%B2%98-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@banana-wuyu/Kotlin-Coroutine-FCM-Push-%EC%B2%98%EB%A6%AC-Dispatchers.IO-vs-%EC%BB%A4%EC%8A%A4%ED%85%80-%EB%94%94%EC%8A%A4%ED%8C%A8%EC%B2%98-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 02 Jun 2025 02:35:16 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>Kotlin Coroutine 환경에서 대량의 FCM Push 메시지를 처리할 때,
많은 예제나 문서에서는 <code>Dispatchers.IO</code>를 사용하는 것을 기본으로 제시합니다. 그러나 실무에서는 다음과 같은 이유로 커스텀 <code>ThreadPoolTaskExecutor</code>를 Coroutine Dispatcher로 wrapping 하여 사용하는 경우도 많습니다:</p>
<ul>
<li>OutOfMemoryError 방지</li>
<li>Push 트래픽이 시스템 전체에 영향을 주는 것을 방지</li>
</ul>
<p>이 글에서는 <code>Dispatchers.IO</code>와 커스텀 디스패처 각각의 장단점을 비교하고, 어떤 상황에서 어떤 선택이 더 합리적인지를 다룹니다.</p>
<hr>
<h2 id="dispatchersio-특징">Dispatchers.IO 특징</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>자동 확장</td>
<td>CPU 코어 수의 64배까지 자동 스레드 확장</td>
</tr>
<tr>
<td>idle thread 제거</td>
<td>사용하지 않으면 자동 스레드 정리</td>
</tr>
<tr>
<td>용도</td>
<td>파일, DB, 네트워크 등 Blocking I/O 처리</td>
</tr>
<tr>
<td>공유 자원</td>
<td>전체 시스템에서 공용으로 사용됨</td>
</tr>
</tbody></table>
<h3 id="장점">장점</h3>
<ul>
<li>사용이 간단하다 (<code>withContext(Dispatchers.IO)</code>만 쓰면 됨)</li>
<li>대부분의 I/O 작업에 적절한 성능</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>공용 풀이라서 Push 트래픽 폭주 시 다른 작업까지 영향을 받을 수 있음</li>
<li>최대 스레드 수는 제한되지만 강제로 증가 가능 → 메모리 압박 발생</li>
</ul>
<hr>
<h2 id="커스텀-dispatcher-threadpooltaskexecutor-기반">커스텀 Dispatcher (ThreadPoolTaskExecutor 기반)</h2>
<h3 id="목적">목적</h3>
<blockquote>
<p>Push 트래픽이 많아졌을 때 전체 시스템에 영향을 주지 않도록 격리</p>
</blockquote>
<h3 id="예시-코드">예시 코드</h3>
<pre><code class="language-kotlin">@Bean
fun firebasePushExecutor(): ThreadPoolTaskExecutor {
    return ThreadPoolTaskExecutor().apply {
        corePoolSize = 4
        maxPoolSize = 8
        setQueueCapacity(3000)
        setThreadNamePrefix(&quot;FirebasePush-&quot;)
        setRejectedExecutionHandler(ThreadPoolExecutor.CallerRunsPolicy())
        initialize()
    }
}</code></pre>
<pre><code class="language-kotlin">val firebaseDispatcher = firebasePushExecutor.asCoroutineDispatcher()</code></pre>
<h3 id="장점-1">장점</h3>
<ul>
<li>스레드 풀을 격리해서 Push 실패가 다른 기능에 영향을 주지 않음</li>
<li>core/max/thread queue 제어 가능</li>
<li>Micrometer, Prometheus 등을 통한 모니터링 가능</li>
<li>큐가 꽉 찼을 때 reject 정책 설정 가능</li>
</ul>
<h3 id="단점-1">단점</h3>
<ul>
<li>코드가 복잡해지고 관리 부담 증가</li>
</ul>
<hr>
<h2 id="실전-비교-요약">실전 비교 요약</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>Dispatchers.IO</th>
<th>커스텀 Dispatcher</th>
</tr>
</thead>
<tbody><tr>
<td>관리 용이성</td>
<td>자동</td>
<td>직접 관리 필요</td>
</tr>
<tr>
<td>리소스 격리</td>
<td>없음</td>
<td>Push 트래픽 격리 가능</td>
</tr>
<tr>
<td>OOM 방지</td>
<td>제한적</td>
<td>제어 가능</td>
</tr>
<tr>
<td>큐 설정</td>
<td>불가능</td>
<td>명시적 설정 가능</td>
</tr>
<tr>
<td>모니터링</td>
<td>JVM 기반</td>
<td>Spring Actuator 기반</td>
</tr>
</tbody></table>
<hr>
<h2 id="결론">결론</h2>
<blockquote>
<p>Dispatchers.IO는 단순하고 강력하지만, 격리와 안정성 측면에서 커스텀 디스패처가 더 안전한 선택일 수 있다.</p>
</blockquote>
<ul>
<li>Push 트래픽이 시스템 전체를 마비시킬 위험이 있다면 → 커스텀 Dispatcher 사용</li>
<li>단순한 비동기 처리라면 → Dispatchers.IO 사용</li>
</ul>
<hr>
<h2 id="최종-요약-표">최종 요약 표</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>추천</th>
</tr>
</thead>
<tbody><tr>
<td>단순 외부 API / DB I/O</td>
<td>Dispatchers.IO</td>
</tr>
<tr>
<td>대량 푸시, 메시징, 격리 필요</td>
<td>커스텀 디스패처</td>
</tr>
<tr>
<td>시스템 리스크 분리 필요</td>
<td>커스텀 디스패처</td>
</tr>
<tr>
<td>코드 간결성 / 테스트</td>
<td>Dispatchers.IO</td>
</tr>
</tbody></table>
<hr>
<h2 id="부록-fcm-push-처리-구조-예시-코루틴-기반">부록: FCM Push 처리 구조 예시 (코루틴 기반)</h2>
<pre><code class="language-kotlin">val scope = CoroutineScope(firebaseDispatcher + coroutineContext)

val jobs = chunk.map { pushMessage -&gt;
    val fcmMessage = pushMessage.toFcmMessage()
    pushMessage to scope.async {
        try {
            pushMessage to firebaseHttp2Client.send(...)
        } catch (e: Exception) {
            pushMessage to e
        }
    }
}

jobs.forEach { (pushMessage, job) -&gt;
    val result = job.await()
    if (result.second is Throwable) {
        // 실패
    } else {
        // 성공
    }
}</code></pre>
<hr>
<p>실제로 의도적으로 푸시 트래픽을 커스텀 풀로 분리해두면, 장애 전파를 차단하고 운영 안정성을 크게 높일 수 있습니다.</p>
<blockquote>
<p>&quot;자동으로 잘 관리된다고 해서, 모든 상황에 안전한 건 아니다.&quot; → 이게 핵심입니다.</p>
</blockquote>
<hr>
<p>※ 이 글은 개발자에 의해 작성되었으며, 일부 정리 및 요약 과정에서 OpenAI GPT의 도움을 받았습니다.</p>
<p><strong>Note</strong>: This document was authored and reviewed by a developer, with the assistance of OpenAI&#39;s GPT to accelerate summarization and clarity.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[rabbitmq vs sqs]]></title>
            <link>https://velog.io/@banana-wuyu/rabbitmq-vs-sqs</link>
            <guid>https://velog.io/@banana-wuyu/rabbitmq-vs-sqs</guid>
            <pubDate>Thu, 29 May 2025 03:08:48 GMT</pubDate>
            <description><![CDATA[<p>도입: 왜 큐를 고려하게 되었는가?</p>
<p>메시지 서버를 설계하면서 처음에는 HTTP 기반의 요청 처리를 고려했습니다. 하지만 HTTP는 클라이언트의 요청 수에 따라 서버가 직접 응답해야 하므로, 서버의 처리 능력과 상관없이 부하가 한순간에 몰릴 수 있는 위험이 있습니다.</p>
<p>이에 따라 대안으로 떠오른 것이 큐 기반 아키텍처입니다.
Queue를 사용하면 메시지를 먼저 받아 저장한 후, 서버의 처리 능력에 맞게 정량적으로 메시지를 소비할 수 있어 전체 시스템의 안정성과 확장성을 높일 수 있습니다. 특히 AWS ECS와 같은 컨테이너 오케스트레이션 환경에서는, 컨테이너 수를 조절하면서 손쉽게 수평 확장이 가능하다는 점도 큰 장점이었습니다.</p>
<h1 id="rabbitmq-vs-amazon-sqs-spring-환경에서의-메시징-처리-방식-비교">RabbitMQ vs Amazon SQS: Spring 환경에서의 메시징 처리 방식 비교</h1>
<p>Spring을 사용하는 백엔드 시스템에서 메시지 큐를 도입할 때 흔히 비교하는 두 가지 기술은 <strong>RabbitMQ</strong>와 <strong>Amazon SQS</strong>입니다. 각각의 특징과 처리 방식, 재시도, 중복 처리, 성능에 대해 자세히 정리해봅니다.</p>
<hr>
<h2 id="주요-비교-요약">주요 비교 요약</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>RabbitMQ</th>
<th>Amazon SQS (Standard / FIFO)</th>
</tr>
</thead>
<tbody><tr>
<td>비용</td>
<td>인프라 운영 필요 (비쌈)</td>
<td>요청 수 기반 과금 (저렴), 무료 티어 있음</td>
</tr>
<tr>
<td>가독성</td>
<td>설정 복잡 (Exchange, Queue 등)</td>
<td>Spring Cloud AWS 사용 시 설정 단순</td>
</tr>
<tr>
<td>성능</td>
<td>낮은 지연, 높은 성능</td>
<td>지연 있음 (HTTP 기반, long polling)</td>
</tr>
<tr>
<td>호환성</td>
<td>다양한 프로토콜 및 브로커 간 연동 가능</td>
<td>AWS 생태계 중심</td>
</tr>
<tr>
<td>순차 처리</td>
<td>가능 (설계 필요)</td>
<td>FIFO 큐 사용 시 보장</td>
</tr>
</tbody></table>
<hr>
<h2 id="재시도-및-중복-가능성">재시도 및 중복 가능성</h2>
<h3 id="rabbitmq">RabbitMQ</h3>
<ul>
<li><strong>재시도</strong>: <code>nack</code> 및 <code>requeue</code>로 직접 제어 가능.</li>
<li><strong>중복 가능성</strong>: 존재. 예외 발생 시 재전송될 수 있음.</li>
<li><strong>Exactly-once 보장 불가</strong> → <strong>idempotent 처리 권장</strong>.</li>
</ul>
<h3 id="sqs">SQS</h3>
<ul>
<li><strong>재시도</strong>: Visibility Timeout 이후 자동 재전송.</li>
<li><strong>중복 가능성</strong>: Standard는 중복 가능. FIFO는 exactly-once 보장.</li>
<li><strong>Dead Letter Queue(DLQ)</strong> 설정 가능.</li>
</ul>
<hr>
<h2 id="메시지-처리-방식-push-vs-pull">메시지 처리 방식 (Push vs Pull)</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>RabbitMQ</th>
<th>Amazon SQS</th>
</tr>
</thead>
<tbody><tr>
<td>메시지 전달 방식</td>
<td>Push (브로커가 Consumer에게 전송)</td>
<td>Pull (Consumer가 직접 요청)</td>
</tr>
<tr>
<td>처리 흐름</td>
<td><code>basic.consume</code> → 메시지 전달 → ack</td>
<td><code>receiveMessage()</code> → 처리 → <code>deleteMessage()</code></td>
</tr>
<tr>
<td>실시간성</td>
<td>매우 우수</td>
<td>상대적으로 지연 존재</td>
</tr>
<tr>
<td>병렬 처리</td>
<td><code>prefetchCount</code>, <code>concurrency</code>로 제어</td>
<td><code>concurrency</code>, <code>maxMessages</code>, <code>waitTime</code> 설정 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="rabbitmq-push-방식의-안정성">RabbitMQ Push 방식의 안정성</h2>
<h3 id="우려-push가-과도하게-밀어주면-과부하-위험-있음">우려: Push가 과도하게 밀어주면 과부하 위험 있음</h3>
<h3 id="해결-back-pressure-설정">해결: Back-pressure 설정</h3>
<ul>
<li><strong><code>prefetchCount</code></strong>: Consumer가 동시에 받을 수 있는 메시지 수 제한</li>
<li><strong><code>ackMode</code></strong>: 수동 ack 설정으로 처리 후 메시지 제거</li>
<li><strong><code>concurrency</code></strong>: 동시 처리 스레드 수 조절</li>
</ul>
<pre><code class="language-kotlin">@RabbitListener(queues = [&quot;my.queue&quot;], ackMode = &quot;MANUAL&quot;)
fun handleMessage(message: Message, channel: Channel) {
    try {
        // 처리 로직
        channel.basicAck(message.messageProperties.deliveryTag, false)
    } catch (e: Exception) {
        channel.basicNack(message.messageProperties.deliveryTag, false, true)
    }
}</code></pre>
<blockquote>
<p> 제대로 구성하면 <strong>Push 방식도 안정적으로 처리량 제어 가능</strong>.</p>
</blockquote>
<hr>
<h2 id="결론-정리">결론 정리</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>추천 큐 시스템</th>
</tr>
</thead>
<tbody><tr>
<td>실시간 이벤트, 빠른 응답 필요</td>
<td>RabbitMQ</td>
</tr>
<tr>
<td>서버리스, 저비용 비동기 처리</td>
<td>Amazon SQS</td>
</tr>
<tr>
<td>순서 보장 필요, 중복 없어야 함</td>
<td>Amazon SQS FIFO</td>
</tr>
<tr>
<td>복잡한 메시지 라우팅, 고성능 처리</td>
<td>RabbitMQ</td>
</tr>
</tbody></table>
<hr>
<h2 id="마지막-요약">마지막 요약</h2>
<blockquote>
<p>RabbitMQ는 <strong>Push 기반</strong>이지만, <code>prefetch</code>, <code>ack</code>, <code>concurrency</code> 설정을 통해 안정적으로 처리량 제어 가능.<br>SQS는 <strong>Pull 기반</strong>으로 구조가 단순하고 서버리스에 적합.<br>두 방식 모두 idempotent 처리 필수!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[PUSH 대량 발송 속도 이슈]]></title>
            <link>https://velog.io/@banana-wuyu/PUSH-%EB%8C%80%EB%9F%89-%EB%B0%9C%EC%86%A1-%EC%86%8D%EB%8F%84-%EC%9D%B4%EC%8A%88</link>
            <guid>https://velog.io/@banana-wuyu/PUSH-%EB%8C%80%EB%9F%89-%EB%B0%9C%EC%86%A1-%EC%86%8D%EB%8F%84-%EC%9D%B4%EC%8A%88</guid>
            <pubDate>Sun, 30 Mar 2025 14:27:32 GMT</pubDate>
            <description><![CDATA[<h1 id="fcm-push-발송-성능-개선">FCM Push 발송 성능 개선</h1>
<h2 id="1-문제-상황">1. 문제 상황</h2>
<p>기존 시스템에서는 Firebase Admin SDK를 사용해 FCM Push 알림을 발송</p>
<ul>
<li><strong>vCPU</strong>: 1</li>
<li><strong>Memory</strong>: 2GB
하루 수차례에 걸쳐 대량의 Push 메시지를 처리해야 했고, 약 <strong>4만 건의 Push 발송에 10분 이상 소요</strong></li>
</ul>
<hr>
<h2 id="2-원인-분석">2. 원인 분석</h2>
<h3 id="21-firebase-admin-java-sdk-구조-변경">2.1 firebase-admin-java SDK 구조 변경</h3>
<p>FCM SDK의 구조 변경으로 인해 발송 처리 방식에 근본적인 차이가 발생한 것이 성능 저하의 주요 원인이었다.</p>
<ul>
<li><p><strong>firebase-admin-java 9.2.0 이전</strong></p>
<ul>
<li><code>sendAll</code> 메서드로 batch API(<code>https://fcm.googleapis.com/batch</code>)를 사용</li>
<li>다수의 메시지를 <strong>1회의 HTTP 요청으로 처리</strong></li>
</ul>
</li>
<li><p><strong>firebase-admin-java 9.2.0 이후</strong></p>
<ul>
<li><code>sendAll</code>은 Deprecated</li>
<li>대체 메서드인 <code>sendEachForMulticast</code>는 <strong>메시지 1건당 HTTP 요청 1건</strong>을 수행</li>
<li>예: 4만 건 발송 시 4만 건의 HTTP 요청 발생 → 과도한 연결 및 리소스 사용</li>
</ul>
</li>
</ul>
<hr>
<h2 id="3-개선-작업">3. 개선 작업</h2>
<h3 id="31-http2-적용을-통한-병렬-전송-최적화">3.1 HTTP/2 적용을 통한 병렬 전송 최적화</h3>
<ul>
<li>SDK 버전을 <strong>9.4.3</strong>으로 업그레이드하고, <code>ApacheHttp2Transport</code>를 통해 HTTP/2 기반 전송을 적용함.</li>
<li>FCM 서버는 HTTP/2를 지원하며, multiplexing 기능을 활용하면 하나의 커넥션에서 다수의 요청을 동시에 처리할 수 있어 성능 개선에 효과적.</li>
</ul>
<h4 id="설정-예시">설정 예시</h4>
<pre><code class="language-kotlin">val client = HttpAsyncClients.createHttp2System()

val options = FirebaseOptions.builder()
    .setHttpTransport(ApacheHttp2Transport(client))

FirebaseApp.initializeApp(options)</code></pre>
<h3 id="의존성-kotlin">의존성 (Kotlin)</h3>
<pre><code class="language-kotlin">implementation(&quot;org.apache.httpcomponents.core5:httpcore5-h2:5.3&quot;)
implementation(&quot;org.apache.httpcomponents.core5:httpcore5:5.3&quot;)
implementation(&quot;org.apache.httpcomponents.client5:httpclient5:5.4&quot;)</code></pre>
<h3 id="개선-결과">개선 결과</h3>
<ul>
<li>6만 건 발송 기준, 약 4분 소요</li>
<li>기존 대비 처리 속도 약 2.5배 향상</li>
</ul>
<hr>
<h2 id="32-webclient--비동기-방식으로-재구현">3.2 WebClient + 비동기 방식으로 재구현</h2>
<p>HTTP/2 적용만으로도 성능은 개선되었지만, 그 이상의 개선을 위해 다음과 같은 추가 조치를 진행함:</p>
<ul>
<li>기존 <code>firebase-admin-java</code>는 APM(Datadog) trace가 정상적으로 기록되지 않음</li>
<li>완전한 비동기 처리를 위해 <strong>Spring WebClient</strong>를 활용하여 <strong>직접 HTTP 요청을 구현</strong></li>
</ul>
<h3 id="고려-사항">고려 사항</h3>
<p>과거 비동기 방식 시도 당시 OOM(Out Of Memory) 이슈가 있었기 때문에, 다음과 같은 제한을 추가로 적용함:</p>
<ul>
<li><code>ThreadPoolTaskExecutor</code>를 사용하여 스레드 자원 제어<ul>
<li>최대 스레드 수: CPU * 2 (실환경 기준 2개)</li>
<li>대기 큐 크기: 최대 3,000건</li>
</ul>
</li>
<li>현재 사용 중인 스레드 수와 큐 잔여량을 고려하여 <strong>청크 단위로 분할 요청 처리</strong></li>
</ul>
<h3 id="개선-결과-1">개선 결과</h3>
<ul>
<li>6만 건 기준 약 1분 59초 소요</li>
<li>SDK 방식 대비 약 2배 이상 빠름</li>
</ul>
<hr>
<h2 id="4-운영-중-발생한-문제-및-대응">4. 운영 중 발생한 문제 및 대응</h2>
<h3 id="에러-발생">에러 발생</h3>
<p>운영 환경에서 전체 6만 건 중 약 400건에서 다음과 같은 오류가 발생하며 발송 실패:</p>
<pre><code>Error while acquiring from reactor.netty.internal.shaded.reactor.pool.SimpleDequePool</code></pre><h3 id="원인-분석">원인 분석</h3>
<ul>
<li>최초에는 HTTP connection pool 부족 문제로 판단했지만, 실제로는 커넥션 수는 충분했음</li>
<li>문제는 HTTP/2의 <strong>stream 수 제한</strong>에 의해 발생함<ul>
<li>여러 요청이 특정 커넥션에 집중될 경우, 해당 커넥션의 최대 stream 수를 초과할 수 있음</li>
<li>이 경우 요청이 block되거나, pool acquire 실패 발생</li>
</ul>
</li>
</ul>
<h3 id="조치">조치</h3>
<ul>
<li><code>Http2AllocationStrategy</code> 설정을 통해 connection pool의 요청 분산 처리</li>
<li>각 커넥션에 균등하게 요청이 분산되도록 구성</li>
<li>Reactor Netty의 connection 관련 설정을 최적화</li>
</ul>
<h3 id="재처리">재처리</h3>
<ul>
<li>실패한 약 400건은 별도의 큐에 저장</li>
<li>재전송 로직을 통해 순차적으로 재시도하여 <strong>모두 정상 발송 완료</strong></li>
</ul>
<hr>
<h2 id="5-최종-성능-비교">5. 최종 성능 비교</h2>
<table>
<thead>
<tr>
<th>개선 단계</th>
<th>처리 건수</th>
<th>처리 시간</th>
</tr>
</thead>
<tbody><tr>
<td>기존 방식 (firebase-admin-java)</td>
<td>40,000</td>
<td>약 10분</td>
</tr>
<tr>
<td>HTTP/2 적용 후</td>
<td>60,000</td>
<td>약 4분</td>
</tr>
<tr>
<td>WebClient + 비동기 처리</td>
<td>60,000</td>
<td>약 1분 59초</td>
</tr>
</tbody></table>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[인증 PHP -> Kotlin Spring 전환]]></title>
            <link>https://velog.io/@banana-wuyu/%EC%9D%B8%EC%A6%9D-PHP-Kotlin-Spring-%EC%A0%84%ED%99%98</link>
            <guid>https://velog.io/@banana-wuyu/%EC%9D%B8%EC%A6%9D-PHP-Kotlin-Spring-%EC%A0%84%ED%99%98</guid>
            <pubDate>Fri, 03 Jan 2025 20:02:04 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p>e-commerce 도메인에 기존 PHP 레거시 코드를 kotlin으로 변환하는 작업
변환 작업을 최우선으로 생각해서 각각 다른 팀에 팀원이 모여서 작업을 한다.</p>
<h2 id="목표">목표</h2>
<p>팀의 목표는 결제를 제외한 모든 페이지 전환이다. 그 중에 제일 먼저 배포되는 인증 첫 번째 작업을 성공적으로 배포하는 것을 목표로 합니다.</p>
<p>작업 자체가 모든 페이지를 PHP-&gt;Kotlin 으로 한번에 변환하는 것이 아닌, 부분 부분 변환을 하는 작업이다. 그 중에 제일 선행되어야 하는 작업은 인증이다. 모든 페이지에서 필요한 작업이기 때문이다.</p>
<h2 id="스펙">스펙</h2>
<p>FE: next, typescript, react, graphql, rest api
BE: kotlin, spring boot, graphql, rest api</p>
<h2 id="과제">과제</h2>
<ol>
<li>기존에 사용자들은 어떠한 영향도 없어야 한다.</li>
<li>1 기존에 로그인한 사용자 유지</li>
<li>0부터 시작하는 백엔드 개발</li>
<li>1 모니터링 시스템</li>
<li>2 git 전략</li>
<li>개발</li>
<li>배포</li>
<li>트러블 슈팅</li>
<li>1 NAVER 인증시 일부 유저의 약관동의 정보 가져오기 404 오류</li>
<li>2 JWT 브라우저 쿠키 만료시간 설정 오류</li>
</ol>
<h3 id="기존에-사용자들은-어떠한-영향도-없어야-한다">기존에 사용자들은 어떠한 영향도 없어야 한다.</h3>
<p><code>달리는 차 바퀴를 교체 하기</code>
이미 운영되는 도메인이기 때문에 어떠한 영향도 없어야 한다.
기존 사용자는 어떠한 변경점을 느끼지 못 해야한다.</p>
<p>kotlin에는 PHP 에 대한 어떠한 종속성도 가지면 안된다.
모든 페이지가 kotlin으로 전환시에 또 다른 추가작업이 필요하고 그에 따라 더 많은 리소스가 낭비되기 때문이다. 또한, 굳이 레거시 시스템을 신규 작업에 넣어야할 필요성이 없다.</p>
<h4 id="기존에-로그인한-사용자-유지">기존에 로그인한 사용자 유지</h4>
<h5 id="문제">문제</h5>
<p>사용자의 첫 진입점은 PHP , next(kotlin) 두 곳중에 1곳이다.
사용자가 PHP 페이지에 첫 진입 후 next(kotlin)에 접근하면 로그인이 유지되지 않는다.</p>
<p>PHP의 경우 CI_SESSION 이라는 키로 세션 아이디를 쿠키에 저장하고 있다.
kotlin 의 경우 JWT 토큰을 쿠키에 저장하고 있다.</p>
<p>매번 PHP 진입 할때도 JWT 토큰을 쿠키에 저장하면 되지 않는가?
그렇다면 문제가 발생한다. kotlin 으로 변환된 페이지에서 로그아웃을 하더라도, PHP에 로그인이 유지되면서 JWT 토큰을 쿠키에 저장하는 문제가 생긴다.</p>
<h4 id="해결">해결</h4>
<ol>
<li>전환 작업 운영 배포전 3개월간 PHP에 USER ID 기준으로 JWT 토큰을 생성하는 작업을 먼저 배포한다.</li>
<li>PHP에도 JWT가 없으면 로그아웃 시킨다.</li>
<li>PHP JWT가 있고 CI_SESSION(PHP 인증 정보)가 없는 경우 CI_SESSION(PHP 인증 정보)를 새로 생성한다.</li>
<li>전환 작업 운영 배포시에 2, 3번 작업도 PHP도 같이 배포한다.</li>
</ol>
<p>모든 인증 기준은 JWT 로 통일한다.</p>
<table>
<thead>
<tr>
<th align="center">JWT 쿠키</th>
<th align="center">CI_SESSION(PHP 인증 정보)</th>
<th align="center">로그인 여부</th>
</tr>
</thead>
<tbody><tr>
<td align="center">O</td>
<td align="center">X</td>
<td align="center">O</td>
</tr>
<tr>
<td align="center">O</td>
<td align="center">O</td>
<td align="center">O</td>
</tr>
<tr>
<td align="center">X</td>
<td align="center">O</td>
<td align="center">X</td>
</tr>
<tr>
<td align="center">X</td>
<td align="center">X</td>
<td align="center">X</td>
</tr>
<tr>
<td align="center">### 0부터 시작하는 백엔드 개발</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">#### 모니터링 시스템</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">#### git 전략</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">해당 프로젝트 자체가 각각 다른 팀에 있는 인원이 같이 진행하기 때문에 각각 사용하고 있는 git 전략 및 배포 전략이 달라 통일이 필요했습니다.</td>
<td align="center"></td>
<td align="center"></td>
</tr>
</tbody></table>
<p><a href="https://velog.io/@banana-wuyu/Git-Flow">Git-Flow</a>
Git-Flow 전략을 사용하는 방식 채택</p>
<ol>
<li>러닝커브가 높지않다.</li>
<li>기존 도메인에서도 사용하는 사람이 다수라 해당 전략 사용<h4 id="모니터링-시스템">모니터링 시스템</h4>
<h5 id="익셉션-처리">익셉션 처리</h5>
목적<ul>
<li>프론트엔드에서 익셉션 별로 처리하기 쉽도록</li>
<li>모니터링시 명확하게 확인 가능하도록</li>
</ul>
</li>
</ol>
<p>도메인 별로 익셉션 처리</p>
<ul>
<li>각각 도메인 별로 익셉션을 처리 할 수 있도록 <code>open class BusinessException : RuntimeException</code> 클래스 추가</li>
<li>각각 도메인 별로 위에 <code>BusinessException</code> 을 상속 받아 익셉션 코드, 메시지를 만들 수 있도록 한다.</li>
</ul>
<p>프론트에서 익셉션 정보를 확인 가능하도록 처리</p>
<ul>
<li><a href="https://netflix.github.io/dgs/error-handling/#the-typederror-interface">https://netflix.github.io/dgs/error-handling/#the-typederror-interface</a></li>
<li>익셉션 별로 각각 메시지와 코드를 맵핑해서 프론트엔드에서 확인가능</li>
<li>BusinessException, AccessDeniedException 와 같이 정의된 익셉션 처리 및 정의 되지 않은 RuntimeException 익셉션을 가공하여 response 생성</li>
</ul>
<h5 id="datadog-사용">DataDog 사용</h5>
<p>datadog monitor 에 error 에 대한 모니터링을 추가해서 slack 으로 알림 오도록 추가</p>
<p><a href="https://docs.datadoghq.com/ko/monitors/">https://docs.datadoghq.com/ko/monitors/</a></p>
<h3 id="개발">개발</h3>
<ol>
<li>로그인 및 회원가입 구조 설계<ul>
<li>Delegate를 사용해 각 역할(Role)에 특화된 검증 로직을 위임하여 재사용성을 높이고 유지보수성을 개선.</li>
<li>로그인 공통 기능 Façade 및 OAuth Service 공통 인터페이스로 구현하도록 설계</li>
</ul>
</li>
</ol>
<h3 id="배포">배포</h3>
<p>각각 종속성이 강하기 때문에 배포전에 순서를 정하고 배포한다.</p>
<p>배포는 아래 순서로 배포한다.</p>
<ol>
<li>kotlin -&gt; 배포 후 postman 혹은 기타 다른 툴로 api 동작 운영에서 확인</li>
<li>PHP -&gt; 배포 후 기존 로그인 사용자 유지 확인</li>
<li>next -&gt; 프론트 배포 후 모니터링 시작</li>
</ol>
<p>조금 더 시간이 있었더라면 카나리 배포 처럼 운영 환경에서 리스크를 줄이는 방법을 적용했었어야 했다.</p>
<h3 id="트러블-슈팅">트러블 슈팅</h3>
<h4 id="naver-인증시-일부-유저의-약관동의-정보-가져오기-404-오류">NAVER 인증시 일부 유저의 약관동의 정보 가져오기 404 오류</h4>
<h5 id="확인">확인</h5>
<ol>
<li>배포 직후 로그에 naver 인증시에 404 오류 발생 확인</li>
<li>내부직원중에 일부가 로그인 안된다는 내용 전파</li>
</ol>
<h5 id="원인">원인</h5>
<p>외부 API 라서 원인 확인이 어려웠으나, 동일한 케이스가 다른 곳에서도 발생했다는 것을 확인 naver api 에 문제가 있어 정확한 원인 확인이 어려웠습니다.</p>
<p><a href="https://developers.naver.com/forum/posts/34228">https://developers.naver.com/forum/posts/34228</a>
<a href="https://developers.naver.com/forum/posts/34684">https://developers.naver.com/forum/posts/34684</a></p>
<h5 id="해결-1">해결</h5>
<p>naver 인증시에 마켓팅 동의 선택 항목의 정보를 가져와야하기문에 해당 플로우를 생략할 수 없었습니다.
하지만, OAuth를 활용한 로그인이 아닌 ID/PW의 경우 따로 마켓팅 동의를 받고 있는 로직이 있어 해당 부분을 약관동의 정보를 가져올때 404 오류 발생시 따로 동의를 받을 수 있도록 플로우 추가 후 핫픽스 배포</p>
<h4 id="jwt-브라우저-쿠키-만료시간-설정-오류">JWT 브라우저 쿠키 만료시간 설정 오류</h4>
<h5 id="확인-1">확인</h5>
<p>로그인이 안된다는 내용의 슬랙을 전파 받고 확인</p>
<p>BE, FE 각각 로그 확인
BE 확인 내용에서는 로그인에 JWT가 제대로 반환된부분 확인</p>
<h5 id="원인-1">원인</h5>
<p>FE 에서 JWT 쿠키 생성시 만료시간을 설정할때 빌드 시점기준으로 시간을 더해 주는 방식으로 되어 이미 만료된 쿠키를 생성하여 쿠키가 제대로 생성되지 않아 오류 발생</p>
<h5 id="해결-2">해결</h5>
<p>1차 배포 - FE 에서 JWT 쿠키 만료시간 하드코딩 후 핫픽스 배포
2차 배포 - JWT 쿠키 생성시점에 만료시간 생성하도록 로직 수정 후 배포</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[테스트 오류 Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration]]></title>
            <link>https://velog.io/@banana-wuyu/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%98%A4%EB%A5%98-Unable-to-find-a-SpringBootConfiguration-you-need-to-use-ContextConfiguration</link>
            <guid>https://velog.io/@banana-wuyu/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%98%A4%EB%A5%98-Unable-to-find-a-SpringBootConfiguration-you-need-to-use-ContextConfiguration</guid>
            <pubDate>Tue, 24 Dec 2024 17:55:47 GMT</pubDate>
            <description><![CDATA[<pre><code>Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration</code></pre><p>테스트하는 모듈에 <strong>@SpringBootApplication</strong> 이 없어서 발생</p>
<h3 id="해결">해결</h3>
<p>테스트 패키지에 아래 추가</p>
<pre><code>@SpringBootApplication
public class TestApplication {
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[배포 속도의 중요]]></title>
            <link>https://velog.io/@banana-wuyu/%EB%B0%B0%ED%8F%AC-%EC%86%8D%EB%8F%84%EC%9D%98-%EC%A4%91%EC%9A%94</link>
            <guid>https://velog.io/@banana-wuyu/%EB%B0%B0%ED%8F%AC-%EC%86%8D%EB%8F%84%EC%9D%98-%EC%A4%91%EC%9A%94</guid>
            <pubDate>Mon, 23 Dec 2024 21:46:20 GMT</pubDate>
            <description><![CDATA[<h3 id="배포-속도는-중요했다">배포 속도는 중요했다</h3>
<p>핫픽스 건으로 배포할때 실제 개발 수정시간은 1분이더라도 배포하는데 5분이 걸리면 실제 운영까지 적용되는 시간은 최소 6분이다.</p>
<p>크리티컬 이슈의 경우 무엇보다 속도가 중요한데 이럴때 배포 버튼만 누르고 기다리면 답답했던 경우가 있었다</p>
<p>해결 방법은 배포 플로우중에 테스트 부분을 병렬로 진행하는 방법으로 배포 시간을 단축했던 경험이 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git-Flow]]></title>
            <link>https://velog.io/@banana-wuyu/Git-Flow</link>
            <guid>https://velog.io/@banana-wuyu/Git-Flow</guid>
            <pubDate>Sun, 22 Dec 2024 19:39:03 GMT</pubDate>
            <description><![CDATA[<p>정기 배포시 유용한 전략</p>
<table>
<thead>
<tr>
<th>브랜치</th>
<th>작업</th>
<th>머지</th>
</tr>
</thead>
<tbody><tr>
<td>master</td>
<td>-</td>
<td>develop</td>
</tr>
<tr>
<td>hotfix/*</td>
<td>master 오류로 바로 배포가 필요한 작업</td>
<td>master</td>
</tr>
<tr>
<td>develop</td>
<td>현재 개발중인 작업 해당 브랜치 기준으로 release/* 브랜치 생성</td>
<td>-</td>
</tr>
<tr>
<td>bugfix/*</td>
<td>develop 브랜치에 있는 오류 PR 리뷰 후 develop 머지</td>
<td>develop</td>
</tr>
<tr>
<td>feature/*</td>
<td>신규 작업 PR 리뷰 후 develop 머지</td>
<td>develop</td>
</tr>
<tr>
<td>release/*</td>
<td>배포 작업 배포후 develop, master 브랜치에 머지</td>
<td>master, develop</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/396ab367-b7b5-48ad-9bfd-d556e773d977/image.png" alt=""></p>
<p>참조</p>
<ul>
<li><a href="https://techblog.woowahan.com/2553/">https://techblog.woowahan.com/2553/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[git rebase]]></title>
            <link>https://velog.io/@banana-wuyu/git-rebase</link>
            <guid>https://velog.io/@banana-wuyu/git-rebase</guid>
            <pubDate>Sun, 22 Dec 2024 19:35:00 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>말 그대로 베이스를 재배치 브랜치의 시작점을 재설정
branch의 변경사항을 최신 상태로 유지가 가능
커밋 라인을 정리하여 히스토리를 깔끔하게 유지</p>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/22912546-6fee-42bd-a569-28c699c2dae2/image.png" alt=""></p>
<h2 id="사용">사용</h2>
<p>리베이스 사용법</p>
<p>최근 3개의 커밋을 interactive rebase 한다</p>
<pre><code class="language-bash">git rebase -i head~3</code></pre>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/15bbbff5-64c0-4152-ae70-81b4827cbcad/image.png" alt=""></p>
<h3 id="p-pick---커밋-내역">p, pick - 커밋 내역</h3>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/113e4878-7283-4abf-9858-6a865a338ebc/image.png" alt=""></p>
<h3 id="r-reword---커밋-코멘트-수정">r, reword - 커밋 코멘트 수정</h3>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/8cd745c0-7123-4562-857a-00b621950817/image.png" alt=""></p>
<p><code>wq</code> 로 저장 종료</p>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/8a3c1b51-ca9e-4c64-9b58-9e99b246bd90/image.png" alt=""></p>
<p>현재 “0번 feature - 커밋 1” 메세지를 “0번 feature - 커밋 1 (수정)“ 으로 변경</p>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/42b34e6d-0a4d-4778-b419-398eebd4659f/image.png" alt=""></p>
<p><code>wq</code> 로 저장 종료</p>
<h3 id="e-edit---커밋-수정">e, edit - 커밋 수정</h3>
<p>2번째 커밋인 <code>9ad336b</code> 커밋의 내용 수정</p>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/64bd946a-eb94-482b-aade-c16cac8f632b/image.png" alt=""></p>
<p><code>wq</code> 로 저장 종료</p>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/6eb20f9a-860d-43e2-8b11-08297763d9c3/image.png" alt=""></p>
<p>IDE 에서 수정할 내용 수정 후</p>
<p><code>git add .</code> 수정 내용 추가</p>
<p><code>git commit --amend</code> </p>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/1f58d72f-b247-41b9-8b7f-144f729de55e/image.png" alt=""></p>
<p><code>wq</code> 로 저장 종료</p>
<p><code>git rebase --continue</code> 로 진행</p>
<h3 id="s-squash---여러-커밋-합치기">s, squash - 여러 커밋 합치기</h3>
<p>커밋 2, 커밋 3 을 커밋 1로 합치기</p>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/e5def882-677a-4ee8-b72d-5fe6bcb02248/image.png" alt=""></p>
<p>충돌이 난경우 충돌 부분 수정 후 <code>git rebase --continue</code> 로 진행</p>
<h3 id="d-drop---커밋-삭제">d, drop - 커밋 삭제</h3>
<p>커밋 2 삭제</p>
<p><img src="https://velog.velcdn.com/images/banana-wuyu/post/00fc7a75-4d06-46ff-b18e-941c73394495/image.png" alt=""></p>
<h3 id="리베이스-취소">리베이스 취소</h3>
<p>⚠️ 로컬에서 리베이스 작업한 경우만 사용가능합니다.</p>
<p>reflog로 이전내역 확인 후 <code>git rest 커밋번호</code>로 해당 커밋으로 초기화</p>
<h2 id="참조">참조</h2>
<ul>
<li><a href="https://www.tugberkugurlu.com/archive/resistance-against-london-tube-map-commit-history-a-k-a--git-merge-hell">https://www.tugberkugurlu.com/archive/resistance-against-london-tube-map-commit-history-a-k-a--git-merge-hell</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GQL] 비동기 처리 하면서]]></title>
            <link>https://velog.io/@banana-wuyu/GQL-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%ED%95%98%EB%A9%B4%EC%84%9C</link>
            <guid>https://velog.io/@banana-wuyu/GQL-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%ED%95%98%EB%A9%B4%EC%84%9C</guid>
            <pubDate>Sun, 22 Dec 2024 19:28:55 GMT</pubDate>
            <description><![CDATA[<p>비동기가 왜 필요한가?
2개의 쿼리 조회시 속도가 너무 느려 사용 최대 로컬에서 500ms 발생</p>
<p>적용해도 문제 없나?
쿼리 자체가 개별적으로 동작하는 쿼리라 이슈 없음</p>
<p>왜 느린가?
외부 서비스에 요청해서 결과값을 가져오는데 이때 속도가 이슈</p>
<p>결과
운영에서 평균 80ms</p>
<p>참고 : <a href="https://velog.io/@banana-wuyu/spring-osiv">https://velog.io/@banana-wuyu/spring-osiv</a></p>
<h3 id="결과-샘플-코드">결과 샘플 코드</h3>
<pre><code>@DgsComponent
class SampleQuery(
    private val dgsAsyncTaskExecutor: Executor,
) {
    @Async
    @DgsData
    fun sample1(): CompletableFuture&lt;Sample&gt; {
        return CompletableFuture.supplyAsync({
        // 로직
        }, dgsAsyncTaskExecutor)
    }

    @Async
    @DgsData
    fun sample2(): CompletableFuture&lt;Sample&gt; {
        return CompletableFuture.supplyAsync({
        // 로직
        }, dgsAsyncTaskExecutor)
    }</code></pre><h2 id="작업중-오류">작업중 오류</h2>
<h3 id="이슈">이슈</h3>
<ol>
<li>Apparent connection leak detected</li>
</ol>
<h3 id="해결">해결</h3>
<h3 id="apparent-connection-leak-detected">Apparent connection leak detected</h3>
<h3 id="누수-확인-로그-추가">누수 확인 로그 추가</h3>
<pre><code>logging:
  level:
    com: trace
    org.springframework.transaction.interceptor: trace
spring:
  datasource-main:
    hikari:
      leak-detection-threshold: 2000</code></pre><p>인증 처리를 하는 Resolver에서 오류 발생
유저 정보 조회 후 DB 커넥션이 제대로 닫히지 않아서 오류 발생</p>
<h3 id="해결방법">해결방법</h3>
<p>osiv off로 수정 - <a href="https://kth990303.tistory.com/427">https://kth990303.tistory.com/427</a>
    - 현재 코드에서는 lazy-load 가 있는 entity를 밖에서 사용하는 경우가 있어서 해당 방법 사용시 영향범위 파악도 어렵고, 테스트가 어려워 해당 방법은 사용 불가능 (해당 방법 사용)
<del>- 다른 방법 확인중
    - async용 userResolver 따로 만들어서 사용 가능한지 확인중 - 해당 resolver에서 db 커넥션 강제로 끊어서 사용 (이런식으로 해도 되는지 모름)</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA count 확장 함수]]></title>
            <link>https://velog.io/@banana-wuyu/JPA-count-%ED%99%95%EC%9E%A5-%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@banana-wuyu/JPA-count-%ED%99%95%EC%9E%A5-%ED%95%A8%EC%88%98</guid>
            <pubDate>Sun, 22 Dec 2024 18:30:40 GMT</pubDate>
            <description><![CDATA[<p>jpa 에서 쿼리 작성 후 해당 쿼리의 count 를 가져올때 동일한 조건을 또 써야되서 불편해서 추가한 확장 함수</p>
<pre><code>// JPAQuery 확장 함수로, 페이징 처리를 하면서 전체 개수를 함께 조회하는 기능을 제공
fun &lt;T&gt; JPAQuery&lt;T&gt;.listWithTotalCountFetch(pageable: Pageable, countEntity: EntityPathBase&lt;T&gt;): ListWithTotalCount&lt;T&gt; {
    // 현재 쿼리를 복제하여 총 개수를 가져오는 쿼리 생성
    val query = this
    val totalCnt = query.clone()
        .select(countEntity, countEntity.countDistinct()) // countDistinct()를 사용하여 중복 없는 개수를 계산
        .fetchOne() // 결과를 단일 값으로 가져옴

    // 페이징 처리: Pageable 객체가 unpaged인 경우 전체 데이터를 가져오고, 그렇지 않으면 페이징 처리된 데이터를 가져옴
    val list = if (pageable.isUnpaged) {
        query.fetch() // 페이징 없이 전체 데이터를 조회
    } else {
        query.offset(pageable.offset) // 시작 위치 설정
            .limit(pageable.pageSize.toLong()) // 페이지 크기 설정
            .fetch() // 페이징된 데이터를 조회
    }

    // 조회된 데이터와 전체 개수를 포함하는 ListWithTotalCount 객체 반환
    return ListWithTotalCount(
        list,
        totalCnt?.get(1, Long::class.java)!!.toInt() // totalCnt의 두 번째 값(Long)을 Int로 변환하여 반환
    )
}

// 페이징 처리된 데이터와 전체 개수를 함께 담는 데이터 클래스
data class ListWithTotalCount&lt;T&gt;(
    val items: List&lt;T&gt;, // 조회된 데이터 리스트
    val totalCount: Int // 전체 데이터 개수
)

// 데이터 리스트와 마지막 키를 함께 담는 데이터 클래스 (Key 기반 페이징 용도)
data class ListWithLastKey&lt;T&gt;(
    val items: List&lt;T&gt;, // 조회된 데이터 리스트
    val lastKey: String?, // 마지막 키 (없으면 null)
)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[kotlin 기본값(디폴트 인자)]]></title>
            <link>https://velog.io/@banana-wuyu/kotlin-%EA%B8%B0%EB%B3%B8%EA%B0%92%EB%94%94%ED%8F%B4%ED%8A%B8-%EC%9D%B8%EC%9E%90</link>
            <guid>https://velog.io/@banana-wuyu/kotlin-%EA%B8%B0%EB%B3%B8%EA%B0%92%EB%94%94%ED%8F%B4%ED%8A%B8-%EC%9D%B8%EC%9E%90</guid>
            <pubDate>Sun, 22 Dec 2024 18:21:47 GMT</pubDate>
            <description><![CDATA[<p>가능하면 사용안하는게 좋은것 같다.</p>
<ol>
<li>명시적이지 않다. 필수 값인지 그냥 사용해도 되는지</li>
<li>휴먼 에러 만약 실수로라도 값을 입력하지 안은경우 오류 발생 가능</li>
</ol>
<p>같이 일하는 동료의 오류를 찾으면서 경험했던 내용이다.</p>
<p>상품 정보가 정상적으로 나오지 않았던 오류였다.
원인은 프론트엔드에 리턴값을 모델링하는 부분에서 사용한는 객체 인자중에 디폴트값이 설정되어 있어서 값이 없는데 빈 문자열을 반환해서 오류가 발생</p>
<p>해당 오류에 원인 파악하는데에도 운영에 read DB를 연동해서 디버그 모드로 코드를 1개씩 실행하며 원인을 파악했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[osiv 설정]]></title>
            <link>https://velog.io/@banana-wuyu/spring-osiv</link>
            <guid>https://velog.io/@banana-wuyu/spring-osiv</guid>
            <pubDate>Sun, 22 Dec 2024 18:04:13 GMT</pubDate>
            <description><![CDATA[<p>Open Session In View </p>
<p>개인적인 생각을 먼저 말하면
false 로 사용하는게 좋을것 같다. false -&gt; true 변경은 크게 이슈가 없지만, true -&gt; false 변경시 프로젝트에서 어떤 사이드 이펙트가 발생할지 모른다.</p>
<p>이전에 프로젝트를 진행했을때 비동기를 사용하려고 했을때 Apparent connection leak detected이 발생했다. 이때문에  true -&gt; false 로 변경하려고 했을때 LazyInitializationException이 발생</p>
<p>LazyInitializationException 원인은 lazy loding을 사용한 entity의 컬럼을 트랜젝션 밖에서 사용하고 있어서 발생</p>
<p>osiv false 로 사용하면 명시적으로 트랜젝션이 걸린 부분만 DB 커넥션을 유지하는데 이때 트랜젝션 밖에서 lazy 로딩한 entity를 사용하면 이미 커넥션을 끊기 상태에서 DB 요청을해서 오류 발생</p>
<h2 id="원인">원인</h2>
<p>entity가 트랜젝션 밖에서 사용한는 것 부터 잘 못되었다.
모델링 후 사용하는게 정상적이다.</p>
<h2 id="해결">해결</h2>
<p>osiv off 설정 후 entity를 밖에서 사용하는 코드 모든 부분 수정</p>
]]></description>
        </item>
    </channel>
</rss>