<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>do_dev.log</title>
        <link>https://velog.io/</link>
        <description>우당탕탕</description>
        <lastBuildDate>Fri, 22 May 2026 06:22:37 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>do_dev.log</title>
            <url>https://velog.velcdn.com/images/do_dev/profile/2e4f0d9a-8f61-4b71-b293-9aa6bccafa51/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. do_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/do_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[VSCode에서 Claude API 연결하는 법 (Continue.dev) — Claude 에이전트 팀 시리즈 1편]]></title>
            <link>https://velog.io/@do_dev/VSCode%EC%97%90%EC%84%9C-Claude-API-%EC%97%B0%EA%B2%B0%ED%95%98%EB%8A%94-%EB%B2%95-Continue.dev-Claude-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%ED%8C%80-%EC%8B%9C%EB%A6%AC%EC%A6%88-1%ED%8E%B8</link>
            <guid>https://velog.io/@do_dev/VSCode%EC%97%90%EC%84%9C-Claude-API-%EC%97%B0%EA%B2%B0%ED%95%98%EB%8A%94-%EB%B2%95-Continue.dev-Claude-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%ED%8C%80-%EC%8B%9C%EB%A6%AC%EC%A6%88-1%ED%8E%B8</guid>
            <pubDate>Fri, 22 May 2026 06:22:37 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="date-2026-05-22">Date: 2026-05-22</h2>
<blockquote>
<p>이 시리즈는 Claude 에이전트 팀을 구성해서 React + Spring Boot 포트폴리오 웹사이트를 만드는 과정을 기록한다.
1편에서는 VSCode 안에서 Claude를 채팅으로 쓸 수 있도록 환경을 세팅한다.</p>
</blockquote>
<h2 id="왜-continuedev인가">왜 Continue.dev인가?</h2>
<p>GitHub Copilot Chat 대신 <strong>Continue.dev</strong>를 선택한 이유는 간단하다.</p>
<ul>
<li>원하는 모델(Claude)을 직접 연결할 수 있다</li>
<li>VSCode 채팅창에서 코드 파일을 컨텍스트로 넘기기 편하다</li>
<li>에이전트별 Custom Commands 등록이 가능하다</li>
<li>오픈소스라 설정을 내 마음대로 제어할 수 있다</li>
</ul>
<h2 id="전체-세팅-순서">전체 세팅 순서</h2>
<ol>
<li>Continue.dev 설치</li>
<li>Anthropic API 키 발급</li>
<li>Claude 모델 연결</li>
<li>동작 확인</li>
</ol>
<hr>
<h2 id="1단계--continuedev-설치">1단계 — Continue.dev 설치</h2>
<p>VSCode Extensions 탭에서 <code>Continue</code> 검색 후 설치한다.</p>
<p>또는 터미널에서:</p>
<pre><code class="language-bash">code --install-extension Continue.continue</code></pre>
<p>설치하면 VSCode 좌측 사이드바에 Continue 아이콘이 생긴다.</p>
<hr>
<h2 id="2단계--anthropic-api-키-발급">2단계 — Anthropic API 키 발급</h2>
<p><a href="https://console.anthropic.com">console.anthropic.com</a> 접속 → <strong>API Keys</strong> 탭 → <strong>Create Key</strong></p>
<blockquote>
<p>⚠️ 키는 생성 시 한 번만 보여준다. 반드시 복사해서 안전한 곳에 저장할 것.</p>
</blockquote>
<p>무료 크레딧이 있으니 처음엔 결제 없이 테스트 가능하다.</p>
<hr>
<h2 id="3단계--claude-모델-연결">3단계 — Claude 모델 연결</h2>
<p>Continue 설정 파일을 열어 Claude를 연결한다.</p>
<p><strong>방법 1: UI에서 설정</strong></p>
<p>Continue 사이드바 하단 설정 아이콘 → <code>Add Model</code> → <code>Anthropic</code> 선택 → API 키 입력</p>
<p><strong>방법 2: config.json 직접 수정 (추천)</strong></p>
<p><code>~/.continue/config.json</code> 파일을 열어 아래 내용을 추가한다.</p>
<pre><code class="language-json">{
  &quot;models&quot;: [
    {
      &quot;title&quot;: &quot;Claude Sonnet 4.6&quot;,
      &quot;provider&quot;: &quot;anthropic&quot;,
      &quot;model&quot;: &quot;claude-sonnet-4-6&quot;,
      &quot;apiKey&quot;: &quot;sk-ant-여기에_API_키_입력&quot;
    }
  ]
}</code></pre>
<blockquote>
<p>⚠️ API 키를 절대 Git에 커밋하지 말 것. <code>.gitignore</code>에 <code>config.json</code>을 추가하거나 환경변수로 관리한다.</p>
</blockquote>
<pre><code class="language-bash"># .gitignore
.continue/config.json</code></pre>
<hr>
<h2 id="4단계--동작-확인">4단계 — 동작 확인</h2>
<p>VSCode에서 <code>Cmd+L</code> (Mac) 또는 <code>Ctrl+L</code> (Windows) 로 Continue 채팅창을 연다.</p>
<p>아래처럼 입력해보자.</p>
<pre><code>안녕! 지금 어떤 모델이야?</code></pre><p>Claude가 응답하면 성공이다.</p>
<hr>
<h2 id="에이전트-팀을-위한-custom-commands-등록-보너스">에이전트 팀을 위한 Custom Commands 등록 (보너스)</h2>
<p>이 시리즈의 핵심인 <strong>에이전트 역할 전환</strong>을 편하게 하려면 Custom Commands를 등록해두면 좋다.</p>
<p><code>~/.continue/config.json</code>에 아래 내용을 추가한다.</p>
<pre><code class="language-json">{
  &quot;models&quot;: [...],
  &quot;customCommands&quot;: [
    {
      &quot;name&quot;: &quot;fe&quot;,
      &quot;description&quot;: &quot;Frontend Agent 모드로 전환&quot;,
      &quot;prompt&quot;: &quot;You are the Frontend Agent. Stack: React 18, TypeScript, Tailwind CSS. Only write frontend code. Always follow the conventions in CLAUDE.md.&quot;
    },
    {
      &quot;name&quot;: &quot;be&quot;,
      &quot;description&quot;: &quot;Backend Agent 모드로 전환&quot;,
      &quot;prompt&quot;: &quot;You are the Backend Agent. Stack: Spring Boot 3, Java 21, PostgreSQL. Only write backend code. Always follow the conventions in CLAUDE.md.&quot;
    },
    {
      &quot;name&quot;: &quot;qa&quot;,
      &quot;description&quot;: &quot;QA Agent 모드로 전환&quot;,
      &quot;prompt&quot;: &quot;You are the QA Agent. Review the code for bugs, security issues, and convention violations. Output a structured review report.&quot;
    }
  ]
}</code></pre>
<p>채팅창에서 <code>/fe</code>, <code>/be</code>, <code>/qa</code> 를 입력하면 해당 에이전트 모드로 바로 전환된다.</p>
<hr>
<h2 id="다음-편-예고">다음 편 예고</h2>
<p>2편에서는 실제 프로젝트 폴더를 만들고, <strong>에이전트 팀의 두뇌인 <code>CLAUDE.md</code></strong> 를 작성한다.
LLM에게 장기 기억이 없는 이유와, 그걸 <code>CLAUDE.md</code>로 보완하는 원리도 같이 설명한다.</p>
<blockquote>
<p>시리즈 전체 코드는 GitHub에서 공개 예정.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] M8i 인스턴스 업그레이드 후 Oracle DB 연결이 갑자기 끊기는 현상]]></title>
            <link>https://velog.io/@do_dev/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-M8i-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%ED%9B%84-Oracle-DB-%EC%97%B0%EA%B2%B0%EC%9D%B4-%EA%B0%91%EC%9E%90%EA%B8%B0-%EB%81%8A%EA%B8%B0%EB%8A%94-%ED%98%84%EC%83%81</link>
            <guid>https://velog.io/@do_dev/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-M8i-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%ED%9B%84-Oracle-DB-%EC%97%B0%EA%B2%B0%EC%9D%B4-%EA%B0%91%EC%9E%90%EA%B8%B0-%EB%81%8A%EA%B8%B0%EB%8A%94-%ED%98%84%EC%83%81</guid>
            <pubDate>Fri, 22 May 2026 02:07:46 GMT</pubDate>
            <description><![CDATA[<h3 id="date-2026-05-22">Date: 2026-05-22</h3>
<p>EC2 인스턴스 타입을 <code>C5</code>에서 <code>M8i</code>로 변경한 이후, Oracle DB의 idle 상태 TCP 연결이 7~10분 후 아무런 에러 없이 끊기는 현상이 발생했다. 원인은 <strong>최신 Nitro Card(Nitro v6)</strong> 의 기본 connection tracking idle timeout 변경이었다.</p>
<h2 id="현상">현상</h2>
<p>EC2 인스턴스를 <code>C5</code> → <code>M8i</code>로 변경 후, Oracle DB의 idle TCP 연결이 <strong>7~10분 후 알림 없이 끊기는 &quot;silent drop&quot;</strong> 발생. 애플리케이션 로그에는 아무런 에러가 찍히지 않는다.</p>
<h2 id="원인">원인</h2>
<h3 id="aws-ec2-security-group의-connection-tracking">AWS EC2 Security Group의 Connection Tracking</h3>
<p>AWS EC2의 Security Group은 <strong>stateful 방화벽</strong>이다. 연결 상태를 추적(connection tracking)해서 응답 트래픽을 자동으로 허용한다. 이 connection tracking에는 <strong>idle timeout</strong>이 존재하며, timeout이 만료되면 해당 연결 정보가 제거된다.</p>
<blockquote>
<p>연결 정보가 제거된 후 패킷이 전송되면, Security Group은 이를 <strong>새로운 연결로 인식하지 못하고 drop</strong>한다. 이것이 바로 &quot;silent drop&quot; 현상의 원인이다.</p>
</blockquote>
<h3 id="c5-vs-m8i-무엇이-달라졌나">C5 vs M8i: 무엇이 달라졌나</h3>
<table>
<thead>
<tr>
<th>인스턴스</th>
<th>Nitro 세대</th>
<th>TCP Established Timeout</th>
</tr>
</thead>
<tbody><tr>
<td><code>C5</code></td>
<td>4~5세대 Nitro Card</td>
<td>432,000초 (5일)</td>
</tr>
<tr>
<td><code>M8i</code></td>
<td>6세대 (Nitro v6)</td>
<td><strong>350초 (약 5분 50초)</strong></td>
</tr>
</tbody></table>
<blockquote>
<p>⚠️ <strong>영향 범위:</strong> M8i뿐 아니라 Nitro v6를 사용하는 <code>C8i</code>, <code>R8i</code> 등 최신 세대 인스턴스 모두 해당된다. 이는 성능 최적화를 위해 AWS가 의도적으로 변경한 기본값이며, AWS 공식 문서에서도 안내하고 있다.</p>
</blockquote>
<h3 id="장애-흐름">장애 흐름</h3>
<ul>
<li><strong>0:00</strong> — 애플리케이션 ↔ Oracle DB 사이에 TCP 연결 수립. HikariCP pool에 유지됨.</li>
<li><strong>0:00 ~ 5:50</strong> — 쿼리 없음. 연결은 idle 상태 유지.</li>
<li><strong>5:50 (350초)</strong> — Nitro v6 connection tracking 테이블에서 해당 연결 정보 제거됨.</li>
<li><strong>이후 쿼리 시도</strong> — Security Group이 패킷을 신규 연결로 인식하지 못함 → <strong>silent drop</strong> → 커넥션 오류 발생.</li>
</ul>
<h2 id="조치">조치</h2>
<ol>
<li><strong>M8i → 7세대 인스턴스로 롤백</strong> — 즉각적인 서비스 안정화 우선</li>
<li><strong>추후 최적 옵션 검토</strong> — Nitro v6 전환 시 keepalive 튜닝 등 워크로드 특성에 맞는 설정 사전 검토 후 재전환 예정</li>
</ol>
<h2 id="근본-해결책-hikaricp-keepalive-설정">근본 해결책: HikariCP Keepalive 설정</h2>
<p>Nitro v6 인스턴스를 계속 사용해야 한다면, 350초 안에 keepalive 패킷을 보내도록 설정한다. 충분한 여유를 두고 <strong>2분(120,000ms)</strong> 으로 설정하는 것을 권장한다.</p>
<h3 id="applicationproperties">application.properties</h3>
<pre><code class="language-properties">spring.datasource.hikari.keepalive-time=120000</code></pre>
<p>커스텀 prefix 사용 시:</p>
<pre><code class="language-properties"># ❌ 무시됨
legacy.datasource.keepalive-time=120000

# ✅ HikariCP에 실제 적용됨
legacy.datasource.hikari.keepalive-time=120000</code></pre>
<blockquote>
<p><code>.hikari.</code> 없이 설정하면 HikariCP에 <strong>적용되지 않고 무시</strong>된다. 반드시 <code>hikari</code> 네임스페이스를 포함해야 한다.</p>
</blockquote>
<h3 id="bean으로-직접-등록하는-경우">Bean으로 직접 등록하는 경우</h3>
<pre><code class="language-java">@Bean
@ConfigurationProperties(&quot;legacy.datasource.hikari&quot;)
public HikariDataSource legacyDataSource() {
    return new HikariDataSource();
}</code></pre>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>✅ <strong>원인:</strong> Nitro v6(M8i, C8i, R8i 등)은 TCP Established idle timeout이 5일 → <strong>350초</strong>로 단축됨</li>
<li>✅ <strong>현상:</strong> Idle 커넥션이 350초 후 Security Group에서 제거 → 다음 쿼리 시 silent drop</li>
<li>✅ <strong>즉각 조치:</strong> 7세대 인스턴스로 롤백</li>
<li>✅ <strong>근본 해결:</strong> HikariCP <code>keepalive-time=120000</code> 설정으로 350초 이내 keepalive 전송</li>
</ul>
<blockquote>
<p>⚠️ <strong>참고:</strong> Nitro v6 인스턴스(M8i, C8i, R8i 등 최신 세대)로 전환 시 동일 현상 발생 가능. <strong>워크로드의 idle 커넥션 특성을 사전에 반드시 검토</strong>할 것.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DCommerce] 6편 - k6 성능 테스트로 Redis 동시성 검증하기]]></title>
            <link>https://velog.io/@do_dev/DCommerce-6%ED%8E%B8-k6-%EC%84%B1%EB%8A%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A1%9C-Redis-%EB%8F%99%EC%8B%9C%EC%84%B1-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@do_dev/DCommerce-6%ED%8E%B8-k6-%EC%84%B1%EB%8A%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A1%9C-Redis-%EB%8F%99%EC%8B%9C%EC%84%B1-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 19 May 2026 07:04:58 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>5편까지 Redis 동시성 제어와 Kafka 비동기 처리를 구현했다. 이번 편에서는 k6를 사용해서 실제로 동시 주문이 들어왔을 때 재고가 정확히 차감되는지 성능 테스트로 검증했다.</p>
<hr>
<h2 id="k6란">k6란?</h2>
<p>k6는 JavaScript로 테스트 스크립트를 작성할 수 있는 오픈소스 성능 테스트 툴이다. Postman처럼 API를 호출하는데, 동시에 수백 명이 요청하는 상황을 시뮬레이션할 수 있다.</p>
<pre><code class="language-bash">brew install k6</code></pre>
<hr>
<h2 id="테스트-시나리오">테스트 시나리오</h2>
<p><strong>목표</strong>: 재고 100개인 상품에 200명이 동시에 주문했을 때, 100건만 성공하고 나머지는 재고 부족으로 차단되는지 검증</p>
<hr>
<h2 id="테스트-스크립트">테스트 스크립트</h2>
<pre><code class="language-javascript">import http from &#39;k6/http&#39;;
import { check, sleep } from &#39;k6&#39;;

export const options = {
    vus: 200,        // 동시 사용자 200명
    duration: &#39;10s&#39;, // 10초 동안
};

export default function () {
    const token = &#39;Bearer {JWT토큰}&#39;;

    const payload = JSON.stringify({
        productId: 2,
        quantity: 1,
    });

    const params = {
        headers: {
            &#39;Content-Type&#39;: &#39;application/json&#39;,
            &#39;Authorization&#39;: token,
        },
    };

    const res = http.post(&#39;http://localhost:8080/order&#39;, payload, params);

    check(res, {
        &#39;주문 성공 (201)&#39;: (r) =&gt; r.status === 201,
        &#39;재고 부족 (400)&#39;: (r) =&gt; r.status === 400,
    });

    sleep(1);
}</code></pre>
<hr>
<h2 id="테스트-결과">테스트 결과</h2>
<pre><code>동시 사용자: 200명
재고: 100개
총 요청: 2000건

주문 성공 (201): 100건  ✅
재고 부족 (400): 1802건 ✅
평균 응답 시간: 54ms
최대 응답 시간: 689ms
처리량: 182건/s</code></pre><p><strong>200명이 동시에 요청했지만 딱 100건만 성공했다. 재고를 초과한 주문은 0건.</strong></p>
<hr>
<h2 id="redis-동시성-제어-핵심-코드">Redis 동시성 제어 핵심 코드</h2>
<pre><code class="language-java">// StockService.java
public Long decreaseStock(Long productId, int quantity) {
    String key = &quot;stock:&quot; + productId;
    return redisTemplate.opsForValue().increment(key, -quantity);
}

public Long increaseStock(Long productId, int quantity) {
    String key = &quot;stock:&quot; + productId;
    return redisTemplate.opsForValue().increment(key, quantity);
}</code></pre>
<pre><code class="language-java">// OrderServiceImpl.java
Long remainStock = stockService.decreaseStock(
        createOrderRequestDTO.getProductId(), createOrderRequestDTO.getQuantity()
);

// 재고 부족 체크
if (remainStock &lt; 0) {
    stockService.increaseStock(createOrderRequestDTO.getProductId(), createOrderRequestDTO.getQuantity());
    throw new IllegalArgumentException(&quot;재고가 부족합니다.&quot;);
}</code></pre>
<p>Redis의 <code>increment</code> 연산은 원자적(Atomic)으로 동작하기 때문에 동시에 여러 요청이 들어와도 정확하게 재고가 차감된다.</p>
<hr>
<h2 id="redis가-없었다면">Redis가 없었다면?</h2>
<p>DB에서 재고를 읽고 차감하는 방식은 아래와 같은 문제가 생긴다.</p>
<pre><code>Thread A: 재고 조회 → 5개 남음
Thread B: 재고 조회 → 5개 남음  ← A가 차감하기 전에 읽음
Thread A: 재고 차감 → 4개
Thread B: 재고 차감 → 4개  ← 실제론 3개여야 하는데!</code></pre><p>이런 Race Condition을 Redis의 원자적 연산으로 해결했다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>k6 성능 테스트를 통해 Redis 동시성 제어가 실제로 동작함을 검증했다.</p>
<p>5편에 걸쳐 DCommerce 프로젝트를 완성했다.</p>
<pre><code>1편 - Entity 설계 + 회원가입
2편 - JWT 로그인
3편 - 상품 + 주문 API
4편 - Redis 동시성 제어
5편 - Kafka 비동기 이벤트
6편 - k6 성능 테스트 (현재)</code></pre><p>처음에는 운영 업무만 하다 보니 직접 구조를 짜가며 해볼 기회가 많이 없었는데, 이번 프로젝트를 통해 Spring Boot + Redis + Kafka 조합을 직접 설계하고 구현하며 찍먹해볼 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DCommerce] 5편 - Kafka로 주문 이벤트 비동기 처리하기]]></title>
            <link>https://velog.io/@do_dev/DCommerce-5%ED%8E%B8-Kafka%EB%A1%9C-%EC%A3%BC%EB%AC%B8-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@do_dev/DCommerce-5%ED%8E%B8-Kafka%EB%A1%9C-%EC%A3%BC%EB%AC%B8-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 15 May 2026 08:34:00 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>4편에서 Redis로 동시성 문제를 해결했다. 이번 편에서는 Kafka를 도입해서 주문 완료 이벤트를 비동기로 처리하는 구조를 만들었다.</p>
<hr>
<h2 id="왜-kafka인가">왜 Kafka인가?</h2>
<p>주문이 완료되면 이메일 발송, 포인트 적립, 배송 시작 등 여러 작업이 필요하다. 이걸 동기로 처리하면 문제가 생긴다.</p>
<ul>
<li>이메일 서버가 느리면 주문 API도 느려짐</li>
<li>포인트 서비스가 죽으면 주문도 실패</li>
</ul>
<p>Kafka를 쓰면 주문 서비스는 메시지만 던지고 끝. 나머지 서비스들이 알아서 처리한다.</p>
<hr>
<h2 id="kafka-핵심-개념">Kafka 핵심 개념</h2>
<p><strong>Topic</strong> — 메시지를 저장하는 공간. 우리 프로젝트에서는 <code>order-complete</code>.</p>
<p><strong>Producer</strong> — 메시지를 Topic에 발행하는 쪽. 주문 서비스가 담당.</p>
<p><strong>Consumer</strong> — Topic을 구독해서 메시지를 읽는 쪽. 이메일/포인트 서비스가 담당.</p>
<p><strong>Consumer Group</strong> — 같은 Topic을 여러 서비스가 독립적으로 구독할 수 있음.</p>
<p><strong>Offset</strong> — Consumer가 어디까지 읽었는지 기록. 장애 복구 시 이어서 읽기 가능.</p>
<hr>
<h2 id="구현">구현</h2>
<h3 id="1-의존성-추가">1. 의존성 추가</h3>
<pre><code class="language-groovy">implementation &#39;org.springframework.kafka:spring-kafka&#39;</code></pre>
<h3 id="2-kafkaconfig">2. KafkaConfig</h3>
<pre><code class="language-java">@Configuration
public class KafkaConfig {

    @Bean
    public ProducerFactory&lt;String, String&gt; producerFactory(
            @Value(&quot;${spring.kafka.bootstrap-servers}&quot;) String bootstrapServers) {
        Map&lt;String, Object&gt; config = new HashMap&lt;&gt;();
        config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        return new DefaultKafkaProducerFactory&lt;&gt;(config);
    }

    @Bean
    public KafkaTemplate&lt;String, String&gt; kafkaTemplate(ProducerFactory&lt;String, String&gt; producerFactory) {
        return new KafkaTemplate&lt;&gt;(producerFactory);
    }

    @Bean
    public ConsumerFactory&lt;String, String&gt; consumerFactory(
            @Value(&quot;${spring.kafka.bootstrap-servers}&quot;) String bootstrapServers) {
        Map&lt;String, Object&gt; config = new HashMap&lt;&gt;();
        config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        config.put(ConsumerConfig.GROUP_ID_CONFIG, &quot;order-group&quot;);
        config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        config.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, &quot;earliest&quot;);
        return new DefaultKafkaConsumerFactory&lt;&gt;(config);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory&lt;String, String&gt; kafkaListenerContainerFactory(
            ConsumerFactory&lt;String, String&gt; consumerFactory) {
        ConcurrentKafkaListenerContainerFactory&lt;String, String&gt; factory =
                new ConcurrentKafkaListenerContainerFactory&lt;&gt;();
        factory.setConsumerFactory(consumerFactory);
        return factory;
    }
}</code></pre>
<h3 id="3-ordereventproducer">3. OrderEventProducer</h3>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class OrderEventProducer {

    private final KafkaTemplate&lt;String, String&gt; kafkaTemplate;

    public void sendOrderComplete(Long orderId) {
        kafkaTemplate.send(&quot;order-complete&quot;, String.valueOf(orderId));
    }
}</code></pre>
<h3 id="4-ordereventconsumer">4. OrderEventConsumer</h3>
<pre><code class="language-java">@Component
public class OrderEventConsumer {

    private static final Logger log = LoggerFactory.getLogger(OrderEventConsumer.class);

    @KafkaListener(topics = &quot;order-complete&quot;, groupId = &quot;order-group&quot;)
    public void handleOrderComplete(String orderId) {
        log.info(&quot;주문 완료 이벤트 수신 — orderId: {}&quot;, orderId);
    }
}</code></pre>
<h3 id="5-orderservice에-이벤트-발행-추가">5. OrderService에 이벤트 발행 추가</h3>
<pre><code class="language-java">orderRepository.save(order);
orderItemRepository.save(orderItem);
orderEventProducer.sendOrderComplete(order.getId()); // 주문 완료 이벤트 발행</code></pre>
<h3 id="6-applicationproperties">6. application.properties</h3>
<pre><code class="language-properties">spring.kafka.bootstrap-servers=localhost:9092

spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer

spring.kafka.consumer.group-id=order-group
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.auto-offset-reset=earliest</code></pre>
<hr>
<h2 id="동작-확인">동작 확인</h2>
<p>주문 API 호출 후 Kafka Topic에 메시지가 쌓이는 것을 확인했다.</p>
<pre><code class="language-bash">kafka-console-consumer --topic order-complete --bootstrap-server localhost:9092 --from-beginning</code></pre>
<p>orderId가 순서대로 출력되는 것을 확인했다.</p>
<hr>
<h2 id="at-least-once-전략">At Least Once 전략</h2>
<p>Kafka Consumer 전략은 크게 세 가지다.</p>
<table>
<thead>
<tr>
<th>전략</th>
<th>설명</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>At Most Once</td>
<td>offset 먼저 커밋</td>
<td>메시지 유실 가능</td>
</tr>
<tr>
<td>At Least Once</td>
<td>처리 후 offset 커밋</td>
<td>중복 가능, 멱등성 필요</td>
</tr>
<tr>
<td>Exactly Once</td>
<td>딱 한 번만 처리</td>
<td>구현 복잡, 성능 저하</td>
</tr>
</tbody></table>
<p>우리 프로젝트는 <strong>At Least Once</strong> 전략을 채택했다. 중복 처리 방지는 orderId 기반 체크로 멱등성을 보장할 수 있다.</p>
<blockquote>
<p>&quot;At Least Once 전략을 사용하고, 중복 처리 방지를 위해 orderId 기반 멱등성을 보장했습니다.&quot;</p>
</blockquote>
<hr>
<h2 id="마치며">마치며</h2>
<p>이번 편에서 Kafka를 도입해서 주문 완료 이벤트를 비동기로 발행하는 구조를 완성했다.
다음 편에서는 성능 테스트와 README, GitHub 정리를 진행할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DCommerce] 4편 - Redis로 재고 동시성 문제 해결하기]]></title>
            <link>https://velog.io/@do_dev/DCommerce-4%ED%8E%B8-Redis%EB%A1%9C-%EC%9E%AC%EA%B3%A0-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@do_dev/DCommerce-4%ED%8E%B8-Redis%EB%A1%9C-%EC%9E%AC%EA%B3%A0-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 07 May 2026 16:52:27 GMT</pubDate>
            <description><![CDATA[<h1 id="dcommerce-4편---redis로-재고-동시성-문제-해결하기">[DCommerce] 4편 - Redis로 재고 동시성 문제 해결하기</h1>
<h2 id="왜-동시성-문제가-발생할까">왜 동시성 문제가 발생할까?</h2>
<p>기존 코드는 이런 흐름이었다:</p>
<pre><code>1. DB에서 재고 조회
2. 재고 확인
3. 재고 차감
4. DB 저장</code></pre><p>동시에 2명이 주문하면:</p>
<pre><code>재고: 1개

A: 재고 조회 → 1개 있음
B: 재고 조회 → 1개 있음 (A가 아직 차감 안 함)
A: 재고 차감 → 0개
B: 재고 차감 → -1개 💥 overselling 발생!</code></pre><p>재고가 마이너스가 되는 <strong>overselling 문제</strong>가 발생한다.</p>
<hr>
<h2 id="redis로-해결하는-이유">Redis로 해결하는 이유</h2>
<p>Redis의 <code>increment</code> 연산은 <strong>원자적(Atomic)</strong> 으로 처리된다.</p>
<pre><code>동시에 100명이 요청해도 Redis는 순서대로 하나씩 처리
→ 재고가 절대 음수가 되지 않음</code></pre><hr>
<h2 id="redis-설정">Redis 설정</h2>
<h3 id="의존성-추가">의존성 추가</h3>
<pre><code class="language-gradle">implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;</code></pre>
<h3 id="applicationyml">application.yml</h3>
<pre><code class="language-yaml">spring:
  data:
    redis:
      host: localhost
      port: 6379</code></pre>
<h3 id="redisconfig">RedisConfig</h3>
<pre><code class="language-java">@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate&lt;String, String&gt; redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate&lt;String, String&gt; template = new RedisTemplate&lt;&gt;();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }
}</code></pre>
<hr>
<h2 id="stockservice-구현">StockService 구현</h2>
<p>Redis에 재고를 관리하는 서비스.
키 형식은 <code>stock:{productId}</code> 로 상품별로 관리한다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class StockService {

    private final RedisTemplate&lt;String, String&gt; redisTemplate;

    private String getStockKey(Long productId) {
        return &quot;stock:&quot; + productId;
    }

    // 재고 초기화 (상품 등록 시 호출)
    public void initStock(Long productId, int quantity) {
        redisTemplate.opsForValue().set(getStockKey(productId), String.valueOf(quantity));
    }

    // 재고 차감 (원자적 연산 핵심!)
    public Long decreaseStock(Long productId, int quantity) {
        return redisTemplate.opsForValue().increment(getStockKey(productId), -quantity);
    }

    // 재고 증가 (차감 실패 시 롤백용)
    public void increaseStock(Long productId, int quantity) {
        redisTemplate.opsForValue().increment(getStockKey(productId), quantity);
    }

    // 재고 조회
    public int getStock(Long productId) {
        String value = redisTemplate.opsForValue().get(getStockKey(productId));
        return value == null ? 0 : Integer.parseInt(value);
    }
}</code></pre>
<hr>
<h2 id="상품-등록-시-redis-재고-초기화">상품 등록 시 Redis 재고 초기화</h2>
<pre><code class="language-java">public void createProduct(ProductCreateRequestDTO dto) {
    Product savedProduct = productRepository.save(product);
    stockService.initStock(savedProduct.getId(), savedProduct.getStockQuantity());
}</code></pre>
<hr>
<p><img src="https://velog.velcdn.com/images/do_dev/post/bb530189-17c8-4a9e-a3c7-9c1b034e5234/image.png" alt=""></p>
<h2 id="주문-시-redis-재고-차감">주문 시 Redis 재고 차감</h2>
<pre><code class="language-java">@Transactional
public void createOrder(Long memberId, CreateOrderRequestDTO dto) {
    // Redis에서 원자적으로 재고 차감
    Long remainStock = stockService.decreaseStock(
            dto.getProductId(),
            dto.getQuantity()
    );

    // 재고 부족 시 롤백
    if (remainStock &lt; 0) {
        stockService.increaseStock(dto.getProductId(), dto.getQuantity());
        throw new IllegalArgumentException(&quot;재고가 부족합니다.&quot;);
    }
}</code></pre>
<p><strong>왜 차감 후 체크하나?</strong>
Redis increment는 차감 후 결과를 반환한다.
음수가 되면 재고 초과이므로 즉시 롤백하고 예외를 던진다.</p>
<hr>
<h2 id="동시성-테스트">동시성 테스트</h2>
<p>재고 7개 상품에 10개 동시 요청:</p>
<pre><code class="language-bash">for i in {1..10}; do
  curl -s -X POST http://localhost:8080/order \
    -H &quot;Authorization: Bearer $TOKEN&quot; \
    -H &quot;Content-Type: application/json&quot; \
    -d &#39;{&quot;productId&quot;: 1, &quot;quantity&quot;: 1}&#39; &amp;
done
wait

redis-cli GET stock:1</code></pre>
<h3 id="결과">결과</h3>
<pre><code>시작 재고: 7개
동시 요청: 10개
결과 재고: 0개 ✅
성공: 7개 / 실패(재고 부족): 3개 ✅</code></pre><p><img src="https://velog.velcdn.com/images/do_dev/post/e372629a-e1b8-46ee-a5aa-f66eeabbd2b3/image.png" alt=""></p>
<p>재고가 음수가 되지 않고 정확히 0에서 멈췄다.</p>
<hr>
<h2 id="정리-포인트">정리 포인트</h2>
<blockquote>
<p>&quot;Redis의 <code>increment</code> 연산은 싱글스레드 기반의 원자적 연산으로, 동시 요청이 몰려도 순서대로 처리되어 overselling을 방지합니다. 10개 동시 요청 테스트에서 재고 초과분은 정상적으로 차단됨을 확인했습니다.&quot;</p>
</blockquote>
<hr>
<h2 id="오늘-배운-것">오늘 배운 것</h2>
<ul>
<li>Redis Key-Value 구조와 활용법</li>
<li><code>increment</code> 원자적 연산으로 동시성 문제 해결</li>
<li>overselling 문제와 해결 방법</li>
<li>재고 차감 후 음수 체크 + 롤백 패턴</li>
<li>동시성 테스트 방법 (curl 병렬 요청)</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DCommerce] 3편 - 상품 API + 주문 API 구현하기]]></title>
            <link>https://velog.io/@do_dev/DCommerce-3%ED%8E%B8-%EC%83%81%ED%92%88-API-%EC%A3%BC%EB%AC%B8-API-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@do_dev/DCommerce-3%ED%8E%B8-%EC%83%81%ED%92%88-API-%EC%A3%BC%EB%AC%B8-API-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 29 Apr 2026 07:34:22 GMT</pubDate>
            <description><![CDATA[<h1 id="dcommerce-3편---상품-api--주문-api-구현하기">[DCommerce] 3편 - 상품 API + 주문 API 구현하기</h1>
<h2 id="요구사항-정의">요구사항 정의</h2>
<h3 id="상품-api">상품 API</h3>
<pre><code>1. 상품 등록 — 상품명, 가격, 재고수량 입력받아 등록
2. 상품 목록 조회 — 전체 상품 목록 조회
3. 상품 단건 조회 — 상품 ID로 특정 상품 조회, 없으면 예외</code></pre><h3 id="주문-api">주문 API</h3>
<pre><code>1. 주문 생성 — 로그인한 회원만 가능 (JWT 필요)
              상품 ID + 수량으로 주문 생성
              재고 부족 시 예외
              주문 생성 시 재고 차감
2. 내 주문 목록 조회 — 로그인한 회원의 주문 목록 조회</code></pre><hr>
<h2 id="정적-팩토리-메서드-static-factory-method">정적 팩토리 메서드 (Static Factory Method)</h2>
<p><code>new</code> 키워드 대신 <strong>static 메서드로 객체를 생성</strong>하는 패턴.</p>
<pre><code class="language-java">// 일반 생성자
ProductListResponseDTO dto = new ProductListResponseDTO(product.getId(), product.getName(), ...);

// 정적 팩토리 메서드
ProductListResponseDTO dto = ProductListResponseDTO.from(product);</code></pre>
<h3 id="장점">장점</h3>
<p><strong>첫째, 이름을 줄 수 있다</strong></p>
<pre><code class="language-java">// 이게 뭘 만드는지 한눈에 알아?
new ProductListResponseDTO(1L, &quot;상품명&quot;, 1000, 10);

// 이건?
ProductListResponseDTO.from(product);  // Product로부터 DTO 만든다</code></pre>
<p><strong>둘째, Entity 변경이 한 곳에 집중된다</strong></p>
<p><code>Product</code> 필드가 추가되거나 바뀌면 <code>from()</code> 메서드 하나만 고치면 된다.</p>
<p><strong>셋째, Setter가 필요 없어진다</strong></p>
<p>Setter 없이 객체를 불변으로 만들 수 있다.</p>
<blockquote>
<p>&quot;정적 팩토리 메서드는 객체 생성의 의도를 명확히 하고, 변환 로직을 한 곳에 모아 유지보수성을 높이기 위해 사용합니다.&quot;</p>
</blockquote>
<hr>
<h2 id="상품-api-구현">상품 API 구현</h2>
<h3 id="productservice">ProductService</h3>
<pre><code class="language-java">@RequiredArgsConstructor
@Service
public class ProductServiceImpl implements ProductService {

    private final ProductRepository productRepository;

    @Override
    public void createProduct(ProductCreateRequestDTO dto) {
        if (dto.getPrice() &lt; 0) {
            throw new IllegalArgumentException(&quot;가격이 음수입니다.&quot;);
        }
        if (dto.getStockQuantity() &lt; 0) {
            throw new IllegalArgumentException(&quot;재고가 0보다 작습니다.&quot;);
        }

        Product product = Product.builder()
                .name(dto.getName())
                .price(dto.getPrice())
                .stockQuantity(dto.getStockQuantity())
                .build();

        productRepository.save(product);
    }

    @Override
    public List&lt;ProductListResponseDTO&gt; getProductList() {
        List&lt;Product&gt; all = productRepository.findAll();
        List&lt;ProductListResponseDTO&gt; result = new ArrayList&lt;&gt;();
        for (Product product : all) {
            result.add(ProductListResponseDTO.from(product));
        }
        return result;
    }

    @Override
    public ProductDetailResponseDTO getProductDetail(Long id) {
        Product product = productRepository.findById(id)
                .orElseThrow(() -&gt; new IllegalArgumentException(&quot;존재하지 않는 상품입니다.&quot;));
        return ProductDetailResponseDTO.from(product);
    }
}</code></pre>
<p><strong>포인트: <code>orElseThrow()</code> 활용</strong>
<code>Optional.get()</code>을 바로 쓰면 NPE 위험이 있다.
<code>orElseThrow()</code>로 값이 없을 때 명확한 예외를 던져야 한다.</p>
<h3 id="productcontroller">ProductController</h3>
<pre><code class="language-java">@RequiredArgsConstructor
@RestController
@RequestMapping(&quot;/product&quot;)
public class ProductController {

    private final ProductService productService;

    @PostMapping
    public ResponseEntity&lt;Void&gt; createProduct(@RequestBody ProductCreateRequestDTO dto) {
        productService.createProduct(dto);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    @GetMapping
    public ResponseEntity&lt;List&lt;ProductListResponseDTO&gt;&gt; getProductList() {
        return ResponseEntity.ok(productService.getProductList());
    }

    @GetMapping(&quot;/{productId}&quot;)
    public ResponseEntity&lt;ProductDetailResponseDTO&gt; getProductDetail(@PathVariable Long productId) {
        return ResponseEntity.ok(productService.getProductDetail(productId));
    }
}</code></pre>
<p><strong>포인트: 등록 API는 201 Created 반환</strong></p>
<blockquote>
<p>&quot;등록 API는 자원이 생성됐음을 명확히 하기 위해 200 OK 대신 201 Created를 반환했습니다.&quot;</p>
</blockquote>
<hr>
<h2 id="주문-api-구현">주문 API 구현</h2>
<h3 id="재고-차감-설계--도메인-메서드">재고 차감 설계 — 도메인 메서드</h3>
<p>재고 차감 로직을 <code>Product</code> Entity 안에 메서드로 만들었다.</p>
<pre><code>재고 차감 로직 → Product 도메인 메서드
재고 차감 호출 → OrderService</code></pre><p>Entity가 자기 상태를 스스로 변경하는 방식 — Setter 없이 안전하게 처리한다.</p>
<pre><code class="language-java">// Product.java
public void decreaseStockQuantity(int quantity) {
    if (this.stockQuantity &lt; quantity) {
        throw new IllegalArgumentException(&quot;재고가 부족합니다.&quot;);
    }
    this.stockQuantity -= quantity;
}</code></pre>
<h3 id="orderservice">OrderService</h3>
<pre><code class="language-java">@RequiredArgsConstructor
@Service
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final ProductRepository productRepository;
    private final OrderRepository orderRepository;
    private final OrderItemRepository orderItemRepository;

    @Transactional
    @Override
    public void createOrder(Long memberId, CreateOrderRequestDTO dto) {
        // 1. 회원 조회
        Member member = memberRepository.findById(memberId)
                .orElseThrow(() -&gt; new IllegalArgumentException(&quot;등록된 회원이 아닙니다.&quot;));

        // 2. 상품 조회
        Product product = productRepository.findById(dto.getProductId())
                .orElseThrow(() -&gt; new IllegalArgumentException(&quot;존재하지 않는 상품입니다.&quot;));

        // 3. 주문 생성
        Order order = Order.builder()
                .member(member)
                .orderPrice((long) (dto.getQuantity() * product.getPrice()))
                .orderStatus(OrderStatus.ORDERED)
                .orderedAt(LocalDateTime.now())
                .build();

        // 4. 주문아이템 생성 (주문 당시 가격 스냅샷)
        OrderItem orderItem = OrderItem.builder()
                .order(order)
                .product(product)
                .quantity(dto.getQuantity())
                .orderPrice(product.getPrice())
                .build();

        // 5. 재고 차감
        product.decreaseStockQuantity(dto.getQuantity());

        // 6. 저장
        orderRepository.save(order);
        orderItemRepository.save(orderItem);
    }
}</code></pre>
<p><strong>포인트 1: @Transactional</strong></p>
<p>재고 차감, 주문 저장, 주문아이템 저장이 하나의 트랜잭션으로 묶여야 한다.
중간에 하나라도 실패하면 전부 롤백된다.</p>
<pre><code class="language-java">// Spring 거로 임포트해야 함
import org.springframework.transaction.annotation.Transactional;
// Jakarta 거 아님!</code></pre>
<p><strong>포인트 2: OrderItem에 가격 스냅샷 저장</strong></p>
<p>상품 가격이 나중에 바뀌어도 주문 당시 가격을 유지하기 위해
<code>OrderItem</code>에 <code>orderPrice</code>를 따로 저장한다.</p>
<h3 id="ordercontroller">OrderController</h3>
<pre><code class="language-java">@RequiredArgsConstructor
@RequestMapping(&quot;/order&quot;)
@RestController
public class OrderController {

    private final OrderService orderService;

    @PostMapping
    public ResponseEntity&lt;Void&gt; createOrder(
            HttpServletRequest request,
            @RequestBody CreateOrderRequestDTO dto) {
        Long memberId = (Long) request.getAttribute(&quot;memberId&quot;);
        orderService.createOrder(memberId, dto);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}</code></pre>
<p><strong>포인트: memberId는 JWT에서 꺼낸다</strong></p>
<p><code>JwtInterceptor</code>에서 토큰 검증 후 <code>request.setAttribute(&quot;memberId&quot;, memberId)</code>로 저장해뒀기 때문에
Controller에서 <code>request.getAttribute(&quot;memberId&quot;)</code>로 꺼내 쓸 수 있다.</p>
<hr>
<h2 id="api-테스트">API 테스트</h2>
<h3 id="상품-등록">상품 등록</h3>
<pre><code>POST http://localhost:8080/product
Content-Type: application/json

{
    &quot;name&quot;: &quot;맥북 프로&quot;,
    &quot;price&quot;: 3000000,
    &quot;stockQuantity&quot;: 10,
    &quot;description&quot;: &quot;M3 Pro 칩셋&quot;
}</code></pre><h3 id="주문-생성">주문 생성</h3>
<pre><code>POST http://localhost:8080/order
Authorization: Bearer {JWT토큰}
Content-Type: application/json

{
    &quot;productId&quot;: 1,
    &quot;quantity&quot;: 2
}</code></pre><hr>
<h2 id="builderdefault-주의사항">@Builder.Default 주의사항</h2>
<p><code>@Builder</code>와 컬렉션 초기화를 함께 쓸 때 경고가 발생한다:</p>
<pre><code class="language-java">// 경고 발생
private List&lt;OrderItem&gt; orderItemList = new ArrayList&lt;&gt;();

// 해결
@Builder.Default
private List&lt;OrderItem&gt; orderItemList = new ArrayList&lt;&gt;();</code></pre>
<p><code>@Builder</code>는 기본값 초기화를 무시하기 때문에 <code>@Builder.Default</code>를 붙여야
빌더로 생성할 때도 빈 리스트가 초기화된다.</p>
<hr>
<h2 id="오늘-배운-것">오늘 배운 것</h2>
<ul>
<li>정적 팩토리 메서드 패턴과 장점</li>
<li><code>orElseThrow()</code>로 안전한 Optional 처리</li>
<li>도메인 메서드로 Entity 상태 변경 (Setter 없이)</li>
<li><code>@Transactional</code>로 원자적 트랜잭션 처리</li>
<li>JWT에서 memberId 꺼내는 방법</li>
<li>201 Created vs 200 OK 차이</li>
<li><code>@Builder.Default</code> 사용법</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[2편 - 로그인 API + JWT 발급 구현하기]]></title>
            <link>https://velog.io/@do_dev/2%ED%8E%B8-%EB%A1%9C%EA%B7%B8%EC%9D%B8-API-JWT-%EB%B0%9C%EA%B8%89-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@do_dev/2%ED%8E%B8-%EB%A1%9C%EA%B7%B8%EC%9D%B8-API-JWT-%EB%B0%9C%EA%B8%89-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 24 Apr 2026 03:40:56 GMT</pubDate>
            <description><![CDATA[<h1 id="로그인-api--jwt-발급-구현하기">로그인 API + JWT 발급 구현하기</h1>
<h2 id="jwt란">JWT란?</h2>
<p>JWT(JSON Web Token)는 로그인 인증에 사용되는 토큰 방식이야.</p>
<p><strong>핵심 포인트: 서버는 토큰을 저장하지 않는다.</strong></p>
<p>일반적인 세션 방식은 서버가 로그인 상태를 저장하지만, JWT는 다르다.
서버는 <code>Secret Key</code>로 서명만 해서 클라이언트에게 토큰을 발급하고,
이후 요청마다 그 서명이 유효한지만 검증한다. DB 조회 없이.</p>
<p>그래서 JWT의 장점은 <strong>Stateless(무상태)</strong> — 서버가 상태를 저장하지 않아도 된다.</p>
<h3 id="jwt-동작-흐름">JWT 동작 흐름</h3>
<pre><code>① 클라이언트 → 서버: email + password 전송
② 서버 → DB: 유저 조회
③ DB → 서버: 유저 정보 반환
④ 서버: 비밀번호 검증 + JWT 생성 (서명)
⑤ 서버 → 클라이언트: JWT 반환
⑥ 이후 API 요청 시: Authorization: Bearer {token} 헤더에 포함
⑦ 서버: 서명 검증 (DB 조회 없이!)</code></pre><h3 id="jwt-구조">JWT 구조</h3>
<p>JWT는 3개 파트로 구성된다:</p>
<pre><code>header.payload.signature

eyJhbGc... . eyJ1c2VySWQiOjF9 . SflKxwRJSMeKKF2QT4...</code></pre><ul>
<li><code>header</code> — 알고리즘 정보 (HS256)</li>
<li><code>payload</code> — 담을 데이터 (userId, email, 만료시간)</li>
<li><code>signature</code> — 위 두 개를 Secret Key로 서명한 값</li>
</ul>
<h3 id="서명이란">서명이란?</h3>
<p>서명은 <strong>&quot;내가 만든 토큰이 맞다&quot;는 증명</strong>이다.</p>
<p>누군가 payload를 <code>{ userId: 99 }</code>로 바꾸면? Secret Key가 없으니 signature를 다시 못 만든다.
서버가 검증할 때 &quot;이 서명 이상한데?&quot; 하고 바로 거부한다.</p>
<blockquote>
<p>&quot;payload가 중간에 변조되지 않았다는 걸 Secret Key로 보증하는 것&quot;</p>
</blockquote>
<hr>
<h2 id="의존성-추가">의존성 추가</h2>
<p><code>build.gradle</code>에 JWT 라이브러리 추가:</p>
<pre><code class="language-gradle">implementation &#39;io.jsonwebtoken:jjwt-api:0.11.5&#39;
runtimeOnly &#39;io.jsonwebtoken:jjwt-impl:0.11.5&#39;
runtimeOnly &#39;io.jsonwebtoken:jjwt-jackson:0.11.5&#39;</code></pre>
<p><code>application.yml</code>에 Secret Key 설정:</p>
<pre><code class="language-yaml">jwt:
  secret: mysecretkey12345678901234567890123456789012
  expiration: 86400000  # 24시간 (ms)</code></pre>
<hr>
<h2 id="jwtprovider-구현">JwtProvider 구현</h2>
<p>JWT 생성, 검증, userId 추출 3가지 기능을 담당하는 유틸 클래스:</p>
<pre><code class="language-java">@Component
public class JwtProvider {

    @Value(&quot;${jwt.secret}&quot;)
    private String secretKey;

    @Value(&quot;${jwt.expiration}&quot;)
    private long expiration;

    // 토큰 생성
    public String generateToken(Long userId, String email) {
        return Jwts.builder()
                .setSubject(String.valueOf(userId))
                .claim(&quot;email&quot;, email)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), SignatureAlgorithm.HS256)
                .compact();
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    // userId 추출
    public Long getUserId(String token) {
        String subject = Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
        return Long.parseLong(subject);
    }
}</code></pre>
<p><strong>왜 try-catch로 검증하나?</strong>
토큰이 만료되거나 서명이 틀리면 <code>Jwts</code>가 예외를 던진다.
그걸 잡아서 <code>false</code>를 반환하는 방식으로 유효성을 검증한다.</p>
<hr>
<h2 id="로그인-api-구현">로그인 API 구현</h2>
<h3 id="memberrepository">MemberRepository</h3>
<pre><code class="language-java">@Repository
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    boolean existsMemberByEmail(String email);
    Optional&lt;Member&gt; findByEmail(String email);  // 로그인용 추가
}</code></pre>
<h3 id="memberservice">MemberService</h3>
<pre><code class="language-java">@Override
public LoginResponseDTO login(LoginRequestDTO loginRequestDTO) {
    // 1. 이메일로 유저 조회
    Member member = memberRepository.findByEmail(loginRequestDTO.getEmail())
            .orElseThrow(() -&gt; new IllegalArgumentException(&quot;가입되지 않은 회원입니다.&quot;));

    // 2. 비밀번호 확인
    if (!passwordEncoder.matches(loginRequestDTO.getPassword(), member.getPassword())) {
        throw new IllegalArgumentException(&quot;비밀번호가 틀렸습니다.&quot;);
    }

    // 3. JWT 발급
    String token = jwtProvider.generateToken(member.getId(), member.getEmail());

    return new LoginResponseDTO(member.getId(), token);
}</code></pre>
<p><strong>포인트 1: <code>orElseThrow()</code> 활용</strong>
<code>Optional.isPresent()</code> 체크보다 훨씬 깔끔하다. 값이 없으면 바로 예외를 던진다.</p>
<p><strong>포인트 2: <code>passwordEncoder.matches()</code> 사용</strong>
비밀번호는 BCrypt로 암호화되어 저장되므로 평문과 직접 비교하면 안 된다.
<code>matches(평문, 암호화된값)</code>으로 비교해야 한다.</p>
<h3 id="loginresponsedto">LoginResponseDTO</h3>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class LoginResponseDTO {
    private Long memberId;
    private String token;
}</code></pre>
<h3 id="membercontroller">MemberController</h3>
<pre><code class="language-java">@PostMapping(&quot;/login&quot;)
public ResponseEntity&lt;LoginResponseDTO&gt; login(@RequestBody LoginRequestDTO loginRequestDTO) {
    LoginResponseDTO response = memberService.login(loginRequestDTO);
    return ResponseEntity.ok(response);
}</code></pre>
<hr>
<h2 id="jwtinterceptor-설정">JwtInterceptor 설정</h2>
<p>로그인, 회원가입 URI는 토큰 검증 없이 통과시키도록 설정:</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class JwtInterceptor implements HandlerInterceptor {

    private final JwtProvider jwtProvider;

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        String uri = request.getRequestURI();

        // 로그인, 회원가입은 토큰 검증 스킵
        if (uri.startsWith(&quot;/member/login&quot;) || uri.startsWith(&quot;/member/signUp&quot;)) {
            return true;
        }

        String authHeader = request.getHeader(&quot;Authorization&quot;);
        if (authHeader == null || !authHeader.startsWith(&quot;Bearer &quot;)) {
            response.setStatus(401);
            return false;
        }

        String token = authHeader.substring(7);
        if (!jwtProvider.validateToken(token)) {
            response.setStatus(401);
            return false;
        }

        Long memberId = jwtProvider.getUserId(token);
        request.setAttribute(&quot;memberId&quot;, memberId);
        return true;
    }
}</code></pre>
<hr>
<h2 id="테스트-결과">테스트 결과</h2>
<pre><code>POST http://localhost:8080/member/login

{
    &quot;email&quot;: &quot;test@test.com&quot;,
    &quot;password&quot;: &quot;1234&quot;
}</code></pre><p>응답:</p>
<pre><code class="language-json">{
    &quot;memberId&quot;: 1,
    &quot;token&quot;: &quot;eyJhbGciOiJIUzI1NiJ9.eyJ...&quot;
}</code></pre>
<hr>
<h2 id="주의사항-ddl-auto-설정">주의사항: ddl-auto 설정</h2>
<p>개발 중 <code>ddl-auto: create</code>로 설정하면 서버 재시작 시 테이블이 드롭되고 데이터가 날아간다.
데이터를 유지하려면 <code>update</code>로 변경해야 한다.</p>
<pre><code class="language-yaml">spring:
  jpa:
    hibernate:
      ddl-auto: update  # create → update</code></pre>
<table>
<thead>
<tr>
<th>옵션</th>
<th>동작</th>
</tr>
</thead>
<tbody><tr>
<td>create</td>
<td>매번 테이블 드롭 후 재생성 (데이터 삭제)</td>
</tr>
<tr>
<td>update</td>
<td>변경사항만 반영, 데이터 유지</td>
</tr>
<tr>
<td>validate</td>
<td>테이블 구조 검증만 (변경 없음)</td>
</tr>
<tr>
<td>none</td>
<td>아무것도 안 함</td>
</tr>
</tbody></table>
<hr>
<h2 id="오늘-배운-것">오늘 배운 것</h2>
<ul>
<li>JWT 구조와 Stateless 인증 방식</li>
<li>서명(Signature)의 역할 — 변조 방지</li>
<li>JwtProvider 구현 (생성/검증/추출)</li>
<li><code>passwordEncoder.matches()</code>로 암호화 비밀번호 비교</li>
<li><code>Optional.orElseThrow()</code> 패턴</li>
<li><code>ddl-auto: create</code> vs <code>update</code> 차이</li>
</ul>
<hr>
<p><em>다음 포스팅: 상품 API + 주문 API 구현</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot 프로젝트 설계 — ERD와 첫 API 만들기]]></title>
            <link>https://velog.io/@do_dev/Spring-Boot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%A4%EA%B3%84-ERD%EC%99%80-%EC%B2%AB-API-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@do_dev/Spring-Boot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%A4%EA%B3%84-ERD%EC%99%80-%EC%B2%AB-API-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Fri, 24 Apr 2026 00:54:05 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-boot-프로젝트-설계--erd와-첫-api-만들기">Spring Boot 프로젝트 설계 — ERD와 첫 API 만들기</h1>
<h2 id="프로젝트-개요">프로젝트 개요</h2>
<p>대용량 주문 처리 API 서버를 만들면서 Java 백엔드 기술을 정리합니다.</p>
<p><strong>기술 스택</strong></p>
<ul>
<li>Spring Boot 3.2.x</li>
<li>Java 17</li>
<li>MySQL + JPA</li>
<li>Redis </li>
<li>Kafka </li>
<li>JWT 인증 </li>
</ul>
<hr>
<h2 id="erd-설계">ERD 설계</h2>
<p>주문 시스템에 필요한 테이블 5개를 설계했습니다.</p>
<pre><code>Member ─ 1:N ─ Order ─ 1:N ─ OrderItem ─ N:1 ─ Product
                 │
                1:1
                 │
             Delivery</code></pre><table>
<thead>
<tr>
<th>테이블</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>members</td>
<td>회원 정보</td>
</tr>
<tr>
<td>orders</td>
<td>주문 정보</td>
</tr>
<tr>
<td>order_items</td>
<td>주문 상품 목록</td>
</tr>
<tr>
<td>products</td>
<td>상품 정보</td>
</tr>
<tr>
<td>deliveries</td>
<td>배송 정보</td>
</tr>
</tbody></table>
<h3 id="order_items에-price를-따로-저장하는-이유">order_items에 price를 따로 저장하는 이유</h3>
<p>주문 당시 가격을 스냅샷으로 저장합니다.
상품 가격이 나중에 변경되더라도 주문 이력은 결제 시점 금액을 유지해야 하기 때문입니다.</p>
<hr>
<h2 id="entity-설계">Entity 설계</h2>
<h3 id="memberjava">Member.java</h3>
<pre><code class="language-java">@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String password;

    private String userName;

    @OneToMany(mappedBy = &quot;member&quot;)
    private List&lt;Order&gt; orderList;

    @CreationTimestamp
    private LocalDateTime createdAt;
}</code></pre>
<h3 id="orderjava">Order.java</h3>
<pre><code class="language-java">@Getter
@Table(name = &quot;orders&quot;)
@Entity
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;ORDER_ID&quot;)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

    @OneToMany(mappedBy = &quot;order&quot;)
    private List&lt;OrderItem&gt; orderItemList = new ArrayList&lt;&gt;();

    @OneToOne(mappedBy = &quot;order&quot;, fetch = FetchType.LAZY)
    private Delivery delivery;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus;

    private Long orderPrice;

    @Enumerated(EnumType.STRING)
    private PaymentStatus paymentStatus;

    @Enumerated(EnumType.STRING)
    private OrderType orderType;

    private LocalDateTime orderedAt;
    private LocalDateTime paidAt;
}</code></pre>
<h3 id="orderitemjava">OrderItem.java</h3>
<pre><code class="language-java">@Getter
@Table(name = &quot;ORDER_ITEM&quot;)
@Entity
public class OrderItem {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;ORDER_ITEM_ID&quot;)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;order_id&quot;)
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;product_id&quot;)
    private Product product;

    private int quantity;
    private int orderPrice;
}</code></pre>
<h3 id="productjava">Product.java</h3>
<pre><code class="language-java">@Getter
@Table(name = &quot;product&quot;)
@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;PRODUCT_ID&quot;)
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;
    private LocalDateTime createdAt;
}</code></pre>
<h3 id="deliveryjava">Delivery.java</h3>
<pre><code class="language-java">@Getter
@Table(name = &quot;deliveries&quot;)
@Entity
public class Delivery {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;order_id&quot;)
    private Order order;

    private String address;

    @Enumerated(EnumType.STRING)
    private DeliveryEnum status;

    private LocalDateTime createdAt;
}</code></pre>
<hr>
<h2 id="연관관계-정리">연관관계 정리</h2>
<h3 id="mappedby란">mappedBy란?</h3>
<p>양방향 연관관계에서 연관관계의 주인을 지정합니다.
주인이 아닌 쪽은 외래키를 관리하지 않고 조회만 가능합니다.</p>
<pre><code class="language-java">// Order가 주인 — member_id 외래키 관리
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;member_id&quot;)
private Member member;

// Member는 주인 아님 — 읽기만 가능
@OneToMany(mappedBy = &quot;member&quot;)
private List&lt;Order&gt; orderList;</code></pre>
<h3 id="fetchtypelazy를-쓰는-이유">FetchType.LAZY를 쓰는 이유</h3>
<p>연관된 Entity를 실제로 사용할 때 쿼리를 날립니다.
EAGER는 연관 Entity를 무조건 즉시 로딩해서 불필요한 쿼리가 발생할 수 있습니다.</p>
<hr>
<h2 id="회원가입-api">회원가입 API</h2>
<h3 id="memberrepositoryjava">MemberRepository.java</h3>
<pre><code class="language-java">@Repository
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    boolean existsMemberByEmail(String email);
}</code></pre>
<h3 id="memberservicejava">MemberService.java</h3>
<pre><code class="language-java">public Long signUp(SignUpRequest signUpRequest) {
    // 이메일 중복 체크
    boolean isAlreadyMember = memberRepository.existsMemberByEmail(signUpRequest.getEmail());
    if (isAlreadyMember) {
        throw new IllegalArgumentException(&quot;이미 가입된 회원 이메일입니다.&quot;);
    }

    // 비밀번호 암호화
    String encodePassword = passwordEncoder.encode(signUpRequest.getPassword());

    // 빌더 패턴으로 객체 생성
    Member member = Member.builder()
            .email(signUpRequest.getEmail())
            .password(encodePassword)
            .userName(signUpRequest.getUserName())
            .build();

    memberRepository.save(member);
    return member.getId();
}</code></pre>
<h3 id="membercontrollerjava">MemberController.java</h3>
<pre><code class="language-java">@RequiredArgsConstructor
@RestController
@RequestMapping(&quot;/member&quot;)
public class MemberController {

    private final MemberService memberService;

    @PostMapping(&quot;/signUp&quot;)
    public ResponseEntity&lt;Long&gt; signUp(@RequestBody SignUpRequest signUpRequest) {
        memberService.signUp(signUpRequest);
        return ResponseEntity.ok().build();
    }
}</code></pre>
<h3 id="api-호출-테스트">API 호출 테스트</h3>
<pre><code>POST http://localhost:8080/member/signUp
Content-Type: application/json

{
    &quot;email&quot;: &quot;test@test.com&quot;,
    &quot;password&quot;: &quot;1234&quot;,
    &quot;userName&quot;: &quot;홍길동&quot;
}</code></pre><hr>
<h2 id="오늘-배운-것">오늘 배운 것</h2>
<ul>
<li>JPA Entity 설계 및 연관관계 매핑 (1:N, N:1, 1:1)</li>
<li>mappedBy로 연관관계 주인 지정</li>
<li>@Builder + @NoArgsConstructor 함께 쓰는 법</li>
<li>Entity에 @Setter를 쓰면 안 되는 이유</li>
<li>이메일 중복체크 + 비밀번호 암호화 적용</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[데이터베이스] Key 개념 정리 (PK, FK, Unique, Composite Key)]]></title>
            <link>https://velog.io/@do_dev/dbkey</link>
            <guid>https://velog.io/@do_dev/dbkey</guid>
            <pubDate>Thu, 05 Mar 2026 14:37:48 GMT</pubDate>
            <description><![CDATA[<h3 id="1-primary-key-pk">1. Primary Key (PK)</h3>
<p>Primary Key는 테이블에서 각 행(row)을 유일하게 식별하는 컬럼입니다.</p>
<p>즉 중복될 수 없고 NULL도 허용되지 않습니다.</p>
<p>예시</p>
<pre><code>CREATE TABLE member (
    id BIGINT PRIMARY KEY,
    email VARCHAR(255),
    name VARCHAR(100)
);</code></pre><p>여기서 id가 PK입니다.</p>
<h4 id="pk-특징">PK 특징</h4>
<blockquote>
<ul>
<li>유일성 (Uniqueness)</li>
</ul>
</blockquote>
<ul>
<li>같은 값이 존재할 수 없습니다.</li>
<li>NOT NULL</li>
<li>테이블당 하나만 존재</li>
<li>Index 자동 생성</li>
<li>대부분의 데이터베이스에서는 Primary Key를 생성하면 자동으로 Index가 생성됩니다.</li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th>PK</th>
<th>FK</th>
</tr>
</thead>
<tbody><tr>
<td>의미</td>
<td>기본키</td>
<td>외래키</td>
</tr>
<tr>
<td>역할</td>
<td>행 식별</td>
<td>다른 테이블 참조</td>
</tr>
<tr>
<td>중복</td>
<td>불가능</td>
<td>가능</td>
</tr>
<tr>
<td>NULL</td>
<td>불가능</td>
<td>가능</td>
</tr>
<tr>
<td>개수</td>
<td>테이블당 1개</td>
<td>여러개 가능</td>
</tr>
</tbody></table>
<h3 id="2-foreign-key-fk">2. Foreign Key (FK)</h3>
<p>Foreign Key는 다른 테이블의 PK를 참조하는 컬럼입니다.
즉 테이블 간 관계(Relation) 를 만들기 위한 키입니다.</p>
<pre><code>CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    member_id BIGINT,
    FOREIGN KEY (member_id) REFERENCES member(id)
);</code></pre><table>
<thead>
<tr>
<th>id</th>
<th>member_id</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
</tr>
<tr>
<td>3</td>
<td>2</td>
</tr>
</tbody></table>
<blockquote>
<p>orders.member_id → member.id 관계가 Foreign Key입니다.</p>
</blockquote>
<h4 id="fk의-역할">FK의 역할</h4>
<p>Foreign Key는 데이터 무결성(Data Integrity) 을 보장합니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>PK</th>
<th>FK</th>
</tr>
</thead>
<tbody><tr>
<td>의미</td>
<td>기본키</td>
<td>외래키</td>
</tr>
<tr>
<td>역할</td>
<td>행 식별</td>
<td>다른 테이블 참조</td>
</tr>
<tr>
<td>중복</td>
<td>불가능</td>
<td>가능</td>
</tr>
<tr>
<td>NULL</td>
<td>불가능</td>
<td>가능</td>
</tr>
<tr>
<td>개수</td>
<td>테이블당 1개</td>
<td>여러개 가능</td>
</tr>
</tbody></table>
<h3 id="3-unique-key">3. Unique Key</h3>
<p>Unique Key는 중복을 허용하지 않는 제약조건입니다.
즉 식별자 역할이 아니라 데이터 중복 방지 목적으로 사용됩니다.</p>
<h4 id="uk-특징">UK 특징</h4>
<blockquote>
<ul>
<li>중복 불가능</li>
</ul>
</blockquote>
<ul>
<li>NULL 허용 (DB에 따라 여러 개 가능)</li>
<li>여러 개 생성 가능</li>
<li>Index 생성</li>
<li>식별자 역할은 아님</li>
</ul>
<pre><code>CREATE TABLE member (
    id BIGINT PRIMARY KEY,
    email VARCHAR(255) UNIQUE
);</code></pre><table>
<thead>
<tr>
<th>구분</th>
<th>PK</th>
<th>UNIQUE</th>
</tr>
</thead>
<tbody><tr>
<td>NULL</td>
<td>불가능</td>
<td>가능</td>
</tr>
<tr>
<td>개수</td>
<td>1개</td>
<td>여러개</td>
</tr>
<tr>
<td>역할</td>
<td>기본 식별자</td>
<td>중복 방지</td>
</tr>
</tbody></table>
<h3 id="4-composite-key-복합키">4. Composite Key (복합키)</h3>
<p>Composite Key는 여러 컬럼을 합쳐서 하나의 PK로 사용하는 것입니다.</p>
<h4 id="jpa에서-composite-key-매핑">JPA에서 Composite Key 매핑</h4>
<p>JPA에서는 복합키를 사용할 때 두 가지 방법이 있습니다.</p>
<p>1️⃣ @IdClass</p>
<pre><code>@Entity
@IdClass(OrderItemId.class)
public class OrderItem {

    @Id
    private Long orderId;

    @Id
    private Long productId;

}
</code></pre><pre><code>public class OrderItemId implements Serializable {

    private Long orderId;
    private Long productId;

}</code></pre><p>2️⃣ @EmbeddedId </p>
<pre><code>@Entity
public class OrderItem {

    @EmbeddedId
    private OrderItemId id;

}</code></pre><pre><code>@Embeddable
public class OrderItemId implements Serializable {

    private Long orderId;
    private Long productId;

}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[주니어 탈출기] IoC와 DI 부수기 ]]></title>
            <link>https://velog.io/@do_dev/%EC%A3%BC%EB%8B%88%EC%96%B4-%ED%83%88%EC%B6%9C%EA%B8%B0-IoC%EC%99%80-DI-%EB%B6%80%EC%88%98%EA%B8%B0</link>
            <guid>https://velog.io/@do_dev/%EC%A3%BC%EB%8B%88%EC%96%B4-%ED%83%88%EC%B6%9C%EA%B8%B0-IoC%EC%99%80-DI-%EB%B6%80%EC%88%98%EA%B8%B0</guid>
            <pubDate>Sun, 31 Aug 2025 11:01:12 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-ioc와-di-깊게-파헤치기-컨테이너-빈-의존성-주입">Spring IoC와 DI 깊게 파헤치기: 컨테이너, 빈, 의존성 주입</h1>
<h2 id="1-들어가기-why">1. 들어가기 (Why?)</h2>
<ul>
<li>이 개념을 공부해야 하는 이유  </li>
<li>실무에서 IoC/DI가 중요한 상황 설명  </li>
</ul>
<hr>
<h2 id="2-기본-개념-정리-what">2. 기본 개념 정리 (What?)</h2>
<ul>
<li><p><strong>IoC (Inversion of Control)</strong>  </p>
<ul>
<li>정의  </li>
<li>특징  </li>
</ul>
</li>
<li><p><strong>DI (Dependency Injection)</strong>  </p>
<ul>
<li>정의  </li>
<li>주입 방식 (생성자, Setter, 필드)  </li>
</ul>
</li>
<li><p><strong>Spring Container &amp; Bean</strong>  </p>
<ul>
<li>Bean 개념  </li>
<li>ApplicationContext 역할  </li>
</ul>
</li>
</ul>
<hr>
<h2 id="3-동작-과정-how">3. 동작 과정 (How?)</h2>
<ul>
<li>IoC 컨테이너가 Bean을 생성하고 관리하는 흐름  </li>
<li>의존성 주입이 일어나는 과정  </li>
<li>Bean LifeCycle (생성 → 주입 → 초기화 → 소멸)  </li>
</ul>
<hr>
<h2 id="4-내부-동작-원리-deep-dive">4. 내부 동작 원리 (Deep Dive)</h2>
<ul>
<li>ApplicationContext 동작 과정  </li>
<li>Bean 등록 및 주입 과정 설명  </li>
<li>AOP/프록시와 연결되는 지점  </li>
</ul>
<hr>
<h2 id="5-실무-인사이트-real-world">5. 실무 인사이트 (Real World)</h2>
<ul>
<li>IoC/DI가 테스트 코드에 미치는 영향  </li>
<li>아키텍처 설계 확장성에 기여하는 사례  </li>
<li>실제 프로젝트에서 부딪힌 문제 (예: 순환 참조)  </li>
</ul>
<hr>
<h2 id="6-자주-하는-실수--베스트-프랙티스">6. 자주 하는 실수 &amp; 베스트 프랙티스</h2>
<ul>
<li>필드 주입(@Autowired) 남용의 문제점  </li>
<li>생성자 주입 권장 이유  </li>
<li>순환 참조 문제 해결 방법  </li>
<li>Bean 등록 중복 방지 전략  </li>
</ul>
<hr>
<h2 id="7-정리-summary">7. 정리 (Summary)</h2>
<ul>
<li>IoC 핵심 요약  </li>
<li>DI 핵심 요약  </li>
<li>오늘 글을 통해 얻을 수 있는 교훈 한 줄  </li>
</ul>
<hr>
<h2 id="8-참고-자료">8. 참고 자료</h2>
<ul>
<li><a href="https://docs.spring.io/spring-framework/reference/core/">Spring 공식 문서</a>  </li>
<li><em>토비의 스프링</em> (이일민 저)  </li>
<li>기타 참고 블로그/자료  </li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준  11050번 이항계수 JAVA 풀이
]]></title>
            <link>https://velog.io/@do_dev/%EB%B0%B1%EC%A4%80-11050%EB%B2%88-%EC%9D%B4%ED%95%AD%EA%B3%84%EC%88%98-JAVA-%ED%92%80%EC%9D%B4</link>
            <guid>https://velog.io/@do_dev/%EB%B0%B1%EC%A4%80-11050%EB%B2%88-%EC%9D%B4%ED%95%AD%EA%B3%84%EC%88%98-JAVA-%ED%92%80%EC%9D%B4</guid>
            <pubDate>Sun, 31 Aug 2025 10:56:45 GMT</pubDate>
            <description><![CDATA[<h1 id="boj-11050--이항-계수">BOJ 11050 | 이항 계수</h1>
<ul>
<li><strong>문제 번호</strong>: 11050  </li>
<li><strong>푼 날짜</strong>: 2025-08-31  </li>
</ul>
<hr>
<h2 id="📌-문제-요약">📌 문제 요약</h2>
<p>이항 계수가 무엇인지 이해하고 공식화 하면 되는 문제!</p>
<hr>
<h2 id="💡-접근-아이디어">💡 접근 아이디어</h2>
<ol>
<li>이항계수 공식을 적용하자 </li>
<li>헷갈려서 나누는 분모를 자꾸 여러번틀림 </li>
</ol>
<hr>
<h2 id="🛠️-java-구현">🛠️ Java 구현</h2>
<pre><code class="language-java">package BaekJoon.basic.bruteForce;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Q11050 {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        StringTokenizer st = new StringTokenizer(br.readLine());
        int a = Integer.parseInt(st.nextToken());
        int b = Integer.parseInt(st.nextToken());

        if(b ==1){
            System.out.println(a);
            return;
        }else if(b==0){
            System.out.println(1);
            return ;
        }

        int ans = 1;

        for(int i=0; i&lt;b; i++){
            ans *= a-i ;
        }

        ans = ans/makeFactorial(b);

        System.out.println(ans);
    }

    public static int makeFactorial(int n ){
        int result = 1 ;
        for(int i=1; i&lt;=n; i++){
            result *= i;
        }
        return result;
    }
}

</code></pre>
<h2 id="스스로-피드백-해보기">스스로 피드백 해보기</h2>
<h4 id="구글링해서-공식을-다시-알게됐고-오랜만에-팩토리얼-사용">&gt; 구글링해서 공식을 다시 알게됐고 오랜만에 팩토리얼 사용</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1032번 명령 프롬프트 JAVA 풀이  ]]></title>
            <link>https://velog.io/@do_dev/B1BOJ-1032-%EB%AA%85%EB%A0%B9-%ED%94%84%EB%A1%AC%ED%94%84%ED%8A%B8</link>
            <guid>https://velog.io/@do_dev/B1BOJ-1032-%EB%AA%85%EB%A0%B9-%ED%94%84%EB%A1%AC%ED%94%84%ED%8A%B8</guid>
            <pubDate>Sun, 31 Aug 2025 10:04:21 GMT</pubDate>
            <description><![CDATA[<h1 id="boj-1032--명령-프롬프트">BOJ 1032 | 명령 프롬프트</h1>
<ul>
<li><strong>문제 번호</strong>: 1032  </li>
<li><strong>푼 날짜</strong>: 2025-08-31  </li>
</ul>
<hr>
<h2 id="📌-문제-요약">📌 문제 요약</h2>
<p>배열을 입력받아 n개의 배열에서 다른값을 가진 인덱스에 ? 를 넣어주면되는 문제 </p>
<hr>
<h2 id="💡-접근-아이디어">💡 접근 아이디어</h2>
<ol>
<li>숫자를 입력받아보자</li>
<li>숫자만큼 String[] 배열을 입력받아보자 </li>
<li>배열을 비교하고 다르면 ? 를 넣는것을 구현해보자 </li>
</ol>
<hr>
<h2 id="🛠️-java-구현">🛠️ Java 구현</h2>
<pre><code class="language-java">package BaekJoon.basic.bruteForce;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Q1032 {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int n = Integer.parseInt(br.readLine());

        String[] arr = new String[n];

        for (int i = 0; i &lt; n; i++) {
            arr[i] = br.readLine();
        }

        int len = arr[0].length();

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i &lt; len; i++) {
            char c = arr[0].charAt(i);
            boolean isSame = true;

            for (int j = 1; j &lt; n; j++) {
                if (arr[j].charAt(i) != c) {
                    isSame = false;
                    break; 
                }
            }

            if (isSame) {
                sb.append(c);
            } else {
                sb.append(&quot;?&quot;);
            }
        }


        System.out.println(sb);

    }
}

</code></pre>
<h2 id="스스로-피드백-해보기">스스로 피드백 해보기</h2>
<h4 id="두달만에-문제를-풀어서-브론즈1부터-시도해보았다">&gt; 두달만에 문제를 풀어서 브론즈1부터 시도해보았다</h4>
<h4 id="입출력-연습으로-느껴져서-할만했다">&gt; 입출력 연습으로 느껴져서 할만했다</h4>
<h4 id="근데-2번-틀렸다-flag로-구분할때-좀-헷갈려서-index를-초과되게-append되어-에러가-났었고-break로-불필요한-구분제거했다">&gt; 근데 2번 틀렸다 flag로 구분할때 좀 헷갈려서 index를 초과되게 append되어 에러가 났었고 break로 불필요한 구분제거했다</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1107번 리모컨 JAVA 풀이
]]></title>
            <link>https://velog.io/@do_dev/%EB%B0%B1%EC%A4%80-1107%EB%B2%88-%EB%A6%AC%EB%AA%A8%EC%BB%A8-JAVA-%ED%92%80%EC%9D%B4</link>
            <guid>https://velog.io/@do_dev/%EB%B0%B1%EC%A4%80-1107%EB%B2%88-%EB%A6%AC%EB%AA%A8%EC%BB%A8-JAVA-%ED%92%80%EC%9D%B4</guid>
            <pubDate>Tue, 10 Jun 2025 02:19:49 GMT</pubDate>
            <description><![CDATA[<h1 id="boj-1107--리모컨">BOJ) 1107 | 리모컨</h1>
<ul>
<li><strong>문제 번호</strong>: 1107</li>
<li><strong>푼 날짜</strong>: 2025-06-10  </li>
</ul>
<hr>
<h2 id="문제-링크">문제 링크</h2>
<p><a href="https://www.acmicpc.net/problem/1107">https://www.acmicpc.net/problem/1107</a></p>
<h2 id="📌-문제-요약">📌 문제 요약</h2>
<hr>
<h2 id="💡-접근-아이디어">💡 접근 아이디어</h2>
<hr>
<h2 id="다음에-떠-올려-봐야할-것">다음에 떠 올려 봐야할 것</h2>
<hr>
<h2 id="🛠️-java-구현">🛠️ Java 구현</h2>
<pre><code class="language-java">package BaekJoon.basic;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Q1476 {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();

        StringTokenizer st = new StringTokenizer(br.readLine());
        int e = Integer.parseInt(st.nextToken());
        int s = Integer.parseInt(st.nextToken());
        int m = Integer.parseInt(st.nextToken());

        int year = 0;

        while(true) {
            year++;  
            if ((year - e) % 15 == 0 &amp;&amp; (year - s) % 28 == 0 &amp;&amp; (year - m) % 19 == 0) {
                break;
            }
        }

        System.out.println(year);

    }
}
</code></pre>
<h2 id="스스로-피드백-해보기">스스로 피드백 해보기</h2>
<h4 id="구현력이-아직-매우-부족하다">&gt; 구현력이 아직 매우 부족하다.</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1476번 날짜계산 JAVA 풀이
]]></title>
            <link>https://velog.io/@do_dev/Temp-Title</link>
            <guid>https://velog.io/@do_dev/Temp-Title</guid>
            <pubDate>Mon, 09 Jun 2025 00:21:27 GMT</pubDate>
            <description><![CDATA[<h1 id="boj-1476--날짜-계산">BOJ 1476 | 날짜 계산</h1>
<ul>
<li><strong>문제 번호</strong>: 1476  </li>
<li><strong>푼 날짜</strong>: 2025-06-08  </li>
</ul>
<hr>
<h2 id="📌-문제-요약">📌 문제 요약</h2>
<p>각 주기가 있고 15, 28, 19..
각 주기에 값이 주어지면 공통 주기로 인해 몇년인지 구하는 문제</p>
<hr>
<h2 id="💡-접근-아이디어">💡 접근 아이디어</h2>
<ol>
<li>1 16 16 의 첫번째 테스트 케이스 분석</li>
<li>16년 일때 지구의 값이 1인데 16%15가 1임을 확인</li>
<li>각 주기의 나머지를 통해 년도를 구해본다. </li>
<li>주어진 주기 E,S,M 의 값의 나머지가 모두 0이면 해당 년도가 정답이다. </li>
</ol>
<hr>
<h2 id="🛠️-java-구현">🛠️ Java 구현</h2>
<pre><code class="language-java">package BaekJoon.basic;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Q1476 {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();

        StringTokenizer st = new StringTokenizer(br.readLine());
        int e = Integer.parseInt(st.nextToken());
        int s = Integer.parseInt(st.nextToken());
        int m = Integer.parseInt(st.nextToken());

        int year = 0;

        while(true) {
            year++;  
            if ((year - e) % 15 == 0 &amp;&amp; (year - s) % 28 == 0 &amp;&amp; (year - m) % 19 == 0) {
                break;
            }
        }

        System.out.println(year);

    }
}
</code></pre>
<h2 id="스스로-피드백-해보기">스스로 피드백 해보기</h2>
<h4 id="구현력이-아직-매우-부족하다">&gt; 구현력이 아직 매우 부족하다.</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 3085번 사탕게임 JAVA 풀이]]></title>
            <link>https://velog.io/@do_dev/%EB%B0%B1%EC%A4%80-%EA%B8%B0%EC%B4%88-%EC%8B%9C%EB%A6%AC%EC%A6%88</link>
            <guid>https://velog.io/@do_dev/%EB%B0%B1%EC%A4%80-%EA%B8%B0%EC%B4%88-%EC%8B%9C%EB%A6%AC%EC%A6%88</guid>
            <pubDate>Sun, 08 Jun 2025 07:18:07 GMT</pubDate>
            <description><![CDATA[<h2 id="다시-풀게된-문제-🎯">다시 풀게된 문제 🎯</h2>
<h1 id="boj-3085--사탕-게임-🍬">BOJ 3085 | 사탕 게임 🍬</h1>
<ul>
<li><strong>문제 번호</strong>: 3085  </li>
<li><strong>푼 날짜</strong>: 2025-06-08  </li>
</ul>
<hr>
<h2 id="📌-문제-요약">📌 문제 요약</h2>
<p>N×N 보드에 다양한 색 사탕이 있고,<br>인접한 사탕을 <strong>한 번 교환</strong>할 수 있다.<br>이때 <strong>가장 긴 연속 같은 색 사탕</strong> 길이를 구하는 문제.</p>
<hr>
<h2 id="💡-접근-아이디어">💡 접근 아이디어</h2>
<ol>
<li>보드의 모든 칸 <code>(i,j)</code>에 대해  </li>
<li>우측 <code>(i,j+1)</code> 또는 아래 <code>(i+1,j)</code>와 교환  </li>
<li>교환 후 현재 보드에서 <code>calculateMaxCandies()</code> 호출 → 최대 연속 길이 계산  </li>
<li>원상복구  </li>
<li>최댓값 갱신  </li>
</ol>
<p><code>calculateMaxCandies()</code>는 다음과 같이 구현:</p>
<ul>
<li>각 행과 각 열에 대해  <ul>
<li>인접 같은 컬러면 <code>count++</code>, 다른 컬러면 <code>최대값 갱신</code> 후 <code>count = 1</code> 재설정</li>
</ul>
</li>
</ul>
<hr>
<h2 id="🛠️-java-구현">🛠️ Java 구현</h2>
<pre><code class="language-java">package BaekJoon.basic;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Q3085 {

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();

        int maxLen = 0;

        int N = Integer.parseInt(br.readLine());

        char[][] board = new char[N][N];

        for (int i = 0; i &lt; N; i++) {
            board[i] = br.readLine().toCharArray();
        }

        for(int i=0; i&lt;N; i++) {
            for(int j=0; j&lt;N; j++){

                if(j &lt;N-1){
                    swap(board,i,j,i,j+1);
                    maxLen=Math.max(maxLen,calculateMaxLength(board,N));
                    swap(board,i,j,i,j+1);
                }

                if(i &lt; N-1){
                    swap(board,i,j, i+1,j);
                    maxLen = Math.max(maxLen,calculateMaxLength(board,N));
                    swap(board,i,j,i+1,j);
                }

            }
        }

        System.out.println(maxLen);

    }

    public static void swap(char[][] board, int x1, int y1 , int x2, int y2){
        char temp = board[x1][y1];
        board[x1][y1] = board[x2][y2];
        board[x2][y2] = temp;
    }

    public static int calculateMaxLength (char[][] board, int N) {
        int maxLen = 0;

        for(int i=0; i&lt; N ; i++){
            int countRow = 1;
            int countCol = 1;

            for(int j=1; j&lt;N; j++) {
                if (board[i][j] == board[i][j - 1]) {
                    countRow++;
                } else {
                    maxLen = Math.max(maxLen, countRow);
                    countRow = 1;
                }

                if (board[j][i] == board[j - 1][i]) {
                    countCol++;
                } else {
                    maxLen = Math.max(countCol, maxLen);
                    countCol = 1;
                }
            }

            maxLen= Math.max(maxLen,countRow);
            maxLen = Math.max(maxLen,countCol);
        }

        return maxLen

    }
}</code></pre>
<h2 id="스스로-피드백-해보기">스스로 피드백 해보기</h2>
<h4 id="구현력이-아직-매우-부족하다">&gt; 구현력이 아직 매우 부족하다.</h4>
<h4 id="문제를-제대로-읽지않는다">&gt; 문제를 제대로 읽지않는다.</h4>
<h4 id="정의-내리는것을-귀찮아하지말아라">&gt; 정의 내리는것을 귀찮아하지말아라.</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[이펙티브 자바 부수기 #1]]></title>
            <link>https://velog.io/@do_dev/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EB%B6%80%EC%88%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@do_dev/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EB%B6%80%EC%88%98%EA%B8%B0-1</guid>
            <pubDate>Mon, 28 Apr 2025 10:13:12 GMT</pubDate>
            <description><![CDATA[<h1 id="✨-effective-java-정리-아이템-단위">✨ Effective Java 정리 (아이템 단위)</h1>
<h2 id="📚-개요">📚 개요</h2>
<ul>
<li><strong>책 제목</strong>: Effective Java</li>
<li><strong>저자</strong>: Joshua Bloch</li>
<li><strong>공부 범위</strong>: (예: Item 1 ~ Item 5)</li>
<li><strong>목표</strong>: (예: &quot;자바 코드를 더 안전하고 깔끔하게 작성하는 방법을 학습&quot;)</li>
</ul>
<hr>
<h2 id="🗂️-학습한-아이템-요약">🗂️ 학습한 아이템 요약</h2>
<h3 id="✅-item-1-아이템-제목">✅ Item 1: (아이템 제목)</h3>
<ul>
<li><strong>핵심 내용 요약</strong></li>
<li><strong>왜 중요한가?</strong></li>
<li><strong>예시 코드</strong><pre><code class="language-java">// 예시 코드



</code></pre>
</li>
</ul>
<hr>
<p><strong>원하면</strong>  </p>
<ul>
<li>&quot;Item 1~5 양식 예시&quot;나  </li>
<li>&quot;Item별 블로그 꾸미기용 이모지 추천&quot;  </li>
<li>&quot;효율적인 블로그 연재 계획 짜는 방법&quot; 도 추가로 만들어줄게.</li>
</ul>
<p>추가로 받을래? ✨<br>(예: &quot;응, Item 1~5 양식도 보여줘!&quot; 이런 식으로 알려줘!)</p>
]]></description>
        </item>
    </channel>
</rss>