<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>devel-history.log</title>
        <link>https://velog.io/</link>
        <description>개발 공부 기록</description>
        <lastBuildDate>Wed, 04 Mar 2026 13:12:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>devel-history.log</title>
            <url>https://velog.velcdn.com/images/devel-history/profile/b197845e-9953-408d-a983-85bee51af29b/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. devel-history.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/devel-history" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[JAVA로 배우는 멀티스레딩과 비동기 처리]]></title>
            <link>https://velog.io/@devel-history/JAVA%EC%9D%98-%EB%A9%80%ED%8B%B0%EC%8A%A4%EB%A0%88%EB%94%A9%EA%B3%BC-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@devel-history/JAVA%EC%9D%98-%EB%A9%80%ED%8B%B0%EC%8A%A4%EB%A0%88%EB%94%A9%EA%B3%BC-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Wed, 04 Mar 2026 13:12:33 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<h2 id="멀티스레딩-비동기">멀티스레딩?, 비동기?</h2>
<h3 id="멀티스레딩">멀티스레딩</h3>
<p><img src="https://velog.velcdn.com/images/devel-history/post/4b815438-0052-411c-9a68-5a71b037f21e/image.png" alt="">
그림에 왼쪽은 싱글스레드, 오른쪽은 멀티스레드이다. </p>
<h3 id="비동기">비동기</h3>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://technote-mezza.tistory.com/69">https://technote-mezza.tistory.com/69</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[WAS 서버란?]]></title>
            <link>https://velog.io/@devel-history/WAS-%EC%84%9C%EB%B2%84%EB%9E%80</link>
            <guid>https://velog.io/@devel-history/WAS-%EC%84%9C%EB%B2%84%EB%9E%80</guid>
            <pubDate>Tue, 20 Jan 2026 10:32:15 GMT</pubDate>
            <description><![CDATA[<h2 id="was란"><strong>WAS란?</strong></h2>
<p><strong>WAS(Web Application Server)</strong>는 HTTP 프로토콜을 기반으로 사용자의 요청을 받아 <strong>서버에서 애플리케이션을 실행</strong>하고, 그 결과를 HTTP 응답으로 반환하는 <strong>미들웨어(Middleware)</strong>이다.</p>
<p>단순히 저장된 파일을 전달하는 것을 넘어, 요청에 따라 서버 내부 로직을 수행하고 데이터를 가공하여 <strong>동적 콘텐츠를 생성</strong>하며, 나아가 웹 애플리케이션이 안정적으로 실행될 수 있도록 <strong>실행 환경(Runtime Environment)</strong>을 제공하는 것이 핵심 역할이다.</p>
<blockquote>
<p>note)
실무적으로 WAS는 Java의 <strong>Tomcat</strong>처럼 코드가 실행될 수 있는 환경을 제공하며, DB 접근, 트랜잭션 관리, 스레드 관리 등 백엔드의 복잡한 책임을 대신 처리해 주는 서버를 의미한다.</p>
</blockquote>
<hr>
<h2 id="was-탄생-배경"><strong>WAS 탄생 배경</strong></h2>
<p>초기 웹 서버는 미리 만들어진 HTML 파일을 클라이언트에 전달하는 역할만 수행했다. 하지만 웹 서비스가 발전하면서 다음과 같은 한계에 직면했다.</p>
<ul>
<li><strong>동적 데이터의 필요성</strong>
로그인 사용자마다 다른 화면 제공, 실시간 데이터 반영</li>
<li><strong>비즈니스 로직 처리 요구</strong>
결제, 재고 관리, 권한 판단 같은 서버 측 연산 필요</li>
<li><strong>자원 활용의 효율성 문제</strong>
요청마다 프로세스를 생성하는 방식은 비용이 과도했음</li>
</ul>
<p>이러한 문제를 해결하기 위해 <strong>스레드 기반으로 요청을 처리하고</strong>, 애플리케이션 실행을 전담하는 서버인 WAS가 등장했다.</p>
<p>👉 이 구조 덕분에 서버는 다수의 요청을 효율적으로 처리하면서도 복잡한 비즈니스 로직을 안정적으로 실행할 수 있게 되었다.</p>
<hr>
<h2 id="was-vs-web-server"><strong>WAS vs Web Server</strong></h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>Web Server</th>
<th>WAS</th>
</tr>
</thead>
<tbody><tr>
<td><strong>주요 역할</strong></td>
<td>정적 리소스 전달</td>
<td>애플리케이션 실행 및 동적 콘텐츠 생성</td>
</tr>
<tr>
<td><strong>데이터 처리</strong></td>
<td>파일 그대로 전달</td>
<td>DB 조회 및 데이터 가공</td>
</tr>
<tr>
<td><strong>책임 범위</strong></td>
<td>HTTP 처리 집중</td>
<td>로직 실행 + 상태 관리</td>
</tr>
<tr>
<td><strong>대표 예시</strong></td>
<td>Nginx, Apache</td>
<td>Tomcat, Jetty, GlassFish</td>
</tr>
</tbody></table>
<blockquote>
<p>Tip)
현대 아키텍처에서는 보안과 트래픽 분산을 위해 Web Server를 앞단에 두고, WAS를 수평 확장하는 구조가 일반적으로 사용된다.</p>
</blockquote>
<hr>
<h2 id="was가-하는-일"><strong>WAS가 하는 일</strong></h2>
<ol>
<li><strong>프로그램 실행 환경 제공</strong>
Spring과 같은 프레임워크가 동작할 수 있는 서블릿 컨테이너 제공</li>
<li><strong>스레드 관리 (Thread Management)</strong>
Thread Pool을 통해 동시 요청을 안정적으로 처리</li>
<li><strong>DB 커넥션 풀(DBCP) 관리</strong>
DB 연결 비용을 줄이고 성능을 최적화</li>
<li><strong>트랜잭션 관리</strong>
작업 단위의 일관성 보장</li>
<li><strong>보안 및 세션 관리</strong>
인증·인가, HttpSession 기반 사용자 상태 관리</li>
</ol>
<p>👉 이 모든 기능은 개발자가 비즈니스 로직에만 집중할 수 있도록 돕기 위한 것이다.</p>
<h2 id="요청-흐름"><strong>요청 흐름</strong></h2>
<ol>
<li><strong>Web Server 수신:</strong> 클라이언트 요청이 들어오면 Web Server가 정적/동적 요청인지 판단해. </li>
<li><strong>WAS 위임 (Tomcat):</strong> 동적 요청일 경우 WAS(Tomcat)로 넘겨. 이때 Tomcat은 요청을 처리할 <strong>스레드</strong>를 할당 </li>
<li><strong>DispatcherServlet 동작:</strong> Spring의 총지배인인 <strong>DispatcherServlet</strong>이 요청을 받아 어떤 컨트롤러가 처리할지 결정해. </li>
<li><strong>비즈니스 로직 실행 (Service):</strong> 실제 핵심 로직이 담긴 Service 레이어에서 연산이 수행 </li>
<li><strong>DB 연동 (JPA/Repository):</strong> JPA를 통해 DB에서 데이터를 가져오거나 저장 </li>
<li><strong>응답 반환:</strong> 가공된 데이터를 다시 DispatcherServlet -&gt; Tomcat -&gt; Web Server를 거쳐 클라이언트에게 HTTP 응답으로 전달
<img src="https://velog.velcdn.com/images/devel-history/post/9e607192-11f6-4fb1-9954-d6e71ab860bd/image.png" alt=""></li>
</ol>
<p>이 과정에서 개발자는 비즈니스 로직에만 집중할 수 있고, 스레드 관리와 자원 관리는 WAS가 대신 책임진다.</p>
<hr>
<h2 id="was와-thread-per-request-model"><strong>WAS와 Thread Per Request Model</strong></h2>
<h3 id="thread-per-request-model이란">Thread Per Request Model이란?</h3>
<p><strong>Thread Per Request Model</strong>은 하나의 HTTP 요청을 하나의 스레드가 전담하여 처리하는 방식이다.</p>
<ul>
<li>요청 1개 → 스레드 1개 할당</li>
<li>요청 처리 완료 → 스레드 반환</li>
</ul>
<p>전통적인 WAS(Tomcat, Jetty)는 이 모델을 기본 동시성 처리 방식으로 사용한다.</p>
<hr>
<h3 id="was-내부-동작-흐름-tomcat-기준">WAS 내부 동작 흐름 (Tomcat 기준)</h3>
<ol>
<li>클라이언트 요청 수신</li>
<li>Web Server가 동적 요청을 WAS로 전달</li>
<li>Tomcat Connector가 Thread Pool에서 스레드 할당</li>
<li>DispatcherServlet 실행</li>
<li>Controller → Service → Repository 순으로 로직 수행</li>
<li>응답 생성 후 스레드 반환</li>
</ol>
<p>이 과정에서 <strong>요청의 시작부터 끝까지 동일한 스레드가 사용된다</strong>.</p>
<hr>
<h3 id="thread-per-request-model의-한계">Thread Per Request Model의 한계</h3>
<ul>
<li><strong>Blocking I/O 문제</strong>
DB나 외부 API 응답을 기다리는 동안 스레드는 대기 상태</li>
<li><strong>동시성 확장의 한계</strong>
동시 요청 수 증가 = 스레드 증가 → 메모리 및 컨텍스트 스위칭 비용 증가</li>
</ul>
<p>👉 트래픽이 급증하면 Thread Pool 고갈로 장애가 발생할 수 있다.</p>
<blockquote>
<h3 id="한계를-해결한-방법">한계를 해결한 방법</h3>
<table>
<thead>
<tr>
<th>기능</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>Thread Pool</td>
<td>스레드 수 제한 및 재사용</td>
</tr>
<tr>
<td>Queue</td>
<td>초과 요청 대기</td>
</tr>
<tr>
<td>Timeout</td>
<td>비정상 요청 종료</td>
</tr>
<tr>
<td>DBCP</td>
<td>DB 대기 시간 단축</td>
</tr>
<tr>
<td>트랜잭션 관리</td>
<td>스레드 단위 작업 일관성</td>
</tr>
</tbody></table>
</blockquote>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://mark-kim.blog/server_performance_tpr_thread_state/">https://mark-kim.blog/server_performance_tpr_thread_state/</a></li>
<li><a href="https://www.youtube.com/watch?v=NyhbNtOq0Bc">https://www.youtube.com/watch?v=NyhbNtOq0Bc</a></li>
<li><a href="https://helloworld-88.tistory.com/71">https://helloworld-88.tistory.com/71</a></li>
<li><a href="https://velog.io/@developerjun0615/WEB-WAS-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C">https://velog.io/@developerjun0615/WEB-WAS-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA Garbe Collector ]]></title>
            <link>https://velog.io/@devel-history/JAVA-Garbe-Collector</link>
            <guid>https://velog.io/@devel-history/JAVA-Garbe-Collector</guid>
            <pubDate>Thu, 18 Dec 2025 10:42:37 GMT</pubDate>
            <description><![CDATA[<h2 id="gc란">GC란?</h2>
<p><strong>Heap</strong> 영역에서 동적으로 할당된 객체 중 더 이상 참조되지 않는 메모리(garbage)를<br>JVM이 자동으로 탐지하여 제거하는 프로세스이다.</p>
<p>C/C++처럼 개발자가 직접 메모리를 해제하지 않아도 되므로<br>메모리 관리 부담을 줄일 수 있지만,<br>GC 역시 비용이 존재하기 때문에 동작 원리를 이해하는 것이 중요하다.</p>
<h2 id="gc-기본-동작-개념">GC 기본 동작 개념</h2>
<p>GC는 모든 객체를 무작위로 삭제하지 않는다.<br><strong>GC Root</strong>에서 시작해 참조 관계를 따라가며 “살아있는 객체”를 판단한다.</p>
<blockquote>
<p>※ 실제 GC 동작은 GC 알고리즘에 따라 다르며,<br>Young Generation은 <strong>Copy 방식</strong>,  
Old Generation은 <strong>Mark-Sweep(-Compact)</strong> 방식이 주로 사용된다.</p>
</blockquote>
<h2 id="gc-공통-동작-순서-개념적-흐름">GC 공통 동작 순서 (개념적 흐름)</h2>
<ol>
<li><p><strong>Stop-The-World (STW)</strong><br>GC를 수행하기 위해 GC 스레드를 제외한 모든 애플리케이션 스레드를 일시 중단한다.<br>STW 시간이 길어질수록 서비스 지연(Latency)이 증가한다.</p>
</li>
<li><p><strong>Mark</strong><br>GC Root(Stack의 지역 변수, Method Area의 static 변수, JNI 참조 등)에서 시작해<br>참조 가능한 객체를 탐색하고 Reachable로 표시한다.</p>
</li>
<li><p><strong>Sweep</strong><br>Mark되지 않은(Unreachable) 객체를 제거한다.<br>※ Young GC에서는 개별 Sweep 대신 Copy 방식이 사용된다.</p>
</li>
<li><p><strong>Compact (GC 알고리즘에 따라 수행)</strong><br>살아남은 객체를 한쪽으로 이동시켜 메모리 파편화(Fragmentation)를 제거하고<br>연속된 메모리 공간을 확보한다.</p>
</li>
</ol>
<h2 id="jvm-heap-메모리-영역">JVM Heap 메모리 영역</h2>
<p><img src="https://velog.velcdn.com/images/devel-history/post/eff174b1-8174-4e59-88f3-db6b5a9b283f/image.png" alt="JVM Heap 구조"></p>
<h3 id="new--young-generation">New / Young Generation</h3>
<p>새롭게 생성된 객체가 할당되며, 생명 주기가 짧은 객체가 주로 머무는 영역이다.</p>
<ul>
<li><p><strong>Eden</strong><br><code>new</code> 키워드로 생성된 객체가 최초로 할당되는 영역<br>→ Bump-the-pointer 방식으로 빠른 객체 할당이 가능하다.</p>
</li>
<li><p><strong>Survivor (From / To)</strong><br>Minor GC에서 살아남은 객체가 이동하는 영역<br>두 영역 중 하나는 항상 비어 있으며,<br>GC마다 살아남은 객체를 다른 쪽으로 복사(Copy)한다.</p>
</li>
<li><p><strong>관련 옵션</strong><br><code>-XX:NewSize</code>, <code>-XX:MaxNewSize</code></p>
</li>
<li><p><strong>성능 포인트</strong><br>Young 영역이 너무 작으면 객체가 빠르게 Old 영역으로 Promotion되어<br>Major GC 부하가 증가할 수 있다. (Premature Promotion)</p>
</li>
</ul>
<h3 id="old-generation-tenured-generation">Old Generation (Tenured Generation)</h3>
<p>Young Generation에서 여러 번의 GC를 거쳐 살아남은 <strong>장수 객체</strong>가 저장되는 영역이다.</p>
<ul>
<li><p><strong>Tenured</strong><br>Survivor 영역에서 age 임계값(기본 15)을 초과한 객체가 Promotion 되어 이동한다.</p>
</li>
<li><p><strong>특징</strong>  </p>
<ul>
<li>객체 생존율이 높음  </li>
<li>GC 비용이 큼  </li>
<li>STW 시간이 길어 성능에 직접적인 영향</li>
</ul>
</li>
<li><p><strong>관련 옵션</strong><br><code>-Xms</code>, <code>-Xmx</code> (Young 영역을 제외한 나머지)</p>
</li>
<li><p><strong>성능 포인트</strong><br>Old 영역이 가득 차면 <strong>Major GC 또는 Full GC</strong>가 발생한다.<br>Heap이 큰 환경일수록 STW 시간을 줄이는 것이 핵심 튜닝 포인트이다.</p>
</li>
</ul>
<h3 id="metaspace-native-area">Metaspace (Native Area)</h3>
<p>Java 8부터 도입된 영역으로, JVM이 로드한 클래스 메타데이터를 저장한다.</p>
<ul>
<li><p><strong>PermGen vs Metaspace</strong></p>
<ul>
<li>PermGen: Heap 영역 (Java 7 이하)</li>
<li>Metaspace: Native Memory 사용 (Java 8+)</li>
</ul>
</li>
<li><p><strong>특징</strong></p>
<ul>
<li>OS 메모리를 사용하여 PermGen 시절보다 OOM 발생 빈도 감소</li>
<li>무한하지 않으며 설정에 따라 <code>OutOfMemoryError: Metaspace</code> 발생 가능</li>
</ul>
</li>
<li><p><strong>관련 옵션</strong>
<code>-XX:MetaspaceSize</code>, <code>-XX:MaxMetaspaceSize</code></p>
</li>
</ul>
<hr>
<h2 id="자세한-gc-동작-순서">자세한 GC 동작 순서</h2>
<p>GC는 Heap 전체를 한 번에 정리하지 않고,<br><strong>Young Generation 중심의 Minor GC</strong>와  
<strong>Old Generation 중심의 Major / Full GC</strong>로 나뉘어 수행된다.</p>
<p>대부분의 GC는 Minor GC이며,<br>Major GC는 발생 빈도는 낮지만 비용이 크다.</p>
<p><img src="https://velog.velcdn.com/images/devel-history/post/ae709bdb-ab2d-46cc-a834-f1f2e77387c6/image.png" alt="GC 흐름"></p>
<h3 id="객체-생성">객체 생성</h3>
<p>애플리케이션에서 객체를 생성하면 Eden 영역에 할당된다.<br>Eden은 연속된 메모리 구조를 사용하여 빠른 할당이 가능하다.</p>
<p>→ Eden이 가득 차면 <strong>Minor GC</strong>가 트리거된다.</p>
<h3 id="minor-gc-young-generation-gc">Minor GC (Young Generation GC)</h3>
<p>Young Generation(Eden + Survivor)만을 대상으로 수행된다.</p>
<blockquote>
<h4 id="📌-minor-gc-특징">📌 Minor GC 특징</h4>
</blockquote>
<ol>
<li>자주 발생</li>
<li>Copying 기반 → 메모리 파편화 없음</li>
<li>Major GC보다 STW 시간이 짧음</li>
</ol>
<h4 id="1-stop-the-world-stw">1. Stop-The-World (STW)</h4>
<ul>
<li>모든 애플리케이션 스레드 일시 정지</li>
<li>Young 영역만 처리하므로 STW 시간이 비교적 짧다</li>
</ul>
<h4 id="2-mark-reachability-분석">2. Mark (Reachability 분석)</h4>
<ul>
<li>GC Root에서 시작해 Eden과 Survivor 영역의 객체 중</li>
<li>참조 중인 객체만 Reachable로 표시</li>
</ul>
<h4 id="3-copy-young-gc의-핵심">3. Copy (Young GC의 핵심)</h4>
<ul>
<li>Reachable 객체를 Eden → Survivor(To)로 복사</li>
<li>From 영역의 살아있는 객체도 함께 복사</li>
<li>이 과정에서 객체 age 증가</li>
<li>Eden과 From 영역은 통째로 비워진다</li>
</ul>
<h4 id="4-promotion">4. Promotion</h4>
<ul>
<li>age 임계값을 초과한 객체는 Old Generation으로 이동</li>
<li>Survivor 공간이 부족해도 강제로 Promotion 발생 가능</li>
<li>Old 영역에 공간이 없으면 Promotion Failure → OOM 발생 가능</li>
</ul>
<p>→ Premature Promotion은 Major GC를 앞당기는 주요 원인이다.</p>
<h3 id="major-gc--full-gc-old-generation-gc">Major GC / Full GC (Old Generation GC)</h3>
<p>Old Generation을 대상으로 수행되며,<br>상황에 따라 Heap 전체를 정리하는 Full GC로 확장될 수 있다.</p>
<blockquote>
<p>📒 실무에서는 Major GC와 Full GC를 혼용해 부르기도 하지만, 엄밀히는 Full GC는 Heap 전체(Young + Old + Metaspace)를 대상으로 한다.</p>
</blockquote>
<blockquote>
<h4 id="📌-major--full-gc-특징">📌 Major / Full GC 특징</h4>
</blockquote>
<ul>
<li>발생 빈도 낮음</li>
<li>STW 시간이 길어 서비스 지연 유발</li>
<li>GC 튜닝의 핵심 대상</li>
<li>CMS, G1, ZGC, Shenandoah 등은 STW 최소화를 목표로 설계됨</li>
</ul>
<h4 id="1-stop-the-world-stw-1">1. Stop-The-World (STW)</h4>
<ul>
<li>Minor GC보다 훨씬 긴 STW 발생</li>
</ul>
<h4 id="2-mark">2. Mark</h4>
<ul>
<li>GC Root부터 Heap 전체를 스캔</li>
<li>살아있는 객체를 Reachable로 표시</li>
</ul>
<h4 id="3-sweep">3. Sweep</h4>
<ul>
<li>Unreachable 객체 제거</li>
</ul>
<h4 id="4-compact-알고리즘에-따라">4. Compact (알고리즘에 따라)</h4>
<ul>
<li>살아남은 객체를 한쪽으로 밀어 메모리 파편화 제거</li>
<li>연속된 메모리 공간 확보</li>
</ul>
<h3 id="gc-종료-후">GC 종료 후</h3>
<ul>
<li>STW 해제 후 애플리케이션 재개</li>
<li>GC가 잦거나 Major GC 시간이 길어질 경우</li>
</ul>
<p>→ <strong>Latency 증가, Throughput 감소</strong>로 이어진다.</p>
<h2 id="정리">정리</h2>
<p>GC는 자동이지만 공짜는 아니다.<br>객체 생명 주기를 잘못 설계하면 GC 비용은 그대로 성능 문제로 드러난다.</p>
<p><strong>GC 튜닝의 핵심은</strong></p>
<ul>
<li>Minor GC를 빠르게,</li>
<li>Major GC를 최대한 늦게,</li>
<li>STW 시간을 최소화하는 것이다.</li>
</ul>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98GC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC">https://inpa.tistory.com/entry/JAVA-☕-가비지-컬렉션GC-동작-원리-알고리즘-💯-총정리</a></li>
<li><a href="https://velog.io/@yarogono/Java%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%ED%84%B0Garbage-Collector%EB%9E%80#%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%ED%84%B0%EC%99%80-%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98%EC%9D%98-%EC%B0%A8%EC%9D%B4%EB%8A%94">[Java]가비지 컬렉터(Garbage Collector)란?</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java Spring boot에 BDD 적용 with Cucumber]]></title>
            <link>https://velog.io/@devel-history/Java-Spring-boot%EC%97%90-BDD-%EC%A0%81%EC%9A%A9-with-Cucumber</link>
            <guid>https://velog.io/@devel-history/Java-Spring-boot%EC%97%90-BDD-%EC%A0%81%EC%9A%A9-with-Cucumber</guid>
            <pubDate>Thu, 04 Dec 2025 10:23:06 GMT</pubDate>
            <description><![CDATA[<h2 id="bdd">BDD</h2>
<p>TDD에서 파생됨, TDD는 리소스가 많이 들어서 개발 시간 지연이라는 문제를 발생 시킴 그렇기에 BDD로 행동에 대한 중점 테스트를 하기 시작함, 하지만 TDD와 BDD는 상호보완 관계 테스트하는 영역이 다르기에 더 안전한 서비스를 만들 수 있음 즉 <strong>대화를 통해 요구사항을 예제로 명확 할 수 있다</strong></p>
<h3 id="사용이점">사용이점</h3>
<p>기능 명세서, 테스트 케이스, 매뉴얼 등등 여러 문서를 통합해서 작성 가능하고  &quot;비개발자&quot;뿐 아닌 &quot;개발자&quot; 모두 확인하면서 살아있는 문서를 만들 수 있다.
특히 <strong>Single Source of Truth</strong>를 유지하는 것에 도움을 준다. </p>
<h3 id="bdd-사용-순서">BDD 사용 순서</h3>
<p><strong>1.명세 작성</strong>
PO, 기획자, 개발자 등이 모여 Gherkin 문법으로 .feature 파일을 작성하고 모두 동의를 해야한다.(이 시점에서 아직 구현 코드도 작성 안 함) 
 -&gt; <strong>Ex)</strong> Given 계산기에 2와 3을 입력하고...</p>
<p><strong>2.Step Definition 자동화</strong>
개발자는 합의된 .feature 파일을 가져와 <strong>Step Definition (Java 테스트 코드)</strong>을 작성한다. (이때, 테스트를 실행하면 Step Definition 코드는 실제 서비스 로직을 찾지 못해 실패(Red) 상태가 되기에 TDD의 Red 단계와 유사)</p>
<p><strong>3. 기능 구현</strong></p>
<p>개발자는 이 실패한 Step Definition(테스트 코드)을 통과시키기 위해 <strong>최소한의 서비스 로직(생산 코드)</strong>을 작성합니다.</p>
<h3 id="테스트-코드를-나중에-작성해도-될까">테스트 코드를 나중에 작성해도 될까?</h3>
<p>결론은 그러면 안 된다. BDD는 통합 테스트로 이루어질 확률이 높다. 그렇기에 초기에 구상한 테스트 코드에 의존성이 추가되거나 변경되는 것은 당연하다. BDD의  <strong>테스트 코드</strong> 비즈니스 로직이 아닌 <strong>기술적인 구현 세부사항</strong>에 의존해야 하기에 변경은 상관없다. 테스트 코드 변경을 감수하고 비즈니스 코드를 작성하면 된다. </p>
<hr>
<h2 id="cucumber">Cucumber</h2>
<ul>
<li>spring boot에서 사용할 프레임 워크, .feature 파일 작성 하여 </li>
<li>jira 티켓에 [given, when, then]을 작성 -&gt; 개발자와 비개발자 모두 확인할 수 있도록 (Gherkin 문법으로 작성 필수)</li>
</ul>
<hr>
<h2 id="프로젝트-적용-예시">프로젝트 적용 예시</h2>
<p>주어진 <code>.feature</code> 시나리오를 바탕으로 <strong>Java Step Definition</strong> 코드를 작성하겠다. 
이 과정은 <strong>Gherkin 문장</strong>을 <strong>실제 Java 클래스의 메소드</strong>에 연결(Glue)하고, 그 메소드 안에서 애플리케이션의 핵심 로직을 호출하는 방식으로 진된다. </p>
<h4 id="feature-더하기-케이스">.feature 더하기 케이스</h4>
<pre><code class="language-json">Feature: 계산기 기능

  Scenario: 두 수를 더한다
    Given 계산기에 2와 3을 입력하고
    When 더하기를 수행하면
    Then 결과는 5가 되어야 한다</code></pre>
<h4 id="더하기-케이스가-n개-예시">더하기 케이스가 n개 예시</h4>
<pre><code class="language-.md">Feature: 계산기 기능

  Scenario Outline: 여러 수의 덧셈 검증
    Given 계산기에 &lt;num1&gt;과 &lt;num2&gt;를 입력하고
    When 더하기를 수행하면
    Then 결과는 &lt;result&gt;가 되어야 한다

    Examples:
      | num1 | num2 | result |
      | 2    | 3    | 5      |
      | 10   | 20   | 30     |
      | -1   | 5    | 4      |</code></pre>
<hr>
<h3 id="feature를-위한-java-step-definition"><code>.feature</code>를 위한 Java Step Definition</h3>
<p><strong>1단계: 프로젝트 환경 준비 (Calculator.java)</strong></p>
<p>테스트할 핵심 로직을 가진 간단한 <code>Calculator</code> 클래스를 준비합니다. 이 클래스는 Spring Bean으로 간주하고 테스트 환경에서 주입받아 사용합니다.</p>
<pre><code class="language-java">// src/main/java/com/example/app/Calculator.java

package com.example.app;

// 실제 애플리케이션의 핵심 로직
public class Calculator {
    private int result;

    public void input(int a, int b) {
        // 실제 입력 처리 로직 (필요하다면)
    }

    public void add(int a, int b) {
        this.result = a + b;
    }

    public int getResult() {
        return this.result;
    }
}</code></pre>
<p><strong>2단계: Step Definition 클래스 작성 (CalculatorSteps.java)</strong></p>
<p><code>.feature</code> 파일의 Gherkin 문장들을 처리할 Java 클래스를 작성한다. 보통 <code>src/test/java/com/example/steps</code>와 같은 패키지에 위치하며, Spring 환경을 사용하기 위한 설정이 포함된다.</p>
<pre><code class="language-java">// src/test/java/com/example/steps/CalculatorSteps.java

package com.example.steps;

import com.example.app.Calculator;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import org.springframework.beans.factory.annotation.Autowired;
import static org.junit.jupiter.api.Assertions.assertEquals;

// 1. SpringBootTest 어노테이션을 사용하여 Spring 컨텍스트를 로드합니다.
//    (실제 Spring Boot 환경에서는 별도의 @ContextConfiguration 설정이 필요할 수 있습니다.)

public class CalculatorSteps {

    // 2. 테스트 대상인 Calculator Bean을 주입받기 위한 필드
    private Calculator calculator = new Calculator(); // 간단 예시를 위해 직접 인스턴스화

    // 테스트에 사용할 임시 변수
    private int number1;
    private int number2;
    private int actualResult;

    // =========================================================================
    // 3. GIVEN 절 구현: Given 계산기에 2와 3을 입력하고
    // =========================================================================
    // 정규 표현식: ^...$는 문장의 시작과 끝을 의미합니다. (\\d+)는 숫자를 캡처합니다.
    @Given(&quot;계산기에 {int}와 {int}을 입력하고&quot;)
    public void 계산기에_두_수를_입력하고(int a, int b) {
        // Cucumber가 feature 파일에서 &#39;2&#39;와 &#39;3&#39;을 추출하여 a와 b 인수로 전달합니다.
        this.number1 = a;
        this.number2 = b;
        // 입력 처리가 있다면 여기서 로직을 호출합니다.
    }

    // =========================================================================
    // 4. WHEN 절 구현: When 더하기를 수행하면
    // =========================================================================
    @When(&quot;더하기를 수행하면&quot;)
    public void 더하기를_수행하면() {
        // 실제 애플리케이션의 핵심 로직을 호출합니다.
        calculator.add(this.number1, this.number2);
        this.actualResult = calculator.getResult(); // 결과를 저장합니다.
    }

    // =========================================================================
    // 5. THEN 절 구현: Then 결과는 5가 되어야 한다
    // =========================================================================
    @Then(&quot;결과는 {int}가 되어야 한다&quot;)
    public void 결과는_기대값과_같아야_한다(int expectedResult) {
        // Cucumber가 feature 파일에서 &#39;5&#39;를 추출하여 expectedResult 인수로 전달합니다.

        // JUnit의 Assertion을 사용하여 실제 결과와 기대 결과를 비교하여 검증합니다.
        assertEquals(expectedResult, this.actualResult, &quot;두 수의 덧셈 결과가 기대값과 다릅니다.&quot;);
    }
}</code></pre>
<p><strong>* 3단계: 테스트 러너 실행*</strong></p>
<p><code>CucumberTestRunner</code> 실행시 Cucumber 동작 순서</p>
<ol>
<li><code>.feature</code> 파일의 <code>Given 계산기에 2와 3을 입력하고</code> 문장을 읽습니다.</li>
<li><code>CalculatorSteps.java</code>의 <code>@Given(&quot;계산기에 {int}와 {int}을 입력하고&quot;)</code> 메소드를 찾습니다.</li>
<li><code>2</code>와 <code>3</code>을 추출하여 <code>number1</code>과 <code>number2</code> 변수에 저장합니다.</li>
<li><code>When 더하기를 수행하면</code> 문장을 읽고, <code>@When(&quot;더하기를 수행하면&quot;)</code> 메소드를 실행합니다.</li>
<li><code>calculator.add(2, 3)</code>을 호출하고, 결과 <code>5</code>를 <code>actualResult</code>에 저장합니다.</li>
<li><code>Then 결과는 5가 되어야 한다</code> 문장을 읽고, <code>@Then(&quot;결과는 {int}가 되어야 한다&quot;)</code> 메소드를 실행합니다.</li>
<li><code>assertEquals(5, actualResult)</code>를 실행하여 테스트를 성공으로 처리합니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[DDD를 프로젝트에 적용하려면?]]></title>
            <link>https://velog.io/@devel-history/DDD%EB%A5%BC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%98%EB%A0%A4%EB%A9%B4</link>
            <guid>https://velog.io/@devel-history/DDD%EB%A5%BC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%98%EB%A0%A4%EB%A9%B4</guid>
            <pubDate>Wed, 03 Dec 2025 09:20:10 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p>프로젝트에서 DDD 방법론을 사용하자는 의견이 나왔다. 저번 프로젝트에서도 사용을 했고 이벤트 스토밍으로 유비쿼터스 언어를 정할 수 있어서 좋다고 생각했는데 이번에는 좀 더 본격적으로 사용하면 좋을거라고 생각으로 조사한 내용을 정리하려고 한다. </p>
<h2 id="ddd란">DDD란?</h2>
<p><strong>DDD</strong>란 domain drvien design으로 복잡한 비즈니스 문제 영역(도메인)을 소프트웨어 설계의 중심에 두는 개발 방법론으로, 소프트웨어 구현 방법론 중 1개이며 애자일 + DDD 또는 워터풀 + DDD로 사용 가능하다. </p>
<h3 id="ddd-특징">DDD 특징</h3>
<ul>
<li><strong>도메인 로직에 집중</strong>: 데이터베이스 중심의 설계가 아닌, 순수한 비즈니스 규칙과 로직에 초점 맞춤</li>
<li><strong>유비쿼터스 언어</strong>: 도메인 전문가와 개발자가 분석, 설계, 그리고 코드에 이르기까지 동일한 표현과 단어로 구성된 통일된 언어 체계 구축</li>
<li><strong>모델과 코드의 일치</strong>: 소프트웨어의 <strong>엔티티</strong>와 도메인 개념을 가능한 한 가깝게 일치시켜, 도메인에서 발생하는 변화를 코드에 쉽게 반영</li>
</ul>
<hr>
<h2 id="📝-ddd-적용-1차-도메인-분석과-전략적-설계">📝 DDD 적용 1차: 도메인 분석과 전략적 설계</h2>
<p>1차 과정은 도메인을 깊이 이해하고, 그 이해를 소프트웨어 모델로 풀어내는 반복적인 설계·구현 과정을 진행한다. 아래 단계들은 순서가 아닌 여러 번 왕복하며 모델을 다듬어 가는 <strong>순환적인 활동(Continuous Refinement)</strong>이다. (1차 과정은 팀원 모두가 같이 하여 도메인에 대해 자세히 아는 것이 중ㅇ요하다) </p>
<h3 id="1-이벤트-스토밍-워크숍으로-도메인-탐색">1. 이벤트 스토밍 워크숍으로 도메인 탐색</h3>
<p>도메인 전문가와 개발자가 한자리에 모여, 비즈니스 흐름에서 발생하는 중요한 사건들을 <strong>&quot;도메인 이벤트&quot;</strong> 형태로 시간 순서대로 나열한다. 이를 통해 복잡한 비즈니스 프로세스를 빠르게 시각화하고, 팀이 공유하는 <strong>유비쿼터스 언어</strong>의 기초를 만듭니다. 
<a href="https://www.youtube.com/watch?v=gihxS6eE1DM">이벤트 스토밍 진행 방법</a></p>
<h3 id="2-전략적-설계-경계-컨텍스트와-컨텍스트-매핑">2. 전략적 설계: 경계 컨텍스트와 컨텍스트 매핑</h3>
<p>이벤트 스토밍에서 드러난 이벤트 흐름과 용어의 의미 변화 지점을 기준으로, 도메인을 여러 개의 <strong>경계 컨텍스트(Bounded Context)</strong>로 나눕니다. 각 컨텍스트 간의 통합 방식과 의존 관계는 <strong>컨텍스트 매핑(Context Mapping)</strong>으로 표현하한다.  전체 시스템의 <strong>협력 구조</strong>를 정의하는 단계</p>
<h3 id="3-전술적-설계-1-애그리거트와-도메인-모델링">3. 전술적 설계 1: 애그리거트와 도메인 모델링</h3>
<p>각 경계 컨텍스트 내부에서 비즈니스 규칙과 트랜잭션 일관성을 함께 묶어야 하는 범위를 중심으로 <strong>애그리거트(Aggregate)</strong>를 선정한다. <strong>애그리거트 루트, 엔티티, 값 객체</strong>와 같은 <strong>전술적 패턴</strong>을 사용해 모델을 구체화하고, 애그리거트가 경계 컨텍스트 내의 <strong>일관성 규칙</strong>을 잘 표현하도록 정제한다. </p>
<h3 id="4-전술적-설계-2-유스케이스-기반-행위-정의">4. 전술적 설계 2: 유스케이스 기반 행위 정의</h3>
<p>유스케이스와 사용자 스토리를 분석해 사용자가 시스템에서 수행하는 작업(Command)과 그에 따른 도메인 동작을 정리합니다. 이를 바탕으로 애그리거트와 도메인 서비스의 메서드, 응용 서비스의 흐름을 정의하며, 필요 시 앞 단계(애그리거트 설계나 경계 컨텍스트 설계)로 돌아가 모델을 반복적으로 개선한다.</p>
<hr>
<h2 id="🛠️-ddd-적용-2차-구현-설계와-개발">🛠️ DDD 적용 2차: 구현 설계와 개발</h2>
<p>1차에서 도메인 모델과 경계 컨텍스트를 정리했다면, 2차에서는 이를 실제 시스템 구조와 코드로 옮겨가는 작업을 진행한다. 이 단계 역시 구현을 하면서 다시 모델을 수정하는 순환 구조를 가진다. (2차부터는 개발자들이 모여서 의논하는 것이 핵심이다.) </p>
<h3 id="1-도메인-모델을-담을-아키텍처-선택">1. 도메인 모델을 담을 아키텍처 선택</h3>
<p>도메인 계층을 기술 세부사항(웹, DB 등)으로부터 분리할 수 있는 [<strong>레이어드</strong>,<strong>헥사고날</strong>, <strong>클린 아키텍처</strong> 등등] 패턴을 선택한다. (필요 시 CQRS나 이벤트 소싱을 고려 가능하다)</p>
<ul>
<li><strong>구조 명확화:</strong> &quot;도메인 레이어에 애그리거트·엔티티·값 객체·도메인 서비스가 위치하고, 응용 레이어/어댑터 레이어가 이를 이용한다&quot;는 구조를 명확</li>
</ul>
<h3 id="2-테스트-전략-및-개발-방식-정의-필수x">2. 테스트 전략 및 개발 방식 정의 (필수X)</h3>
<p>도메인 모델을 신뢰할 수 있게 유지하기 위해 어떤 테스트 전략을 쓸지 결정해야한다. <strong>유비쿼터스 언어</strong>를 그대로 사용하는 테스트 코드 작성을 목표로 합니다.</p>
<ul>
<li><strong>테스트 대상:</strong> 도메인 모델을 대상으로 하는 <strong>단위 테스트</strong>, 유스케이스 단위의 <strong>통합 테스트</strong>, 경계 컨텍스트 간 협력을 검증하는 <strong>E2E 테스트</strong> 등을 정의한다.</li>
<li><strong>개발 방식:</strong> 팀 상황에 따라 <strong>TDD, ATDD, BDD</strong> 등 적용 여부와 범위를 정한다. </li>
</ul>
<h3 id="3-구현-및-반복적-리팩터링">3. 구현 및 반복적 리팩터링</h3>
<p>1차에서 정의한 경계 컨텍스트·애그리거트·유스케이스를 기준으로 응용 서비스, 도메인 모델, 인프라스트럭처 등등을 구현합니다. </p>
<ul>
<li>구현 과정에서 발견되는 모델의 부족함이나 불편함은 다시 1차 설계 단계로 피드백하여, <strong>모델과 코드가 함께 발전(Continuous Refinement)</strong>하도록 반복적으로 <strong>리팩터링</strong>한다. </li>
</ul>
<h2 id="결론">결론</h2>
<p>DDD와 함께 사용할 수 있는 방법론이 있다고 생각하지 못 했다. DDD가 방법론이라서 다른 것을 사용 안 하다고 여겼는데 조사를 통해서 다른 것임을 확인하고 어떻게 하면 DDD에서 추구하는 바를 프로젝트 전반에 녹여 낼 수 있을까 고민하게 되어서 조사하는 시간이 즐겁고 재미있었다. </p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://www.msaschool.io/operation/design/design-two/">https://www.msaschool.io/operation/design/design-two/</a></li>
<li><a href="https://tech.kakaopay.com/post/backend-domain-driven-design/#step2---domain-model--function">https://tech.kakaopay.com/post/backend-domain-driven-design/#step2---domain-model--function</a></li>
<li><a href="https://ksh-coding.tistory.com/103">https://ksh-coding.tistory.com/103</a></li>
<li><a href="https://www.linkedin.com/pulse/strategic-vs-tactical-domain-driven-design-ddd-vintageglobal-lisbe/">https://www.linkedin.com/pulse/strategic-vs-tactical-domain-driven-design-ddd-vintageglobal-lisbe/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[비관적 락 -> 낙관적 락으로 변경 ]]></title>
            <link>https://velog.io/@devel-history/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD%EC%9C%BC%EB%A1%9C-%EB%AA%BB-%ED%95%B4%EC%9A%94</link>
            <guid>https://velog.io/@devel-history/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD%EC%9C%BC%EB%A1%9C-%EB%AA%BB-%ED%95%B4%EC%9A%94</guid>
            <pubDate>Mon, 01 Dec 2025 09:13:15 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p>부트캠프에서 진행한 스타벅스 클론에서 동시성 이슈를 비관적 락으로 해결하였다. 그런데 낙관적 락으로 해결을 왜 안 했냐는 질문을 받았고 주문에 대한 내용은 중요한 정보이기에 비관적 락으로 해결했다라는 나의 답은 근거가 없는 거라고 답변을 들었기에 2개를 비교하는 실험을 진행하려고 한다. </p>
<h3 id="이전-프로젝트-내용-비-관적락-사용">이전 프로젝트 내용 (비 관적락 사용)</h3>
<ul>
<li><p><a href="https://velog.io/@devel-history/GOORM-DEEP-DIVE-%EB%B0%B1%EC%97%94%EB%93%9C-3%ED%9A%8C%EC%B0%A8%EC%8A%A4%ED%83%80%EB%B2%85%EC%8A%A4-%ED%81%B4%EB%A1%A0-%EC%A3%BC%EB%AC%B8-%EB%B2%88%ED%98%B8-%EA%B5%AC%ED%98%84-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%98%A4%EB%A5%98-%EC%A0%95%EB%A6%AC">주문 번호 구현, 동시성 오류 정리
</a></p>
</li>
<li><p><a href="https://velog.io/@devel-history/GOORM-DEEP-DIVE-%EB%B0%B1%EC%97%94%EB%93%9C-3%ED%9A%8C%EC%B0%A8%EC%8A%A4%ED%83%80%EB%B2%85%EC%8A%A4-%ED%81%B4%EB%A1%A0-%EC%A3%BC%EB%AC%B8-%EB%B2%88%ED%98%B8-%EA%B5%AC%ED%98%84-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%98%A4%EB%A5%98-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BDPessimistic-Lock%EC%9C%BC%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0">주문 번호 구현, 동시성 오류 비관적 락(Pessimistic Lock)으로 해결하기
</a></p>
<h2 id="테스트-환경">테스트 환경</h2>
</li>
<li><p><strong>API</strong>: <code>POST /api/v1/orders</code> (주문 생성)</p>
</li>
<li><p><strong>동시성 제어 대상</strong>: <code>OrderDailyCounter</code> (매장별 일일 주문번호 카운터)</p>
</li>
<li><p><strong>테스트 도구</strong>: k6 Testing</p>
</li>
<li><p><strong>테스트 기간</strong>: 1분, 100 RPS</p>
</li>
<li><p><strong>github repo</strong>: <a href="https://github.com/Cori1304-Seong/starbucks-backend-v2">https://github.com/Cori1304-Seong/starbucks-backend-v2</a></p>
</li>
<li><p>** 낙관적락 적용 PR**: <a href="https://github.com/Cori1304-Seong/starbucks-backend-v2/pull/1">https://github.com/Cori1304-Seong/starbucks-backend-v2/pull/1</a></p>
<h2 id="낙-관적락-1차-테스트">낙 관적락 1차 테스트</h2>
<p>1분 동안 100 RPS를 발생 시켰다. 당연히 실패율 0%에 비관적락 이상에 결과를 보여 줄 주 알았는데 아래 표와 같이 이상한 결과를 얻었다. </p>
</li>
</ul>
<h3 id="성능-비교표">성능 비교표</h3>
<table>
<thead>
<tr>
<th>락 방식</th>
<th>부하 (req/s)</th>
<th>총 요청수</th>
<th>실패율</th>
<th>평균 응답시간</th>
<th>최대 응답시간</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td><strong>비관적 락</strong></td>
<td>100</td>
<td>6000</td>
<td><strong>0%</strong></td>
<td>~15ms</td>
<td>~50ms</td>
<td>✅ <strong>성공</strong></td>
</tr>
<tr>
<td><strong>낙관적 락</strong></td>
<td>30</td>
<td>1800</td>
<td><strong>0.8-2.4%</strong></td>
<td>~15-18ms</td>
<td>~487ms</td>
<td>⚠️ 불안정</td>
</tr>
<tr>
<td><strong>낙관적 락</strong></td>
<td>100</td>
<td>121</td>
<td><strong>99.2%</strong></td>
<td>39,623ms</td>
<td>60,003ms</td>
<td>❌ <strong>실패</strong></td>
</tr>
</tbody></table>
<h2 id="1차-테스트-원인-분석">1차 테스트 원인 분석</h2>
<p><strong>원인</strong>: 부하 테스트시 1개에 매장에만 요청을 보내는 것이 문제였다.</p>
<h3 id="상세-설명">상세 설명</h3>
<p>기존 테스트는 모든 주문이 1개에 매장으로 향한다. 이 경우 동시성 이슈가 있던 <strong>주문번호 생성 로직</strong>은 100% 충돌이 발생한다. 매장은 주문번호 생성을 위해서 주문이 들어오면 counter +1을 한 값을 주문번호로 만든다. 이때 동시에 요청이 들어오면 충돌이 발생했고 비관적 락은 충돌을 일어나는 것을 전제로 만든 로직이었고 낙관적 락은 그렇지 않았던게 문제 였다.<br>(낙관적 락은 충돌이 일어나지 않고 서로 다른 레코드에 적용해야 하는데 테스트 환경이 그렇지 못한 것이었다.) 
<img src="https://velog.velcdn.com/images/devel-history/post/51d9ab8c-0a34-4cbc-9c8a-8a7295078222/image.png" alt=""></p>
<h3 id="시퀀스-다이어그램">시퀀스 다이어그램</h3>
<p><img src="https://velog.velcdn.com/images/devel-history/post/352ee1f4-d5d7-4d04-b667-a2a6e8b0e005/image.png" alt=""></p>
<h3 id="k6-코드-변경">k6 코드 변경</h3>
<pre><code class="language-js">// 문제 코드
function makeOrderPayload(email, idx) {
  return JSON.stringify({
    storeId: 1, -&gt; 원인 
    pickupType: &quot;STORE_PICKUP&quot;,
    requestMemo: `주문자: ${email} - 얼음 많이, 샷 연하게 부탁드려요!`,
    cardNumber: `1234-5678-9${String(idx).padStart(3, &quot;0&quot;)}-0000`,
    orderItems: [
      {
     //... 


// 수정 코드, storeId를 고정이 아닌 1~10인 랜덤 값으로 설정 (DB에 매장은 10개만 존재) 
function makeOrderPayload(email, idx) {
  const randomStoreId = Math.floor(Math.random() * 10) + 1; // 1~10 사이의 랜덤 storeId
  return JSON.stringify({
    storeId: randomStoreId,
    pickupType: &quot;STORE_PICKUP&quot;,
    requestMemo: `주문자: ${email} - 얼음 많이, 샷 연하게 부탁드려요!`,
    cardNumber: `1234-5678-9${String(idx).padStart(3, &quot;0&quot;)}-0000`,
    orderItems: [
      {
</code></pre>
<h3 id="원인-요약">원인 요약</h3>
<ul>
<li>spring boot에 문제 X, k6 부하 테스트 시나리오 문제 </li>
<li>기존은 1개 매장에 1분 동안 100RPS, 현재는 10개에 매장에 1분 동안 100RPS</li>
<li>동일안 레코드에서 충돌 발생, 낙관적 락으로 해결할 수 없었기에 결과가 이상한게 아닌 테스트 시나리오가 문제</li>
</ul>
<h2 id="낙-관적락-2차-테스트">낙 관적락 2차 테스트</h2>
<p>k6 코드 변경 후 드디어 원하던 결과를 얻었다. </p>
<h3 id="성능-비교표-1">성능 비교표</h3>
<table>
<thead>
<tr>
<th>락 방식</th>
<th>부하 (req/s)</th>
<th>총 요청수</th>
<th>실패율</th>
<th>평균 응답시간</th>
<th>최대 응답시간</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td><strong>비관적 락</strong></td>
<td>100</td>
<td>6000</td>
<td><strong>0%</strong></td>
<td>~15ms</td>
<td>~50ms</td>
<td>✅ <strong>성공</strong></td>
</tr>
<tr>
<td><strong>낙관적 락</strong></td>
<td>100</td>
<td>6000</td>
<td><strong>0%</strong></td>
<td>~11ms</td>
<td>107.63ms</td>
<td>✅ <strong>성공</strong></td>
</tr>
</tbody></table>
<h2 id="결론">결론</h2>
<p>처음 시도에서는 많이 당황했다. java 코드 문제라고 생각해서 AI에게 계속 물어도 해결지 않았던 문제였지만 낙관적 락을 다시 공부하면서 문제를 해결하였다. 이래서 CS 지식 즉 이론이 중요하다고 생각하게 되었다. </p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://ksh-coding.tistory.com/125#2.%20Base%20Code%EC%9D%98%20%EB%AC%B8%EC%A0%9C%EC%A0%90%20(%EB%8F%99%EC%8B%9C%EC%84%B1)-1">https://ksh-coding.tistory.com/125#2.%20Base%20Code%EC%9D%98%20%EB%AC%B8%EC%A0%9C%EC%A0%90%20(%EB%8F%99%EC%8B%9C%EC%84%B1)-1</a></li>
<li><a href="https://chaewsscode.tistory.com/242">https://chaewsscode.tistory.com/242</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[DDD 레이어드 VS 헥사고날 (java, spring boot)]]></title>
            <link>https://velog.io/@devel-history/DDD-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@devel-history/DDD-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Fri, 28 Nov 2025 07:41:33 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p>사이드 프로젝트에서 DDD 방법론을 이용해서 프로젝트를 진행할 예정이다. 그런데 아키텍처 선택에 대한 문제가 있기에 layered와 hexagonal 2개를 비교하려고 한다. (clean architechture는 논외로 치고 글을 작성했다) </p>
<h2 id="layered">Layered</h2>
<p><img src="https://velog.velcdn.com/images/devel-history/post/c8d959fa-aca7-478c-8ed4-8c0d29925f39/image.png" alt="">
Layered Architecture는 소프트웨어 시스템을 논리적인 책임에 따라 수직적인 계층으로 분리하여 설계하는 가장 보편적인 아키텍처이다. spring boot로 프로젝트를 진행하였다면 다들 Layered를 사용해본 경험을 가지고 있을 것이다. (Controller, Service, Repository, Entity 4개로 계층을 나눠서 사용했기에 Layered로 사용했을 거라고 추측하였다)</p>
<h3 id="핵심-규칙">핵심 규칙</h3>
<ul>
<li><strong>관심사 분리</strong> : 각 계층은 오직 하나의 특정한 책임만 수행한다. </li>
<li><strong>단방향 의존성</strong> : 아래 계층은 위 계층을 모르고, 오직 아래 계층에게만 서비스를 요청하고 의존해야 한다. </li>
</ul>
<h3 id="폴더-구조-예시">폴더 구조 예시</h3>
<p>Infrastructure, Application, Domain, Presentation</p>
<pre><code class="language-java">src/main/java/com/example/project/
 ├─ presentation/
 │    └─ article/
 │         ├─ ArticleController.java
 │         └─ ArticleDto.java
 │
 ├─ application/
 │    └─ article/
 │         └─ ArticleService.java
 │
 ├─ domain/
 │    └─ article/
 │         ├─ Article.java
 │         ├─ ArticleRepository.java   ← interface
 │         └─ ArticleService.java      ← DomainService(선택)
 │
 └─ infrastructure/
      └─ article/
           ├─ JpaArticleRepository.java  ← Repository 구현체
           └─ ArticleEntityMapper.java   ← JPA 엔티티 변환
</code></pre>
<h2 id="hexagonal">Hexagonal</h2>
<p><img src="https://velog.velcdn.com/images/devel-history/post/a87465c6-b2d0-48f7-9023-87947ffc048c/image.png" alt=""></p>
<p>hexagonal은 비즈니스 로직을 중심에 두고, 외부 세계와의 모든 상호작용을 <strong>포트(Port)</strong>와 <strong>어댑터(Adapter)</strong>라는 표준화된 인터페이스를 사용하는 아키텍처이다. </p>
<h3 id="핵심-규칙-1">핵심 규칙</h3>
<p><strong>모든 의존성은 안쪽을 향함(의존성 역전의 원칙)</strong> : 비즈니스 코어는 외부 기술(예: 특정 DB)에 대해 전혀 알지 못해야 한다. 코어는 자신이 필요한 기능을 <strong>인터페이스(포트)</strong>로만 정의하고, 실제 구현은 외부의 <strong>어댑터</strong>가 담당한다.</p>
<ol>
<li>외부가 내부에 의존</li>
<li>비즈니스 로직은 기술 독립적</li>
<li>외부 시스템 없이 테스트 가능 (의존성 없이)</li>
</ol>
<h3 id="폴더-구조-예시-1">폴더 구조 예시</h3>
<pre><code class="language-java">
src/main/java/com/example/project/
 ├─ domain/  // 1. Core: Domain Layer (가장 중심, 순수한 비즈니스 로직)
 │    └─ article/
 │         └─ Article.java                    // 핵심 도메인 모델, 비즈니스 규칙 포함
 │
 ├─ application/ // 2. Application Layer (Use Case 정의 및 구현)
 │    ├─ port/
 │    │    ├─ in/
 │    │    │    └─ ArticleCommandPort.java    // [인바운드 포트] 외부 요청을 처리하는 인터페이스 정의
 │    │    └─ out/
 │    │          └─ ArticleLoadPort.java       // [아웃바운드 포트] DB 접근 등 외부 호출을 위한 인터페이스 정의
 │    └─ service/
 │         └─ ArticleCommandService.java     // [Use Case 구현체] ArticleCommandPort 인터페이스 구현
 │
 └─ adapter/   // 3. Adapters Layer (인프라 및 입출력 기술 의존성 관리)
      ├─ in/
      │    └─ web/
      │          ├─ ArticleWebAdapter.java     // [인바운드 어댑터] Spring Controller 역할. Port 호출
      │          └─ ArticleRequest.java        // Web DTO
      └─ out/
          └─ persistence/
              ├─ JpaArticleAdapter.java      // [아웃바운드 어댑터] ArticleLoadPort 인터페이스 구현 (DB 연동)
              └─ JpaArticleEntity.java       // JPA 전용 엔티티</code></pre>
<hr>
<h2 id="코드를-통한-비교">코드를 통한 비교</h2>
<p>글을 읽고 또 읽어도 잘 모르겠다는 생각이 들었기에 코드를 직접 만들어 보기로 하였다. 숙소 예약 기능을 가진 프로젝트를 생성하고 아키텍처를 비교해 보겠다.
<a href="https://github.com/Cori1304-Seong/architecture-pratice/tree/main/DDD">공부한 프로젝트</a> (코드와 폴더 구조에대해서 의견을 주시면 긍정적으로 반영하겠습니다. )</p>
<p>프로젝트는 3개에 도메인 기능이 있는데 [숙소 관리,예약 관리, 유저 관리]를 중심으로 구현하였다. </p>
<h3 id="도메인-별로-파일-개수-비교">도메인 별로 파일 개수 비교</h3>
<p>두 아키턱처에서 파일 개수가 나는 이유는 Layered의 Service 1개 역할을 Hexagonal에서는 UseCase Port + Service + Repository Port로 분리하고, 추가로 DTO, Mapper, Adapter를 더해서 총 7-10개 파일이 더 필요하기 때문이다. </p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Layered Files</th>
<th>Hexagonal Files</th>
<th>Increase</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Lodging</strong></td>
<td>4 files</td>
<td>11 files</td>
<td>+175%</td>
</tr>
<tr>
<td><strong>Reservation</strong></td>
<td>4 files</td>
<td>14 files</td>
<td>+250%</td>
</tr>
<tr>
<td><strong>User</strong></td>
<td>3 files</td>
<td>13 files</td>
<td>+333%</td>
</tr>
<tr>
<td><strong>Application Entry</strong></td>
<td>1 file</td>
<td>1 file</td>
<td>-</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td><strong>14 files</strong></td>
<td><strong>38 files</strong></td>
<td><strong>+171%</strong></td>
</tr>
</tbody></table>
<h2 id="결론">결론</h2>
<p>Hexagonal은 구조를 통해 강제적으로 <strong>SOLID 원칙</strong>을 준수하도록 만드는 방법이라고 느껴진다.환경을 통해서 개발자를 가이드하는 것, 이것이 좋은 아키텍처가 아닐까라는 생각이 들었다.</p>
<p>하지만 동시에 &quot;Hexagonal은 모든 프로젝트에 실질적인 도움을 줄까?&quot;라는 의문도 들었다. <a href="https://tech.kakaopay.com/post/home-hexagonal-architecture/">Hexagonal Architecture, 진짜 하실 건가요?</a>와 <a href="https://okky.kr/questions/1463012">okky 커뮤니트</a>를 읽고 또 생각에 잠긴다. </p>
<h3 id="최종-결론">최종 결론</h3>
<p>결론은 또 케바케다. 결국 아키텍처는 <strong>현재 팀의 상황과 프로젝트 특성</strong>에 맞춰 선택해야 하며, &quot;무조건 Hexagonal이 좋다&quot; 또는 &quot;과도하다&quot;는 이분법적 사고보다는 <strong>트레이드오프를 이해하고 의식적으로 선택</strong>하는 것이 중요하다.</p>
<p>개인적으로 이번 조사를 통해 느낀 점은, Hexagonal Architecture는 
<strong>&quot;초기 비용을 지불하고 장기 이익을 얻는 투자&quot;</strong>라는 것이다.  프로젝트가 그만한 투자 가치가 있는지 판단하는 것이 핵심이다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://velog.io/@gayeong39/SPRING-%EB%A0%88%EC%9D%B4%EC%96%B4%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98">https://velog.io/@gayeong39/SPRING-%EB%A0%88%EC%9D%B4%EC%96%B4%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</a>
<a href="https://msolo021015.medium.com/layered-architecture-deep-dive-c0a5f5a9aa37">https://msolo021015.medium.com/layered-architecture-deep-dive-c0a5f5a9aa37</a></li>
<li><a href="https://curiousjinan.tistory.com/entry/spring-hexagonal-architecture">https://curiousjinan.tistory.com/entry/spring-hexagonal-architecture</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[내가 알던 REST API와 처음 REST API는 달랐다?!]]></title>
            <link>https://velog.io/@devel-history/%EB%82%B4%EA%B0%80-%EC%95%8C%EB%8D%98-REST-API%EC%99%80-%EC%B2%98%EC%9D%8C-REST-API%EB%8A%94-%EB%8B%AC%EB%9E%90%EB%8B%A4</link>
            <guid>https://velog.io/@devel-history/%EB%82%B4%EA%B0%80-%EC%95%8C%EB%8D%98-REST-API%EC%99%80-%EC%B2%98%EC%9D%8C-REST-API%EB%8A%94-%EB%8B%AC%EB%9E%90%EB%8B%A4</guid>
            <pubDate>Thu, 20 Nov 2025 08:47:38 GMT</pubDate>
            <description><![CDATA[<h2 id="🧐-rest-api가-뭐죠">🧐 REST API가 뭐죠?</h2>
<p>만약 개발자 면접에서 &quot;<em>REST API가 무엇인지 설명해 줄래요?</em>&quot;라고 질문하면 뭐라고 답할까? 아래와 같이 대답 할 수 있다</p>
<blockquote>
<p>&quot;REST API는 웹의 기본 프로토콜인 <strong>HTTP</strong>를 기반으로, <strong>자원(Resource)</strong>을 이름(URI)으로 구분하고 해당 자원에 대한 행위(CRUD)를 HTTP 메서드를 통해 처리하도록 설계된 아키텍처 스타일입니다. 한마디로, 웹 상에서 데이터를 효율적으로 주고받기 위한 <strong>표준화된 통신 규약</strong>이라고 할 수 있습니다.&quot;</p>
</blockquote>
<p><strong>모범답안</strong>을 말하고 안심하겠지만, 2000년에 발표된 논문을 보면 <strong>로이 필딩(Roy Fielding)이 정의한 본래의 REST</strong>와는 거리가 있어 아쉬워할 것입니다. (REST 아키텍처 스타일 논문 작성자가 로이 필딩입니다.)</p>
<p>로이 필딩이 발표한 <strong>진정한 REST API</strong>가 무엇인지 자세히 살펴봅시다.</p>
<hr>
<h2 id="😮-rest-api의-6가지-조건-by-roy-fielding">😮 REST API의 6가지 조건 (by Roy Fielding)</h2>
<p>API가 <strong>RESTful</strong> 하다고 인정받으려면, 로이 필딩이 제시한 REST의 6가지 아키텍처 <strong>제약 조건</strong>을 만족해야 한ㄷ.</p>
<h3 id="1-클라이언트-서버-구조-client-server">1. 클라이언트-서버 구조 (Client-Server)</h3>
<ul>
<li><strong>규칙:</strong> 클라이언트(예: 웹 브라우저)와 서버(API 제공)가 <strong>독립적으로 분리</strong>되어야 합니다. 서버는 리소스 처리와 비즈니스 로직을, 클라이언트는 사용자 인터페이스를 담당한다.</li>
<li><strong>장점:</strong> 각 부분이 독립적으로 개발 및 배포될 수 있어 <strong>상호 의존성이 낮아진다.</strong></li>
</ul>
<h3 id="2-무상태성-stateless">2. 무상태성 (Stateless)</h3>
<ul>
<li><strong>규칙:</strong> 서버는 클라이언트의 <strong>요청 간 상태 정보</strong>(세션, 로그인 정보 등)를 저장하지 않는다. 모든 요청은 처리에 필요한 모든 정보를 <strong>스스로 포함</strong>해야 한다.</li>
<li><strong>장점:</strong> 서버의 구현이 단순해지고, <strong>수평적 확장성</strong>과 <strong>로드 밸런싱</strong>에 매우 유리</li>
</ul>
<h3 id="3-캐시-처리-가능-cacheable">3. 캐시 처리 가능 (Cacheable)</h3>
<ul>
<li><strong>규칙:</strong> 서버 응답은 데이터가 <strong>캐싱 가능한지 여부</strong>를 명시한다. (예: HTTP 헤더).</li>
<li><strong>장점:</strong> 캐싱을 통해 불필요한 요청을 줄여 <strong>네트워크 트래픽을 감소</strong>시키고 성능을 향상시킬 수 있다.</li>
</ul>
<h3 id="4-계층화-시스템-layered-system">4. 계층화 시스템 (Layered System)</h3>
<ul>
<li><strong>규칙:</strong> 클라이언트는 서버에 직접 연결되었는지, 혹은 중간 계층(프록시, 로드 밸런서, 방화벽 등)을 통해 연결되었는지 <strong>알 필요가 없어야</strong> 한다.</li>
<li><strong>장점:</strong> 시스템 보안 및 확장성을 위해 유연하게 <strong>중간 계층을 추가하고 변경</strong>할 수 있다.</li>
</ul>
<h3 id="5-균일한-인터페이스-uniform-interface">5. 균일한 인터페이스 (Uniform Interface)</h3>
<ul>
<li><p><strong>규칙:</strong> 시스템 전체에 걸쳐 <strong>일관되고 통일된 방식</strong>으로 상호작용이 가능해야 합니다. 이는 다음 네 가지 하위 원칙을 포함한다.</p>
<ol>
<li><strong>리소스 식별</strong>: <strong>URI</strong>를 사용하여 자원을 명확하게 식별 가능해야 한다</li>
<li><strong>메시지를 통한 자원 조작</strong>: <strong>HTTP 메서드</strong>(GET, POST, PUT, DELETE)를 사용하여 자원에 대한 행위를 표현 가능해야 한다.</li>
<li><strong>자기 서술적 메시지</strong>: 메시지(요청/응답)만으로 그 뜻을 완전히 파악할 수 있어야 한다. (예: HTTP 헤더에 데이터 타입 명시).</li>
<li><strong>HATEOAS</strong>: 응답에 현재 자원과 연관된 <strong>하이퍼링크</strong>를 포함하여, 클라이언트가 다음 상태로 전이하기 위해 수행할 수 있는 모든 작업을 동적으로 찾을 수 있어야 한다.</li>
</ol>
</li>
<li><p><strong>장점:</strong> API가 간결하고 예측 가능해지며, 클라이언트와 서버의 구현이 분리되어 독립적인 진화가 가능해진다.</p>
</li>
</ul>
<h3 id="6-code-on-demand-선택적-조건">6. Code-On-Demand (선택적 조건)</h3>
<ul>
<li><strong>규칙:</strong> 서버가 클라이언트에게 실행 가능한 코드를 전송하여 <strong>클라이언트의 기능을 일시적으로 확장</strong>할 수 있어야 한다. (예: 자바스크립트).</li>
<li><strong>참고:</strong> 이 조건은 <strong>선택 사항(Optional)</strong>이며, 필수 조건은 아닙니다.</li>
<li><strong>장점:</strong> 클라이언트의 기능을 서버가 동적으로 확장할 수 있는 유연성을 제공한다.</li>
</ul>
<hr>
<h2 id="완벽하게-rest-api를-지키지-않는-이유">완벽하게 REST API를 지키지 않는 이유</h2>
<p>지금까지 개발하고 참고했던 API들을 되돌아보면, 6가지 조건을 모두 만족했는지 의문이 들었다. 그런데도 왜 사람들은 조건을 만족시키지 못했는데도 <strong>REST API</strong>라고 부르는 걸까?</p>
<h3 id="1-rest는-대용량-하이퍼미디어-시스템을-위한-설계다">1. REST는 대용량 하이퍼미디어 시스템을 위한 설계다</h3>
<p>필딩의 논문 제목은 &quot;네트워크 기반 소프트웨어 아키텍처의 아키텍처 스타일과 설계&quot;이다. 즉, 특정 API 구현 방법에 대한 논문이 아니라 <strong>웹과 같은 대용량 분산 하이퍼미디어 시스템</strong>에서 사용할 아키텍처 원칙을 소개한 것이다.</p>
<p>현대의 REST API는 주로 <strong>데이터 교환(CRUD)</strong>에 집중하지만, 필딩의 REST는 <strong>상태 전이(HATEOAS)</strong>를 통한 시스템의 독립적 진화에 더 초점을 맞췄다는 점에서 <strong>목적과 사용처에 차이</strong>가 발생하였다.</p>
<h3 id="2-hateoas-구현은-비효율적이다">2. HATEOAS 구현은 비효율적이다.</h3>
<p>필딩의 REST에서 가장 중요한 원칙은 <strong>HATEOAS</strong>이다. HATEOAS를 사용하면 응답에 데이터뿐만 아니라 해당 데이터와 관련된 다음 요청에 필요한 <strong>URI</strong>를 포함하여 반환하며, 클라이언트가 <strong>서버와 동적으로 상호작용</strong>이 가능하게 한다.</p>
<h4 id="hateoas-예시-상태-전이"><strong>&lt;HATEOAS 예시: 상태 전이&gt;</strong></h4>
<p>HATEOAS의 진정한 가치는 <strong>자원의 상태에 따라 다음 액션을 동적으로 제공</strong>하는 데 있다.</p>
<p><strong>A. 주문 완료 전 상태</strong></p>
<pre><code class="language-JSON">{
    &quot;order_id&quot;: 500,
    &quot;status&quot;: &quot;PENDING&quot;,
    &quot;total_price&quot;: 45000,
    &quot;_links&quot;: {
        &quot;self&quot;: { &quot;href&quot;: &quot;/orders/500&quot; },
        &quot;pay&quot;: { &quot;href&quot;: &quot;/orders/500/payment&quot;, &quot;method&quot;: &quot;POST&quot; },
        &quot;cancel&quot;: { &quot;href&quot;: &quot;/orders/500/cancel&quot;, &quot;method&quot;: &quot;POST&quot; }
    }
}</code></pre>
<blockquote>
<p><strong>클라이언트 행동:</strong> 클라이언트는 <code>status</code>가 <strong>PENDING</strong>일 때, 응답의 <code>pay</code> 또는 <code>cancel</code> 링크만 보고 해당 작업을 수행</p>
</blockquote>
<p><strong>B. 주문 완료 후 상태</strong></p>
<p>결제를 완료하면 서버는 응답 데이터를 변경하여 <code>pay</code> 링크를 제거하고 <code>refund</code> 링크를 추가한다.</p>
<pre><code class="language-json">{
    &quot;order_id&quot;: 500,
    &quot;status&quot;: &quot;COMPLETED&quot;,
    &quot;total_price&quot;: 45000,
    &quot;payment_date&quot;: &quot;2025-11-20&quot;,
    &quot;_links&quot;: {
        &quot;self&quot;: { &quot;href&quot;: &quot;/orders/500&quot; },
        &quot;refund&quot;: { &quot;href&quot;: &quot;/orders/500/refund&quot;, &quot;method&quot;: &quot;POST&quot; } 
    }
}</code></pre>
<blockquote>
<p><strong>클라이언트 행동:</strong> 클라이언트는 이제 <code>pay</code> 링크가 없다는 것을 확인하고 결제 버튼을 숨기거나 비활성화하며, <code>refund</code> 링크를 통해 환불 요청만 할 수 있다.</p>
</blockquote>
<p>하지만 현실적으로 HATEOAS를 구현하면 서버와 클라이언트 모두 <strong>개발 복잡성이 증가</strong>하여 <strong>효율성이 저하</strong>된다. 그렇기 때문에 거의 모든 상용 API는 HATEOAS를 사용하지 않는다.(필딩은 이를 <strong>HTTP API</strong>로 불러주길 원한다....)</p>
<h3 id="3-soap의-대안과-http의-편리성">3. SOAP의 대안과 HTTP의 편리성</h3>
<p>2000년대 초반에는 XML 기반의 <strong>SOAP</strong>를 사용하는 API가 주를 이루었으나, SOAP는 XML 사용으로 인한 <strong>오버헤드</strong>와 <strong>복잡성</strong>이 존재했습니다. 이때, HTTP라는 이미 널리 사용되는 간단한 프로토콜을 활용하는 RESTful 방식이 편리한 대안으로 부상했습니다.</p>
<p>개발자들이 REST의 핵심인 <strong>&#39;URI로 자원 식별&#39;</strong>과 <strong>&#39;HTTP 메서드로 행위 표현&#39;</strong>이라는 가장 쉽고 강력한 두 가지 원칙만 차용하고, 복잡한 HATEOAS를 생략했습니다. 즉, <strong>편의성과 실용주의</strong> 덕분에 HTTP 통신이 필요한 곳에 무분별하게 사용되며, <strong>&#39;REST API&#39;라는 용어가 대중화 및 일반화</strong>된 것입니다.</p>
<h3 id="4-이미-관념이-잡혔다">4. 이미 관념이 잡혔다.</h3>
<p>4번은 3번에 연장선이다. REST API라고 생각했던 (HTTP API)라는 이름을 바꿀 수 없게 되었다. 이미 문서와 글 그리고 회의때도 사용했고 의미 전달이 되기 때문이다. 예를 들어서 &quot;옥동자&quot;를 가지고 설명하면 원래는** 옥(玉)같이 예쁜 어린 아들, 몹시 소중한 아들** 이라는 뜻이지만, 개그맨 1명에 등장으로 의미가 별질되었다. </p>
<h2 id="결론">결론</h2>
<p>현제 REST API는 필딩 아저씨의 API와는 다른 것이다. 목적은 같을 수 있지만 다르다. 테세우스의 배와 같다고 생각이 든다. 누구의 관점을 볼 것인가. 참 재미있는거 같다. 그래도 빌딩 아저씨 덕분에 REST API를 사용할 수 있다는 것은 맞다고 생각을 하기에 고맙다라는 마음이 든다. SOAP는 좀..  </p>
<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://velog.io/@alswp006/WEB-RESTfeat.-%EB%A1%9C%EC%9D%B4-%ED%95%84%EB%94%A9%EA%B3%BC-%EC%9E%98%EB%AA%BB%EB%90%9C-REST">[WEB] REST(feat. 로이 필딩과 잘못된 REST)</a></p>
<p><a href="https://witch.work/ko/translations/misappropriated-rest-dissertation">흔히 잘못 이해되는 로이 필딩의 REST 논문</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[사이드 프로젝트 REST API 설계 with FE]]></title>
            <link>https://velog.io/@devel-history/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-REST-API-%EC%84%A4%EA%B3%84-with-FE</link>
            <guid>https://velog.io/@devel-history/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-REST-API-%EC%84%A4%EA%B3%84-with-FE</guid>
            <pubDate>Thu, 20 Nov 2025 02:02:28 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>이번주 일요일 25.11.19에 1차 개발자 회의 주제로 API 설계 회의를 진행하려고 한다. 그때를 위해서 REST API에 대한 기록과 의논할 부분이 무엇이 있을지 서술하려고 한다. (아직 기획이 정리되지 않았기에 세부적인 내용을 정할 수 없지만 미래 정할 수 있는것을 정하려고 한다)</p>
<h2 id="rest-api란">REST API란?</h2>
<blockquote>
<p>REST API는 REST 아키텍처 스타일의 설계 원칙을 따르는 애플리케이션 프로그래밍 인터페이스(API)입니다. REST는 Representational State Transfer의 약자로, 웹 API를 구축하는 방법에 대한 일련의 규칙과 지침입니다.  <a href="https://www.redhat.com/en/topics/api/what-is-a-rest-api">Red Hat 인용</a></p>
</blockquote>
<p>Rest API란 REST한 규칙을 지키는 API를 말하고 이 규칙을 지킨 API를 RESTful하다고 칭 합니다.</p>
<h3 id="rest-api-예제">REST API 예제</h3>
<p> <strong>👥 기본 자원 관리: 사용자 (<code>/users</code>)</strong></p>
<table>
<thead>
<tr>
<th align="left">작업 (CRUD)</th>
<th align="left">HTTP 메서드</th>
<th align="left">URI 경로 예시</th>
<th align="left">설명</th>
<th align="left">일반적인 상태 코드</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>모두 조회</strong> (Read)</td>
<td align="left"><code>GET</code></td>
<td align="left"><code>/users</code></td>
<td align="left">모든 사용자 목록을 조회합니다.</td>
<td align="left"><code>200 OK</code></td>
</tr>
<tr>
<td align="left"><strong>단일 조회</strong> (Read)</td>
<td align="left"><code>GET</code></td>
<td align="left"><code>/users/{id}</code></td>
<td align="left">특정 ID의 사용자를 조회합니다. (예: <code>/users/123</code>)</td>
<td align="left"><code>200 OK</code>, <code>404 Not Found</code></td>
</tr>
<tr>
<td align="left"><strong>생성</strong> (Create)</td>
<td align="left"><code>POST</code></td>
<td align="left"><code>/users</code></td>
<td align="left">새로운 사용자를 생성합니다. (요청 본문 사용)</td>
<td align="left"><code>201 Created</code></td>
</tr>
<tr>
<td align="left"><strong>전체 수정</strong> (Update)</td>
<td align="left"><code>PUT</code></td>
<td align="left"><code>/users/{id}</code></td>
<td align="left">특정 자원을 <strong>전체</strong> 데이터로 교체/업데이트합니다.</td>
<td align="left"><code>200 OK</code>, <code>204 No Content</code></td>
</tr>
<tr>
<td align="left"><strong>일부 수정</strong> (Update)</td>
<td align="left"><code>PATCH</code></td>
<td align="left"><code>/users/{id}</code></td>
<td align="left">특정 자원의 <strong>일부</strong> 속성만 수정합니다.</td>
<td align="left"><code>200 OK</code>, <code>204 No Content</code></td>
</tr>
<tr>
<td align="left"><strong>삭제</strong> (Delete)</td>
<td align="left"><code>DELETE</code></td>
<td align="left"><code>/users/{id}</code></td>
<td align="left">특정 자원을 삭제합니다.</td>
<td align="left"><code>204 No Content</code></td>
</tr>
</tbody></table>
<p><strong><em>🔗 관계형 자원 관리: 게시물과 댓글</em></strong></p>
<table>
<thead>
<tr>
<th align="left">작업</th>
<th align="left">HTTP 메서드</th>
<th align="left">URI 경로 예시</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>자식 목록 조회</strong></td>
<td align="left"><code>GET</code></td>
<td align="left"><code>/posts/123/comments</code></td>
<td align="left">게시물 123에 속한 모든 댓글을 조회합니다.</td>
</tr>
<tr>
<td align="left"><strong>자식 생성</strong></td>
<td align="left"><code>POST</code></td>
<td align="left"><code>/posts/123/comments</code></td>
<td align="left">게시물 123에 새 댓글을 생성합니다.</td>
</tr>
<tr>
<td align="left"><strong>특정 자식 조회</strong></td>
<td align="left"><code>GET</code></td>
<td align="left"><code>/posts/123/comments/456</code></td>
<td align="left">게시물 123의 댓글 456을 조회합니다.</td>
</tr>
</tbody></table>
<p><strong><em>⚙️ 기타 행위 및 쿼리 표현</em></strong></p>
<table>
<thead>
<tr>
<th align="left">상황</th>
<th align="left">HTTP 메서드</th>
<th align="left">URI 경로 예시</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>특정 행위</strong> (액션)</td>
<td align="left"><code>POST</code></td>
<td align="left"><code>/orders/500/pay</code></td>
<td align="left">주문 500에 대해 결제(<code>pay</code>)를 실행합니다.</td>
</tr>
<tr>
<td align="left"><strong>검색/필터링</strong></td>
<td align="left"><code>GET</code></td>
<td align="left"><code>/products?category=food&amp;sort=price</code></td>
<td align="left">쿼리 파라미터를 사용하여 자원을 검색하거나 필터링합니다.</td>
</tr>
</tbody></table>
<hr>
<h2 id="의논할-부분">의논할 부분</h2>
<ol>
<li><p>JWT 사용시 refresh와 access Token을 cookie only로 설정?</p>
</li>
<li><p>이미지 사용시 multipart/form-data 사용? CDN 사용? (Server-Side Upload or Direct Upload)</p>
</li>
<li><p>Exception 처리 </p>
<ul>
<li>body에 들어갈 데이터를 어떻게 둘것인가? 
<code>timestamp</code>,<code>status</code>, <code>msg</code>, <code>code</code>, <code>details</code> </li>
</ul>
<pre><code class="language-json">/*예시*/
{
   &quot;timestamp&quot;: &quot;2025-11-22T07:30:00Z&quot;,
   &quot;status&quot;: 401,
   &quot;code&quot;: &quot;AUTH_001&quot;,
   &quot;message&quot;: &quot;Access token expired. Please refresh your token.&quot;,
   &quot;details&quot;: null
}</code></pre>
<ol start="4">
<li>success에도 code가 필요한가?</li>
</ol>
</li>
</ol>
<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://velog.io/@couchcoding/HTTP%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90%EC%A0%95%EC%9D%98%EC%99%80-%EA%B5%AC%EC%A1%B0">HTTP에 대해서 알아보자(정의와 구조)</a>
<a href="https://velog.io/@couchcoding/%EA%B0%9C%EB%B0%9C-%EC%B4%88%EB%B3%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-RESTful-API-%EC%84%A4%EA%B3%84-%EA%B0%80%EC%9D%B4%EB%93%9C">개발 초보를 위한 RESTful API 설계 가이드</a>
<a href="https://cloud.google.com/discover/what-is-rest-api?hl=ko">(구글) REST API란 무엇인가요?
</a> <a href="https://gorilla-ohgiraffers.tistory.com/3">[REST API] Roy Fielding이 말하는 6가지 제약 조건 - Chat 상우</a></p>
<h2 id="회의-후-결과물">회의 후 결과물</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL index 실습 기록]]></title>
            <link>https://velog.io/@devel-history/MySQL-index-%EC%8B%A4%EC%8A%B5-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@devel-history/MySQL-index-%EC%8B%A4%EC%8A%B5-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Wed, 19 Nov 2025 08:22:15 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>과거 프로젝트를 진행하면서 spring과 서버에 대해서는 공부를 집중적으로 했지만 DB에 대해서는 적게 공부하였기 때문에 DB에 대한 공부를 하던 중 index 실습중에 마주친 문제와 index가 왜 필요한지에대해서 기술하기 위해 작성한 글이다.</p>
<h2 id="index란">Index란?</h2>
<p>DB에서 <strong>데이터를 빠르게 검색하고 접근하기 위해 테이블의 하나 또는 여러 column을 이용하여 생성하는 자료 구조</strong>로 B-Tree 또는 B+Tree구조로 이뤄어 졌고 책의 목차 또는 색인처럼 동작하며 full Table Sca하는 대신, 인덱스를 통해 데이터가 저장된 위치를 즉시 찾아갈 수 있게 합니다</p>
<h3 id="index의-목적">index의 목적</h3>
<p>조회(SELECT) 작업의 성능을 크게 향상시키기 위해(그렇기에 사입, 삭재, 갱신시 오버해드가 발생할 수 있다. 그렇기에 전략적으로 사용해야 한다.) </p>
<blockquote>
<h3 id="백과사전-비유로-index-설명">백과사전 비유로 index 설명</h3>
</blockquote>
<ul>
<li>DB 테이블 (Table): 백과사전의 <strong>&#39;본문 전체&#39;</strong>입니다. (수많은 데이터가 순서 없이, 혹은 특정 순서로 저장되어 있는 곳)<blockquote>
</blockquote>
</li>
<li>DB 인덱스 (Index): 백과사전의 <strong>&#39;맨 뒤에 있는 찾아보기 (색인)&#39;</strong>입니다.</li>
<li>데이터 검색 (Query): 인덱스가 없는 경우: 백과사전에서 특정 단어(예: &#39;양자역학&#39;)를 찾으려면 첫 페이지부터 마지막 페이지까지 한 장씩 넘겨가며 눈으로 직접 찾아야 합니다. (이것이 Full Table Scan)<blockquote>
<ul>
<li>인덱스가 있는 경우: 먼저 색인 페이지를 펼칩니다. 색인에서 &#39;양자역학&#39;을 찾으면 옆에 <strong>&#39;357페이지&#39;</strong>라고 적혀 있습니다. 바로 357페이지로 넘어가 원하는 정보를 즉시 찾습니다. (이것이 Index Scan)</li>
</ul>
</blockquote>
</li>
</ul>
<hr>
<h2 id="실습-진행">실습 진행</h2>
<ol>
<li><a href="https://github.com/Cori1304-Seong/DB-pratice/tree/main/MySQL/study_index">repo</a> clone </li>
<li>repo의 init.sql 문 실행 (DB에 테이블과 row 생성)</li>
<li>full scan과 range 조회 SQL 실행 후 시간과 결과 비교</li>
</ol>
<h2 id="마주친-문제">마주친 문제</h2>
<h3 id="1-cte-동작-안-됨">1. CTE 동작 안 됨</h3>
<p>실습을 진행하기 위해서는 DB에 대량에 데이터가 필요했고 CTE를 사용해서 sql을 만들어 init.sql을 구정했다. 하지만 CTE 사용에서 에러가 발생했다.
<a href="https://jjon.tistory.com/entry/MySQL-80-%EC%8B%A0%EA%B8%B0%EB%8A%A5-CTECommon-Table-Expression-%ED%99%9C%EC%9A%A9">MySQL 8.0 신기술 CTE</a></p>
<p><strong>&lt;문제 내용&gt;</strong>
WITH RECURSIVE CTE를 이용하여 30만 개 숫자를 생성하는 과정에서 발생
<code>ERROR 1137 (HY000): Can&#39;t reopen table: &#39;d4&#39;</code></p>
<p><strong>&lt;문제 원인&gt;</strong></p>
<ol>
<li>MySQL이 CTE를 “재사용 가능 데이터셋&quot;으로 보지 않는 구조이지만 임시 테이블이라고 생각하고 사용하였다. (MySQL에서 CTE를 &quot;쿼리 구조를 단순화하는 도구&quot;로만 사용할 것을 권장한다)</li>
<li>CTE가 INSERT 대상 테이블과 충돌,INSERT … SELECT 구문 안에서 CTE가 동시에 참조되면 MySQL은 동일한 CTE를 2번 오픈하려다 실패함.
[에러 sql]
``` sql</li>
</ol>
<p>-- 실행 중 MySQL이 내부적으로 CTE를 두 번 읽으려 할 때, 
-- &quot;Can&#39;t reopen table&quot; 오류가 발생함
WITH RECURSIVE customer_seq AS (
  SELECT 0 AS id
  UNION ALL
  SELECT id + 1
  FROM customer_seq
  WHERE id + 1 &lt; @customer_target
)</p>
<p>INSERT INTO customer_seq
SELECT ... FROM customer_seq;</p>
<pre><code>**&lt;문제 해결&gt;**
재귀 CTE를 완전히 제거하고 숫자 생성 TEMP TABLE 방식으로 모두 교체
``` sql
CREATE TEMPORARY TABLE seq_100k (id INT PRIMARY KEY);

INSERT INTO seq_100k (id)
SELECT a.N + b.N * 1000 AS id
FROM 
    (SELECT @row1:=@row1+1 AS N FROM information_schema.columns, (SELECT @row1:= -1) r LIMIT 1000) a,
    (SELECT @row2:=@row2+1 AS N FROM information_schema.columns, (SELECT @row2:= -1) r LIMIT 100) b
LIMIT 100000;</code></pre><h3 id="2-조회시-범위-지정">2. 조회시 범위 지정</h3>
<p><strong>&lt;문제 내용&gt;</strong>
<code>city</code>와 <code>created_at</code>에 복합 인덱스가 있음에도 불구하고, <code>IGNORE INDEX</code>를 통한 full scan에 총 응답 시간이 range보다 빠른 것을 확인 </p>
<pre><code class="language-sql">-- 문제의 쿼리 1: 인덱스 사용 (하지만 더 느림)
-- Total Time: 380 ms
SELECT *
FROM customers
WHERE city = &#39;Seoul&#39;
  AND created_at BETWEEN &#39;2024-01-01&#39; AND &#39;2024-12-31&#39;;

-- 문제의 쿼리 2: 인덱스 무시 (하지만 더 빠름)
-- Total Time: 155 ms
SELECT *
FROM customers IGNORE INDEX (idx_customers_city_created_at)
WHERE city = &#39;Seoul&#39;
  AND created_at BETWEEN &#39;2024-01-01&#39; AND &#39;2024-12-31&#39;;</code></pre>
<p><strong>&lt;문제 원인&gt;</strong></p>
<ol>
<li><strong>조회한 데이터가 많음</strong>
<code>EXPLAIN</code>을 통해 확인한 결과, 옵티마이저는 인덱스를 사용했지만(<code>type: range</code>) 조건에 해당하는 데이터가 약 2,000건 이상으로 많았다.</li>
<li><strong>Bookmark Lookup 사용</strong> 
<code>SELECT *</code> 때문에, 1번에서 찾은 2,000여 개의 <code>PRIMARY KEY</code>를 가지고 실제 테이블에 2,000번 접근하여 나머지 컬럼(<code>id</code>, <code>name</code>, <code>email</code> 등)을 가져왔다. </li>
</ol>
<p>이 2개 과정에서 발생하는 대량의 <code>Random I/O</code> 비용이 테이블 전체를 순차적으로 읽는 <code>Sequential I/O</code> 비용보다 더 컸기 때문에 성능 역전 현상이 발생한 것이었다.
<strong>&lt;문제 해결&gt;</strong>
&quot;커버링 인덱스&quot;로 성능 극대화로 해결 <a href="https://theliar.tistory.com/15">Covering Index로 쿼리 성능 개선하기</a></p>
<pre><code class="language-sql">-- AS-IS: 모든 컬럼 조회
SELECT * FROM customers ...

-- TO-BE: 필요한 컬럼만 명시
SELECT id, name, email, city, created_at FROM customers 

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

-- AS-IS: 기존 인덱스
 INDEX idx_customers_city_created_at (city, created_at)

-- TO-BE: 커버링 인덱스
CREATE INDEX idx_customers_cover ON customers (city, created_at, id, name, email);</code></pre>
<p>적용 후, <code>EXPLAIN</code> 결과의 <code>Extra</code> 필드에 <strong><code>Using index</code></strong>가 표시되었고 <code>fetching</code> 시간이 줄며 index를 사용한 조회가 더 효율적인 것을 실험하였다. </p>
<pre><code class="language-sql">SELECT SQL_NO_CACHE id, name, email, city, created_at
             FROM customers
             WHERE city = &#39;Seoul&#39;
               AND created_at BETWEEN &#39;2025-10-01&#39; AND &#39;2025-10-02 23:59:59&#39;
             LIMIT 10
10 rows retrieved starting from 1 in 156 ms (execution: 35 ms, fetching: 121 ms)

SELECT SQL_NO_CACHE id, name, email, city, created_at
             FROM customers IGNORE INDEX (idx_customers_cover)
             WHERE city = &#39;Seoul&#39;
               AND created_at BETWEEN &#39;2025-10-01&#39; AND &#39;2025-10-02 23:59:59&#39;
             LIMIT 10
 10 rows retrieved starting from 1 in 227 ms (execution: 18 ms, fetching: 209 ms)
</code></pre>
<h2 id="느낀점">느낀점</h2>
<p>index가 당연히 빠를거라고 간다하게 실습한 내용인데 잘못된 index 적용으로 full scan이 더 빠른 결과를 보게 되었다. 개발자의 실수가 이렇게 치명적일 수 있구나 다시 한 번 느끼게 되었고 테스트 하지 않았다면 잘못을 몰랐을 거라는 것에 테스트에 중요성을 더욱 더 깨닮게 되었다. </p>
<h2 id="참고한-자료">참고한 자료</h2>
<p><a href="https://mangkyu.tistory.com/96">망나니 개발자</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[<모여행> 프로젝트 인증/인가 개선 방향]]></title>
            <link>https://velog.io/@devel-history/%EB%AA%A8%EC%97%AC%ED%96%89-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-%EA%B0%9C%EC%84%A0-%EB%B0%A9%ED%96%A5</link>
            <guid>https://velog.io/@devel-history/%EB%AA%A8%EC%97%AC%ED%96%89-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-%EA%B0%9C%EC%84%A0-%EB%B0%A9%ED%96%A5</guid>
            <pubDate>Wed, 22 Oct 2025 06:25:18 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>구름 부트캠프후 모여행 프로젝트를 출시까지 진행하자고 팀원들이 모이게 되었고 함께 출시하기전 기획을 재정비하는 동안 인증/인가에서 개선 사항을 찾고 정리한 글이다.</p>
<h2 id="개선-사항">개선 사항</h2>
<h3 id="1-회원-탈퇴-기능">1. 회원 탈퇴 기능</h3>
<ul>
<li>*<em>현황: *</em> 회원 탈퇴 기능 부재 </li>
<li>*<em>개선 제안: *</em> <ol>
<li>Oauth 탈퇴 포함 </li>
<li>soft delete 사용 </li>
</ol>
</li>
</ul>
<h3 id="2-게스트-초대-기능-url-사용으로-변경">2. 게스트 초대 기능 URL 사용으로 변경</h3>
<ul>
<li>*<em>현황: *</em> 불안정</li>
<li>*<em>개선 제안: *</em> <ol>
<li>링크 공유로 변경 요청 <a href="https://velog.io/@devel-history/%EA%B5%AC%EB%A6%84-3%EC%B0%A8-%ED%95%A9%EB%B0%98-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Spring-Security#5-%EA%B2%B0%EB%A1%A0-%EB%B0%8F-%ED%96%A5%ED%9B%84-%EA%B3%84%ED%9A%8D">참고자료</a></li>
<li>URL path를 어떻게 할지 의논 필요 </li>
</ol>
</li>
</ul>
<h3 id="3-admin-인증인가-필요">3. admin 인증/인가 필요</h3>
<ul>
<li>*<em>현황: *</em> admin 관려된 기능이 추가될 수 있기에 필요 </li>
<li>*<em>개선 제안: *</em> <ol>
<li>admin 계정 생성 개수 제한 또는 관리자가 직접 생성만 가능</li>
<li>IP 또는 이메일 인증으로 보안 강화 </li>
</ol>
</li>
</ul>
<h3 id="4-refreshtoken-탈취시-보안-정책">4. refreshToken 탈취시 보안 정책</h3>
<ul>
<li>*<em>현황: *</em> refreshToken을 read only로 cookie에 저장 중, </li>
<li>*<em>개선 제안: *</em> <ol>
<li>Dpop 설계: <a href="https://blog.logto.io/ko/oidc-dpop">참고링크</a></li>
</ol>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[<모여행>Spring Security, UserDetails는 어디에 저장하나? ]]></title>
            <link>https://velog.io/@devel-history/%EB%AA%A8%EC%97%AC%ED%96%89Spring-Security-UserDetails%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EB%82%98</link>
            <guid>https://velog.io/@devel-history/%EB%AA%A8%EC%97%AC%ED%96%89Spring-Security-UserDetails%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EB%82%98</guid>
            <pubDate>Tue, 21 Oct 2025 02:17:04 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p>Spring security를 다시 공부하다가 &lt;모영행&gt; 프로젝트의 인증/인가 로직에 문제가 없는지 확인하는 중에 UserDetails를   Authentication 객체 저장하고 @AuthenticationPrincipal를 이용해서 사용했는데 이 방법이 표준에 부합하고 효율적인 방법이었는지 의문이 들게 되어 글을 작성하게 되었다. 
(JWT를 사용하여 세션/폼을 사용하지 않았다.)</p>
<h2 id="분석">분석</h2>
<h3 id="spring-security-인증-흐름-표준-ver">Spring Security 인증 흐름 (표준 Ver.)</h3>
<p>먼저, 정석적인 Spring Security의 인증 흐름이 무엇인지 보겠다. (JWT가 아닌 세션/폼 로그인 기준)</p>
<ol>
<li>사용자가 아이디/비밀번호를 Form으로 전송</li>
<li><code>AuthenticationFilter</code>가 <code>UsernamePasswordAuthenticationToken</code>을 생성해 <code>AuthenticationManager</code>에게 전달 (이때 토큰의 <code>principal</code>은 사용자가 입력한 <strong>아이디(String)</strong>)</li>
<li><code>AuthenticationManager</code>는 등록된 <code>AuthenticationProvider</code>에게 인증 위임</li>
<li><code>AuthenticationProvider</code>는 <code>UserDetailsService</code>를 통해 DB에서 사용자 정보 조회 (<code>loadUserByUsername()</code>)</li>
<li><code>UserDetailsService</code>는 조회된 정보를 바탕으로 <strong><code>UserDetails</code> 객체를 생성후 반환</strong></li>
<li><code>AuthenticationProvider</code>는 반환된 <code>UserDetails</code>와 입력된 비밀번호를 비교하여 인증에 성공하면, <strong>&#39;인증된&#39; <code>UsernamePasswordAuthenticationToken</code>을 새로 생성</strong> <code>AuthenticationManager</code>에게 돌려줌</li>
<li><code>AuthenticationManager</code>는 이 토큰을 <code>AuthenticationFilter</code>로 반환</li>
<li><code>AuthenticationFilter</code>는 최종적으로 인증된 토큰을 <strong><code>SecurityContextHolder</code>에 저장</strong></li>
</ol>
<p>🤔사실 이 부분에서 궁금증이 시작되었다. 5번에서 <code>UserDetails</code>를 만들었는데, 8번에서는 <code>Authentication</code> 토큰을 저장합니다. <code>UserDetails</code>를 저장하는 방식은 잘못된건가?</p>
<h3 id="의문의-답-authentication-객체의-구조">의문의 답: Authentication 객체의 구조</h3>
<p>답은 6번 단계에서 새로 생성되는 <strong>&#39;인증된&#39; <code>Authentication</code> 객체</strong>의 구조에 있었다. <code>Authentication</code> 인터페이스의 정의를 보면 내부에 인증의 주체, 즉 <strong>사용자 정보(<code>Principal</code>)</strong>를 담을 수 있는 <code>getPrincipal()</code> 메서드를 가지고 있고 Spring Security는 인증에 성공하면, <code>UserDetailsService</code>가 반환한 바로 그 <strong><code>UserDetails</code> 객체를 이 <code>principal</code> 필드에 담아서</strong> 새로운 <code>Authentication</code> 객체를 만드는 것이었다. </p>
<pre><code class="language-java">public interface Authentication extends Principal, Serializable {
    // ... 다른 메서드들 ...

    // ⭐⭐⭐ 바로 이 녀석! ⭐⭐⭐
    // 이 인증의 주체(Principal)는 누구인가?
    Object getPrincipal();
}</code></pre>
<pre><code class="language-java">// 6번 단계에서 일어나는 일 (개념적인 코드)
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

// 비밀번호 검증 후...

// &#39;인증된&#39; 토큰을 새로 생성한다!
// 첫 번째 인자(principal)로 userDetails 객체를 넣어준다.
Authentication authenticatedToken = new UsernamePasswordAuthenticationToken(
    userDetails, // ⭐ 여기에 UserDetails가 쏙!
    null,        // 더 이상 비밀번호는 필요 없으므로 null 처리
    userDetails.getAuthorities() // 권한 정보
);</code></pre>
<p>결국 <code>SecurityContextHolder</code>에 저장되는 것은 <code>Authentication</code> 객체가 맞지만, 그 객체는 <strong><code>principal</code>이라는 이름으로 <code>UserDetails</code>를 품고 있는 것이다.</strong> </p>
<h3 id="authenticationprincipal은-어떻게-동작하는가">@AuthenticationPrincipal은 어떻게 동작하는가?</h3>
<p>컨트롤러에서 사용한 <code>@AuthenticationPrincipal</code>은 어떻게 <code>UserDetails</code>를 바로 꺼내주는 걸까? ❓</p>
<p><code>@AuthenticationPrincipal CustomUserDetails userDetails</code></p>
<p>이 어노테이션은 Spring이 자동으로 처리해주는 <strong>&#39;편의 기능(Syntactic Sugar)&#39;</strong> 이다. </p>
<ol>
<li><code>SecurityContextHolder</code>에서 현재 스레드의 <code>SecurityContext</code>를 가져온다. (<code>.getContext()</code>)</li>
<li><code>SecurityContext</code>에서 <code>Authentication</code> 객체를 꺼낸다. (<code>.getAuthentication()</code>)</li>
<li><code>Authentication</code> 객체에서 <code>Principal</code> 객체를 꺼낸다. (<code>.getPrincipal()</code>)</li>
<li>꺼내온 <code>Principal</code> 객체를 파라미터에 선언된 <code>CustomUserDetails</code> 타입으로 안전하게 형변환하여 주입해준다.</li>
</ol>
<p>이 모든 과정을 어노테이션 하나로 압축했기 때문에, 마치 <code>UserDetails</code>가 <code>SecurityContext</code>에 직접 저장된 것처럼 느꼈고 의문을 제기하게 되었다. </p>
<h2 id="결론">결론</h2>
<ul>
<li><code>SecurityContextHolder</code>는 <code>Authentication</code> 객체를 저장한다.</li>
<li>인증된 <code>Authentication</code> 객체는 <code>principal</code> 필드에 <code>UserDetails</code> 객체를 품고 있다.</li>
<li><code>@AuthenticationPrincipal</code>은 <code>Authentication</code> 객체 속 <code>UserDetails</code>를 편리하게 꺼내주는 마법 같은 어노테이션이다.</li>
</ul>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><p><a href="https://jeonyoungho.github.io/posts/Spring-Security-%EA%B0%9C%EC%9A%94-%EB%B0%8F-%EB%8F%99%EC%9E%91%EA%B3%BC%EC%A0%95/">https://jeonyoungho.github.io/posts/Spring-Security-%EA%B0%9C%EC%9A%94-%EB%B0%8F-%EB%8F%99%EC%9E%91%EA%B3%BC%EC%A0%95/</a></p>
</li>
<li><p><a href="https://wildeveloperetrain.tistory.com/324">https://wildeveloperetrain.tistory.com/324</a></p>
</li>
<li><p><a href="https://www.inflearn.com/community/questions/937560/authenticationprincipal-%EC%82%AC%EC%9A%A9%EC%9D%B4-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%9D%B4%EC%9C%A0%EA%B0%80?srsltid=AfmBOoqp0WlaTJD5iXzRHwkivhhtC7t2F6VgWfQby79G6JrkByCBbgci">https://www.inflearn.com/community/questions/937560/authenticationprincipal-%EC%82%AC%EC%9A%A9%EC%9D%B4-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%9D%B4%EC%9C%A0%EA%B0%80?srsltid=AfmBOoqp0WlaTJD5iXzRHwkivhhtC7t2F6VgWfQby79G6JrkByCBbgci</a></p>
</li>
<li><p><a href="https://velog.io/@yuureru/Spring-Security-%EB%8F%99%EC%9E%91%EA%B3%BC%EC%A0%95-%EA%B5%AC%EC%A1%B0">https://velog.io/@yuureru/Spring-Security-%EB%8F%99%EC%9E%91%EA%B3%BC%EC%A0%95-%EA%B5%AC%EC%A1%B0</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[PR AI 비교: Gemini Code Assist vs. CodeRabbit]]></title>
            <link>https://velog.io/@devel-history/PR-AI-Gemini%EB%83%90-Coderabbit%EB%83%90-%EA%B7%B8%EA%B2%83%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EB%8B%A4</link>
            <guid>https://velog.io/@devel-history/PR-AI-Gemini%EB%83%90-Coderabbit%EB%83%90-%EA%B7%B8%EA%B2%83%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EB%8B%A4</guid>
            <pubDate>Wed, 08 Oct 2025 05:58:17 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<ul>
<li><strong>현황:</strong> 구름 딥다이브 3차 동료들과 사이드 프로젝트를 진행 중입니다.</li>
<li><strong>기존 문제:</strong> 기존에 사용하던 <strong>CodeRabbit</strong>은 생성하는 리뷰 코멘트의 양이 많아 <strong>가독성이 저해</strong>되고 피로도가 높다는 의견이 있었습니다.</li>
<li><strong>대안 검토:</strong> 다른 팀이 사용 중인 <strong>Gemini Code Assist</strong>는 리뷰가 비교적 짧고 간결하다는 정보를 얻었습니다.</li>
<li><strong>목표:</strong> 팀 차원에서 최적의 AI 리뷰어를 선택하기 위해 두 도구를 비교 분석합니다.</li>
</ul>
<h2 id="ai-코드-리뷰어-소개">AI 코드 리뷰어 소개</h2>
<h3 id="gemini-code-assist">Gemini Code Assist</h3>
<p><strong>개발 주체</strong></p>
<ul>
<li>Google (구글)</li>
<li><a href="https://developers.google.com/gemini-code-assist/docs/overview?hl=ko">공식 문서 바로가기</a></li>
</ul>
<p><strong>AI 모델</strong></p>
<ul>
<li>Gemini 2.5 (최신 Gemini 계열 LLM, 구글 파운데이션 모델)</li>
</ul>
<p><strong>동작 방식</strong></p>
<ol>
<li><strong>컨텍스트 수집:</strong> PR 변경 내역(diff), 제목·설명, 프로젝트 관련 파일 등을 선별적으로 수집, <strong>1M 토큰</strong> 내 컨텍스트 구성</li>
<li><strong>AI 분석 및 피드백 생성:</strong> Gemini 모델이 코드 품질, 보안, 성능, 스타일을 점검.</li>
<li><strong>결과 출력 (Output to PR):</strong> 요약 및 인라인 코멘트를 PR에 자동 게시.</li>
</ol>
<p><strong>학습 데이터</strong> (공식적으로 공개된 범위)</p>
<ul>
<li>공개 GitHub 코드 일부</li>
<li>Google 내부 기술 문서, Google Cloud 관련 자료</li>
<li>사용자가 설치 시 허용한 저장소 코드(프로젝트 코드)</li>
</ul>
<blockquote>
<p>🔍 근거: Gemini Code Assist 문서에 “codebase awareness up to 1M tokens” 및 “custom style guides 적용 가능” 명시됨.<br>출처: <a href="https://developers.google.com/gemini-code-assist/resources/quotas?utm_source=chatgpt.com">developers.google.com</a>, <a href="https://blog.google/technology/developers/gemini-code-assist-free/?utm_source=chatgpt.com">blog.google</a></p>
</blockquote>
<p><strong>특징 &amp; 장점</strong></p>
<ul>
<li><strong>대규모 컨텍스트:</strong> 최대 약 1,000,000 토큰 컨텍스트 윈도우 활용 (무료 버전&lt;개인용&gt;은 128,000 토큰).</li>
<li><strong>실시간 코드 보조:</strong> 오류 탐지, 함수 제안, 테스트/디버깅 지원.</li>
<li><strong>보안·컴플라이언스:</strong> Google Cloud 기반 엔터프라이즈 보안 체계 제공.</li>
</ul>
<p><strong>운영 현황 (2025년 10월 기준)</strong></p>
<ul>
<li>AI PR 리뷰 분야 3위
<img src="https://velog.velcdn.com/images/devel-history/post/716bd5ba-7367-42e9-9b66-6fc5ae86af6d/image.png" alt="Gemini 랭킹 예시"></li>
<li>Google Cloud, Android Studio 기반 개발팀에서 주로 사용 추정</li>
</ul>
<h3 id="coderabbit">CodeRabbit</h3>
<p><strong>개발 주체</strong></p>
<ul>
<li>CodeRabbit Inc.</li>
<li><a href="https://github.com/coderabbitai">오픈소스 저장소 바로가기</a></li>
</ul>
<p><strong>AI 모델</strong></p>
<ul>
<li>최신 LLM(대규모 언어 모델) 기반</li>
<li>구체적 모델명은 비공개 (최신 GPT 계열 추정)</li>
<li><a href="https://docs.coderabbit.ai/overview/why-coderabbit">공식 문서 참고</a></li>
</ul>
<p><strong>동작 방식</strong></p>
<ol>
<li><strong>컨텍스트 엔지니어링:</strong> 맥락 및 프로젝트 구조 전반 수집</li>
<li><strong>코드 그래프 분석:</strong> 시스템 전체 연결·의존성 파악</li>
<li><strong>리뷰 에이전트:</strong> AI가 리뷰 코멘트 및 개선안 생성</li>
<li><strong>검증 에이전트:</strong> 노이즈 제거, 실제 반영 가능한 제안만 필터링</li>
<li><strong>결과 출력 (Output to PR):</strong> 최종 리뷰를 PR에 자동 등록
<img src="https://velog.velcdn.com/images/devel-history/post/ed263ab8-dce8-40c7-80ed-49db3fae506a/image.jpeg" alt=""></li>
</ol>
<p><strong>학습 데이터</strong></p>
<ul>
<li>공개 GitHub 코드</li>
<li>자체 플랫폼에서 수집된 코드 리뷰 데이터</li>
<li>사용자 프로젝트 코드 (레포를 샌드박스에 복제 후 분석)</li>
</ul>
<blockquote>
<p>🔍 근거: CodeRabbit 공식 문서에서 “sandboxed repository clone” 및 “integrated linters/SAST” 언급.<br>출처: <a href="https://docs.coderabbit.ai/overview/why-coderabbit">docs.coderabbit.ai</a></p>
</blockquote>
<p><strong>특징 &amp; 장점</strong></p>
<ul>
<li>컨텍스트 엔지니어링 기반 정밀 분석</li>
<li>린터·SAST 결과 반영 → 구조화된 보안 점검 가능</li>
<li>자동 생성 다이어그램 제공 (코드 관계 시각화)</li>
<li>“샌드박스 환경”에서 코드 실행 후 리뷰 제공</li>
</ul>
<p><strong>운영 현황 (2025년 10월 기준)</strong></p>
<ul>
<li>AI PR 리뷰 분야 1위
<img src="https://velog.velcdn.com/images/devel-history/post/716bd5ba-7367-42e9-9b66-6fc5ae86af6d/image.png" alt="CodeRabbit 랭킹 예시"></li>
<li>2025년 1월 보안 사고 발생 (현재 취약점 수정 완료)</li>
<li>다양한 스타트업·팀에서 활발히 사용 중</li>
</ul>
<hr>
<h2 id="비교-요약">비교 요약</h2>
<p>가격 비교는 제외했음. (사이드 프로젝트에서는 무료 버전 사용 예정)</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Gemini Code Assist</th>
<th>CodeRabbit</th>
</tr>
</thead>
<tbody><tr>
<td><strong>개발사</strong></td>
<td>Google</td>
<td>CodeRabbit Inc.</td>
</tr>
<tr>
<td><strong>공식 모델</strong></td>
<td>Gemini 2.5</td>
<td>비공개 (GPT 계열 추정)</td>
</tr>
<tr>
<td><strong>컨텍스트 처리</strong></td>
<td>최대 1M 토큰 컨텍스트 윈도우</td>
<td>컨텍스트 엔지니어링 + 코드 그래프 분석</td>
</tr>
<tr>
<td><strong>리뷰 강점</strong></td>
<td>짧고 간결한 리뷰, Google Cloud 통합</td>
<td>정밀 분석, 린터/SAST 통합, 다이어그램</td>
</tr>
<tr>
<td><strong>출력 방식</strong></td>
<td>요약 + 인라인 주석</td>
<td>요약 + 인라인 주석 + 시각 자료</td>
</tr>
<tr>
<td><strong>보안</strong></td>
<td>Google Cloud 엔터프라이즈 체계</td>
<td>샌드박스 기반 분석 (과거 보안 사고 존재)</td>
</tr>
<tr>
<td><strong>시장 현황</strong></td>
<td>Google 생태계 중심 확산</td>
<td>오픈소스 및 스타트업 중심 확산</td>
</tr>
</tbody></table>
<hr>
<h2 id="사용-후기">사용 후기</h2>
<p>조사하면서 흥미로웠던 점은, 일부 글에서 Gemini code assist무료 버전(개인용)은 128,000 토큰으로 제공한다는 것이다. 하지만 사이드 프로젝트에서는 큰 제약이 되지 않으므로 문제 없다고 판단했다.  </p>
<h3 id="사용-후기-사이트">사용 후기 사이트</h3>
<p><strong>Gemini Code Assist</strong>  </p>
<ul>
<li><a href="https://thenewstack.io/gemini-code-assist-review-code-completions-need-improvement/?utm_source=chatgpt.com">Gemini Code Assist Review: Code Completions Need Improvement (The New Stack)</a>  </li>
<li><a href="https://fornewchallenge.tistory.com/entry/%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8Gemini-Code-Assist-AI-%EC%BD%94%EB%94%A9-%EB%8F%84%EA%B5%AC%EB%A1%9C-%EC%83%9D%EC%82%B0%EC%84%B1%EC%9D%84-%EA%B7%B9%EB%8C%80%ED%99%94%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95">Gemini Code Assist: AI 코딩 도구로 생산성을 극대화하는 법 (Tistory 블로그)</a>  </li>
<li><a href="https://tilnote.io/pages/68848ab165132c82f720d2ec">Gemini Code Assist 기능 및 요금제 안내 (TILNote)</a>  </li>
</ul>
<p><strong>CodeRabbit</strong>  </p>
<ul>
<li><a href="https://apidog.com/kr/blog/coderabbit-review-kr/">AI 코드 리뷰 도구 CodeRabbit 사용 후기 및 장단점 분석 (APIDog)</a>  </li>
<li><a href="https://www.reddit.com/r/ChatGPTCoding/comments/15qamnx/coderabbit_code_review_github_prs_with_gpt4/?utm_source=chatgpt.com">CodeRabbit: Code review GitHub PRs with GPT-4 (Reddit)</a>  </li>
<li><a href="https://samkong-dev.tistory.com/entry/%EC%B5%9C%EA%B3%A0%EC%9D%98-Flutter-AI-pr-reviewer%EB%A5%BC-%EC%B0%BE%EC%95%84%EC%84%9C">최고의 Flutter AI PR Reviewer를 찾아서 (Tistory 블로그)</a>  </li>
</ul>
<hr>
<h2 id="추가-비교-사용-후기-요약">추가 비교 (사용 후기 요약)</h2>
<ul>
<li><p><strong>코멘트 길이/양</strong>  </p>
<ul>
<li>Gemini: 간결하고 요점 집중 → 협업 시 가독성 좋음  </li>
<li>CodeRabbit: 리뷰가 많고 세밀 → 도움이 되지만 피로감 유발  </li>
</ul>
</li>
<li><p><strong>정확성/품질</strong>  </p>
<ul>
<li>Gemini: 전반적으로 무난, 일부 누락 있음  </li>
<li>CodeRabbit: 꼼꼼하고 정밀, 품질 일관  </li>
</ul>
</li>
<li><p><strong>특화 기능</strong>  </p>
<ul>
<li>Gemini: Google Cloud·Android 생태계 연동, 대규모 컨텍스트 처리  </li>
<li>CodeRabbit: 다이어그램/그래프 제공, 고도화된 SAST/린터 통합  </li>
</ul>
</li>
<li><p><strong>협업/속도</strong>  </p>
<ul>
<li>Gemini: 빠른 반영, 개인·팀 환경에 부드럽게 연동  </li>
<li>CodeRabbit: 즉각 리뷰 제공, 협업 최적화  </li>
</ul>
</li>
<li><p><strong>단점</strong>  </p>
<ul>
<li>Gemini: 일부 답변 누락, 세밀한 이슈 캐치는 부족  </li>
<li>CodeRabbit: 정보량 과다, 초기 셋팅 오류, 과거 보안 취약점 이력  </li>
</ul>
</li>
</ul>
<hr>
<h2 id="결론">결론</h2>
<p>기존에 사용하던 CodeRabbit은 리뷰가 과도하게 많아 불편함이 있었고, 이 때문에 Gemini를 고려하게 되었다. 조사 과정에서 알게 된 점은 <strong>CodeRabbit은 자체 리뷰를 다른 AI로 재검증</strong>하지만, Gemini는 그렇지 않다는 것이다. 아마 이 차이가 리뷰 양의 차이를 만드는 원인일 것이다.  </p>
<p>아직 어떤 도구를 선택해야 할지는 확신이 서지 않는다. 두 서비스 모두 훌륭하다.<br>개인적으로는 <strong>CodeRabbit</strong>을 선호한다. 다이어그램 제공 덕분에 팀원이 코드를 이해하는 데 도움이 되고, 과도한 리뷰를 통해 오히려 놓칠 수 있는 부분을 되돌아보게 만들기 때문이다.</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://turingpost.co.kr/p/coderabbit-context-engineering">[전문가 기고] &#39;AI 기반 코드 리뷰&#39;를 혁신하는 컨텍스트 엔지니어링의 비밀</a></li>
<li><a href="https://yozm.wishket.com/magazine/detail/3233/">커서/코드레빗 개발팀이 말하는 AI 코딩 도구 잘 쓰는 법</a></li>
<li><a href="https://tilnote.io/pages/68a56c77fba69b6fc5be7e83">AI 코드 리뷰 도구 CodeRabbit에서 발견된 치명적 보안 취약점과 그 해결 과정</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[<모여행> k6를 선택한 이유 & 성능 테스트의 중요성 ]]></title>
            <link>https://velog.io/@devel-history/%EB%AA%A8%EC%97%AC%ED%96%89-k6%EB%A5%BC-%EC%84%A0%ED%83%9D%ED%95%9C-%EC%9D%B4%EC%9C%A0-%EC%84%B1%EB%8A%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1</link>
            <guid>https://velog.io/@devel-history/%EB%AA%A8%EC%97%AC%ED%96%89-k6%EB%A5%BC-%EC%84%A0%ED%83%9D%ED%95%9C-%EC%9D%B4%EC%9C%A0-%EC%84%B1%EB%8A%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1</guid>
            <pubDate>Sat, 27 Sep 2025 08:41:00 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>이 문서는 왜 성능 테스트 툴로 k6를 선택 했는지와 성능 테스가 무엇인지 기술한 글 입니다. </p>
<h2 id="성능-테스트란">성능 테스트란?</h2>
<h3 id="성능-테스트-정의">성능 테스트 정의</h3>
<p><strong>성능 테스트(Performance Testing)</strong>는 소프트웨어나 시스템이 특정 작업 부하에서 얼마나 빠르게, 그리고 안정적으로 동작하는지를 평가하는 모든 테스트 활동을 총칭합니다. 단순히 기능이 올바르게 작동하는지를 확인하는 것을 넘어, 시스템의 속도, 확장성, 안정성을 측정하는 것이 핵심 (광범위하게 사용되기에 성능 테스트 범위와 함께 설명)</p>
<h3 id="성능-테스트의-범위">성능 테스트의 범위</h3>
<p>성능 테스트는 단순히 시스템의 &#39;속도&#39;만 측정하는 것이 아니라, 다양한 측면에서 시스템의 한계와 효율성을 평가합니다. 대표적인 테스트 유형은 다음과 같습니다.</p>
<p><strong>부하 테스트(Load Testing)</strong>: 시스템이 예상되는 정상적인 사용자 수와 트랜잭션 양을 처리할 수 있는지 확인합니다. 예를 들어, 웹사이트에 동시에 1,000명의 사용자가 접속했을 때 응답 시간이 적절한지 테스트합니다.</p>
<p><strong>스트레스 테스트(Stress Testing)</strong>: 시스템이 최대 부하 용량을 초과하는 비정상적인 상황에서 어떻게 동작하는지 확인합니다. 시스템이 갑작스러운 트래픽 폭증에 무너지지 않고, 복구 능력을 갖추고 있는지 검증합니다.</p>
<p><strong>내구성 테스트(Endurance Testing)</strong>: 시스템이 장시간 동안 지속적인 부하를 견딜 수 있는지 확인합니다. 메모리 누수나 성능 저하 없이 안정적으로 운영되는지 테스트합니다.</p>
<p><strong>볼륨 테스트(Volume Testing)</strong>: 대량의 데이터(데이터베이스, 파일 등)를 처리할 때 시스템의 성능 저하가 발생하는지 확인합니다.</p>
<h3 id="성능테스트-지표와-예시">성능테스트 지표와 예시</h3>
<p><strong>응답 시간(Response Time)</strong>: 사용자가 요청을 보낸 시점부터 응답을 받기까지 걸리는 시간</p>
<p>예시: &quot;상품 구매&quot; 요청에 대한 평균 응답 시간은 5초, 이는 사용자가 상품을 구매하기 위해 5초를 기다려야 한다는 의미로, 사용자 경험이 부정적이라는 것을 유추할 수 있습니다.</p>
<p><strong>TPS (Transactions Per Second)</strong>: 시스템이 1초 동안 처리할 수 있는 트랜잭션(요청)의 수</p>
<p>예시: 초당 100건의 구매 요청을 처리할 수 있는 시스템있고 만약 이벤트로 인해 초당 200건의 요청이 몰리면, 시스템은 정상적으로 처리하지 못하고 응답 시간이 지연되거나 오류가 발생할거라고 예측 가능합니다.</p>
<p><strong>오류율(Error Rate)</strong>: 테스트 도중 발생하는 오류의 비율</p>
<p>예시: 부하가 증가함에 따라 오류율이 5%로 상승했습니다. 이는 과부하 상황에서 시스템의 안정성이 저하되어 요청을 제대로 처리하지 못하고 있다는 것을 의미합니다.</p>
<p><strong>CPU/메모리 사용량</strong>: 시스템 자원(CPU, 메모리)의 사용률</p>
<p>예시: 동시 접속자 수가 500명을 초과하자, CPU 사용량이 90%까지 치솟는다면 이는 시스템이 처리할 수 있는 부하의 한계에 도달했음을 보여주는 병목 지표입니다.</p>
<p><strong>동시 사용자 수(Concurrent Users)</strong>: 시스템이 성능 저하 없이 동시에 처리할 수 있는 최대 사용자 수</p>
<p>예시: A 시스템은 1,000명의 동시 사용자까지는 응답 시간 2초 이내를 유지하지만, 1,200명이 접속하는 순간부터 응답 시간이 급격히 늘어납니다.</p>
<h3 id="성능-테스트의-중요성">성능 테스트의 중요성</h3>
<ol>
<li><p><strong>사용자 경험(UX) 증진 및 이탈률 감소</strong>
대표적으러 2초의 법칙과 3초의 마지노선 이론에 따르면 사용자는 웹페이지 이용시 2~3초라는 시간이 지나면 시비스 이탈률이 급격히 증가한다고 합니다. 성능 테스트는 이러한 잠재적 문제를 사전에 파악하고 해결하여, 사용자들이 빠르고 쾌적한 환경에서 서비스를 이용할 것이라고 예측할 수 있도록 도와줍니다. </p>
</li>
<li><p><strong>비즈니스 손실 예방</strong>
예측하지 못한 트래픽 폭증으로 시스템이 마비 또는 지연된다면, 그 기간 동안의 모든 비즈니스 활동(예: 온라인 판매, 서비스 제공)이 중단됩니다. 이는 곧바로 매출 손실, 기업 이미지 실추, 그리고 고객 신뢰도 하락으로 이어집니다. 성능 테스트는 시스템의 한계점을 미리 파악하고, 잠재적인 위험 상황에 대비하여 비즈니스 연속성을 확보할 수 있도록 도와줍니다. </p>
</li>
<li><p><strong>시스템의 안정성과 확장성 확보</strong>
성능 테스트를 통해 얻은 지표(CPU/메모리 사용량, 오류율 등)는 시스템의 병목 현상과 잠재적 오류를 발견하는 데 도움을 줍니다. 이러한 데이터를 바탕으로 시스템을 최적화하면, 더 적은 자원으로도 더 많은 부하를 효율적으로 처리할 수 있습니다. 이는 비용 효율적인 시스템 운영을 가능하게 하며, 향후 더 많은 사용자를 수용하기 위한 확장 계획을 수립하는데 도움을 줍니다. </p>
</li>
</ol>
<hr>
<h2 id="성능-테스트-툴">성능 테스트 툴</h2>
<p><a href="https://velog.io/@dongvelop/%EC%84%B1%EB%8A%A5%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%88%B4-%EC%86%8C%EA%B0%9C">성능 테스트 툴 소개</a>에서 내용을 스크립하였습니다. </p>
<h3 id="apache-jmeter">Apache JMeter</h3>
<p>Apache JMeter는 강력한 성능 테스트 도구로, 다양한 프로토콜을 지원한다.</p>
<p><strong>장점 및 특징</strong></p>
<ul>
<li>다양한 프로토콜 지원:  HTTP, HTTPS, FTP, JDBC, JMS, LDAP, SOAP, TCP 등 다양한 프로토콜 지원</li>
<li>확장성:  플러그인을 통해 기능을 확장 가능</li>
<li>분산 테스트: 여러 머신에서 테스트를 실행하여 부하를 분산할 수 있다.</li>
<li>강력한 GUI: 사용자 친화적인 GUI로 테스트 계획을 쉽게 작성할 수 있다.</li>
<li>스크립트 지원: BeanShell, Groovy 등의 스크립트를 사용하여 복잡한 테스트 시나리오를 작성할 수 있다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>고사양 요구: 많은 리소스를 소모하므로 고사양의 하드웨어가 필요할 수 있다.</li>
<li>복잡한 설정: 고급 기능을 사용하려면 복잡한 설정과 스크립팅이 필요할 수 있다.</li>
<li>실시간 모니터링 제약: 실시간으로 대규모 테스트를 모니터링하는 데 제약이 있을 수 있다.
러닝 커브 (기본적인 사용은 쉽지만, 고급 기능을 익히는 데는 시간이 필요함)</li>
</ul>
<h3 id="gatling">Gatling</h3>
<p>실시간 그래프와 함께 상세한 보고서를 제공하는 유료 성능 테스트 도구 (JDK 8 이상이 요구)</p>
<p><strong>장점</strong></p>
<ul>
<li>고성능: 비동기 방식으로 많은 사용자 요청을 처리할 수 있다.</li>
<li>확장성: Scala를 기반으로 스크립트를 작성하여 복잡한 테스트 시나리오를 구현할 수 있다.</li>
<li>실시간 보고: 테스트 진행 중 실시간으로 성능 지표를 모니터링할 수 있다.</li>
<li>자동 보고서 생성: 테스트가 끝나면 상세한 HTML 보고서를 자동으로 생성한다.</li>
<li>DevOps 친화적: 코드로 시나리오를 작성하기 때문에 버전 관리 시스템과 CI/CD 파이프라인에 통합이 용이</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>유료 정책</li>
<li>러닝 커브: Scala 언어에 익숙하지 않은 사용자는 시나리오 작성이 어려울 수 있다.</li>
<li>복잡한 설정: 고급 기능을 사용하기 위해서는 복잡한 설정과 코드 작성이 필요할 수 있다.</li>
<li>자원 요구: 대규모 테스트를 실행할 때는 높은 하드웨어 자원이 필요할 수 있다.</li>
</ul>
<h3 id="k6">K6</h3>
<p>성능 테스트 스크립트를 작성하고 실행할 수 있는 오픈 소스 도구</p>
<p><strong>장점</strong></p>
<ul>
<li>CLI 도구: 커맨드 라인 인터페이스(CLI)를 통해 쉽게 테스트를 실행하고 결과를 확인할 수 있다.</li>
<li>고성능: 단일 프로세스로 수천 개의 가상 사용자를 처리할 수 있다.</li>
<li>확장성: Grafana와 같은 모니터링 도구와 쉽게 통합할 수 있다.</li>
<li>DevOps 친화적: CI/CD 파이프라인에 통합하여 자동화된 테스트 환경을 구축하기 용이하다.</li>
<li>경량화: 비교적 가벼운 도구로, 빠르고 간편하게 성능 테스트를 수행할 수 있다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>고급 기능 한계: JMeter나 Gatling에 비해 고급 기능이 부족할 수 있다.</li>
<li>제한된 프로토콜 지원: HTTP/HTTPS 프로토콜 위주로 지원
다양한 프로토콜을 필요로 하는 경우 한계가 있을 수 있다.</li>
<li>러닝 커브: JavaScript에 익숙하지 않은 사용자는 초기 학습이 필요할 수 있다.</li>
</ul>
<h3 id="locust">Locust</h3>
<p><strong>장점</strong></p>
<ul>
<li>실시간 피드백: 웹 인터페이스에서 실시간으로 결과를 확인할 수 있어 테스트 진행 상황을 쉽게 모니터링할 수 있다.</li>
<li>확장성: 분산 로드를 통해 대규모 부하 테스트가 가능하다.</li>
<li>커스터마이즈 가능: Python의 강력한 기능을 활용하여 복잡한 시나리오를 쉽게 작성할 수 있다.</li>
<li>경량화: 간편한 설치와 사용으로 빠르게 테스트를 시작할 수 있다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>고급 기능 한계: JMeter와 같은 도구에 비해 고급 기능이 부족할 수 있다.
성능 제한: 대규모 테스트에서는 성능 제한이 있을 수 있으며, 분산 로드를 필요로 할 수 있다.
Python 의존성: Python에 익숙하지 않은 사용자는 초기 학습이 필요할 수 있다.</li>
</ul>
<h2 id="k6를-선택한-이유">k6를 선택한 이유</h2>
<h3 id="테스트-툴-희망조건">테스트 툴 희망조건</h3>
<ol>
<li>가볍운 성능 (로컬에서 돌리기 위해서) </li>
<li>직관적인 사용 방법</li>
<li>테스트 방법과 결과 공유가 간편한 것</li>
<li>학습 곡선이 낮을 것</li>
</ol>
<h3 id="k6를-선택한-이유-1">k6를 선택한 이유</h3>
<ol>
<li>가벼운 성능 (로컬 테스트): K6는 Go 언어로 개발되어 매우 경량화되어 있고, 다른 툴들 많은 자원을 소모하는 것과 달리, K6는 비교적 적은 리소스로 테스트 가능 합니다.</li>
</ol>
<blockquote>
<p><a href="https://grafana.com/blog/2021/01/27/k6-vs-jmeter-comparison/">(grafana) Comparing k6 and JMeter for load testing</a> 에서 제공한 결과입니다. 표와 같이 K6는 비교했던 테스트 툴들에 비해서 메모리 사용량이 적으면서 RTS가 많은 것을 볼 수 있습니다. 
<img src="https://velog.velcdn.com/images/devel-history/post/d2f2ceb6-5a2e-4b51-8ff7-1b743ec13983/image.png" alt="">
<strong>RTS</strong>: Requests Per Second는 &#39;초당 요청 수&#39;를 의미합니다. 즉, 1초 동안 시스템이 처리할 수 있는 요청의 수를 나타내는 성능 지표</p>
</blockquote>
<ol start="2">
<li><p>직관적인 사용 방법 및 쉬운 공유: K6는 CLI 기반의 도구로, 복잡한 GUI 없이 명령어를 통해 테스트를 실행하고 결과를 즉시 확인할 수 있습니다. 테스트 결과를 JSON 형태로 출력할 수 있어, 이를 Jira나 Confluence 같은 협업 도구에 쉽게 첨부하여 팀원들과의 공유를 간편하게 만들 수 있습니다.</p>
</li>
<li><p>낮은 학습 곡선: K6는 자바스크립트(JavaScript) 기반으로 스크립트를 작성합니다. 이는 대다수의 웹 개발자에게 익숙한 언어이므로, 별도의 언어 학습(예: Gatling의 Scala, Locust의 Python) 없이도 팀원들이 빠르게 테스트 스크립트를 작성하고 유지보수할 수 있습니다. 이는 테스트 도입의 초기 장벽을 크게 낮춥니다.</p>
</li>
</ol>
<h2 id="결론">결론</h2>
<p>성능 테스트는 개발의 마지막 단계에서 시스템의 안정성과 비즈니스 가치를 최종적으로 검증하는 중요한 절차입니다. &lt;모여행&gt;은 모든 기능 구현 이후, 최종적인 시스템의 완성도를 높이기 위해 k6를 활용하여 아래 3가지를 목표를 수행하여 더 좋은 서비스를 개발하겠습니다. </p>
<p><strong>초기 목표</strong>: MVP 기능(항공권 검색/예매)에 대해 응답 시간 3초 이내, 오류율 1% 미만을 목표로 부하 테스트를 진행하겠습니다.</p>
<p><strong>테스트 자동화</strong>: 향후 CI/CD 파이프라인에 K6를 연동하여 코드 배포 시마다 자동으로 성능 회귀 테스트를 수행하도록 하겠습니다.</p>
<p><strong>지속적 관리</strong>: Grafana와 연동하여 주요 지표를 지속적으로 모니터링하고, 성능 저하가 감지될 경우 선제적으로 대응하겠습니다.</p>
<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://velog.io/@dongvelop/%EC%84%B1%EB%8A%A5%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%88%B4-%EC%86%8C%EA%B0%9C">(velog) 성능테스트 툴 소개
</a></p>
<p><a href="https://velog.io/@eastperson/Spring-Boot-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-K6-Grafana%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%B4%EB%B3%B4%EA%B8%B0">(velog)Spring Boot로 K6 &amp; Grafana를 활용한 부하테스트 해보기</a></p>
<p><a href="https://f-lab.kr/blog/importance%20of%20performance%20Testing">(f-lab) 성능 테스트의 중요성과 목적 그리고 효과
</a></p>
<p><a href="https://f-lab.kr/blog/importance%20of%20performance%20Testing">(f-lab) 성능 테스트 중요성</a></p>
<p><a href="https://tech.kakao.com/posts/679">(kakao-tech) 실시간 메시징 시스템 개발 - “성능 테스트 설계와 분석”
</a></p>
<p><a href="https://baeji-develop.tistory.com/118">성능테스트 (부하테스트 도구 비교) - jmeter, k6, ngrinder, locust</a>
<a href="https://web.dev/case-studies/t-mobile-case-study?hl=en">https://web.dev/case-studies/t-mobile-case-study?hl=en</a></p>
<p><a href="https://loosie.tistory.com/821">https://loosie.tistory.com/821</a></p>
<p><a href="https://www.mabl.com/articles/what-is-performance-testing">https://www.mabl.com/articles/what-is-performance-testing</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[구름 모여행 테스트 전략 문서]]></title>
            <link>https://velog.io/@devel-history/%EA%B5%AC%EB%A6%84-%EB%AA%A8%EC%97%AC%ED%96%89-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A0%84%EB%9E%B5-%EB%AC%B8%EC%84%9C</link>
            <guid>https://velog.io/@devel-history/%EA%B5%AC%EB%A6%84-%EB%AA%A8%EC%97%AC%ED%96%89-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A0%84%EB%9E%B5-%EB%AC%B8%EC%84%9C</guid>
            <pubDate>Wed, 24 Sep 2025 08:06:11 GMT</pubDate>
            <description><![CDATA[<h1 id="모여행-백엔드-테스트-전략-문서">모여행 백엔드 테스트 전략 문서</h1>
<h2 id="개요">개요</h2>
<p>이 문서는 모여행 백엔드 프로젝트의 테스트 전략을 정의합니다. 우리의 목표는 높은 품질의 코드를 유지하고, 버그를 조기에 발견하며, 리팩토링을 자신있게 수행할 수 있는 환경을 만드는 것입니다.</p>
<h3 id="테스트의-중요성">테스트의 중요성</h3>
<ul>
<li>코드 품질 보장</li>
<li>버그 조기 발견</li>
<li>안전한 리팩토링 지원</li>
<li>문서화 역할</li>
<li>설계 개선 유도</li>
</ul>
<h2 id="테스트-환경">테스트 환경</h2>
<h3 id="기술-스택">기술 스택</h3>
<ul>
<li><strong>테스트 프레임워크</strong>: JUnit 5</li>
<li><strong>통합 테스트 도구</strong>: Testcontainers</li>
<li><strong>목킹 프레임워크</strong>: Mockito</li>
<li><strong>API 테스트</strong>: REST Assured</li>
<li><strong>로컬 개발 환경</strong>: Docker Compose</li>
<li><strong>CI/CD</strong>: GitHub Actions</li>
</ul>
<h2 id="테스트-계층">테스트 계층</h2>
<h3 id="1-서비스-계층-테스트">1. 서비스 계층 테스트</h3>
<h4 id="11-단위-테스트-unit-tests">1.1 단위 테스트 (Unit Tests)</h4>
<ul>
<li><strong>대상</strong>: Service 클래스의 개별 메서드</li>
<li><strong>도구</strong>: JUnit 5, Mockito</li>
<li><strong>특징</strong>:<ul>
<li>Repository와 외부 의존성을 Mock 처리</li>
<li>복잡한 비즈니스 로직 검증에 집중</li>
<li>빠른 실행 속도로 즉각적인 피드백</li>
</ul>
</li>
</ul>
<pre><code class="language-java">// 서비스 단위 테스트 예시
@ExtendWith(MockitoExtension.class)
class TravelServiceTest {
    @Mock
    private TravelRepository travelRepository;

    @InjectMocks
    private TravelService travelService;

    @Test
    void 여행_일정_생성_단위테스트() {
        // given
        TravelPlanRequest request = new TravelPlanRequest(...);
        when(travelRepository.save(any())).thenReturn(...);

        // when
        TravelPlanResponse response = travelService.createPlan(request);

        // then
        assertThat(response).isNotNull();
        verify(travelRepository, times(1)).save(any());
    }
}</code></pre>
<h4 id="12-서비스-통합-테스트-service-integration-tests">1.2 서비스 통합 테스트 (Service Integration Tests)</h4>
<ul>
<li><strong>대상</strong>: Service와 Repository 간의 상호작용</li>
<li><strong>도구</strong>: JUnit 5, TestContainers, Spring Boot Test</li>
<li><strong>특징</strong>:<ul>
<li>실제 데이터베이스 사용 (TestContainers)</li>
<li>Repository 계층과의 결합 테스트</li>
<li>복잡한 쿼리나 트랜잭션 검증</li>
</ul>
</li>
</ul>
<pre><code class="language-java">// 서비스 통합 테스트 예시
@SpringBootTest
@Testcontainers
class TravelServiceIntegrationTest {
    @Container
    static PostgreSQLContainer&lt;?&gt; postgres = new PostgreSQLContainer&lt;&gt;(&quot;postgres:14&quot;);

    @Autowired
    private TravelService travelService;

    @Autowired
    private TravelRepository travelRepository;

    @Test
    void 여행_일정_저장_및_조회_통합테스트() {
        // given
        TravelPlanRequest request = new TravelPlanRequest(...);

        // when
        TravelPlanResponse created = travelService.createPlan(request);
        Travel found = travelRepository.findById(created.getId()).orElseThrow();

        // then
        assertThat(found.getTitle()).isEqualTo(request.getTitle());
        // 복잡한 비즈니스 규칙이나 DB 상태 검증
    }
}</code></pre>
<h3 id="2-api-엔드포인트-테스트-end-to-end-tests">2. API 엔드포인트 테스트 (End-to-End Tests)</h3>
<ul>
<li><strong>대상</strong>: REST API 엔드포인트</li>
<li><strong>도구</strong>: TestContainers, Spring Boot Test, REST Assured</li>
<li><strong>특징</strong>:<ul>
<li>실제 애플리케이션 환경에서 전체 플로우 테스트</li>
<li>HTTP 요청/응답 검증</li>
<li>실제 데이터베이스 사용</li>
<li>보안, 인증/인가 포함한 전체 시나리오 검증</li>
</ul>
</li>
</ul>
<pre><code class="language-java">// API 엔드포인트 테스트 예시
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class TravelApiTest {
    @Container
    static PostgreSQLContainer&lt;?&gt; postgres = new PostgreSQLContainer&lt;&gt;(&quot;postgres:14&quot;);

    @LocalServerPort
    private Integer port;

    @Autowired
    private TravelRepository travelRepository; // 테스트 데이터 준비 및 검증용

    @Test
    void 여행_일정_생성_API_테스트() {
        // given
        TravelPlanRequest request = new TravelPlanRequest(...);

        // when-then
        String travelId = given()
            .contentType(ContentType.JSON)
            .body(request)
        .when()
            .post(&quot;/api/v1/travels&quot;)
        .then()
            .statusCode(200)
            .extract().jsonPath().getString(&quot;id&quot;);

        // 데이터베이스 상태 검증
        Travel savedTravel = travelRepository.findById(travelId).orElseThrow();
        assertThat(savedTravel.getTitle()).isEqualTo(request.getTitle());
    }
}</code></pre>
<ul>
<li>시나리오 기반 테스트</li>
</ul>
<h3 id="테스트-전략-가이드">테스트 전략 가이드</h3>
<h4 id="1-서비스-계층-테스트-전략">1. 서비스 계층 테스트 전략</h4>
<h5 id="단위-테스트-vs-통합-테스트-선택-기준">단위 테스트 vs 통합 테스트 선택 기준</h5>
<p><strong>단위 테스트를 사용하는 경우:</strong></p>
<ul>
<li>단순한 CRUD 연산</li>
<li>단일 Repository에만 의존하는 로직</li>
<li>비즈니스 규칙 검증이 주목적인 경우</li>
</ul>
<pre><code class="language-java">// 단위 테스트 예시: 단순 CRUD
@Test
void 여행_일정_상태_변경_테스트() {
    when(travelRepository.findById(any())).thenReturn(Optional.of(travel));
    travelService.updateTravelStatus(id, TravelStatus.COMPLETED);
    verify(travelRepository).save(travelCaptor.capture());
    assertThat(travelCaptor.getValue().getStatus()).isEqualTo(TravelStatus.COMPLETED);
}</code></pre>
<p><strong>통합 테스트를 사용하는 경우:</strong></p>
<ul>
<li>복수의 Repository 사용 (예: 여행과 참가자 동시 처리)</li>
<li>복잡한 조회 쿼리와 조인</li>
<li>트랜잭션 처리 검증 필요</li>
</ul>
<pre><code class="language-java">// 통합 테스트 예시: 복합 연산
@Test
void 여행_참가자_추가_및_인원수_검증() {
    // given
    Travel travel = travelRepository.save(new Travel(&quot;...&quot;, 5)); // 최대 5명

    // when
    travelService.addParticipant(travel.getId(), participant);

    // then
    Travel updated = travelRepository.findWithParticipants(travel.getId());
    assertThat(updated.getParticipants()).hasSize(1);

    // 추가 참가자 5명 등록 시도
    assertThrows(TravelFullException.class, () -&gt; {
        for (int i = 0; i &lt; 5; i++) {
            travelService.addParticipant(travel.getId(), new Participant());
        }
    });
}</code></pre>
<h4 id="2-api-테스트-전략">2. API 테스트 전략</h4>
<h5 id="테스트-시나리오-구성">테스트 시나리오 구성</h5>
<ul>
<li>단순 API 호출이 아닌 실제 사용자 시나리오 기반</li>
<li>인증/인가 포함</li>
<li>데이터베이스 상태 검증</li>
</ul>
<pre><code class="language-java">// 시나리오 기반 API 테스트 예시
@Test
void 여행_일정_생성_및_참가자_추가_시나리오() {
    // 1. 여행 일정 생성
    String travelId = given()
        .header(&quot;Authorization&quot;, &quot;Bearer &quot; + hostToken)
        .contentType(ContentType.JSON)
        .body(travelRequest)
    .when()
        .post(&quot;/api/v1/travels&quot;)
    .then()
        .statusCode(200)
        .extract().jsonPath().getString(&quot;id&quot;);

    // 2. 참가자 추가
    given()
        .header(&quot;Authorization&quot;, &quot;Bearer &quot; + participantToken)
        .contentType(ContentType.JSON)
    .when()
        .post(&quot;/api/v1/travels/&quot; + travelId + &quot;/participants&quot;)
    .then()
        .statusCode(200);

    // 3. 데이터베이스 상태 검증
    Travel travel = travelRepository.findWithParticipants(travelId);
    assertThat(travel.getParticipants()).hasSize(1);
    assertThat(travel.getParticipants().get(0).getUserId())
        .isEqualTo(participantUserId);
}
}

## 테스트 구현 가이드라인

### 1. 테스트 케이스 작성 규칙

#### 테스트 클래스 구조
```java
// 예시: 여행 서비스 테스트 구조
class TravelServiceTest {
    @Nested
    class 여행_일정_생성 {
        @Test
        void 정상_경우() { ... }

        @Test
        void 유효하지_않은_데이터_입력시() { ... }
    }

    @Nested
    class 여행_참가자_추가 { ... }
}</code></pre>
<h4 id="테스트-메서드-명명">테스트 메서드 명명</h4>
<ul>
<li>한글 메서드명 허용 (가독성 우선)</li>
<li><code>[테스트_시나리오]_[기대_결과]</code> 형식 권장<ul>
<li>예: <code>유효하지_않은_데이터_입력시_예외_발생()</code></li>
</ul>
</li>
</ul>
<h3 id="2-테스트-데이터-관리">2. 테스트 데이터 관리</h3>
<h4 id="테스트-픽스처-사용">테스트 픽스처 사용</h4>
<pre><code class="language-java">@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TravelServiceTest {
    private static final Travel SAMPLE_TRAVEL = Travel.builder()
        .title(&quot;제주도 여행&quot;)
        .maxParticipants(5)
        .build();

    private static final TravelRequest VALID_REQUEST = TravelRequest.builder()
        .title(&quot;제주도 여행&quot;)
        .maxParticipants(5)
        .build();
}</code></pre>
<h4 id="테스트-데이터-빌더-패턴">테스트 데이터 빌더 패턴</h4>
<pre><code class="language-java">class TestTravelBuilder {
    public static Travel createTravel() {
        return Travel.builder()
            .title(&quot;기본 여행&quot;)
            .maxParticipants(5)
            .build();
    }

    public static Travel createTravelWithParticipants(int participantCount) {
        Travel travel = createTravel();
        for (int i = 0; i &lt; participantCount; i++) {
            travel.addParticipant(new Participant());
        }
        return travel;
    }
}</code></pre>
<h3 id="3-예외-처리-테스트">3. 예외 처리 테스트</h3>
<ul>
<li>예외 상황에 대한 테스트 케이스 필수</li>
<li>예외 메시지와 상태 코드 검증</li>
</ul>
<pre><code class="language-java">@Test
void 최대_인원_초과_시_예외_발생() {
    // given
    Travel travel = TestTravelBuilder.createTravelWithParticipants(5);
    when(travelRepository.findById(any())).thenReturn(Optional.of(travel));

    // when &amp; then
    assertThrows(TravelFullException.class, () -&gt;
        travelService.addParticipant(travel.getId(), new Participant())
    );
}</code></pre>
<ul>
<li>테스트 메서드: <code>[테스트시나리오_예상결과]</code></li>
<li>한글 메서드명 허용 (가독성이 더 좋은 경우)</li>
</ul>
<h3 id="2-테스트-구조">2. 테스트 구조</h3>
<ul>
<li>Given-When-Then 패턴 사용</li>
<li>각 섹션을 주석으로 구분</li>
<li>테스트 설명은 명확하고 구체적으로</li>
</ul>
<h3 id="3-테스트-데이터">3. 테스트 데이터</h3>
<ul>
<li>테스트 픽스처 활용</li>
<li>테스트 유틸리티 클래스 구현</li>
<li>의미 있는 테스트 데이터 사용</li>
</ul>
<h3 id="4-모범-사례">4. 모범 사례</h3>
<ul>
<li>하나의 테스트는 하나의 동작만 검증</li>
<li>불필요한 검증 피하기</li>
<li>테스트 간 독립성 유지</li>
<li>실패하는 케이스도 반드시 테스트</li>
</ul>
<hr>
<h2 id="cicd-파이프라인-운영-전략">CI/CD 파이프라인 운영 전략</h2>
<h3 id="1-테스트-실행-및-모니터링">1. 테스트 실행 및 모니터링</h3>
<h4 id="11-테스트-레벨별-실행-전략">1.1 테스트 레벨별 실행 전략</h4>
<p><strong>단위 테스트 (PR 기반)</strong></p>
<ul>
<li>실행 트리거: PR 생성/업데이트</li>
<li>목표 실행 시간: 3분 이내</li>
<li>실패 시 조치: PR 머지 블록</li>
</ul>
<p><strong>통합 테스트 (PR 기반)</strong></p>
<ul>
<li>실행 트리거: 메인/개발 브랜치 PR</li>
<li>목표 실행 시간: 10분 이내</li>
<li>실패 시 조치: PR 머지 블록</li>
</ul>
<p><strong>E2E 테스트 (일간 실행)</strong></p>
<ul>
<li>실행 주기: 매일 새벽 3시</li>
<li>목표 실행 시간: 30분 이내</li>
<li>실패 시 조치: 테스트 실패 대응팀 지정 및 긴급 조치 (미확정)</li>
</ul>
<h4 id="12-테스트-실패-대응-체계">1.2 테스트 실패 대응 체계</h4>
<p><strong>실패 유형별 대응 전략</strong></p>
<ol>
<li><p>단위 테스트 실패</p>
<ul>
<li>담당: PR 작성자</li>
<li>조치: 즉시 코드 수정</li>
<li>SLA: 4시간 이내 (미확정)</li>
</ul>
</li>
<li><p>통합 테스트 실패</p>
<ul>
<li>담당: PR 작성자 + 테스트 담당자</li>
<li>조치: 원인 분석 후 수정</li>
<li>SLA: 1일 이내 (미확정)</li>
</ul>
</li>
<li><p>E2E 테스트 실패</p>
<ul>
<li>담당: 테스트 대응팀 (2인 1팀)</li>
<li>조치: 원인 분석 및 해결 계획 수립</li>
<li>SLA: 2일 이내 (미확정)</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[구름 3차 합반 프로젝트  비로그인 공유 기능 기술 명세서
]]></title>
            <link>https://velog.io/@devel-history/%EA%B5%AC%EB%A6%84-3%EC%B0%A8-%ED%95%A9%EB%B0%98-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Spring-Security</link>
            <guid>https://velog.io/@devel-history/%EA%B5%AC%EB%A6%84-3%EC%B0%A8-%ED%95%A9%EB%B0%98-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Spring-Security</guid>
            <pubDate>Mon, 22 Sep 2025 14:14:49 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p>구름톤 딥다이브에서 PM, PD, FE, BE 총 4개의 직군이 협업하여 5주 동안 프로젝트를 제작하였습니다. 본 문서는 프로젝트의 핵심 기능인 인증/인가 시스템에 대한 기술 명세를 다룹니다.</p>
<h2 id="1-서비스-소개">1. 서비스 소개</h2>
<p>여행 계획 서비스의 주요 차별점은 비로그인 사용자도 손쉽게 접근하고 협업할 수 있는 기능입니다. 기존 서비스들의 높은 진입 장벽(필수 회원가입)을 해결하여 즉각적인 협업이 가능한 서비스를 구현하였습니다.</p>
<h3 id="11-문제-정의">1.1 문제 정의</h3>
<ul>
<li>대부분의 여행 계획 서비스들은 로그인한 사용자만 접근 가능</li>
<li>여행 계획을 공유하려면 상대방도 회원가입이 필요</li>
<li>즉석에서 계획을 보거나 수정하기 어려움</li>
</ul>
<h3 id="12-해결해야-할-문제">1.2 해결해야 할 문제</h3>
<ul>
<li>비로그인 사용자의 접근성 제한</li>
<li>불필요한 회원가입으로 인한 사용자 이탈</li>
<li>즉각적인 협업의 어려움</li>
<li>외부 공유의 번거로움</li>
</ul>
<h3 id="13-개발환경-및-인프라">1.3 개발환경 및 인프라</h3>
<h4 id="기술-스택">기술 스택</h4>
<ul>
<li><strong>Backend Framework</strong>: Spring Boot 3.5.4</li>
<li><strong>Java Version</strong>: Java 21</li>
<li><strong>Database</strong>:<ul>
<li>Primary: MySQL 8.0.42</li>
<li>Cache: Redis 7.2</li>
</ul>
</li>
<li><strong>Security</strong>:<ul>
<li>Spring Security</li>
<li>JWT (0.11.5)</li>
</ul>
</li>
<li><strong>API Documentation</strong>:<ul>
<li>Springdoc OpenAPI (Swagger) 2.8.10</li>
</ul>
</li>
<li><strong>ORM &amp; Query</strong>:<ul>
<li>Spring Data JPA</li>
<li>Querydsl 6.11</li>
</ul>
</li>
<li><strong>Monitoring</strong>:<ul>
<li>Spring Actuator</li>
<li>Prometheus/Micrometer</li>
</ul>
</li>
<li><strong>Testing</strong>:<ul>
<li>JUnit 5</li>
<li>Testcontainers</li>
</ul>
</li>
</ul>
<h4 id="개발-및-배포-환경">개발 및 배포 환경</h4>
<ol>
<li><p><strong>로컬 개발 환경</strong></p>
<ul>
<li>Docker Compose를 활용한 컨테이너 관리</li>
<li>MySQL(3306), Redis(6379) 포트 매핑</li>
</ul>
</li>
<li><p><strong>테스트 환경</strong></p>
<ul>
<li>Testcontainers를 활용한 통합 테스트</li>
<li>GitHub Actions CI/CD 파이프라인</li>
</ul>
</li>
<li><p><strong>모니터링 시스템</strong></p>
<ul>
<li>Actuator 엔드포인트 활용</li>
<li>Prometheus 메트릭스 수집</li>
</ul>
</li>
</ol>
<ul>
<li>JWT (JSON Web Token) 0.11.5</li>
<li><strong>API Documentation</strong>: SpringDoc OpenAPI (Swagger) 2.8.10</li>
<li><strong>ORM</strong>:<ul>
<li>Spring Data JPA</li>
<li>Querydsl 6.11</li>
</ul>
</li>
<li><strong>Monitoring</strong>:<ul>
<li>Spring Actuator</li>
<li>Prometheus</li>
</ul>
</li>
<li><strong>Testing</strong>:<ul>
<li>JUnit</li>
<li>Spring Boot Test</li>
<li>Testcontainers</li>
</ul>
</li>
</ul>
<h4 id="인프라-구성">인프라 구성</h4>
<ul>
<li><strong>컨테이너화</strong>: Docker Compose를 통한 로컬 개발 환경 구성</li>
<li><strong>데이터베이스 구성</strong>:<ul>
<li>MySQL: 3306 포트</li>
<li>Redis: 6379 포트</li>
</ul>
</li>
<li><strong>CI/CD</strong>: GitHub Actions를 통한 자동화된 빌드 및 테스트</li>
</ul>
<h2 id="2-설계">2. 설계</h2>
<h3 id="21-기획-요구사항-분석">2.1 기획 요구사항 분석</h3>
<ol>
<li><strong>세분화된 권한 체계</strong>:<ul>
<li>총 5개 ROLE [Owner, Member, Traveler, Guest, Viewer]</li>
<li>각 역할별 차등적 권한 부여</li>
</ul>
</li>
<li><strong>URL 기반 공유 시스템</strong>:<ul>
<li>간편한 공유 링크 생성</li>
<li>권한별 차별화된 링크 생성</li>
</ul>
</li>
<li><strong>비로그인 접근 지원</strong>:<ul>
<li>Traveler, Guest 역할은 링크를 통한 비로그인 접근 가능</li>
</ul>
</li>
<li><strong>보안 체계</strong>:<ul>
<li>초대자가 생성한 링크를 통해서만 접근 가능</li>
<li>링크의 유효성 검증</li>
</ul>
</li>
</ol>
<h3 id="22-권한-체계-상세">2.2 권한 체계 상세</h3>
<p><img src="https://velog.velcdn.com/images/devel-history/post/00508947-5b07-4613-af63-875e57a93225/image.png" alt="권한 체계 다이어그램"></p>
<h3 id="23-개발자-관점-요구사항">2.3 개발자 관점 요구사항</h3>
<ol>
<li><strong>통합된 인증 플로우</strong>:<ul>
<li>로그인/비로그인 사용자를 위한 단일 인증 흐름</li>
<li>코드 복잡도 최소화</li>
</ul>
</li>
<li><strong>Role 기반 접근 제어</strong>:<ul>
<li>각 API 엔드포인트별 최소 필요 Role 정의</li>
<li>Spring Security의 @PreAuthorize 활용</li>
</ul>
</li>
<li><strong>코드 품질</strong>:<ul>
<li>관심사 분리 원칙 준수</li>
<li>중복 코드 최소화</li>
</ul>
</li>
<li><strong>기술 스택 활용</strong>:<ul>
<li>Spring Security와 JWT의 효율적 통합</li>
<li>Redis를 활용한 토큰 관리</li>
</ul>
</li>
</ol>
<h3 id="24-인가authorization-계층">2.4 인가(Authorization) 계층</h3>
<pre><code>Team 레벨
└── Owner/Member (팀의 모든 프로젝트 접근/편집)
    └── Project 레벨
        └── Traveler (특정 프로젝트 접근/편집)
            └── 외부 접근 레벨
                ├── Guest (읽기/쓰기)
                └── Viewer (읽기 전용)</code></pre><h2 id="3-구현-방식-비교-분석">3. 구현 방식 비교 분석</h2>
<h3 id="31-게스트-계정-생성-방식">3.1 게스트 계정 생성 방식</h3>
<p><img src="https://velog.velcdn.com/images/devel-history/post/0de30533-3536-48aa-8e5c-24446e38dfc3/image.png" alt="게스트 계정 방식"></p>
<h4 id="구현-방법">구현 방법</h4>
<ol>
<li><p><strong>게스트 계정 생성 프로세스</strong></p>
<ul>
<li>프로젝트 ID와 권한 레벨에 따른 게스트 계정 생성</li>
<li>임시 비밀번호 생성 및 암호화 저장</li>
<li>게스트 계정 메타데이터 (생성 시간, 마지막 접속 등) 기록</li>
<li>초대 이메일 전송 및 접속 정보 전달</li>
</ul>
</li>
<li><p><strong>권한 관리 시스템</strong></p>
<ul>
<li>Spring Security의 UserDetails 인터페이스 구현</li>
<li>역할 기반 권한 관리 (RBAC) 적용</li>
<li>세션 관리 및 자동 만료 처리</li>
</ul>
</li>
<li><p><strong>데이터 관리 메커니즘</strong></p>
<ul>
<li>JPA 엔티티 관계 설정 (프로젝트-게스트 매핑)</li>
<li>자동 데이터 정리 (Scheduled 태스크)</li>
<li>사용자 활동 로깅 및 추적</li>
</ul>
</li>
</ol>
<h4 id="장단점-분석">장단점 분석</h4>
<p><strong>장점</strong></p>
<ul>
<li>기존 인증 시스템 활용 가능</li>
<li>사용자 행동 추적 용이</li>
<li>권한 관리 용이</li>
<li>세부적인 액세스 컨트롤 가능</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>불필요한 계정 데이터 증가</li>
<li>임시 계정 관리 부담</li>
<li>DB 의존도가 높음</li>
<li>스케일링 시 관리 복잡성 증가</li>
</ul>
<h4 id="보안-강화-방안">보안 강화 방안</h4>
<ol>
<li><p><strong>계정 보안</strong></p>
<ul>
<li>정기적인 비밀번호 만료 및 갱신</li>
<li>접속 IP 제한 및 모니터링</li>
<li>비활성 계정 자동 정리</li>
</ul>
</li>
<li><p><strong>데이터 보호</strong></p>
<ul>
<li>게스트 데이터 암호화 저장</li>
<li>접근 로그 기록 및 감사</li>
<li>데이터 백업 및 복구 정책</li>
</ul>
</li>
</ol>
<h3 id="32-일회용-토큰-방식">3.2 일회용 토큰 방식</h3>
<p><img src="https://velog.velcdn.com/images/devel-history/post/890cf26d-d0c4-4744-8839-c476ae276bdd/image.png" alt="토큰 방식"></p>
<h4 id="구현-방법-1">구현 방법</h4>
<ol>
<li><p><strong>토큰 생성 프로세스</strong></p>
<ul>
<li>프로젝트 ID, 권한 레벨, 유효기간을 포함한 JWT 토큰 생성</li>
<li>난수화된 토큰 ID 및 서명 추가</li>
<li>Redis에 토큰 메타데이터 저장 (사용 횟수, 생성자 정보 등)</li>
<li>초대 URL 생성 및 전송</li>
</ul>
</li>
<li><p><strong>토큰 검증 시스템</strong></p>
<ul>
<li>Redis에서 토큰 유효성 검사</li>
<li>JWT 서명 및 만료 시간 검증</li>
<li>사용 횟수 추적 및 제한 처리</li>
</ul>
</li>
<li><p><strong>상태 관리 메커니즘</strong></p>
<ul>
<li>Redis TTL을 활용한 자동 만료 처리</li>
<li>토큰 사용 현황 모니터링</li>
<li>비정상 접근 탐지 및 차단</li>
</ul>
</li>
</ol>
<h4 id="장단점-분석-1">장단점 분석</h4>
<p><strong>장점</strong></p>
<ul>
<li>높은 보안성</li>
<li>사용 횟수 제한 가능</li>
<li>토큰 즉시 무효화 가능</li>
<li>세부적인 액세스 컨트롤</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>Redis 의존성</li>
<li>토큰 관리 복잡성</li>
<li>재사용 불가능</li>
<li>인프라 운영 비용 증가</li>
</ul>
<h4 id="보안-강화-방안-1">보안 강화 방안</h4>
<ol>
<li><p><strong>토큰 보안</strong></p>
<ul>
<li>암호화 방식의 정기적 업데이트</li>
<li>이중 인증 옵션 제공</li>
<li>토큰 사용 패턴 분석</li>
</ul>
</li>
<li><p><strong>시스템 보호</strong></p>
<ul>
<li>Redis 클러스터링 구성</li>
<li>데이터 독립성 보장</li>
<li>재난 복구 전략 수립</li>
</ul>
</li>
</ol>
<h3 id="33-링크-공유-방식-채택">3.3 링크 공유 방식 (채택)</h3>
<p><img src="https://velog.velcdn.com/images/devel-history/post/ba56c4e6-0bd5-490c-8c06-95bc94703576/image.png" alt="링크 공유 방식"></p>
<h4 id="구현-방법-2">구현 방법</h4>
<ol>
<li><p><strong>링크 생성 프로세스</strong></p>
<ul>
<li>프로젝트 ID와 권한 레벨(Guest/Viewer)을 포함한 페이로드 생성</li>
<li>시스템 비밀키를 사용하여 페이로드에 대한 HMAC 서명 생성</li>
<li>만료 시간을 포함한 타임스탬프 추가</li>
<li>페이로드, 서명, 타임스탬프를 Base64로 인코딩하여 단일 문자열로 변환</li>
</ul>
</li>
<li><p><strong>링크 검증 프로세스</strong></p>
<ul>
<li>수신된 링크를 Base64 디코딩하여 구성 요소 분리</li>
<li>타임스탬프 검사로 만료 여부 확인</li>
<li>시스템 비밀키로 HMAC 서명 재계산하여 위변조 검증</li>
<li>프로젝트 ID와 권한 레벨 추출하여 접근 권한 부여</li>
</ul>
</li>
<li><p><strong>권한 관리 메커니즘</strong></p>
<ul>
<li>Spring Security의 커스텀 필터를 통해 링크 기반 인증 처리</li>
<li>ThreadLocal에 추출된 권한 정보 저장</li>
<li>@PreAuthorize 어노테이션으로 각 API 엔드포인트 보호</li>
</ul>
</li>
</ol>
<h4 id="장단점-분석-2">장단점 분석</h4>
<p><strong>장점</strong></p>
<ul>
<li>인프라 의존성 없는 Stateless 방식</li>
<li>기존 API 재사용 가능</li>
<li>수평적 확장 용이</li>
<li>구현 및 유지보수 간편</li>
<li>서버 부하 최소화</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>브라우저 히스토리에 노출</li>
<li>공유 링크 회수 어려움</li>
<li>접근 기록 추적 제한적</li>
<li>실시간 권한 변경 어려움</li>
</ul>
<h4 id="보안-강화-방안-2">보안 강화 방안</h4>
<ol>
<li><p><strong>링크 보안</strong></p>
<ul>
<li>짧은 만료 시간 설정 (기본 24시간)</li>
<li>복잡한 난수를 포함한 URL 생성</li>
<li>HTTPS 필수 적용</li>
</ul>
</li>
<li><p><strong>접근 제어</strong></p>
<ul>
<li>IP 기반 접근 제한</li>
<li>동시 접속 제한</li>
<li>비정상 패턴 감지 및 차단</li>
</ul>
</li>
</ol>
<h2 id="4-보안-및-구현-고려사항">4. 보안 및 구현 고려사항</h2>
<h3 id="41-링크-보안">4.1 링크 보안</h3>
<ul>
<li><strong>만료 시간 설정</strong>: 모든 공유 링크에 유효 기간 설정</li>
<li><strong>서명 검증</strong>: HMAC을 이용한 링크 무결성 검증</li>
<li><strong>Rate Limiting</strong>: IP 기반 접근 제한</li>
</ul>
<h3 id="42-권한-관리-및-구현-상세">4.2 권한 관리 및 구현 상세</h3>
<ol>
<li><p><strong>권한 관리 원칙</strong></p>
<pre><code class="language-java">@PreAuthorize(&quot;hasRole(&#39;OWNER&#39;) or hasRole(&#39;MEMBER&#39;)&quot;)
@GetMapping(&quot;/projects/{projectId}&quot;)
public ProjectResponse getProject(@PathVariable Long projectId) {
    // 프로젝트 조회 로직
}</code></pre>
</li>
<li><p><strong>계층형 권한 검증</strong></p>
<ul>
<li>Controller: @PreAuthorize 어노테이션</li>
<li>Service: Custom SecurityUtil 활용</li>
<li>Repository: 연관 관계 기반 검증</li>
</ul>
</li>
<li><p><strong>인증 필터 처리</strong></p>
<pre><code class="language-java">public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
        HttpServletResponse response, FilterChain filterChain) {
        // JWT 검증 및 인증 처리
    }
}</code></pre>
</li>
</ol>
<ul>
<li><strong>감사 로깅</strong>: 주요 작업에 대한 로그 기록</li>
</ul>
<h3 id="43-데이터-보안-및-유효성-검증">4.3 데이터 보안 및 유효성 검증</h3>
<ol>
<li><p><strong>입력 값 검증</strong></p>
<pre><code class="language-java">@Validated
@RequestBody ProjectCreateRequest request {
    @NotBlank
    @Size(max = 100)
    private String title;

    @NotNull
    @Future
    private LocalDateTime startDate;
}</code></pre>
</li>
<li><p><strong>XSS 방지</strong></p>
<ul>
<li>HTML 이스케이프 처리</li>
<li>Content-Security-Policy 적용</li>
</ul>
</li>
<li><p><strong>API 요청 유효성</strong></p>
<ul>
<li>Rate Limiting 적용</li>
<li>요청 크기 제한</li>
<li>인증 헤더 검증</li>
</ul>
</li>
</ol>
<h2 id="5-결론-및-향후-계획">5. 결론 및 향후 계획</h2>
<h3 id="51-핵심-의사결정-사항">5.1 핵심 의사결정 사항</h3>
<ol>
<li><p><strong>링크 공유 방식 채택 이유</strong></p>
<ul>
<li>높은 확장성과 단순한 아키텍처</li>
<li>최소한의 인프라 의존성</li>
<li>사용자 경험 최적화</li>
<li>비용 효율적인 운영 가능</li>
<li>실시간 모니터링 용이</li>
</ul>
</li>
<li><p><strong>보안과 편의성의 균형</strong></p>
<ul>
<li>JWT + HMAC 기반의 안전한 서명</li>
<li>Actuator + Prometheus를 활용한 실시간 모니터링</li>
<li>세부적인 로깅 및 추적 기능</li>
</ul>
</li>
<li><p><strong>보안과 편의성의 균형</strong></p>
<ul>
<li>24시간 제한의 링크 유효기간</li>
<li>권한별 차등적 접근 제어</li>
<li>간편한 공유 프로세스</li>
</ul>
</li>
</ol>
<h3 id="52-향후-개선-계획">5.2 향후 개선 계획</h3>
<ol>
<li><p><strong>단기 개선사항 (1-2개월)</strong></p>
<ul>
<li>링크 관리 대시보드 구현</li>
<li>접근 로그 시스템 강화</li>
<li>실시간 알림 기능 추가</li>
</ul>
</li>
<li><p><strong>중장기 계획 (3-6개월)</strong></p>
<ul>
<li>실시간 권한 변경 기능 구현</li>
<li>OAuth 기반 소셜 로그인 통합</li>
<li>분석 대시보드 개발</li>
</ul>
</li>
</ol>
<h3 id="53-모니터링-및-유지보수">5.3 모니터링 및 유지보수</h3>
<ol>
<li><p><strong>시스템 모니터링</strong></p>
<pre><code class="language-yaml">management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    tags:
      application: moyeohaeng</code></pre>
</li>
<li><p><strong>성능 측정 메트릭스</strong></p>
<ul>
<li>API 응답 시간 (Timer)</li>
<li>동시 사용자 수 (Gauge)</li>
<li>에러 빈도 (Counter)</li>
</ul>
</li>
<li><p><strong>로그 추적</strong></p>
<pre><code class="language-java">@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity&lt;ErrorResponse&gt; handleException(Exception e) {
        log.error(&quot;Unexpected error&quot;, e);
        // 에러 처리 로직
    }
}</code></pre>
</li>
<li><p><strong>트러블슈팅 가이드</strong></p>
<ul>
<li>자주 발생하는 이슈 및 해결 방법</li>
<li>성능 병목 해결 가이드</li>
<li>재난 복구 시나리오</li>
</ul>
</li>
<li><p><strong>운영 가이드</strong></p>
<ul>
<li>시스템 상태 점검 및 정기 점검 절차</li>
<li>로그 분석 및 모니터링 전략</li>
<li>성능 최적화 가이드</li>
</ul>
</li>
<li><p><strong>확장성 고려사항</strong></p>
<ul>
<li>로드밸런싱 및 스케일아웃 전략</li>
<li>캐시 전략 및 Redis 클러스터링</li>
<li>데이터베이스 인덱싱 및 최적화</li>
</ul>
</li>
<li><p><strong>보안 모니터링</strong></p>
<ul>
<li>비정상 접근 탐지</li>
<li>취약점 정기 점검</li>
<li>보안 패치 관리</li>
</ul>
</li>
</ol>
<p>이 기술 명세서는 프로젝트의 현재 상태를 반영하며, 지속적인 업데이트와 개선이 이루어질 예정입니다. 팀원들의 피드백과 실제 운영 경험을 바탕으로 더 나은 서비스를 제공하기 위해 계속해서 발전시켜 나갈 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security CORS 설정 가이드: 복잡도에 따른 최적의 접근법]]></title>
            <link>https://velog.io/@devel-history/Spring-Security-CORS-%EC%84%A4%EC%A0%95-%EA%B0%80%EC%9D%B4%EB%93%9C-%EB%B3%B5%EC%9E%A1%EB%8F%84%EC%97%90-%EB%94%B0%EB%A5%B8-%EC%B5%9C%EC%A0%81%EC%9D%98-%EC%A0%91%EA%B7%BC%EB%B2%95</link>
            <guid>https://velog.io/@devel-history/Spring-Security-CORS-%EC%84%A4%EC%A0%95-%EA%B0%80%EC%9D%B4%EB%93%9C-%EB%B3%B5%EC%9E%A1%EB%8F%84%EC%97%90-%EB%94%B0%EB%A5%B8-%EC%B5%9C%EC%A0%81%EC%9D%98-%EC%A0%91%EA%B7%BC%EB%B2%95</guid>
            <pubDate>Sun, 24 Aug 2025 15:04:59 GMT</pubDate>
            <description><![CDATA[<h2 id="level-1-기본-설정-단일-정책-시나리오">Level 1: 기본 설정 (단일 정책 시나리오)</h2>
<p>대부분의 API가 하나의 동일한 CORS 정책을 공유하는, 일반적인 프로젝트 초기에 가장 적합한 방식입니다.</p>
<h3 id="상황">상황</h3>
<ul>
<li>모든 API 엔드포인트가 <code>http://localhost:3000</code>이나 <code>https://app.moyeohaeng.com</code>과 같은 단일 종류의 클라이언트로부터 요청을 받습니다.</li>
</ul>
<h3 id="추천-방식-암시적-설정-customizerwithdefaults">추천 방식: 암시적 설정 (<code>Customizer.withDefaults()</code>)</h3>
<p><code>SecurityConfig</code>는 CORS 설정의 구체적인 내용을 모르고, 스프링 컨테이너에 등록된 <code>CorsConfigurationSource</code> 빈을 자동으로 가져와 사용합니다. 이를 통해 <strong>설정을 분리하고 코드를 간결하게 유지</strong>할 수 있습니다.</p>
<h4 id="1-applicationyml---설정-외부화">1. <code>application.yml</code> - 설정 외부화</h4>
<p>CORS 정책을 외부 설정 파일로 분리하여 관리합니다.</p>
<pre><code class="language-yaml">cors:
  allowed-origins: http://localhost:3000
  allowed-methods: GET,POST,PUT,DELETE,PATCH,OPTIONS
  allowed-headers: &#39;*&#39;</code></pre>
<h4 id="2-corsconfigjava---설정-빈-생성">2. <code>CorsConfig.java</code> - 설정 빈 생성</h4>
<p><code>@ConfigurationProperties</code>를 사용해 <code>yml</code> 값을 읽어와 <code>CorsConfigurationSource</code> 빈을 생성합니다.</p>
<pre><code class="language-java">@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = &quot;cors&quot;)
public class CorsConfig {

    private List&lt;String&gt; allowedOrigins;
    private List&lt;String&gt; allowedMethods;
    private List&lt;String&gt; allowedHeaders;

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(allowedOrigins);
        config.setAllowedMethods(allowedMethods);
        config.setAllowedHeaders(allowedHeaders);
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration(&quot;/**&quot;, config);
        return source;
    }
}</code></pre>
<h4 id="3-securityconfigjava---자동-설정-적용">3. <code>SecurityConfig.java</code> - 자동 설정 적용</h4>
<p><code>.cors(Customizer.withDefaults())</code> 한 줄만으로 위에서 생성된 빈을 자동으로 적용합니다.</p>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .cors(Customizer.withDefaults()) // 자동으로 CorsConfigurationSource 빈을 찾아 적용
            // ... 기타 설정
            .build();
    }
}</code></pre>
<hr>
<h2 id="level-2-고급-설정-다중-정책-시나리오">Level 2: 고급 설정 (다중 정책 시나리오)</h2>
<p>API의 역할(예: 일반 사용자용, 관리자용)에 따라 여러 개의 다른 CORS 정책을 적용해야 할 때 사용하는 방식입니다.</p>
<h3 id="상황-1">상황</h3>
<ul>
<li>일반 사용자 API (<code>/api/v1/**</code>)는 웹 클라이언트의 접근을 허용합니다.</li>
<li>관리자 API (<code>/api/admin/**</code>)는 내부 관리자 대시보드나 특정 IP에서만 접근을 허용해야 합니다.</li>
</ul>
<h3 id="추천-방식-명시적-의존성-주입">추천 방식: 명시적 의존성 주입</h3>
<p>각기 다른 CORS 설정을 담은 빈을 여러 개 만들고, <code>SecurityFilterChain</code>에서 필요한 설정을 <strong>명시적으로 주입받아 사용</strong>합니다. 이를 통해 <strong>명확한 의존 관계를 설정하고 세밀한 제어</strong>가 가능해집니다.</p>
<h4 id="1-corsconfigjava---여러-개의-설정-빈-생성">1. <code>CorsConfig.java</code> - 여러 개의 설정 빈 생성</h4>
<p><code>@Qualifier</code>를 사용하여 각기 다른 CORS 설정 빈을 이름으로 구분하여 생성합니다.</p>
<pre><code class="language-java">@Configuration
public class CorsConfig {

    @Bean
    @Qualifier(&quot;appCorsSource&quot;)
    public CorsConfigurationSource appCorsConfigurationSource() { /* 웹 클라이언트용 설정 */ }

    @Bean
    @Qualifier(&quot;adminCorsSource&quot;)
    public CorsConfigurationSource adminCorsConfigurationSource() { /* 관리자용 설정 */ }
}</code></pre>
<h4 id="2-securityconfigjava---필터-체인-분리-및-명시적-주입">2. <code>SecurityConfig.java</code> - 필터 체인 분리 및 명시적 주입</h4>
<p>URL 패턴에 따라 <code>SecurityFilterChain</code>을 분리하고, <code>@Qualifier</code>를 통해 해당 필터 체인에 맞는 <code>CorsConfigurationSource</code> 빈을 명시적으로 주입합니다.</p>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    // 일반 사용자 API 보안 설정
    @Bean
    @Order(1)
    public SecurityFilterChain apiFilterChain(HttpSecurity http, @Qualifier(&quot;appCorsSource&quot;) CorsConfigurationSource corsSource) throws Exception {
        http
            .securityMatcher(&quot;/api/v1/**&quot;)
            .cors(cors -&gt; cors.configurationSource(corsSource)) // &#39;appCorsSource&#39; 명시적 사용
            // ...
        return http.build();
    }

    // 관리자 API 보안 설정
    @Bean
    @Order(2)
    public SecurityFilterChain adminFilterChain(HttpSecurity http, @Qualifier(&quot;adminCorsSource&quot;) CorsConfigurationSource corsSource) throws Exception {
        http
            .securityMatcher(&quot;/api/admin/**&quot;)
            .cors(cors -&gt; cors.configurationSource(corsSource)) // &#39;adminCorsSource&#39; 명시적 사용
            // ...
        return http.build();
    }
}</code></pre>
<hr>
<h2 id="결론">결론</h2>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">기본 설정 (암시적)</th>
<th align="left">고급 설정 (명시적)</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>키워드</strong></td>
<td align="left"><code>Customizer.withDefaults()</code></td>
<td align="left">파라미터로 <code>CorsConfigurationSource</code> 주입</td>
</tr>
<tr>
<td align="left"><strong>장점</strong></td>
<td align="left">간결함, 낮은 결합도</td>
<td align="left">명확함, 세밀한 제어</td>
</tr>
<tr>
<td align="left"><strong>적합한 상황</strong></td>
<td align="left">단일 CORS 정책</td>
<td align="left">다중 CORS 정책, 복잡한 보안 규칙</td>
</tr>
</tbody></table>
<p>프로젝트 초기에는 <strong>기본 설정</strong>으로 시작하여 단순함을 유지하고, 기능이 확장되어 API 그룹별로 다른 보안 정책이 필요해지는 시점에 자연스럽게 <strong>고급 설정</strong>으로 리팩토링하는 것이 좋습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CustomUserDetails 역할 및 JWT 인증 흐름 가이드 <spring security>]]></title>
            <link>https://velog.io/@devel-history/spring-security</link>
            <guid>https://velog.io/@devel-history/spring-security</guid>
            <pubDate>Sat, 23 Aug 2025 15:00:14 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>이 문서는 <code>CustomUserDetails</code> 클래스의 역할과 JWT 기반의 stateless 인증 시스템에서의 사용 시점을 명확히 설명합니다.</p>
<h2 id="1-customuserdetails이란">1. CustomUserDetails이란?</h2>
<p><code>CustomUserDetails</code>은 Spring Security의 <code>UserDetails</code> 인터페이스를 구현한 클래스입니다.</p>
<ul>
<li><strong>역할</strong>: 인증이 완료된 사용자의 핵심 정보(ID, 이메일, 권한 등)를 담는 객체입니다.</li>
<li><strong>목적</strong>: Spring Security가 현`재 요청을 보낸 사용자가 누구인지 식별하고, 권한을 확인하는 표준적인 방법을 제공합니다.</li>
<li><strong>특징</strong>: <code>Member</code> 엔티티 전체가 아닌, 인증에 필수적인 최소한의 데이터만 포함하여 보안성과 효율성을 높입니다.</li>
</ul>
<pre><code class="language-java">// CustomUserDetails의 주요 필드
private final Long id;
private final String email;
private final Collection&lt;? extends GrantedAuthority&gt; authorities;</code></pre>
<h2 id="2-로그인-과정-vs-로그인-이후-api-요청">2. 로그인 과정 vs 로그인 이후 API 요청</h2>
<p><code>CustomUserDetails</code>의 사용 시점을 이해하기 위해 두 과정을 구분해야 합니다.</p>
<h3 id="로그인-authentication">로그인 (Authentication)</h3>
<ul>
<li><strong>과정</strong>: 사용자가 이메일과 비밀번호로 로그인을 시도하는 최초의 인증 단계입니다.</li>
<li><strong><code>CustomUserDetails</code> 사용 여부</strong>: <strong>사용하지 않습니다.</strong></li>
<li><strong>핵심 로직</strong>: <code>AuthService</code>에서 전달받은 비밀번호와 DB에 저장된 해시된 비밀번호를 비교하여 사용자를 검증합니다. 검증 성공 시, JWT(Access Token, Refresh Token)를 생성하여 클라이언트에게 반환합니다.</li>
</ul>
<h3 id="로그인-이후-api-요청-authorization">로그인 이후 API 요청 (Authorization)</h3>
<ul>
<li><strong>과정</strong>: 클라이언트가 로그인 시 발급받은 JWT를 HTTP 헤더에 담아 서버 자원을 요청하는 모든 후속 단계입니다.</li>
<li><strong><code>CustomUserDetails</code> 사용 여부</strong>: <strong>매우 중요하게 사용됩니다.</strong></li>
<li><strong>핵심 로D직</strong>:<ol>
<li><strong>JWT 필터 실행</strong>: <code>SecurityConfig</code>에 등록된 커스텀 JWT 필터가 요청을 가로챕니다.</li>
<li><strong>토큰 검증</strong>: 필터는 JWT의 유효성(서명, 만료 시간 등)을 검증합니다.</li>
<li><strong><code>CustomUserDetails</code> 생성</strong>: 토큰이 유효하면, 토큰의 <code>claims</code>에 담긴 사용자 정보(ID, 이메일, 권한 등)를 추출하여 <strong>메모리 상에 <code>CustomUserDetails</code> 객체를 생성</strong>합니다.</li>
<li><strong>SecurityContext 등록</strong>: 생성된 <code>CustomUserDetails</code>을 <code>Authentication</code> 객체로 감싸 <code>SecurityContextHolder</code>에 등록합니다. 이로써 Spring Security는 현재 요청의 주체를 인식하게 됩니다.</li>
<li><strong>컨트롤러/서비스 로직 실행</strong>: <code>@AuthenticationPrincipal</code> 어노테이션을 통해 컨트롤러에서 현재 사용자 정보를 직접 주입받거나, <code>@PreAuthorize</code> 등으로 권한 검사를 수행할 수 있습니다.</li>
</ol>
</li>
</ul>
<h2 id="3-왜-stateless-환경에서-customuserdetails이-필요한가">3. 왜 Stateless 환경에서 CustomUserDetails이 필요한가?</h2>
<p>Stateless 원칙은 서버에 사용자 <strong>세션</strong>을 저장하지 않는다는 의미이지, 요청을 처리하는 <strong>메모리 상의 일시적인 정보</strong>까지 사용하지 않는다는 의미가 아닙니다.</p>
<p><code>CustomUserDetails</code>은 서버에 저장되는 &quot;상태(State)&quot;가 아니라, 매 요청마다 JWT를 기반으로 생성되는 <strong>일회성 &quot;신분증&quot;</strong>입니다. 이 신분증이 있어야 Spring Security의 강력한 보안 기능들을 원활하게 활용할 수 있습니다.</p>
<h2 id="4-인증-흐름-다이어그램-mermaid">4. 인증 흐름 다이어그램 (Mermaid)</h2>
<pre><code class="language-mermaid">sequenceDiagram
    participant Client
    participant &quot;JWT Filter&quot; as Filter
    participant &quot;AuthService/DB&quot; as Service
    participant Controller
    participant &quot;Spring Security&quot; as Security

    Note over Client, Service: 1. 최초 로그인 (Authentication) - CustomUserDetails 사용 안함
    Client-&gt;&gt;Service: 로그인 요청 (이메일, 비밀번호)
    activate Service
    Service-&gt;&gt;Service: DB 정보로 사용자 검증
    Service--&gt;&gt;Client: JWT 발급 (Access/Refresh Token)
    deactivate Service

    Note over Client, Security: 2. API 요청 (Authorization) - CustomUserDetails 사용
    Client-&gt;&gt;Filter: API 요청 (with JWT)
    activate Filter
    Filter-&gt;&gt;Filter: 1. JWT 유효성 검증
    Note right of Filter: 2. 토큰 정보로 CustomUserDetails 생성
    Filter-&gt;&gt;Security: 3. SecurityContext에 Authentication 등록
    Filter-&gt;&gt;Controller: 4. 요청 전달
    deactivate Filter

    activate Controller
    Controller-&gt;&gt;Security: @AuthenticationPrincipal 요청
    activate Security
    Security--&gt;&gt;Controller: SecurityContext에서 CustomUserDetails 제공
    deactivate Security
    Controller--&gt;&gt;Client: API 응답
    deactivate Controller</code></pre>
<h2 id="5-customuserdetails-코드">5 CustomUserDetails 코드</h2>
<pre><code class="language-java">package eightbit.moyeohaeng.global.security;

import java.util.Collection;
import java.util.Collections;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import eightbit.moyeohaeng.domain.member.entity.member.Member;
import lombok.Getter;

@Getter
public class CustomUserDetails implements UserDetails {

    private final Long id;
    private final String email;
    private final Collection&lt;? extends GrantedAuthority&gt; authorities;

    private CustomUserDetails(Long id, String email, Collection&lt;? extends GrantedAuthority&gt; authorities) {
        this.id = id;
        this.email = email;
        this.authorities = authorities;
    }

    public static CustomUserDetails from(Member member) {
        return new CustomUserDetails(
            member.getId(),
            member.getEmail(),
            Collections.singleton(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;))
        );
    }

    @Override
    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public String getUsername() {
        return email;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
</code></pre>
<h2 id="6-요약">6. 요약</h2>
<table>
<thead>
<tr>
<th>시점</th>
<th align="center"><code>CustomUserDetails</code> 사용 여부</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>최초 로그인</strong></td>
<td align="center">❌</td>
<td>이메일/비밀번호로 DB와 직접 비교하여 인증.</td>
</tr>
<tr>
<td><strong>로그인 후 API 요청</strong></td>
<td align="center">✅</td>
<td>JWT 검증 후, 토큰 정보로 <code>CustomUserDetails</code>을 생성하여 SecurityContext에 등록. 이후 권한 검사 및 사용자 정보 참조에 사용.</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[GOORM-DEEP DIVE 백엔드 3회차<스타벅스 클론> 주문 번호 구현, 동시성 오류 비관적 락(Pessimistic Lock)으로 해결하기]]></title>
            <link>https://velog.io/@devel-history/GOORM-DEEP-DIVE-%EB%B0%B1%EC%97%94%EB%93%9C-3%ED%9A%8C%EC%B0%A8%EC%8A%A4%ED%83%80%EB%B2%85%EC%8A%A4-%ED%81%B4%EB%A1%A0-%EC%A3%BC%EB%AC%B8-%EB%B2%88%ED%98%B8-%EA%B5%AC%ED%98%84-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%98%A4%EB%A5%98-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BDPessimistic-Lock%EC%9C%BC%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@devel-history/GOORM-DEEP-DIVE-%EB%B0%B1%EC%97%94%EB%93%9C-3%ED%9A%8C%EC%B0%A8%EC%8A%A4%ED%83%80%EB%B2%85%EC%8A%A4-%ED%81%B4%EB%A1%A0-%EC%A3%BC%EB%AC%B8-%EB%B2%88%ED%98%B8-%EA%B5%AC%ED%98%84-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%98%A4%EB%A5%98-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BDPessimistic-Lock%EC%9C%BC%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 16 Jul 2025 08:47:26 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-왜-비관적-락을-선택했나">🤔 왜 비관적 락을 선택했나?</h2>
<ul>
<li>주문번호는 <strong>매장/날짜별로 반드시 유일</strong>해야 하며,</li>
<li>여러 사용자가 동시에 주문을 생성할 때 <strong>중복된 번호가 발생하면 절대 안 됨</strong></li>
<li>단 하나의 트랜잭션만이 해당 카운터 row에 접근하도록 제어해야 함</li>
</ul>
<h2 id="🛡️-비관적-락pessimistic-lock-적용-이유">🛡️ 비관적 락(Pessimistic Lock) 적용 이유</h2>
<ul>
<li>비관적 락을 사용하면, 해당 row에 대한 트랜잭션이 <strong>완료될 때까지 다른 트랜잭션의 접근이 차단</strong>됨</li>
<li>즉, 동시에 여러 사용자가 주문을 해도 <strong>race condition을 원천적으로 예방</strong></li>
<li>예외 상황(충돌 등)에 대한 처리도 단순해짐</li>
<li>주문 생성 트래픽이 아주 높지 않다면, 성능 저하 없이 <strong>데이터 무결성</strong>을 보장할 수 있음</li>
</ul>
<h2 id="⚙️-jpa에서-비관적-락-적용-방법">⚙️ JPA에서 비관적 락 적용 방법</h2>
<pre><code class="language-java">// Repository에서 비관적 락 쿼리 작성 예시
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(&quot;SELECT c FROM OrderDailyCounter c WHERE c.id = :id&quot;)
Optional&lt;OrderDailyCounter&gt; findByIdForUpdate(@Param(&quot;id&quot;) OrderDailyCounterId id);</code></pre>
<ul>
<li>서비스에서 <code>findByIdForUpdate</code>로 카운터를 조회하면, 해당 row에 쓰기 락이 걸림</li>
<li>락이 해제될 때까지(트랜잭션 종료 시점까지) 다른 트랜잭션은 대기</li>
</ul>
<h2 id="🚦-실제-적용-흐름">🚦 실제 적용 흐름</h2>
<ol>
<li>트랜잭션 시작</li>
<li><code>findByIdForUpdate</code>로 카운터 row 조회(비관적 락)</li>
<li>카운터 값 증가</li>
<li>저장 및 트랜잭션 커밋</li>
<li>락 해제, 다음 트랜잭션 진행</li>
</ol>
<hr>
<h2 id="✅-장점">✅ 장점</h2>
<ul>
<li><strong>Race condition 완벽 방지</strong></li>
<li>예외 상황 처리 단순(재시도 로직 등 불필요)</li>
<li>데이터 무결성 보장</li>
</ul>
<h2 id="⚠️-단점">⚠️ 단점</h2>
<ul>
<li>트래픽이 매우 높은 상황에서는 대기/병목 발생 가능</li>
<li>트랜잭션이 길어지면 데드락 위험</li>
<li>DB에 락이 많이 걸리면 성능 저하 가능</li>
</ul>
<hr>
<h2 id="📝-결론">📝 결론</h2>
<ul>
<li>주문번호처럼 <strong>절대 중복이 허용되지 않는</strong> 데이터에는 비관적 락이 매우 효과적</li>
<li>단, 트래픽 규모와 시스템 특성을 고려해 적용 필요</li>
<li>트래픽이 급격히 늘어날 경우, Redis나 Sequence 등 다른 대안도 함께 검토할 것!</li>
</ul>
<hr>
<h1 id="🛠️-실제-코드-변경-상세-feat71-ordernumber-race-condition">🛠️ 실제 코드 변경 상세 (feat/#71-orderNumber-race-condition)</h1>
<h2 id="1-repository에-비관적-락-메서드-추가">1. Repository에 비관적 락 메서드 추가</h2>
<pre><code class="language-java">@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(&quot;SELECT c FROM OrderDailyCounter c WHERE c.id = :id&quot;)
Optional&lt;OrderDailyCounter&gt; findByIdWithPessimisticLock(@Param(&quot;id&quot;) OrderDailyCounterId id);</code></pre>
<ul>
<li>기존 <code>findById</code> 대신, <strong>PESSIMISTIC_WRITE</strong> 락을 거는 쿼리 메서드를 추가</li>
</ul>
<h2 id="2-서비스-로직에서-락-메서드-사용">2. 서비스 로직에서 락 메서드 사용</h2>
<p>변경 전:</p>
<pre><code class="language-java">OrderDailyCounter counter = orderDailyCounterRepository.findById(id)
    .orElseGet(() -&gt; new OrderDailyCounter(id, 0));</code></pre>
<p>변경 후:</p>
<pre><code class="language-java">OrderDailyCounter counter = orderDailyCounterRepository.findByIdWithPessimisticLock(id)
    .orElseGet(() -&gt; {
        OrderDailyCounter newCounter = new OrderDailyCounter(id, 0);
        orderDailyCounterRepository.saveAndFlush(newCounter);
        return newCounter;
    });</code></pre>
<ul>
<li><strong>findByIdWithPessimisticLock</strong>로 row-level lock을 적용하여 카운터를 조회</li>
<li>만약 해당 row가 없으면 새로 생성 후 즉시 저장(<code>saveAndFlush</code>)하여 락이 적용된 상태로 카운터 관리</li>
</ul>
<h2 id="3-카운터-증가-및-저장">3. 카운터 증가 및 저장</h2>
<p>변경 전/후 동일:</p>
<pre><code class="language-java">counter.increment();
orderDailyCounterRepository.save(counter);</code></pre>
<h2 id="4-전체-흐름">4. 전체 흐름</h2>
<ol>
<li>트랜잭션 시작</li>
<li><strong>findByIdWithPessimisticLock</strong>로 카운터 row 조회(또는 신규 생성)</li>
<li>카운터 값 증가</li>
<li>저장 및 트랜잭션 커밋</li>
<li>락 해제, 다음 트랜잭션 진행</li>
</ol>
<hr>
<h2 id="💡-변경-효과">💡 변경 효과</h2>
<ul>
<li><strong>동시성 문제(race condition) 완전 차단</strong></li>
<li>주문번호 중복 없이, 안정적으로 주문 처리 가능</li>
<li>예외 상황 처리 단순화(재시도 로직 필요 없음)</li>
<li>트래픽이 아주 높지 않다면 성능 저하 없이 데이터 무결성 보장</li>
</ul>
<h3 id="변경후-결과">변경후 결과</h3>
<p><img src="https://velog.velcdn.com/images/devel-history/post/8bc56ce9-3de1-49df-b6d7-a49da74696ef/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/devel-history/post/cf38cc20-074b-4c19-9ce0-68aa106aa49a/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[GOORM-DEEP DIVE 백엔드 3회차<스타벅스 클론> 
주문 번호 구현,  동시성 오류 정리]]></title>
            <link>https://velog.io/@devel-history/GOORM-DEEP-DIVE-%EB%B0%B1%EC%97%94%EB%93%9C-3%ED%9A%8C%EC%B0%A8%EC%8A%A4%ED%83%80%EB%B2%85%EC%8A%A4-%ED%81%B4%EB%A1%A0-%EC%A3%BC%EB%AC%B8-%EB%B2%88%ED%98%B8-%EA%B5%AC%ED%98%84-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%98%A4%EB%A5%98-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@devel-history/GOORM-DEEP-DIVE-%EB%B0%B1%EC%97%94%EB%93%9C-3%ED%9A%8C%EC%B0%A8%EC%8A%A4%ED%83%80%EB%B2%85%EC%8A%A4-%ED%81%B4%EB%A1%A0-%EC%A3%BC%EB%AC%B8-%EB%B2%88%ED%98%B8-%EA%B5%AC%ED%98%84-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%98%A4%EB%A5%98-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 16 Jul 2025 02:35:40 GMT</pubDate>
            <description><![CDATA[<h1 id="📝-배경">📝 배경</h1>
<p>구름 딥 다이브 프로젝트에서 스타벅스 앱 클론을 개발하던 중,<br><strong>주문번호(order number) 중복 에러</strong>를 발견했습니다.</p>
<ul>
<li>천천히 1번씩 주문하면 문제 없음</li>
<li>하지만 k6로 대량 주문 API 호출 시, 100번 중 96번 실패!</li>
<li>대표 에러:<pre><code>[ERROR: duplicate key value violates unique constraint &quot;orders_order_number_key”]</code></pre><img src="https://velog.velcdn.com/images/devel-history/post/7e88c3f8-873a-4ee3-92fe-6c391a0f7c9f/image.png" alt=""></li>
</ul>
<p>이 글은 문제 해결 과정을 기록한 블로그 포스트입니다.</p>
<hr>
<h2 id="🔢-주문번호-조건">🔢 주문번호 조건</h2>
<ul>
<li><strong>매장이 다르면 주문번호가 같아도 됨</strong></li>
<li><strong>같은 매장, 같은 날짜에는 주문번호 중복 불가</strong></li>
<li><strong>매일 주문번호는 1번부터 다시 시작</strong></li>
<li><strong>고객과 매장은 동일 주문번호를 확인</strong></li>
<li><strong>A~Z 매장코드는 옵션, 실제 구현은 A로 통일</strong></li>
</ul>
<table>
<thead>
<tr>
<th>ID(PK)</th>
<th>메뉴</th>
<th>고객 주문번호</th>
<th>매장ID(FK)</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>[아메리카노 1잔, 라떼 2잔]</td>
<td>A-10</td>
<td>100</td>
</tr>
<tr>
<td>2</td>
<td>[아메리카노 3잔, 모카 1잔]</td>
<td>A-10</td>
<td>101</td>
</tr>
<tr>
<td>3</td>
<td>[자바칩 프라푸치노 2잔]</td>
<td>B-33</td>
<td>103</td>
</tr>
</tbody></table>
<hr>
<h2 id="🚀-1차-프로젝트-구현">🚀 1차 프로젝트 구현</h2>
<p>order_daily_counter 테이블의 카운터 값을 이용해 주문번호 생성</p>
<pre><code class="language-java">@Transactional
public String generateOrderNumber(OrderCreateRequest request) {
    LocalDate today = LocalDate.now();
    OrderDailyCounterId id = new OrderDailyCounterId(today, request.storeId());

    OrderDailyCounter counter = orderDailyCounterRepository.findById(id)
            .orElseGet(() -&gt; new OrderDailyCounter(id, 0));

    counter.increment();
    orderDailyCounterRepository.save(counter);

    return &quot;A-&quot; + counter.getCount();
}</code></pre>
<hr>
<h2 id="❓-의문점">❓ 의문점</h2>
<blockquote>
<p><code>generateOrderNumber()</code>에서 이미 저장을 하는데<br><strong>왜 주문번호가 중복(동일)하게 나올까?</strong> 🧐</p>
</blockquote>
<hr>
<h2 id="🛑-원인-분석">🛑 원인 분석</h2>
<ol>
<li>🟠 <strong>@Transactional은 트랜잭션의 원자성만 보장, 동시성 제어는 별도 필요</strong></li>
<li>🟠 <strong>여러 트랜잭션이 동시에 같은 값을 읽고, 각각 증가 후 저장 → race condition 발생</strong></li>
<li>🟠 JPA/Hibernate의 save/merge 방식은 동시성 문제에 취약</li>
</ol>
<blockquote>
<p><img src="https://velog.velcdn.com/images/devel-history/post/10cda61a-c9cb-4f06-8028-167299512843/image.png" alt=""></p>
</blockquote>
<hr>
<h2 id="🛠️-문제-해결-접근-단계">🛠️ 문제 해결 접근 단계</h2>
<ol>
<li><strong>요구사항·제약 재정의</strong></li>
<li><strong>시스템 환경 및 기술 스택 확인</strong></li>
<li><strong>적용 가능한 동시성 제어 기법 조사</strong></li>
</ol>
<hr>
<h3 id="1️⃣-요구사항·제약-재정의">1️⃣ 요구사항·제약 재정의</h3>
<ul>
<li>같은 매장, 같은 날짜에는 주문번호 중복 불가</li>
<li>다른 매장끼리는 주문번호 중복 허용</li>
<li>초당 100건 이상의 주문이 들어올 수 있음 (1000이상까지 목표)</li>
<li>order_daily_counter 테이블은 (date, store_id) 복합키 사용</li>
</ul>
<hr>
<h3 id="2️⃣-시스템-환경-및-기술-스택">2️⃣ 시스템 환경 및 기술 스택</h3>
<blockquote>
<ul>
<li><strong>백엔드</strong>: Spring Boot, JPA(Hibernate)</li>
<li><strong>트랜잭션 관리</strong>: Spring의 @Transactional</li>
<li><strong>DB</strong>: PostgreSQL</li>
<li><strong>운영환경</strong>: JVM 기반, Prometheus 지원</li>
</ul>
</blockquote>
<hr>
<h3 id="3️⃣-적용-가능한-동시성-제어-기법">3️⃣ 적용 가능한 동시성 제어 기법</h3>
<table>
<thead>
<tr>
<th align="center">방법 (영문)</th>
<th align="center">요약</th>
<th align="center">장점</th>
<th align="center">단점</th>
</tr>
</thead>
<tbody><tr>
<td align="center">🟢 <strong>DB Atomic Operation</strong>(원자적 연산)</td>
<td align="center"><code>UPDATE ... SET count = count + 1</code>과 같이 DB 명령어를 활용</td>
<td align="center">구현 간단, DB가 동시성 제어</td>
<td align="center">DB 부하 집중, 트랜잭션 충돌 가능성</td>
</tr>
<tr>
<td align="center">🔵 <strong>DB Sequence</strong></td>
<td align="center">데이터베이스의 시퀀스 객체를 사용하여 고유값 생성</td>
<td align="center">고유값 보장, Race Condition 없음</td>
<td align="center">날짜별 리셋 등 비즈니스 로직과 다를 수 있음</td>
</tr>
<tr>
<td align="center">🟣 <strong>Optimistic Lock</strong>(낙관적 락)</td>
<td align="center"><code>@Version</code> 필드로 버전 충돌 감지 후 재시도</td>
<td align="center">트래픽 낮으면 성능 유리, Deadlock 위험 낮음</td>
<td align="center">충돌 많으면 재시도 비용 증가</td>
</tr>
<tr>
<td align="center">🟡 <strong>Pessimistic Lock(비관적 락)</strong></td>
<td align="center"><code>SELECT FOR UPDATE</code> 구문으로 데이터 읽기 시점에 락을 선점</td>
<td align="center">충돌 발생률이 높을 때 데이터 무결성 확실히 보장</td>
<td align="center">락 대기로 인해 성능 저하, Deadlock 위험 높음</td>
</tr>
<tr>
<td align="center">🟠 <strong>Redis INCR</strong>(증가 연산)</td>
<td align="center">Redis의 <code>INCR</code> (Atomic Increment) 명령 활용</td>
<td align="center">고성능, 확장성 우수</td>
<td align="center">Redis 장애 시 대체 로직 필요, 영속성 주의</td>
</tr>
<tr>
<td align="center">🟤 <strong>DB Upsert</strong>(삽입/갱신)</td>
<td align="center"><code>INSERT ... ON CONFLICT DO UPDATE</code>와 같은 구문 활용</td>
<td align="center">단일 쿼리로 동시성 강함</td>
<td align="center">DB별 문법 상이, 트랜잭션 내 주의 필요</td>
</tr>
</tbody></table>
<hr>
<h2 id="💡-결론">💡 결론</h2>
<ul>
<li>단순 @Transactional + save/merge만으로는 동시성 문제 해결 불가</li>
<li>반드시 DB atomic 연산, Optimistic Lock, Upsert 등 별도의 동시성 제어 필요!</li>
<li>시스템 환경과 요구사항에 맞는 방법을 신중히 선택해야 함</li>
</ul>
<blockquote>
<p><strong>실제 적용 예시, 해결법 상세 구현, 트러블슈팅 경험 등은 별도 포스트로 이어집니다!</strong></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>