<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>david1-p</title>
        <link>https://velog.io/</link>
        <description>DONE IS BETTER THAN PERFECT.</description>
        <lastBuildDate>Mon, 19 Jan 2026 22:41:51 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>david1-p</title>
            <url>https://velog.velcdn.com/images/david1-p/profile/cc1e3d2c-1e22-434b-92bb-b8f11430a7f8/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. david1-p. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/david1-p" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Gradle]]></title>
            <link>https://velog.io/@david1-p/Gradle</link>
            <guid>https://velog.io/@david1-p/Gradle</guid>
            <pubDate>Mon, 19 Jan 2026 22:41:51 GMT</pubDate>
            <description><![CDATA[<h2 id="1-gradle이란">1. Gradle이란?</h2>
<p>Gradle은 Java, Kotlin, Scala 등 JVM 기반 언어에서 주로 사용되는 오픈소스 빌드 자동화 도구입니다. 기존 Ant의 유연성과 Maven의 의존성 관리 편의성을 결합하고 단점을 보완하여 설계되었습니다.</p>
<p><strong>특징</strong></p>
<ul>
<li><strong>고성능</strong>: 증분 빌드(Incremental Build), 빌드 캐시(Build Cache), 데몬 프로세스를 활용하여 빌드 속도를 비약적으로 최적화했습니다.</li>
<li><strong>유연성</strong>: Groovy 또는 Kotlin DSL(Domain Specific Language)을 사용하여 로직이 포함된 복잡한 빌드 스크립트를 작성할 수 있습니다.</li>
<li><strong>확장성</strong>: 다양한 플러그인 생태계를 갖추고 있으며, 커스텀 태스크를 쉽게 정의할 수 있습니다.</li>
<li><strong>멀티 프로젝트 지원</strong>: 대규모 프로젝트를 모듈 단위로 나누어 관리하기 용이하도록 설계되었습니다.</li>
</ul>
<h2 id="2-빌드-자동화-도구의-사용-목적">2. 빌드 자동화 도구의 사용 목적</h2>
<p>빌드 도구는 단순한 컴파일러 실행을 넘어 소프트웨어 생명주기 전반을 관리합니다.</p>
<ul>
<li><strong>생산성 향상</strong>: 컴파일, 테스트, 패키징, 배포 등 반복적인 수동 작업을 자동화합니다.</li>
<li><strong>일관성 보장</strong>: 개발자 로컬, 테스트 서버, 운영 서버 등 어떤 환경에서도 동일한 빌드 결과를 보장합니다.</li>
<li><strong>의존성 관리</strong>: 외부 라이브러리를 자동으로 다운로드하고, 라이브러리 간의 버전 충돌을 효율적으로 관리합니다.</li>
<li><strong>품질 관리</strong>: 테스트 자동화 및 정적 분석을 통해 휴먼 에러를 방지하고 코드 품질을 유지합니다.</li>
<li><strong>CI/CD 연동</strong>: Jenkins, GitHub Actions 등과 연동하여 빌드부터 배포까지의 파이프라인을 구축할 수 있습니다.</li>
</ul>
<h2 id="3-maven-vs-gradle">3. Maven vs Gradle</h2>
<p>가장 큰 차이점은 <strong>빌드 스크립트 작성 방식</strong>과 <strong>성능</strong>입니다. Gradle은 Maven보다 늦게 나온 만큼 성능과 유연성 면에서 우위에 있습니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Maven</th>
<th>Gradle</th>
</tr>
</thead>
<tbody><tr>
<td><strong>스크립트 언어</strong></td>
<td>XML (pom.xml)</td>
<td>Groovy DSL / Kotlin DSL (build.gradle)</td>
</tr>
<tr>
<td><strong>빌드 속도</strong></td>
<td>상대적으로 느림 (전체 빌드 위주)</td>
<td>빠름 (증분 빌드, 빌드 캐시, 데몬 활용)</td>
</tr>
<tr>
<td><strong>의존성 관리</strong></td>
<td>선언적 관리, 상속 구조</td>
<td>유연한 관리, 동적 버전 지원</td>
</tr>
<tr>
<td><strong>가독성</strong></td>
<td>태그 구조로 인해 장황함</td>
<td>코드 기반으로 간결하고 직관적임</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>정해진 라이프사이클에 종속적</td>
<td>스크립트로 로직 제어 및 확장 용이</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>핵심 차이</strong>: Gradle은 작업의 입력과 출력을 확인하여 변경된 부분만 빌드하는 <strong>증분 빌드(Incremental Build)</strong>를 지원하기 때문에, 프로젝트 규모가 커질수록 Maven 대비 압도적인 성능 차이를 보입니다.</p>
</blockquote>
<h2 id="4-dependency-configuration-의존성-설정">4. Dependency Configuration (의존성 설정)</h2>
<p>애플리케이션에 필요한 라이브러리의 사용 범위와 시점을 정의합니다. 올바른 설정은 빌드 속도를 높이고 결과물의 크기를 줄이는 데 필수적입니다.</p>
<p><strong>implementation</strong></p>
<ul>
<li>컴파일 및 런타임에 모두 필요한 의존성입니다.</li>
<li><strong>특징</strong>: 의존하고 있는 라이브러리가 변경되더라도, 해당 모듈을 직접 의존하는 모듈만 재컴파일하면 됩니다. (재컴파일 범위 최소화로 빌드 속도 향상)</li>
<li>대부분의 라이브러리 적용 시 기본적으로 사용합니다.</li>
</ul>
<p><strong>api</strong></p>
<ul>
<li>implementation과 유사하지만, 의존성을 다른 모듈로 <strong>전이</strong>시킵니다.</li>
<li><strong>특징</strong>: A -&gt; B(api) -&gt; C 구조일 때, C가 변경되면 A도 재컴파일됩니다. 라이브러리 개발 등 내부 의존성을 외부로 노출해야 할 때만 신중히 사용해야 합니다.</li>
</ul>
<p><strong>compileOnly</strong></p>
<ul>
<li>컴파일 시점에만 필요하고 런타임에는 포함되지 않는 의존성입니다.</li>
<li>예: Lombok (컴파일 시 코드를 생성하고, 실제 실행 시에는 필요 없음)</li>
</ul>
<p><strong>runtimeOnly</strong></p>
<ul>
<li>컴파일 시점에는 필요 없고, 실행 시점에만 필요한 의존성입니다.</li>
<li>예: DB Driver (코드 상에서는 JDBC 인터페이스만 사용하고, 구현체는 런타임에 로딩)</li>
</ul>
<p><strong>annotationProcessor</strong></p>
<ul>
<li>컴파일 시점에 어노테이션을 분석하고 코드를 생성하는 전처리기입니다.</li>
<li>예: QueryDSL, MapStruct, Lombok</li>
</ul>
<p><strong>testImplementation</strong></p>
<ul>
<li>테스트 코드를 작성하고 실행할 때만 필요한 의존성입니다.</li>
<li>예: JUnit, Mockito, H2 Database</li>
</ul>
<hr>
<h2 id="5-gradle-wrapper-gradlew">5. Gradle Wrapper (gradlew)</h2>
<p>Gradle을 설치하지 않은 환경에서도 프로젝트를 빌드할 수 있도록 도와주는 내장 스크립트입니다.</p>
<ul>
<li>프로젝트 생성 시 포함된 <code>gradlew</code> 스크립트를 사용하면, 개발자 간 혹은 CI 서버 간에 <strong>완벽히 동일한 Gradle 버전</strong>을 사용하도록 강제할 수 있습니다.</li>
<li>협업 시 버전 호환성 문제를 방지하기 위해 로컬에 설치된 gradle 명령어보다 <code>./gradlew</code> 사용을 권장합니다.</li>
</ul>
<h2 id="6-빌드-라이프사이클-build-lifecycle">6. 빌드 라이프사이클 (Build Lifecycle)</h2>
<p>Gradle 빌드는 크게 3단계로 진행됩니다.</p>
<ol>
<li><strong>초기화 (Initialization)</strong>: <code>settings.gradle</code>을 읽어 빌드 대상 프로젝트(모듈)를 결정합니다.</li>
<li><strong>설정 (Configuration)</strong>: 모든 프로젝트의 빌드 스크립트를 실행하여 태스크들의 의존성 그래프를 구성합니다.</li>
<li><strong>실행 (Execution)</strong>: 결정된 태스크 그래프에 따라 실제 작업을 수행합니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[테스트 더블]]></title>
            <link>https://velog.io/@david1-p/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8D%94%EB%B8%94</link>
            <guid>https://velog.io/@david1-p/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8D%94%EB%B8%94</guid>
            <pubDate>Tue, 02 Dec 2025 03:20:30 GMT</pubDate>
            <description><![CDATA[<p>백엔드 개발을 하며 테스트 코드를 작성하다 보면, 실제 데이터베이스나 외부 API와 같은 &#39;의존성&#39;을 그대로 사용하기 어려운 경우가 많습니다. 이때 등장하는 개념이 바로 <strong>테스트 더블(Test Double)</strong>입니다.</p>
<h2 id="1-테스트-더블이란">1. 테스트 더블이란?</h2>
<p>영화 촬영에서 위험한 장면을 대신 연기하는 <strong>스턴트 더블(Stunt Double)</strong>이 있듯이, <strong>테스트 더블은 실제 의존성 객체를 대신하여 테스트에 활용되는 모든 객체</strong>를 통칭하는 용어입니다.</p>
<h3 id="왜-사용할까요">왜 사용할까요?</h3>
<p>실제 의존성을 테스트에 그대로 포함시키면 다음과 같은 문제가 발생할 수 있습니다.</p>
<ul>
<li><strong>비결정적 동작:</strong> 외부 API 서버가 다운되었거나 네트워크 문제로 테스트가 실패할 수 있습니다.</li>
<li><strong>속도 저하:</strong> DB I/O나 네트워크 통신은 테스트 속도를 느리게 만듭니다.</li>
<li><strong>부수 효과(Side Effect):</strong> 테스트 도중 실제 이메일이 발송되거나, 운영 DB 데이터가 변경될 위험이 있습니다.</li>
<li><strong>복잡한 설정:</strong> 의존성 객체를 생성하기 위해 수많은 설정이 필요할 수 있습니다.</li>
</ul>
<p>테스트 더블은 이러한 외부 요인으로부터 테스트를 격리시켜 <strong>빠르고, 안정적이며, 독립적인 테스트 환경</strong>을 만들어 줍니다.</p>
<hr>
<h2 id="2-테스트-더블의-5가지-종류">2. 테스트 더블의 5가지 종류</h2>
<p>테스트 더블은 그 역할과 복잡도에 따라 크게 5가지(Dummy, Stub, Fake, Spy, Mock)로 분류됩니다. (제라드 메스자로스의 분류 기준)</p>
<p><img src="https://velog.velcdn.com/images/david1-p/post/102ba3ac-25dc-456f-9c30-80aae1e0f403/image.jpg" alt=""></p>
<h3 id="1-더미-dummy">1) 더미 (Dummy)</h3>
<ul>
<li><strong>개념:</strong> 가장 기본적인 형태입니다. 객체가 필요하지만 <strong>실제로 기능은 전혀 필요 없는 경우</strong>에 사용합니다.</li>
<li><strong>용도:</strong> 주로 인자 리스트를 채우기 위해 사용되며, 메서드가 호출되어도 아무런 동작을 하지 않거나 예외를 던집니다.</li>
<li><strong>예시:</strong> 생성자의 매개변수로 필요하지만, 테스트 로직에는 전혀 영향을 주지 않는 객체.</li>
</ul>
<!-- end list -->

<pre><code class="language-java">// 단순히 컴파일 에러를 피하기 위해 넘겨주는 껍데기
User dummyUser = new User(); 
boardService.createBoard(dummyUser, &quot;제목&quot;); </code></pre>
<h3 id="2-스텁-stub">2) 스텁 (Stub)</h3>
<ul>
<li><strong>개념:</strong> 더미보다 한 단계 발전한 형태로, <strong>미리 준비된 답변</strong>을 제공하는 객체입니다.</li>
<li><strong>용도:</strong> 테스트 호출에 대해 미리 정의해 둔 결과를 반환합니다. 로직은 없으며, 단순히 원하는 데이터 상태를 만들어줍니다.</li>
<li><strong>예시:</strong> &quot;ID가 1인 사용자를 조회하면, 무조건 Alice 객체를 반환해라&quot;라고 설정.</li>
</ul>
<!-- end list -->

<pre><code class="language-java">// UserRepository의 스텁 구현
public class StubUserRepository implements UserRepository {
    @Override
    public User findById(Long id) {
        // DB 조회 없이 무조건 미리 준비된 객체 반환
        return new User(1L, &quot;Alice&quot;);
    }
}</code></pre>
<h3 id="3-페이크-fake">3) 페이크 (Fake)</h3>
<ul>
<li><strong>개념:</strong> <strong>실제 동작하는 구현</strong>을 가지고 있지만, 프로덕션(운영) 환경에는 적합하지 않은 객체입니다.</li>
<li><strong>용도:</strong> 로직이 실제로 돌아가기 때문에 실제 객체와 가장 유사하게 동작합니다. 하지만 성능이나 메모리 문제로 실제로는 쓰지 않습니다.</li>
<li><strong>예시:</strong> 실제 DB 대신 <code>HashMap</code>이나 <code>ArrayList</code>를 사용하여 메모리 상에서만 데이터를 저장하고 조회하는 가짜 리포지토리(In-Memory Database).</li>
</ul>
<!-- end list -->

<pre><code class="language-java">// 실제 DB 대신 Map을 사용하는 Fake 객체
public class FakeUserRepository implements UserRepository {
    private Map&lt;Long, User&gt; data = new HashMap&lt;&gt;();

    @Override
    public void save(User user) {
        data.put(user.getId(), user);
    }

    @Override
    public User findById(Long id) {
        return data.get(id);
    }
}</code></pre>
<h3 id="4-스파이-spy">4) 스파이 (Spy)</h3>
<ul>
<li><strong>개념:</strong> <strong>자신이 호출된 내역을 몰래 기록</strong>하는 객체입니다. 스텁의 역할을 하면서, 추가적으로 호출 정보를 기록합니다.</li>
<li><strong>용도:</strong> 메서드가 몇 번 호출되었는지, 어떤 인자가 넘어왔는지 등을 검증할 때 사용합니다. 실제 객체를 감싸서(Wrapper) 사용할 수도 있습니다.</li>
<li><strong>예시:</strong> 이메일 발송 서비스가 실제로 호출되었는지, 호출되었다면 수신자가 누구였는지 확인.</li>
</ul>
<!-- end list -->

<pre><code class="language-java">// 호출 여부를 기록하는 Spy
public class SpyEmailService implements EmailService {
    public int sendCount = 0; // 호출 횟수 기록
    public String lastMessage = null;

    @Override
    public void send(String message) {
        this.sendCount++;
        this.lastMessage = message;
    }
}</code></pre>
<h3 id="5-목-mock">5) 목 (Mock)</h3>
<ul>
<li><strong>개념:</strong> <strong>행위(Behavior)를 검증</strong>하기 위해 사용되는 객체입니다.</li>
<li><strong>용도:</strong> &quot;이 메서드가 호출되어야 한다&quot;는 <strong>기대(Expectation)</strong>를 미리 정의해두고, 테스트가 끝난 후 그 기대대로 동작했는지 확인합니다. 기대와 다르게 동작하면 예외를 발생시킵니다.</li>
<li><strong>특징:</strong> 목 프레임워크(Mockito 등)를 사용하면 스텁과 스파이의 기능을 모두 포함하는 강력한 기능을 제공합니다.</li>
</ul>
<!-- end list -->

<pre><code class="language-java">// Mockito를 활용한 Mock 예시
// 1. Mock 생성
EmailService mockEmailService = mock(EmailService.class);

// 2. 행위 수행
orderService.order();

// 3. 행위 검증 (verify): send()가 1번 호출되었는지 확인
verify(mockEmailService, times(1)).send(anyString());</code></pre>
<hr>
<h2 id="3-핵심-구분-상태-검증-vs-행위-검증">3. 핵심 구분: 상태 검증 vs 행위 검증</h2>
<p>테스트 더블을 이해할 때 가장 중요한 기준은 <strong>&quot;무엇을 검증하는가?&quot;</strong>입니다.</p>
<ol>
<li><p><strong>상태 검증 (State Verification):</strong></p>
<ul>
<li>메서드 실행 후, 객체의 <strong>상태(데이터 값)</strong>가 어떻게 변했는지 확인합니다.</li>
<li>주로 <strong>Stub, Fake, Spy</strong>를 사용하여 확인합니다.</li>
<li><em>예: <code>save()</code> 호출 후 <code>findById()</code>로 조회했을 때 데이터가 잘 들어있는가?</em></li>
</ul>
</li>
<li><p><strong>행위 검증 (Behavior Verification):</strong></p>
<ul>
<li>메서드가 실행될 때, 의존하고 있는 다른 객체와 <strong>올바르게 상호작용(호출)</strong> 했는지 확인합니다.</li>
<li>주로 <strong>Mock</strong>을 사용하여 확인합니다.</li>
<li><em>예: <code>order()</code> 호출 시 <code>emailService.send()</code>가 정확히 1번 호출되었는가?</em></li>
</ul>
</li>
</ol>
<blockquote>
<p><strong>Tip:</strong> 보통 실무에서는 <code>Stub</code>으로 상태를 세팅하고, <code>Mock</code>으로 행위를 검증하는 방식이 혼합되어 사용됩니다. 최근의 Mockito 같은 프레임워크는 Mock 객체 하나로 스텁(Stubbing)과 검증(Verifying)을 모두 처리할 수 있어 경계가 모호해지기도 했지만, 개념적으로는 분리해서 이해하는 것이 좋습니다.</p>
</blockquote>
<hr>
<h2 id="4-요약">4. 요약</h2>
<p>테스트 더블은 외부 세계의 불확실성을 제거하고 오직 <strong>나의 로직(System Under Test)</strong>에만 집중할 수 있게 해주는 강력한 도구입니다.</p>
<ul>
<li>단순한 데이터 주입이 필요하면 <strong>Dummy</strong></li>
<li>테스트를 위한 특정 결과값이 필요하면 <strong>Stub</strong></li>
<li>가벼운 로직 구현체가 필요하면 <strong>Fake</strong></li>
<li>호출 내역을 확인하고 싶다면 <strong>Spy</strong></li>
<li>올바르게 호출되었는지 행위를 검증하고 싶다면 <strong>Mock</strong></li>
</ul>
<p>각각의 특징을 잘 이해하고 상황에 맞는 적절한 테스트 더블을 선택한다면, 훨씬 더 견고하고 유지보수하기 쉬운 테스트 코드를 작성할 수 있습니다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[무중단 배포]]></title>
            <link>https://velog.io/@david1-p/%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@david1-p/%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Mon, 01 Dec 2025 05:01:14 GMT</pubDate>
            <description><![CDATA[<h1 id="무중단-배포zero-downtime-deployment-전략-총정리-롤링-블루그린-카나리">무중단 배포(Zero-Downtime Deployment) 전략 총정리: 롤링, 블루/그린, 카나리</h1>
<p>서비스를 운영하다 보면 새로운 기능을 배포하거나 버그를 수정해야 할 일이 빈번하게 발생합니다. 이때 서비스에 다운 타임(정지 시간)이 발생하지 않으면서 새로운 버전의 애플리케이션을 서버에 배포하는 것을 <strong>무중단 배포(Zero-Downtime Deployment)</strong>라고 합니다.</p>
<p>무중단 배포 패턴에는 대표적으로 순차적으로 배포하는 <strong>롤링 배포</strong>, 전체 서버를 통째로 바꾸는 <strong>블루/그린 배포</strong>, 트래픽을 순차적으로 이동시키는 <strong>카나리 배포</strong>가 존재합니다. 각 배포 방식의 특징과 선택 기준, 그리고 주의사항을 정리해 보겠습니다.</p>
<hr>
<h2 id="1-롤링-배포-rolling-deployment">1. 롤링 배포 (Rolling Deployment)</h2>
<p>롤링 배포는 서버를 한 대씩(혹은 그룹 단위로) 순차적으로 업데이트하는 가장 기본적인 방식입니다. 구버전에서 신버전으로 트래픽을 점진적으로 전환합니다.</p>
<ul>
<li><strong>동작 방식:</strong> 로드 밸런서에서 배포 대상 서버를 제외하고, 업데이트를 진행한 뒤 다시 로드 밸런서에 연결하는 과정을 반복합니다.</li>
<li><strong>특징:</strong><ul>
<li>배포가 진행 중인 시점에는 구버전과 신버전이 공존하므로, 새로운 버전은 반드시 <strong>하위 호환성(Backward Compatibility)</strong>을 보장해야 합니다.</li>
<li>새로운 버전을 배포하기 위해 추가적인 인프라를 구축할 필요가 없어 비용적으로 효율적입니다.</li>
<li>배포 중인 서버는 트래픽을 받을 수 없으므로, 남은 서버들에 부하가 집중될 수 있습니다. (서버 용량에 여유가 있어야 함)</li>
<li>서버 대수가 많을 경우 배포 시간이 오래 걸릴 수 있어, 한 번에 여러 대를 배포하는 &#39;배포 윈도우(Batch Size)&#39; 조절이 필요합니다.</li>
</ul>
</li>
</ul>
<h2 id="2-블루그린-배포-bluegreen-deployment">2. 블루/그린 배포 (Blue/Green Deployment)</h2>
<p>블루/그린 배포는 기존 운영 환경(Blue)과 동일한 스펙과 사이즈의 새로운 환경(Green)을 미리 준비하고, 신규 버전을 배포한 후 트래픽을 일제히 신규 버전으로 전환하는 방식입니다.</p>
<ul>
<li><strong>동작 방식:</strong> 신규 버전(Green) 배포 및 테스트가 완료되면, 로드 밸런서나 라우터 설정을 변경하여 모든 트래픽을 Green으로 돌립니다. 기존 Blue 환경은 대기 상태로 두거나 폐기합니다.</li>
<li><strong>특징:</strong><ul>
<li>구버전 환경(Blue)이 그대로 남아있기 때문에 문제 발생 시 즉시 롤백이 가능합니다.</li>
<li>운영 환경과 똑같은 서버 리소스를 추가로 준비해야 하므로 <strong>비용이 두 배로 발생</strong>할 수 있습니다.</li>
<li>새로운 환경으로 트래픽이 한 번에 몰리기 때문에, 서버가 예열되지 않아 초반 성능 저하가 발생할 수 있습니다. (Warm-up 과정 필요)</li>
</ul>
</li>
</ul>
<h2 id="3-카나리-배포-canary-deployment">3. 카나리 배포 (Canary Deployment)</h2>
<p>카나리 배포는 광산의 카나리아 새처럼 위험을 감지하기 위해 소수의 트래픽만 신규 버전으로 분산시킨 후, 문제가 없으면 점차 트래픽 비중을 늘려가는 방식입니다.</p>
<ul>
<li><strong>동작 방식:</strong> 기존 서버와 신규 서버를 구성하고 트래픽을 퍼센티지 단위로 조절합니다. (예: 기존 90%, 신규 10% -&gt; 기존 50%, 신규 50% -&gt; 신규 100%)</li>
<li><strong>특징:</strong><ul>
<li>오류율이나 성능 이슈를 소규모 트래픽에서 미리 감지할 수 있어 리스크 관리에 탁월합니다.</li>
<li><strong>A/B 테스트</strong> 용도로 활용하기 좋습니다.</li>
<li>롤링 배포와 마찬가지로 특정 시점에 두 버전이 공존하므로 하위 호환성 관리가 필수입니다.</li>
<li>동일한 사용자가 새로고침할 때마다 버전이 바뀌지 않도록 <strong>스티키 세션(Sticky Session)</strong> 설정이 필요할 수 있습니다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="한눈에-보는-배포-전략-비교">한눈에 보는 배포 전략 비교</h2>
<table>
<thead>
<tr>
<th align="left">특징</th>
<th align="left">롤링 (Rolling)</th>
<th align="left">블루/그린 (Blue/Green)</th>
<th align="left">카나리 (Canary)</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>배포 속도</strong></td>
<td align="left">느림</td>
<td align="left">빠름 (즉시 전환)</td>
<td align="left">중간 (조절 가능)</td>
</tr>
<tr>
<td align="left"><strong>비용 (리소스)</strong></td>
<td align="left">낮음 (추가 서버 불필요)</td>
<td align="left">높음 (서버 2배 필요)</td>
<td align="left">중간</td>
</tr>
<tr>
<td align="left"><strong>롤백 난이도</strong></td>
<td align="left">어려움 (역배포 필요)</td>
<td align="left">쉬움 (트래픽만 원복)</td>
<td align="left">쉬움</td>
</tr>
<tr>
<td align="left"><strong>위험도</strong></td>
<td align="left">중간</td>
<td align="left">낮음 (테스트 후 전환)</td>
<td align="left">매우 낮음 (검증하며 확대)</td>
</tr>
</tbody></table>
<hr>
<h2 id="어떤-상황에-어떤-전략을-선택해야-할까">어떤 상황에 어떤 전략을 선택해야 할까?</h2>
<p>각 배포 전략은 서비스의 상황과 목적에 따라 선택해야 합니다.</p>
<p><strong>1. 롤링 배포를 선택하는 경우</strong></p>
<ul>
<li><strong>비용 절감이 최우선일 때:</strong> 배포를 위해 새로운 서버를 추가로 생성하는 비용을 감수하기 어려운 경우 적합합니다.</li>
<li><strong>소규모 버그 수정:</strong> 서버 API 구간에서 버그가 생겼을 때, 개발 서버 테스트 후 상용 환경에서 1대만 먼저 배포하여 디버그 레벨 로깅 등으로 검증하고 싶을 때 유용합니다. 문제가 없다면 순차적으로 배포를 완료합니다.</li>
</ul>
<p><strong>2. 블루/그린 배포를 선택하는 경우</strong></p>
<ul>
<li><strong>대규모 업데이트:</strong> 기술 부채 해결이나 아키텍처 변경 등 시스템 전반에 큰 변화가 있을 때 적합합니다.</li>
<li><strong>안정적인 롤백이 중요할 때:</strong> 배포 직후 치명적인 오류가 발생할 가능성이 있어 즉시 이전 버전으로 돌아가야 하는 중요한 서비스에 유리합니다.</li>
</ul>
<p><strong>3. 카나리 배포를 선택하는 경우</strong></p>
<ul>
<li><strong>데이터 기반 검증이 필요할 때:</strong> 신규 기능에 대한 오류율, 성능, 사용자 반응 등을 통계적으로 확인하고 싶을 때 사용합니다.</li>
<li><strong>A/B 테스트:</strong> 특정 사용자 그룹에게만 새로운 UI나 기능을 노출하여 반응을 살피고 싶을 때 채택합니다.</li>
</ul>
<hr>
<h2 id="무중단-배포-시-반드시-고려해야-할-기술적-요소">무중단 배포 시 반드시 고려해야 할 기술적 요소</h2>
<p>성공적인 무중단 배포를 위해서는 단순히 배포 방식뿐만 아니라 인프라와 데이터베이스 레벨에서의 고려가 필요합니다.</p>
<p><strong>1. 데이터베이스 하위 호환성 (Database Backward Compatibility)</strong>
애플리케이션은 무중단으로 배포되더라도 DB 스키마가 변경되면 구버전 애플리케이션에서 오류가 발생할 수 있습니다. 컬럼 삭제나 변경이 필요할 경우, &#39;컬럼 추가 -&gt; 코드 배포 -&gt; 데이터 마이그레이션 -&gt; 컬럼 삭제&#39;와 같이 단계적인 접근이 필요합니다.</p>
<p><strong>2. 우아한 종료 (Graceful Shutdown)</strong>
배포를 위해 서버를 내릴 때, 진행 중이던 요청을 강제로 끊어버리면 사용자는 에러를 경험하게 됩니다. <code>SIGTERM</code> 신호를 받았을 때 새로운 요청은 거부하되, 기존에 처리 중이던 작업은 완료한 후 종료되도록 애플리케이션을 설정해야 합니다.</p>
<p><strong>3. 헬스 체크 (Health Check)</strong>
새로 배포된 서버가 완전히 구동되지 않은 상태에서 트래픽이 유입되면 에러가 발생합니다. 로드 밸런서가 애플리케이션의 상태(Readiness Probe)를 확인하고, 정상적으로 응답할 때만 트래픽을 보내도록 설정해야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Graceful Shutdown]]></title>
            <link>https://velog.io/@david1-p/Graceful-Shutdown</link>
            <guid>https://velog.io/@david1-p/Graceful-Shutdown</guid>
            <pubDate>Tue, 25 Nov 2025 23:18:26 GMT</pubDate>
            <description><![CDATA[<h1 id="graceful-shutdown-안정적인-서버-종료를-위한-전략과-spring-boot-내부-동작">Graceful Shutdown: 안정적인 서버 종료를 위한 전략과 Spring Boot 내부 동작</h1>
<p>백엔드 애플리케이션을 개발하고 운영하다 보면 &#39;배포&#39;는 피할 수 없는 일상적인 작업입니다. 새로운 기능을 추가하거나 버그를 수정하기 위해 기존 프로세스를 종료하고 새로운 프로세스를 실행합니다. 이때 중요한 것은 <strong>&quot;어떻게 종료하느냐&quot;</strong> 입니다.</p>
<p>애플리케이션이 정상적으로 시작되는 것만큼이나 정상적으로 종료되는 과정 또한 서비스의 안정성에 큰 영향을 미칩니다. 이번 글에서는 Graceful Shutdown(우아한 종료)의 필요성과 OS 시그널의 차이, Spring Boot 환경에서의 구체적인 동작 원리, 그리고 <strong>실무 운영 환경(로드밸런서, 쿠버네티스)에서의 고려사항</strong>까지 알아보겠습니다.</p>
<h2 id="1-graceful-shutdown이란">1. Graceful Shutdown이란?</h2>
<p>Graceful Shutdown(우아한 종료)이란 애플리케이션이 종료 신호를 받았을 때 즉시 전원을 끄듯 멈추는 것이 아니라, <strong>현재 처리 중인 작업들을 모두 안전하게 마무리하고 리소스를 정리한 뒤 종료하는 방식</strong>을 의미합니다.</p>
<p>서버 애플리케이션 관점에서 보면 다음과 같은 절차를 따릅니다.</p>
<ol>
<li>종료 신호(SIGTERM 등)를 감지합니다.</li>
<li>로드 밸런서나 연결된 네트워크 레이어로부터 들어오는 <strong>새로운 요청을 차단</strong>합니다.</li>
<li>이미 서버 내에서 <strong>처리 중이던 요청들은 끝까지 수행</strong>합니다.</li>
<li>모든 처리가 완료되거나 타임아웃 시간이 지나면 프로세스를 종료합니다.</li>
</ol>
<h3 id="왜-필요한가">왜 필요한가?</h3>
<p>만약 요청을 처리하는 도중에 프로세스가 즉각적으로(Hard Shutdown) 종료된다면 다음과 같은 문제가 발생할 수 있습니다.</p>
<ul>
<li><strong>트랜잭션 비정상 종료:</strong> 데이터베이스에 쓰기 작업 중 연결이 끊겨 데이터 무결성이 깨질 수 있습니다.</li>
<li><strong>데이터 손실:</strong> 메모리상에 존재하던 미처 저장되지 못한 데이터가 유실될 수 있습니다.</li>
<li><strong>사용자 경험 저하:</strong> 클라이언트는 정상적인 응답 대신 <code>Connection Reset</code> 오류나 <code>500 Internal Server Error</code>를 받게 됩니다.</li>
</ul>
<h2 id="2-종료-시그널-sigterm-vs-sigkill">2. 종료 시그널: SIGTERM vs SIGKILL</h2>
<p>리눅스 및 유닉스 환경에서 프로세스를 종료할 때 사용하는 <code>kill</code> 명령어는 프로세스에 특정 시그널(Signal)을 보냅니다. Graceful Shutdown을 위해서는 <code>SIGTERM</code>과 <code>SIGKILL</code>의 차이를 명확히 알아야 합니다.</p>
<h3 id="sigkill--9">SIGKILL (-9)</h3>
<ul>
<li><strong>의미:</strong> 프로세스 강제 종료</li>
<li><strong>동작:</strong> 프로세스가 종료되기 전에 수행해야 할 정리 작업(Cleanup)을 전혀 실행하지 않고 커널 레벨에서 즉시 프로세스를 제거합니다. 애플리케이션이 시그널을 핸들링할 수 없으므로 Graceful Shutdown이 불가능합니다.</li>
</ul>
<!-- end list -->

<pre><code class="language-bash">kill -9 {PID}</code></pre>
<h3 id="sigterm--15">SIGTERM (-15)</h3>
<ul>
<li><strong>의미:</strong> 프로세스 종료 요청 (Termination Signal)</li>
<li><strong>동작:</strong> 프로세스에게 &quot;종료해달라&quot;는 신호를 보냅니다. 프로세스는 이 시그널을 핸들링할 수 있어, 종료 전 필요한 로직(리소스 정리, 진행 중인 작업 완료 등)을 수행할 수 있습니다.</li>
<li><strong>Graceful Shutdown:</strong> 가능합니다. 배포 스크립트 등에서 별도의 옵션 없이 <code>kill</code> 명령어를 사용하면 기본적으로 이 시그널이 전송됩니다.</li>
</ul>
<!-- end list -->

<pre><code class="language-bash">kill {PID}  # 기본값 -15 (SIGTERM)</code></pre>
<h2 id="3-spring-boot에서의-graceful-shutdown-설정">3. Spring Boot에서의 Graceful Shutdown 설정</h2>
<p>Spring Boot 2.3 버전부터는 설정을 통해 매우 간단하게 Graceful Shutdown을 적용할 수 있습니다.</p>
<h3 id="applicationproperties-설정">application.properties 설정</h3>
<pre><code class="language-properties"># Graceful Shutdown 활성화 (기본값: immediate)
server.shutdown=graceful

# 종료 대기 타임아웃 설정 (기본값: 30s)
spring.lifecycle.timeout-per-shutdown-phase=20s</code></pre>
<p><code>server.shutdown</code>을 <code>graceful</code>로 설정하면 Spring Boot의 내장 웹 서버는 종료 시그널을 받았을 때 새로운 요청을 받지 않고 기존 요청을 처리하기 위해 대기합니다.</p>
<h3 id="주의할-점-타임아웃의-종류">주의할 점: 타임아웃의 종류</h3>
<p>설정에서 <code>spring.lifecycle.timeout-per-shutdown-phase</code>는 Spring 컨테이너의 라이프사이클 빈(Bean)들이 종료되는 데 기다려주는 전체 시간을 의미합니다. 하지만 실제 운영 환경에서는 <strong>Tomcat 자체의 내부 Graceful Shutdown 타임아웃</strong>과 구분해서 이해할 필요가 있습니다. 일반적으로 Spring의 설정이 우선순위를 가지며 전체 종료 과정을 제어하지만, 아주 정밀한 튜닝이 필요한 경우 톰캣 레벨의 설정이 별도로 존재함을 인지해야 합니다.</p>
<h3 id="내장-웹-서버별-동작-차이">내장 웹 서버별 동작 차이</h3>
<p>Spring Boot는 여러 내장 웹 서버를 지원하며, 서버마다 Graceful Shutdown 구현에 차이가 있습니다.</p>
<ul>
<li><strong>Tomcat / Jetty:</strong> 표준적인 Graceful Shutdown 동작을 지원합니다. 네트워크 레이어에서 새로운 연결을 막고 기존 요청을 처리합니다.</li>
<li><strong>Netty (WebFlux):</strong> Reactor Netty의 라이프사이클에 맞춰 동작하며, 비동기 논블로킹 특성에 맞게 처리됩니다.</li>
<li><strong>Undertow:</strong> 과거 버전이나 특정 설정에 따라 공식적인 Graceful Shutdown 지원이 제한적이거나 중단된 경우가 있어, 사용 시 별도의 확인이 필요합니다.</li>
</ul>
<h2 id="4-실무-운영-환경-고려사항-load-balancer--kubernetes">4. 실무 운영 환경 고려사항 (Load Balancer &amp; Kubernetes)</h2>
<p>Graceful Shutdown 설정만으로는 &#39;무중단 배포&#39;를 완벽하게 보장하기 어렵습니다. 실제 서비스는 로드밸런서(LB) 뒤에 존재하기 때문입니다.</p>
<h3 id="로드밸런서의-연결-해제-deregistration">로드밸런서의 연결 해제 (Deregistration)</h3>
<p>Spring Boot가 종료를 시작(<code>SIGTERM</code> 수신)하면 새로운 요청을 거부하지만, 로드밸런서가 이를 인지하고 트래픽을 차단하기까지 <strong>시차</strong>가 발생할 수 있습니다.</p>
<ul>
<li><strong>AWS ALB/NLB:</strong> 대상 그룹(Target Group)에서 인스턴스를 제외하는 <code>Deregistration Delay</code> (기본 300초) 설정이 있습니다.</li>
<li><strong>Kubernetes:</strong> Pod가 종료될 때 <code>preStop</code> hook을 사용하여 로드밸런서(Service) 갱신 시간을 벌어주어야 합니다.</li>
</ul>
<p>따라서 이상적인 종료 시나리오는 다음과 같습니다.</p>
<ol>
<li><strong>Kubernetes/LB:</strong> 트래픽 차단 시작</li>
<li><strong>App:</strong> <code>preStop</code> 등을 이용해 잠시 대기 (기존 요청 처리 + LB 갱신 대기)</li>
<li><strong>App:</strong> <code>SIGTERM</code> 수신 -&gt; Spring Boot Graceful Shutdown 시작</li>
<li><strong>App:</strong> 잔여 작업 완료 후 종료</li>
</ol>
<p>이때, Kubernetes의 <code>terminationGracePeriodSeconds</code>는 Spring Boot의 <code>timeout-per-shutdown-phase</code>보다 넉넉하게 설정해야 프로세스가 강제 종료(<code>SIGKILL</code>) 당하는 것을 방지할 수 있습니다.</p>
<h2 id="5-내부-동작-원리-및-검증-tomcat-기준">5. 내부 동작 원리 및 검증 (Tomcat 기준)</h2>
<p>Spring Boot와 내장 Tomcat은 어떻게 이 기능을 구현했을까요? 내부 코드를 통해 동작 원리를 살펴보겠습니다.</p>
<h3 id="동작-테스트">동작 테스트</h3>
<p>요청 처리에 15초가 걸리는 API를 만들고, 요청 중에 서버를 종료하면 다음과 같은 로그를 확인할 수 있습니다.</p>
<pre><code class="language-text">INFO ... GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
INFO ... GracefulShutdownController : 요청 처리 완료 seq : (1)
INFO ... GracefulShutdown : Graceful shutdown complete</code></pre>
<p>서버 종료 신호가 들어왔음에도 <code>Waiting for active requests to complete</code> 메시지와 함께 기존 요청 처리가 완료될 때까지 기다렸다가 종료됩니다.</p>
<h3 id="코드-레벨-분석">코드 레벨 분석</h3>
<p>Spring Boot가 구동될 때 <code>server.shutdown=graceful</code> 설정이 되어 있다면, <code>TomcatWebServer</code>는 <code>GracefulShutdown</code> 객체를 생성하여 할당합니다.</p>
<pre><code class="language-java">final class GracefulShutdown {
    // ...
    private void doShutdown(GracefulShutdownCallback callback) {
        List&lt;Connector&gt; connectors = getConnectors();

        // 1. 커넥터 종료 (새로운 요청 차단)
        connectors.forEach(this::close); 

        try {
            for (Container host : this.tomcat.getEngine().findChildren()) {
                for (Container context : host.findChildren()) {
                    // 2. 활성화된 요청이 있는지 지속적으로 확인
                    while (isActive(context)) {
                        if (this.aborted) {
                            logger.info(&quot;Graceful shutdown aborted...&quot;);
                            callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
                            return;
                        }
                        Thread.sleep(50); // 50ms 주기로 체크
                    }
                }
            }
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
        }
        // ...
    }
}</code></pre>
<h4 id="1-connector-close의-의미">1. Connector Close의 의미</h4>
<p><code>connectors.forEach(this::close)</code>가 실행되면 Tomcat은 새로운 TCP 연결 수립을 거부합니다. 하지만 <strong>중요한 점은 기존 Keep-Alive 상태의 연결</strong>입니다. Tomcat은 새로운 요청은 막지만, 이미 맺어진 Keep-Alive 연결을 통해 들어와 처리 중인 요청은 끊지 않고 유지합니다. 이후 해당 요청 처리가 끝나면 연결을 자연스럽게 종료합니다.</p>
<h4 id="2-isactive는-무엇을-확인하는가">2. isActive()는 무엇을 확인하는가?</h4>
<p><code>isActive(context)</code> 메서드는 단순히 &quot;무언가 돌고 있다&quot;를 추측하는 것이 아닙니다. 구체적으로는 Tomcat 내부의 <strong><code>StandardWrapperValve</code></strong> 클래스가 관리하는 <strong><code>processingCount</code></strong> 값을 확인합니다.</p>
<ul>
<li><code>processingCount &gt; 0</code>: 현재 서블릿이 요청을 처리 중임</li>
<li><code>processingCount == 0</code>: 처리 중인 요청 없음 (종료 가능)</li>
</ul>
<p>즉, Spring Boot의 Graceful Shutdown은 이 카운트가 0이 될 때까지(혹은 타임아웃이 될 때까지) 루프를 돌며 대기하는 구조입니다.</p>
<h2 id="마치며">마치며</h2>
<p>Graceful Shutdown은 단순한 코드 설정 한 줄(<code>server.shutdown=graceful</code>)로 시작하지만, 그 뒤에는 OS 시그널, 웹 서버의 커넥션 관리, 그리고 인프라 레이어의 트래픽 제어까지 연결된 깊이 있는 기술이 숨어 있습니다.</p>
<p>안정적인 서비스를 운영하기 위해서는 코드 레벨의 설정뿐만 아니라, 로드밸런서와 배포 환경(K8s 등)의 종료 정책을 함께 고려하여 &quot;진정한 우아한 종료&quot;를 설계해야 합니다.</p>
<p>참고) 
<a href="https://velog.io/@byeongju/SpringBoot%EC%9D%98-Graceful-Shutdown">https://velog.io/@byeongju/SpringBoot%EC%9D%98-Graceful-Shutdown</a>
<a href="https://effectivesquid.tistory.com/entry/JVM%EC%9D%98-%EC%A2%85%EB%A3%8C%EC%99%80-Graceful-Shutdown">https://effectivesquid.tistory.com/entry/JVM%EC%9D%98-%EC%A2%85%EB%A3%8C%EC%99%80-Graceful-Shutdown</a>
<a href="https://www.baeldung.com/spring-boot-web-server-shutdown">https://www.baeldung.com/spring-boot-web-server-shutdown</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CQRS 패턴]]></title>
            <link>https://velog.io/@david1-p/CQRS-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@david1-p/CQRS-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Tue, 25 Nov 2025 11:02:40 GMT</pubDate>
            <description><![CDATA[<h1 id="cqrs-패턴이란-명령과-조회의-책임을-분리하자">CQRS 패턴이란? 명령과 조회의 책임을 분리하자</h1>
<p>백엔드 시스템을 개발하다 보면 하나의 도메인 모델이 점점 비대해지는 경험을 하게 됩니다. 비즈니스 로직을 처리하기 위한 복잡한 객체 그래프와, 단순히 화면에 뿌려주기 위한 조회용 데이터가 뒤섞이면서 유지보수가 어려워지죠.</p>
<p>오늘은 이러한 복잡성을 해결하고 시스템을 유연하게 만드는 아키텍처 패턴인 <strong>CQRS (Command Query Responsibility Segregation)</strong>에 대해 알아보겠습니다.</p>
<h2 id="1-cqrs란-무엇인가">1. CQRS란 무엇인가?</h2>
<p><strong>CQRS</strong>는 <strong>Command Query Responsibility Segregation</strong>의 약자로, 단어 그대로 <strong>&#39;명령(Command)과 조회(Query)의 책임을 분리하는 패턴&#39;</strong>을 의미합니다.</p>
<p>우리가 만드는 시스템의 기능은 크게 두 가지로 나뉩니다.</p>
<ol>
<li><strong>명령 (Command):</strong> 시스템의 상태를 변경하는 작업 (예: 주문 생성, 주문 취소, 결제 승인)</li>
<li><strong>조회 (Query):</strong> 시스템의 상태를 반환하는 작업 (예: 주문 리스트 조회, 내 정보 보기)</li>
</ol>
<p><img src="https://velog.velcdn.com/images/david1-p/post/240494e5-ff08-47ec-ac56-b4ff49be8f6d/image.png" alt=""></p>
<p>보통은 하나의 모델(Entity)로 이 두 가지 기능을 모두 처리하려 합니다. 하지만 CQRS는 <strong>&quot;상태를 변경하는 모델과 상태를 조회하는 모델을 분리하자&quot;</strong>는 것이 핵심입니다.</p>
<h3 id="핵심-개념-모델의-분리">핵심 개념: 모델의 분리</h3>
<ul>
<li><strong>명령 모델 (Write Model):</strong> 실제 도메인 로직을 수행합니다. 객체 지향적으로 설계되어 있으며, 데이터의 정합성과 불변성을 보장하는 데 집중합니다.</li>
<li><strong>조회 모델 (Read Model):</strong> 화면(UI)이나 리포트에 보여주기 위한 모델입니다. 복잡한 로직 없이 조회 성능과 편의성에 최적화되어 있습니다.</li>
</ul>
<hr>
<h2 id="2-왜-cqrs를-사용해야-할까요-장점">2. 왜 CQRS를 사용해야 할까요? (장점)</h2>
<p>CQRS를 도입하면 다음과 같은 이점을 얻을 수 있습니다.</p>
<h3 id="1-단일-책임-원칙과-유지보수성-향상">1) 단일 책임 원칙과 유지보수성 향상</h3>
<p>비즈니스 로직(명령)과 단순 조회(쿼리)가 분리되므로 코드가 깔끔해집니다.</p>
<ul>
<li>명령 쪽은 복잡한 비즈니스 정책과 도메인 규칙을 구현하는 데 집중할 수 있습니다.</li>
<li>조회 쪽은 화면에 필요한 데이터를 가장 효율적으로 가져오는 데만 집중할 수 있습니다.</li>
</ul>
<h3 id="2-기술-선택의-유연성-polyglot">2) 기술 선택의 유연성 (Polyglot)</h3>
<p>명령과 조회의 목적이 다르기 때문에, 각 모델에 맞는 <strong>최적의 기술</strong>을 자유롭게 선택할 수 있습니다. 꼭 특정 기술을 써야 하는 것은 아니며, 프로젝트 상황에 맞춰 조합할 수 있습니다.</p>
<ul>
<li><strong>명령 (Command):</strong> 도메인 모델링과 트랜잭션 처리가 강력한 기술<ul>
<li>예: <strong>JPA</strong>, Hibernate 등</li>
</ul>
</li>
<li><strong>조회 (Query):</strong> 복잡한 조인이나 통계성 쿼리, 조회 성능에 유리한 기술<ul>
<li>예: <strong>Querydsl</strong>, <strong>MyBatis</strong>, <strong>JdbcTemplate</strong>, 혹은 <strong>JPA DTO Projection</strong></li>
</ul>
</li>
</ul>
<p>심화 단계에서는 기술뿐만 아니라 <strong>저장소(DB)</strong> 자체를 분리하기도 합니다. (예: 명령은 MySQL, 조회는 Redis나 ElasticSearch 사용)</p>
<hr>
<h2 id="3-cqrs-구현-예제-spring-boot--java">3. CQRS 구현 예제 (Spring Boot &amp; Java)</h2>
<p>가장 현실적이고 많이 사용되는 <strong>&#39;단일 DB 내에서 논리적으로 모델을 분리하는 방식&#39;</strong>을 가정해보겠습니다.</p>
<h3 id="3-1-명령command-모델">3-1. 명령(Command) 모델</h3>
<p>명령 모델은 데이터의 일관성을 지키고 비즈니스 로직을 수행하는 데 집중합니다.
단순한 상태 변경뿐만 아니라, <strong>결제 금액 검증, 재고 수량 체크, 배송 상태 확인 등 도메인의 규칙(Invariant)</strong>을 강제하는 역할을 수행합니다.</p>
<pre><code class="language-java">// [Command] Order Entity (도메인 로직 포함)
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {

    @Id @GeneratedValue
    private Long id;
    private String status;
    // ... 기타 필드들

    // 비즈니스 로직: 주문 취소
    public void cancel() {
        if (this.status.equals(&quot;SHIPPED&quot;)) {
            throw new IllegalStateException(&quot;이미 배송된 상품은 취소가 불가능합니다.&quot;);
        }
        this.status = &quot;CANCELLED&quot;;
    }
}

// [Command] Service
@Service
@Transactional
@RequiredArgsConstructor
public class OrderCommandService {

    private final OrderRepository orderRepository; // JPA Repository

    public void cancelOrder(Long orderId) {
        // 1. 도메인 모델 조회
        Order order = orderRepository.findById(orderId)
                .orElseThrow(() -&gt; new IllegalArgumentException(&quot;Order not found&quot;));

        // 2. 상태 변경 (도메인 로직 수행)
        order.cancel();

        // 3. Dirty Checking으로 자동 업데이트
    }
}</code></pre>
<h3 id="3-2-조회query-모델">3-2. 조회(Query) 모델</h3>
<p>조회 모델은 도메인 로직 없이, <strong>쿼리 최적화 및 화면 표현(View)</strong>에 집중합니다.
단순히 데이터를 가져오는 것을 넘어, 여러 테이블을 조인해서 미리 계산된 결과를 반환하거나(Aggregation), 화면에 딱 맞는 DTO 형태로 데이터를 제공하여 조회 성능을 높입니다.</p>
<pre><code class="language-java">// [Query] OrderData (단순 조회용 DTO)
@Getter
@Setter
public class OrderData {
    private Long orderId;
    private String customerName;
    private String productName;
    private int totalPrice;
    private String status;
}

// [Query] Service
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderQueryService {

    private final OrderMapper orderMapper; // MyBatis Mapper

    public OrderData getOrderDetails(Long orderId) {
        // 복잡한 조인 쿼리 등을 최적화된 SQL로 직접 실행하여 DTO로 반환
        return orderMapper.findOrderDataById(orderId);
    }
}</code></pre>
<hr>
<h2 id="4-cqrs-적용-시-주의할-점-단점">4. CQRS 적용 시 주의할 점 (단점)</h2>
<p>CQRS는 강력하지만 모든 곳에 무조건 적용해야 하는 것은 아닙니다. </p>
<h3 id="1-구현-복잡도-증가">1) 구현 복잡도 증가</h3>
<p>단순한 CRUD 시스템에 CQRS를 적용하면, 오히려 파일 수가 늘어나고 구조가 불필요하게 복잡해질 수 있습니다. 얻을 수 있는 이점과 구현 비용을 잘 비교해야 합니다.</p>
<h3 id="2-데이터-동기화-문제-db-분리-시">2) 데이터 동기화 문제 (DB 분리 시)</h3>
<p>만약 성능을 극대화하기 위해 명령 DB와 조회 DB를 물리적으로 분리한다면, <strong>데이터 동기화</strong> 이슈가 발생합니다.</p>
<ul>
<li>명령 모델의 변경 사항을 조회 모델로 전파하는 과정에서 시차(Lag)가 발생하여 <strong>&#39;결과적 일관성(Eventual Consistency)&#39;</strong> 문제가 생길 수 있습니다.</li>
</ul>
<hr>
<h2 id="5-결론-언제-cqrs를-써야-할까">5. 결론: 언제 CQRS를 써야 할까?</h2>
<p>단순히 &quot;좋아 보여서&quot; 도입하기보다는, 시스템이 다음과 같은 신호를 보낼 때 도입을 고려해야 합니다.</p>
<ol>
<li><strong>도메인 복잡도가 높은 경우:</strong> DDD(도메인 주도 설계)를 적용할 만큼 비즈니스 로직이 복잡할 때.</li>
<li><strong>트래픽의 불균형:</strong> 읽기(Read) 요청이 쓰기(Write) 요청보다 압도적으로 많아, 조회 성능 최적화가 시급할 때.</li>
<li><strong>모델의 괴리:</strong> 화면에 필요한 데이터 형태와 실제 도메인 엔티티의 구조 차이가 너무 커져서 변환 로직이 비대해질 때.</li>
<li><strong>확장성(Scale-out) 이슈:</strong> 조회 기능과 쓰기 기능의 부하가 달라 각각 다르게 스케일링해야 할 때.</li>
</ol>
<p>CQRS라고 해서 반드시 DB를 쪼개거나 메시지 큐를 도입해야 하는 것은 아닙니다. <strong>코드 레벨에서 명령과 조회의 책임을 나누는 것(논리적 CQRS)</strong>만으로도 복잡도를 낮추는 훌륭한 시작이 될 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[의존성 주입(DI)이란?]]></title>
            <link>https://velog.io/@david1-p/%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85DI%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@david1-p/%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85DI%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Wed, 19 Nov 2025 22:59:19 GMT</pubDate>
            <description><![CDATA[<p>백엔드 개발, 특히 스프링(Spring) 프레임워크를 학습하다 보면 <strong>의존성 주입(Dependency Injection, 이하 DI)</strong>이라는 단어를 반드시 마주하게 됩니다. 객체지향 프로그래밍에서 &#39;유연한 설계&#39;를 하기 위해 필수적인 개념입니다.</p>
<p>DI의 핵심 개념과 다양한 주입 방식, 그리고 왜 <strong>생성자 주입</strong>이 권장되는지에 대해 정리해 보겠습니다.</p>
<hr>
<h2 id="1-의존성dependency이란">1. 의존성(Dependency)이란?</h2>
<p>프로그래밍에서 <strong>의존성</strong>이란 무엇일까요?
아주 간단하게 말해 <strong>&quot;A 객체가 어떤 작업을 수행하기 위해 B 객체를 필요로 하는 상황&quot;</strong>을 말합니다.</p>
<blockquote>
<p><strong>&quot;A는 B에 의존한다.&quot;</strong></p>
</blockquote>
<p>코드로 보면 A 클래스 내부에서 B 클래스의 메서드를 호출하거나 사용하고 있는 상태입니다. 이때 가장 흔히 발생하는 문제는 <strong>A가 B를 직접 생성(<code>new</code>)할 때</strong> 발생합니다.</p>
<pre><code class="language-java">public class MemberService {
    // MemberService가 MemoryRepository를 직접 생성 (의존)
    private final MemoryRepository repository = new MemoryRepository();

    public void join() {
        repository.save();
    }
}</code></pre>
<p>위 코드의 문제점은 무엇일까요?
만약 의존하고 있는 객체를 다른 객체로 바꿔야 한다면, 이를 사용하고 있는 <strong>A 객체의 코드도 직접 수정</strong>해야 합니다. 이를 <strong>강한 결합(Tight Coupling)</strong>이라고 합니다. 유연성이 떨어지고 유지보수가 힘든 구조입니다.</p>
<hr>
<h2 id="2-의존성-주입di의-개념">2. 의존성 주입(DI)의 개념</h2>
<p><strong>의존성 주입(DI)</strong>은 객체(A)가 의존하는 다른 객체(B)를 직접 생성하지 않고, <strong>외부(C)에서 생성해서 넘겨주는(주입하는) 방식</strong>입니다.</p>
<ul>
<li><strong>기존:</strong> A가 B를 직접 생성 (A -&gt; B)</li>
<li><strong>DI 적용:</strong> 외부의 제3자(C)가 B를 생성한 뒤 A에게 줌 (C -&gt; A &lt;- B)</li>
</ul>
<p>이때 &#39;외부의 제3자&#39;는 보통 프레임워크(예: 스프링 컨테이너)가 됩니다. 이를 통해 A 객체는 B가 어떻게 생성되는지 알 필요 없이, 자신의 역할에만 집중할 수 있게 됩니다. 이를 <strong>제어의 역전(IoC, Inversion of Control)</strong>이라고도 부릅니다.</p>
<pre><code class="language-java">public class MemberService {

    // 구체적인 클래스(MemoryRepository)가 아닌 인터페이스(Repository)에 의존
    private final Repository repository;

    // 외부에서 생성된 객체를 생성자를 통해 주입받음
    public MemberService(Repository repository) {
        this.repository = repository;
    }

    public void join() {
        repository.save();
    }
}</code></pre>
<p>이제 이 객체는 어떤 구현체가 오든 상관없습니다. 외부에서 무엇을 주입해 주느냐에 따라 동작이 달라집니다. 코드 변경 없이 다양한 실행 구조를 만들 수 있게 된 것이죠.</p>
<hr>
<h2 id="3-의존성-주입의-3가지-방식">3. 의존성 주입의 3가지 방식</h2>
<p>의존성 주입은 <strong>주입을 받는 위치</strong>에 따라 크게 세 가지로 나뉩니다.</p>
<h3 id="1-생성자-주입-constructor-injection">1) 생성자 주입 (Constructor Injection)</h3>
<p>생성자를 통해 의존성을 주입받는 방식입니다.</p>
<pre><code class="language-java">public class OrderService {
    private final DiscountPolicy discountPolicy;

    public OrderService(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}</code></pre>
<ul>
<li><strong>특징:</strong> 객체가 생성될 때 딱 한 번 호출되므로 <strong>의존 관계가 변하지 않거나, 필수적인 경우</strong>에 사용합니다.</li>
</ul>
<h3 id="2-setter-주입-setter-injection">2) Setter 주입 (Setter Injection)</h3>
<p>Setter 메서드(수정자)를 통해 의존성을 주입받는 방식입니다.</p>
<pre><code class="language-java">public class OrderService {
    private DiscountPolicy discountPolicy;

    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}</code></pre>
<ul>
<li><strong>특징:</strong> <strong>선택적이거나 변경 가능성이 있는 의존 관계</strong>에 사용합니다. 다만, 주입받지 않아도 객체가 생성될 수 있어 주의가 필요합니다.</li>
</ul>
<h3 id="3-메서드-주입-method-injection">3) 메서드 주입 (Method Injection)</h3>
<p>메서드 실행 시 인자로 주입받거나, 일반 메서드를 통해 주입받는 방식입니다. 실행할 때마다 의존 대상이 변하는 특수한 경우에 사용되나, 자주 사용되지는 않습니다.</p>
<blockquote>
<p><strong>참고:</strong> <code>@Autowired</code>를 필드에 바로 붙이는 <strong>필드 주입</strong>도 있지만, 외부에서 변경이 불가능해 테스트하기 어렵다는 단점 때문에 최근에는 지양하는 추세입니다.</p>
</blockquote>
<hr>
<h2 id="4-왜-생성자-주입을-써야-할까">4. 왜 &#39;생성자 주입&#39;을 써야 할까?</h2>
<p><strong>생성자 주입을 강력하게 권장</strong>합니다. 다음과 같은 장점들이 있기 때문입니다.</p>
<h3 id="①-불변성immutability-보장">① 불변성(Immutability) 보장</h3>
<p>대부분의 의존 관계는 애플리케이션 종료 시점까지 변하면 안 됩니다.</p>
<ul>
<li>Setter 주입은 메서드를 <code>public</code>으로 열어두어야 하므로, 누군가 실수로 변경할 위험이 있습니다.</li>
<li>생성자 주입은 객체 생성 시 딱 1번만 호출되므로, 이후에 호출될 일이 없어 안전합니다.</li>
</ul>
<h3 id="②-final-키워드-사용-가능">② final 키워드 사용 가능</h3>
<p>생성자 주입을 사용하면 필드에 <code>final</code> 키워드를 사용할 수 있습니다. 이 덕분에 생성자에서 혹시라도 값이 설정되지 않는 오류를 <strong>컴파일 시점</strong>에 바로 막아줍니다</p>
<pre><code class="language-java">private final Repository repository;</code></pre>
<h3 id="③-테스트-코드-작성-용이">③ 테스트 코드 작성 용이</h3>
<p>순수한 자바 코드로 단위 테스트를 작성할 때, 생성자 주입을 사용하면 컴파일러가 필요한 의존성을 알려줍니다.</p>
<ul>
<li>Setter 주입이나 필드 주입은 의존성 없이 객체를 생성할 수 있어, 테스트 도중 <code>NullPointerException</code>이 발생할 수 있습니다.</li>
<li>생성자 주입은 의존성을 넣지 않으면 객체 생성이 불가능하므로, 테스트 시 누락 실수를 방지합니다.</li>
</ul>
<h3 id="④-순환-참조-방지">④ 순환 참조 방지</h3>
<p>A가 B를 참조하고, B가 다시 A를 참조하는 순환 참조가 발생했을 때, 생성자 주입은 서버 구동 시점에 에러를 발생시켜 애플리케이션이 실행되지 않도록 막아줍니다. (문제를 조기에 발견 가능)</p>
<hr>
<h2 id="5-정리">5. 정리</h2>
<p>의존성 주입(DI)은 객체 간의 결합도를 낮춰 <strong>유연하고 변경에 용이한 설계</strong>를 가능하게 합니다.</p>
<ul>
<li><strong>핵심:</strong> 객체 내부에서 직접 <code>new</code> 하지 말고, 외부에서 주입받자.</li>
<li><strong>방식:</strong> 생성자 주입, Setter 주입, 메서드 주입 등이 있다.</li>
<li><strong>결론:</strong> <strong>가급적 &#39;생성자 주입&#39;을 사용하자.</strong> (불변성, 테스트 용이성, 안정성 때문)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[커널아카데미] 백엔드 12기 수료 후기 ]]></title>
            <link>https://velog.io/@david1-p/%EC%BB%A4%EB%84%90%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EB%B0%B1%EC%97%94%EB%93%9C-12%EA%B8%B0-%ED%8C%8C%EC%9D%B4%EB%84%90-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@david1-p/%EC%BB%A4%EB%84%90%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EB%B0%B1%EC%97%94%EB%93%9C-12%EA%B8%B0-%ED%8C%8C%EC%9D%B4%EB%84%90-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Wed, 19 Nov 2025 02:06:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>3월부터 10월까지, 약 7개월간의 부트캠프 생활이 끝났다.<br>
여름 동안 에어컨이 제대로 동작하지 않아 너무 더웠지만, 끝내 <strong>유종의 미를 거두며 우수 수료생으로 마무리</strong>하게 되었다.</p>
<p>글을 쓰는 지금도 지난 7개월이 주마등처럼 스쳐 지나간다.<br>
아무것도 모르던 내가 이제 <strong>Java, Python</strong>으로 코드를 작성하고, <strong>LangChain, LangGraph</strong> 등 LLM을 활용한 챗봇 프로그램을 만들 수 있게 되었다. 내가 이 부트캠프에서 치열하게 배웠던 것들을 기록해보고자 한다.</p>
</blockquote>
<h2 id="1-3월-새로운-시작과-java와의-첫-대면">1. 3월: 새로운 시작과 Java와의 첫 대면</h2>
<h3 id="아무것도-모르던-내가-백엔드-개발자">아무것도 모르던 내가 백엔드 개발자?</h3>
<p>사실 나는 1월 말부터 다른 국비 학원을 다니다가 중도 포기를 하고 커널 아카데미에 합류했다. 6개월 만에 풀스택 개발자(프론트엔드 + 백엔드)가 된다는 게 비전공자 입장에서 현실적으로 와닿지 않았다. HTML/CSS로 스타벅스, 11번가 등 여러 클론 코딩을 해보았지만, 대부분 강사님의 코드를 따라 치는 수준이었고 &quot;과연 이걸 나 혼자 만들 수 있을까?&quot;라는 의구심만 커져갔다.</p>
<p>그러던 중 커널 아카데미에서 남궁성 강사님과 현직자분들의 토크쇼에 참가하게 되었다. 기존 학원에서는 &quot;비전공자는 학점은행제로 전공 이수가 필수고, 정보처리기사도 꼭 따야 한다&quot;라고 했지만, 토크쇼에서 만난 토스, 카카오뱅크, 무신사 등 기업 재직자분들은 달랐다. 학점 이수나 자격증 없이도 실력으로 증명한 케이스를 보며, 내가 가졌던 고정관념이 깨지는 듯한 충격을 받았다.</p>
<p>그 길로 상담을 통해 기존 K-Digital Training 과정(커널 백엔드 12기)을 등록했다. 첫 등교 날, 자바 레벨 테스트를 봤는데 백지로 냈다. 공부를 안 했으니 당연했다(ㅋㅋ). 남궁성 강사님께 Git 사용법 등 기초 프린트물을 받아 공부하며 3월 마지막 주, 그렇게 자바와 조금씩 가까워지기 시작했다.</p>
<hr>
<h2 id="2-4월-객체지향과-씨름하다">2. 4월: 객체지향과 씨름하다</h2>
<p>본격적으로 남궁성 강사님의 Java 수업이 시작되었다. 강사님께서 대여해 주신 『자바의 정석 3판』으로 공부하며, 인강 예습과 실강 복습을 병행했다. 전공자 동기들의 질문과 답변을 들으며 개념을 주워담기도 했고, 별 찍기 문제에서 벽을 느꼈지만 ChatGPT의 도움으로 이해하려 노력했다.</p>
<h3 id="비기너반에서의-성장">비기너반에서의 성장</h3>
<p>테스트 백지 제출의 결과로 나는 &#39;비기너반&#39;에 배정되었다.
오전에는 남궁성 강사님의 수업을, 오후에는 <a href="https://www.youtube.com/channel/UCxdunbb1wIvfufdo1NuLEvg">김송아 강사님</a>의 줌 실시간 강의를 들었다. 김송아 강사님은 코드를 작성하는 흐름과 우리가 어려워하는 부분을 친숙한 예시로 풀어 설명해 주셨다.</p>
<p>여기에 벤티 멘토님의 멘토링까지 더해져 실무 디자인 패턴과 코드 작성법에 대한 자극을 많이 받았다.
과제로 주어진 <strong>문자열 계산기, 자동차 경주, 로또 번호 추출</strong>... 처음엔 문자열 계산기부터 막혔다. 하지만 구글링과 ChatGPT를 활용해 우아한테크코스 기출 문제와 유사함을 알게 되었고, 끝까지 매달려 결국 해냈다. 이 과정을 통해 <strong>객체지향적인 설계</strong>가 무엇인지 조금씩 눈을 뜨게 되었다.</p>
<p>하지만 쓰레드, 람다, 스트림이 등장하면서 내 멘탈은 다시 붕괴되었다. &quot;여긴 어디, 나는 누구?&quot;</p>
<hr>
<h2 id="3-5월-db-모델링과-디자인-패턴">3. 5월: DB 모델링과 디자인 패턴</h2>
<p>Java 수업이 끝나고 DB 수업이 이어졌다. SQL 문법이 생소했지만, 결국 &quot;DB에 저장된 데이터를 어떻게 효율적으로 뽑아오느냐&quot;가 핵심이었다. HackerRank SQL 문제들을 풀며 조금씩 감을 잡았다.</p>
<p>서브쿼리, Equi Join, Outer/Inner Join, 시퀀스, B-Tree 등 기술적 깊이를 더해갔다. 『SQL 모델링』과 『SQL 튜닝』 책을 보며 &quot;이건 또 뭔가&quot; 싶었지만, <strong>SQL 스터디</strong>를 조직해 동료들과 토론하며 개념을 리마인드했다.</p>
<h3 id="코드를-보는-눈을-뜨게-해-준-디자인-패턴-스터디">코드를 보는 눈을 뜨게 해 준 &#39;디자인 패턴 스터디&#39;</h3>
<p>이때부터 <strong>디자인 패턴 스터디</strong>를 시작했는데, 이게 코딩 실력 향상의 전환점이 되었다. 매일 2시간씩 줌으로 모여 30분간 개념 설명, 적용 코드 리뷰, Q&amp;A를 진행했다. &quot;내가 할 수 있을까?&quot; 싶었지만, 부딪혀 보니 &quot;아, 이럴 때 이 패턴을 쓰는구나!&quot;를 깨닫게 되었다.</p>
<p>마지막 주에는 Spring을 배우며 MVC 흐름과 브라우저 에러(404, 500)의 원인을 파악할 수 있게 되었다.</p>
<hr>
<h2 id="4-6월-첫-팀-프로젝트-spao-벤치마킹">4. 6월: 첫 팀 프로젝트 (SPAO 벤치마킹)</h2>
<p>Spring 프레임워크로 로그인, 게시판, MySQL 연동 등 기본기를 다진 후, 2주 차부터 <strong>토이 프로젝트 1~3(쇼핑몰 구현)</strong>이 시작되었다. 우리는 SPAO를 벤치마킹했는데, 나는 <strong>상품 파트</strong>를 담당했고 전공자 팀원이 고객/쿠폰 파트를 맡았다.</p>
<p>첫주 DB 모델링 때, &quot;재고 입출고 내역이 왜 없냐&quot;는 강사님의 날카로운 질문에 정신이 번쩍 들었다. 피드백을 바탕으로 테이블을 재설계하며 많이 배웠다.</p>
<blockquote>
<p>관련 회고: <a href="https://velog.io/@david1-p/%EC%BB%A4%EB%84%90%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EB%B0%B1%EC%97%94%EB%93%9C-12%EA%B8%B0-12%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%ED%86%A0%EC%9D%B4%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1">커널아카데미 백엔드 12기 12주차 회고(토이프로젝트 1)</a>
프로젝트 상세: <a href="https://velog.io/@david1-p/%EC%BB%A4%EB%84%90%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EB%B0%B1%EC%97%94%EB%93%9C-12%EA%B8%B0-14%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%ED%86%A0%EC%9D%B4%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3">토이프로젝트 1~3 회고</a></p>
</blockquote>
<hr>
<h2 id="5-7월-ai와의-만남-그리고-rag-구현">5. 7월: AI와의 만남, 그리고 RAG 구현</h2>
<p>커리큘럼에 따라 Python과 AI 수업을 듣게 되었다. Deep Learning의 기초를 배우며 ChatGPT의 동작 원리를 이해하게 되니 흥미가 폭발했다. <strong>LangChain, LangGraph</strong> 특강을 통해 LLM 애플리케이션 개발에 눈을 뜨게 되었다.</p>
<h3 id="토이-프로젝트-4-persona-book">토이 프로젝트 4: Persona Book</h3>
<p>팀원들은 훌륭했고, 나만 잘하면 되는 상황이었다. Python 문법은 괜찮았지만, <strong>RAG(검색 증강 생성) 구현</strong>이 난관이었다.</p>
<ol>
<li><strong>PDF 전처리 이슈:</strong> 한글 PDF가 제대로 읽히지 않아 10여 개의 로더를 테스트했고, <strong>PyMuPDF</strong>가 가장 성능이 좋아 채택했다.</li>
<li><strong>데이터 정제:</strong> 『자바의 정석』 샘플 PDF의 머리말/꼬리말을 제거하고, 비정형 표(공백 50% 이상)는 과감히 삭제하여 데이터 질을 높였다.</li>
<li><strong>프롬프트 엔지니어링:</strong> 난이도 &#39;상&#39; 문제가 잘 생성되지 않아, <strong>Few-shot Prompting(퓨샷)</strong> 기법을 적용해 예시를 1, 3, 5, 7개로 늘려가며 테스트했다. 그 결과 고난이도 문제와 해설까지 안정적으로 출력해낼 수 있었다.</li>
</ol>
<blockquote>
<p>Github: <a href="https://github.com/PersonaBook/persona-book">Persona Book 프로젝트</a></p>
</blockquote>
<hr>
<h2 id="6-8월-aws-인프라-구축과-파이널-준비">6. 8월: AWS 인프라 구축과 파이널 준비</h2>
<p>파이널 프로젝트를 앞두고 2주간의 정비 시간이 있었다. 지난 프로젝트 때 하루 3시간만 자며 몰입했던 탓인지, 이번엔 AWS 강의(EC2, S3, ECS, Aurora)를 들으며 컨디션을 조절했다. 처음엔 클라우드 비용이 겁났지만, 실습 후 5천 원 정도 청구된 걸 보고 안도했다.</p>
<p>8월 말, 기업 연계 파이널 프로젝트가 시작되었다. 3개 기업의 <strong>RFP(제안요청서)</strong>를 검토하고, &#39;자버(Jobber)&#39;라는 기업을 1지망으로 배정받았다. 대표님 미팅에서 &quot;하고 싶은 기술 다 써보라&quot;는 쿨한 답변을 듣고, 정말 다 해보기로 했다(ㅋㅋ).</p>
<hr>
<h2 id="7-9월-성능-최적화와-비용의-딜레마">7. 9월: 성능 최적화와 비용의 딜레마</h2>
<p>EC2에 프론트엔드, 백엔드, AI 서버를 각각 띄웠더니 비용이 급증했다. 학원 지원금이 있었지만 2주 만에 10만 원이 청구되었다. AI 파트가 <code>t2.small</code>을 쓰고 있었는데 성능 이슈가 있어 팀장님께 &quot;마지막 일주일은 <code>t3.medium</code>으로 올려야 한다&quot;고 말씀드렸다. 팀장님의 흔들리는 동공을 보았지만... 성능을 위해선 어쩔 수 없었다.</p>
<h3 id="핵심-성과-템플릿-생성-시간-20초-→-2초-단축">핵심 성과: 템플릿 생성 시간 20초 → 2초 단축</h3>
<p>가장 뿌듯했던 성과는 성능 최적화였다. 초기엔 템플릿 생성에 20초가 걸렸는데, <strong>비동기 처리와 병렬 처리</strong>를 도입하여 <strong>2~6초 내</strong>로 획기적으로 단축시켰다.</p>
<p><img src="https://velog.velcdn.com/images/david1-p/post/e5338329-e0c0-4838-91ab-a12904f40af3/image.jpg" alt=""></p>
<hr>
<h2 id="8-10월-수료-그리고-새로운-시작">8. 10월: 수료, 그리고 새로운 시작</h2>
<p>대망의 파이널 발표는 내가 맡았다. 강사님께서 발표 자료를 연신 찍으시는 걸 보며 내심 뿌듯했다. 결과는 <strong>우수 수료</strong>.
하지만 기쁨과 동시에 아쉬움이 밀려왔다. &quot;더 열심히 할 수 있었는데...&quot;라는 자책도 들었고, &quot;바로 취업할 수 있을까?&quot; 하는 불안감도 있었다.</p>
<p>어제 드디어 첫 개발자 면접을 봤다. 알고 있는 지식을 쏟아냈지만 결과는 기다려봐야 알 것 같다. 7개월간 아무것도 모르던 비전공자에서, 이제는 문제를 해결하는 개발자로 성장했다. 이 경험을 바탕으로 좋은 소식이 있기를 기대해 본다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 트랜잭션 AOP]]></title>
            <link>https://velog.io/@david1-p/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-AOP</link>
            <guid>https://velog.io/@david1-p/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-AOP</guid>
            <pubDate>Tue, 18 Nov 2025 23:42:13 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-transactional의-동작-원리와-실무-필독-주의사항-a-to-z">[Spring] @Transactional의 동작 원리와 실무 필독 주의사항 (A to Z)</h1>
<p>스프링 백엔드 개발을 하다 보면 가장 많이 사용하는 어노테이션 중 하나가 바로 <code>@Transactional</code>입니다. 이를 <strong>선언적 트랜잭션 관리(Declarative Transaction Management)</strong>라고 부릅니다.</p>
<p>우리가 비즈니스 로직에만 집중할 수 있도록, 복잡한 트랜잭션 시작과 종료 로직을 마법처럼 처리해주는 이 기능은 내부적으로 어떻게 동작할까요? 그리고 실무에서 흔히 겪는 &quot;어? 왜 롤백이 안 되지?&quot; 하는 상황은 왜 발생하는 걸까요?</p>
<p>오늘은 스프링 트랜잭션의 동작 흐름과 핵심 주의사항을 정리해 보겠습니다.</p>
<hr>
<h2 id="1-트랜잭션-aop-동작-원리-3가지-핵심-요소">1. 트랜잭션 AOP 동작 원리 (3가지 핵심 요소)</h2>
<p>@Transactional의 동작 과정을 이해하려면 다음 3가지 요소를 알아야 합니다.</p>
<ol>
<li><strong>트랜잭션 AOP 프록시 (Transaction AOP Proxy)</strong></li>
<li><strong>트랜잭션 매니저 (Transaction Manager)</strong></li>
<li><strong>트랜잭션 동기화 매니저 (Transaction Synchronization Manager)</strong></li>
</ol>
<h3 id="전체-동작-흐름">전체 동작 흐름</h3>
<p>클라이언트가 <code>@Transactional</code>이 붙은 서비스 메서드를 호출했을 때의 흐름은 다음과 같습니다.</p>
<ol>
<li><strong>요청 진입 (Proxy):</strong> 클라이언트가 메서드를 호출하면, 실제 서비스 객체 대신 <strong>트랜잭션 AOP 프록시</strong>가 먼저 호출을 가로챕니다.</li>
<li><strong>트랜잭션 시작 (Manager):</strong> 프록시는 <strong>트랜잭션 매니저</strong>에게 트랜잭션 시작을 요청합니다.</li>
<li><strong>커넥션 획득:</strong> 트랜잭션 매니저는 <code>DataSource</code>를 통해 DB 커넥션을 생성(획득)하고, <code>auto-commit</code>을 <code>false</code>로 설정하여 트랜잭션을 시작합니다.</li>
<li><strong>커넥션 보관 (Sync Manager):</strong> 트랜잭션 매니저는 시작된 커넥션을 <strong>트랜잭션 동기화 매니저</strong>에 보관합니다. (이때 <code>ThreadLocal</code>을 사용하여 멀티스레드 환경에서도 안전하게 보관됩니다.)</li>
<li><strong>비즈니스 로직 실행:</strong> 프록시는 실제 서비스 로직을 호출합니다. 내부에서 리포지토리(DAO)가 실행될 때, <strong>트랜잭션 동기화 매니저에 보관된 커넥션</strong>을 꺼내서 DB 작업을 수행합니다.</li>
<li><strong>트랜잭션 종료:</strong> 비즈니스 로직이 끝나면 프록시로 제어권이 돌아옵니다.<ul>
<li><strong>성공 시:</strong> 트랜잭션 매니저에게 <strong>커밋(Commit)</strong> 요청</li>
<li><strong>예외 발생 시:</strong> 트랜잭션 매니저에게 <strong>롤백(Rollback)</strong> 요청</li>
</ul>
</li>
<li><strong>리소스 정리:</strong> 트랜잭션 매니저는 커넥션을 제거하고, DB 커넥션을 종료(또는 풀에 반환)합니다.</li>
</ol>
<h3 id="핵심-개념-짚고-가기">핵심 개념 짚고 가기</h3>
<ul>
<li><strong>트랜잭션 매니저 (<code>PlatformTransactionManager</code>):</strong> JDBC, JPA 등 기술마다 다른 트랜잭션 관리 코드를 추상화한 인터페이스입니다. 개발자는 기술이 바뀌어도 서비스 코드를 수정할 필요가 없습니다.</li>
<li><strong>트랜잭션 동기화 매니저:</strong> 트랜잭션이 시작된 커넥션을 비즈니스 로직 전반에 걸쳐 유지해줍니다. 덕분에 파라미터로 <code>Connection</code>을 계속 넘겨주지 않아도 됩니다.</li>
</ul>
<hr>
<h2 id="2-코드로-보는-프록시-동작-의사-코드">2. 코드로 보는 프록시 동작 (의사 코드)</h2>
<p>이해를 돕기 위해 스프링이 생성하는 프록시 코드를 단순화해보면 아래와 같습니다.</p>
<p><strong>1) 우리가 작성하는 서비스 코드</strong></p>
<pre><code class="language-java">@Service
public class MemberService {

    @Transactional
    public void join(Member member) {
        // 비즈니스 로직
        memberRepository.save(member); 
        // 여기서는 별도의 커넥션 파라미터가 없어도
        // 동기화 매니저에 있는 커넥션을 사용하여 저장함
    }
}</code></pre>
<p><strong>2) 스프링이 만들어내는 프록시 코드 (핵심논리)</strong> </p>
<pre><code class="language-java">public class MemberServiceProxy { // 실제 서비스와 같은 인터페이스 구현 또는 상속

    private final MemberService target; // 실제 비즈니스 로직 객체
    private final PlatformTransactionManager transactionManager;

    public void join(Member member) {
        // 1. 트랜잭션 시작
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            // 2. 실제 비즈니스 로직 호출
            target.join(member); 

            // 3. 성공 시 커밋
            transactionManager.commit(status);

        } catch (Exception e) {
            // 4. 예외 발생 시 롤백?! (주의: 예외 종류에 따라 다름)
            transactionManager.rollback(status);
            throw e;
        }
    }
}</code></pre>
<p><strong>요약</strong></p>
<p>스프링 트랜잭션 AOP는 <strong>&quot;프록시가 문지기 역할을 하여 트랜잭션을 여닫고, 트랜잭션 매니저가 기술을 추상화하며, 동기화 매니저가 커넥션을 배달해준다&quot;</strong>라고 기억하면 됩니다.</p>
<hr>
<h2 id="spring-트랜잭션-aop-이건-알고-쓰자-전파-속성과-프록시의-함정">[Spring] 트랜잭션 AOP, 이건 알고 쓰자! (전파 속성과 프록시의 함정)</h2>
<p>앞서 살펴본 것처럼 스프링 트랜잭션은 <strong>프록시(Proxy)</strong>를 통해 동작합니다. 이 &quot;프록시 방식&quot;이기 때문에 발생하는 중요한 특징과 주의할 점들이 있습니다. 실무에서 가장 많이 마주치는 이슈인 <strong>트랜잭션 전파(Propagation)</strong>와 <strong>내부 호출(Self-Invocation)</strong> 문제를 알아보겠습니다.</p>
<hr>
<h3 id="1-트랜잭션-전파-propagation-트랜잭션끼리-만나면">1. 트랜잭션 전파 (Propagation): 트랜잭션끼리 만나면?</h3>
<p>서비스 로직이 복잡해지면 트랜잭션이 적용된 메서드가 또 다른 트랜잭션 메서드를 호출하는 경우가 생깁니다. 이때 트랜잭션은 어떻게 동작할까요? 합쳐질까요, 아니면 새로 생길까요? 이를 결정하는 것이 <strong>전파 속성(Propagation)</strong>입니다.</p>
<h4 id="기본값-required-기존-트랜잭션에-참여">기본값: REQUIRED (기존 트랜잭션에 참여)</h4>
<p>별다른 설정을 하지 않으면 디폴트는 <code>REQUIRED</code>입니다.</p>
<ul>
<li><strong>동작:</strong> 이미 진행 중인 트랜잭션이 있으면 그 트랜잭션에 &quot;참여&quot;하고, 없으면 &quot;새로 생성&quot;합니다.</li>
<li><strong>특징:</strong> 하나의 <strong>물리 트랜잭션(DB 커넥션)</strong> 안에서 여러 논리 트랜잭션이 묶이는 형태입니다. 만약 내부 트랜잭션에서 예외가 터져 롤백되면, 외부 트랜잭션까지 모두 롤백됩니다.</li>
</ul>
<h4 id="독립적인-실행-requires_new-항상-새로운-트랜잭션">독립적인 실행: REQUIRES_NEW (항상 새로운 트랜잭션)</h4>
<p>로그를 남기는 기능처럼, 본 로직이 실패해서 롤백되더라도 로그는 반드시 남겨야 하는 경우가 있습니다. 이때는 트랜잭션을 분리해야 합니다.</p>
<ul>
<li><strong>동작:</strong> 진행 중인 트랜잭션이 있더라도 잠시 중단(Suspend)시키고, <strong>완전히 새로운 물리 트랜잭션(새로운 커넥션)</strong>을 엽니다.</li>
<li><strong>특징:</strong> 두 트랜잭션은 서로 영향을 주지 않습니다. 내부가 롤백되어도 외부는 커밋될 수 있습니다.</li>
</ul>
<pre><code class="language-java">@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(Log log) {
    // 외부 트랜잭션의 성공/실패와 무관하게 별도의 커넥션으로 동작
    logRepository.save(log);
}</code></pre>
<h2 id="2-프록시의-치명적-함정-내부-호출-self-invocation">2. 프록시의 치명적 함정: 내부 호출 (Self-Invocation)</h2>
<p><code>@Transactional</code>을 썼는데 트랜잭션이 적용되지 않는 가장 흔한 실수가 바로 <strong>&#39;같은 클래스 내의 메서드 호출&#39;</strong>입니다.</p>
<h3 id="문제-상황">문제 상황</h3>
<p><strong>(여기에 문제 상황 코드 예시를 넣어주세요)</strong></p>
<p>위 코드에서 <code>createOrder()</code>를 호출하면, 내부에서 <code>saveBill()</code>을 호출할 때 <strong>트랜잭션이 적용되지 않습니다.</strong></p>
<h3 id="왜-안될까">왜 안될까?</h3>
<p>이유는 앞서 배운 <strong>프록시 동작 원리</strong> 때문입니다.</p>
<ol>
<li>클라이언트는 프록시 객체의 <code>createOrder()</code>를 호출합니다.</li>
<li><code>createOrder()</code>에는 <code>@Transactional</code>이 없으므로 프록시는 그냥 실제 객체(Target)의 <code>createOrder()</code>를 호출합니다.</li>
<li>실제 객체 내부에서 <code>saveBill()</code>을 호출할 때, 이것은 <code>this.saveBill()</code>입니다. 즉, <strong>프록시를 거치지 않고 자기 자신의 메서드를 직접 호출</strong>하는 것입니다.</li>
<li>프록시를 통과하지 않았으므로 트랜잭션을 시작하는 코드(AOP)가 실행되지 않습니다.</li>
</ol>
<h3 id="해결-방법">해결 방법</h3>
<p>가장 깔끔하고 권장되는 해결책은 <strong>별도의 클래스로 분리</strong>하는 것입니다.</p>
<pre><code>@Service
public class OrderService {

    private final BillService billService; // 별도 서비스 주입

    public void createOrder() {
        // ... 로직 ...
        // 외부 객체의 메서드를 호출하므로 프록시를 거침 -&gt; 트랜잭션 적용 OK ⭕️
        billService.saveBill(); 
    }
}

@Service
public class BillService {
    @Transactional
    public void saveBill() {
        // ...
    }
}</code></pre><hr>
<h2 id="3-소소한-성능-팁-readonly--true">3. 소소한 성능 팁: readOnly = true</h2>
<p>데이터를 조회만 하는 메서드나 서비스에는 가급적 읽기 전용 모드를 사용하는 것이 좋습니다.</p>
<pre><code class="language-java">@Service
@Transactional(readOnly = true) // 기본적으로 읽기 전용으로 설정
public class MemberService {

    // 조회: 최적화 적용됨
    public Member findOne(Long id) { ... }

    // 변경: 쓰기 권한 필요하므로 오버라이딩
    @Transactional 
    public void join(Member member) { ... }
}

* **JPA 사용 시:** 영속성 컨텍스트가 스냅샷을 만들지 않고, 변경 감지(Dirty Checking)를 수행하지 않아 메모리와 성능이 절약됩니다.
* **DB 부하 분산:** DB가 Master-Slave 구조일 때, Slave(읽기 전용) DB로 커넥션을 연결하도록 설정할 수도 있습니다.</code></pre>
<hr>
<h2 id="정리하며">정리하며</h2>
<p>스프링의 선언적 트랜잭션(<code>@Transactional</code>)은 매우 강력하지만, 그 기반 기술인 <strong>AOP와 프록시의 동작 원리</strong>를 이해하지 못하면 예상치 못한 버그를 만날 수 있습니다.</p>
<ol>
<li>트랜잭션은 <strong>프록시</strong>가 시작하고 종료한다.</li>
<li>프록시를 거치지 않는 <strong>내부 호출(Self-Invocation)</strong>은 트랜잭션이 적용되지 않는다.</li>
<li>필요에 따라 <strong>전파 속성</strong>을 조절하거나 <strong>readOnly</strong> 최적화를 활용하자.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[정적 IP 주소 vs 동적 IP 주소]]></title>
            <link>https://velog.io/@david1-p/%EC%A0%95%EC%A0%81-IP-%EC%A3%BC%EC%86%8C-vs-%EB%8F%99%EC%A0%81-IP-%EC%A3%BC%EC%86%8C</link>
            <guid>https://velog.io/@david1-p/%EC%A0%95%EC%A0%81-IP-%EC%A3%BC%EC%86%8C-vs-%EB%8F%99%EC%A0%81-IP-%EC%A3%BC%EC%86%8C</guid>
            <pubDate>Mon, 17 Nov 2025 07:19:44 GMT</pubDate>
            <description><![CDATA[<h2 id="정적-ip-주소-vs-동적-ip-주소-백엔드-개발자를-위한-핵심-정리">정적 IP 주소 vs 동적 IP 주소: 백엔드 개발자를 위한 핵심 정리</h2>
<p>네트워크에서 호스트(컴퓨터, 서버 등)에게 IP를 할당하는 방식은 크게 <strong>정적(Static) 할당 방식</strong>과 <strong>동적(Dynamic) 할당 방식</strong>으로 나뉩니다. 백엔드 시스템을 구성할 때 이 두 가지 방식의 차이점을 이해하는 것은 매우 중요합니다.</p>
<hr>
<h2 id="1-정적-ip-주소-할당-static-ip-allocation">1. 정적 IP 주소 할당 (Static IP Allocation)</h2>
<p><strong>정적 할당 방식</strong>은 호스트에게 IP 주소를 <strong>수동으로 직접 설정</strong>하는 것을 의미합니다.</p>
<p>일반적으로 IP를 정적으로 할당하기 위해서는 다음 정보가 필요합니다.</p>
<ul>
<li>부여하고자 하는 IP 주소</li>
<li>네트워크의 서브넷 마스크 (Subnet Mask)</li>
<li>게이트웨이 (Gateway) 주소</li>
<li>DNS 서버 주소</li>
</ul>
<blockquote>
<p><strong>정적 할당의 한계:</strong>
만약 모든 IP 주소를 정적으로만 할당한다면, 호스트(장비)의 수가 많아질수록 IP 할당 작업이 매우 번거로워질 수 있습니다. 또한, 관리자가 실수로 <strong>중복된 IP를 입력하는 등 실수를 유발</strong>할 가능성이 큽니다.</p>
</blockquote>
<hr>
<h2 id="2-동적-ip-주소-할당-dynamic-ip-allocation">2. 동적 IP 주소 할당 (Dynamic IP Allocation)</h2>
<p><strong>동적 할당 방식</strong>은 정적 할당의 번거로움을 해결하기 위해 등장했습니다. 이름 그대로 <strong>IP를 자동으로 할당</strong>하는 방식이며, 주로 <strong>DHCP(Dynamic Host Configuration Protocol)</strong>라는 프로토콜을 사용합니다.</p>
<ul>
<li><strong>작동 방식:</strong> DHCP는 네트워크 내에서 <strong>사용하지 않는 IP를 찾아 호스트에게 &#39;임대(Lease)&#39;</strong> 해주는 방식으로 작동합니다.</li>
<li><strong>주요 특징:</strong> 동적 할당 방식을 사용하는 경우, IP 주소가 고정적이지 않으며 <strong>임대 기간 만료 등에 따라 바뀔 가능성</strong>이 존재합니다.</li>
<li><strong>버전:</strong> IPv4 환경에서는 DHCPv4, IPv6 환경에서는 DHCPv6가 사용됩니다.</li>
</ul>
<hr>
<h2 id="3-dhcp는-어떻게-ip를-할당할까-dora-4단계">3. DHCP는 어떻게 IP를 할당할까? (DORA 4단계)</h2>
<p>DHCP를 이용한 IP 주소 할당 과정은 <strong>호스트(클라이언트)</strong>와 <strong>DHCP 서버(보통 라우터)</strong> 간의 통신으로 이루어집니다. 이 과정은 <strong>DORA</strong>라고 불리는 4단계로 요약할 수 있습니다.</p>
<h3 id="1-discover-탐색">1) Discover (탐색)</h3>
<p>호스트는 &quot;DHCP 서버를 찾습니다&quot;라는 <strong>Discover 메시지</strong>를 네트워크 전체에 <strong>브로드캐스팅(Broadcast)</strong>하여 DHCP 서버를 찾습니다.</p>
<h3 id="2-offer-제안">2) Offer (제안)</h3>
<p>Discover 메시지를 수신한 DHCP 서버는 &quot;이 IP 주소를 사용하세요&quot;라는 <strong>Offer 메시지</strong>를 호스트에게 전송합니다. 이 메시지에는 호스트에게 할당해 줄 <strong>IP 주소</strong>와 <strong>임대 기간(Lease Time)</strong>이 포함되어 있습니다.</p>
<h3 id="3-request-요청">3) Request (요청)</h3>
<p>호스트는 여러 서버로부터 Offer를 받을 수 있으며, 그중 하나를 선택하여 &quot;이 IP 주소를 사용하겠습니다&quot;라는 <strong>Request 메시지</strong>를 다시 <strong>브로드캐스팅</strong>합니다.</p>
<h3 id="4-acknowledgment-승인">4) Acknowledgment (승인)</h3>
<p>호스트의 요청을 받은 DHCP 서버는 &quot;요청을 승인합니다&quot;라는 <strong>ACK(Acknowledgment) 메시지</strong>를 호스트에게 전송하여 IP 임대를 최종 승인합니다.</p>
<hr>
<h2 id="4-ip-할당-완료-및-갱신">4. IP 할당 완료 및 갱신</h2>
<p>위의 4가지 과정이 모두 끝나면, 클라이언트는 할당받은 IP 주소를 자신의 IP 주소로 설정하고 정해진 임대 기간 동안 사용할 수 있습니다.</p>
<p>임대 기한이 만료되면 DHCP 과정을 다시 반복해야 하지만, 보통 만료되기 전에 <strong>DHCP 임대 갱신(DHCP Lease Renewal)</strong> 과정을 통해 임대 기간을 연장할 수 있습니다.</p>
<hr>
<h2 id="5-백엔드-서버에-정적-ip를-사용하는-이유">5. 백엔드 서버에 정적 IP를 사용하는 이유</h2>
<p>그렇다면 DHCP를 통한 동적 할당이 편리함에도 불구하고, 왜 백엔드 서버(웹 서버, API 서버, 데이터베이스 서버 등)는 <strong>정적 IP를 사용하는 것이 일반적</strong>일까요?</p>
<p>가장 핵심적인 이유는 <strong>&#39;신뢰성&#39;</strong>과 <strong>&#39;접근성&#39;</strong>입니다.</p>
<ul>
<li><p><strong>고정된 주소의 필요성 (신뢰성):</strong> 백엔드 서버는 클라이언트나 다른 서비스가 요청을 보낼 수 있도록 <strong>항상 동일한 주소</strong>에 위치해야 합니다. 만약 서버의 IP가 DHCP에 의해 계속 바뀐다면, 클라이언트는 서버를 찾을 수 없게 됩니다. 이는 매일 주소가 바뀌는 상점과 같습니다.</p>
</li>
<li><p><strong>DNS 및 도메인 연결:</strong> 우리는 보통 <code>api.example.com</code>과 같은 도메인 이름을 사용하여 서버에 접속합니다. DNS(Domain Name System)는 이 도메인 이름을 서버의 IP 주소로 변환해주는 역할을 합니다. 서버 IP가 정적이면, 이 DNS 레코드를 한 번만 설정해두면 됩니다. 하지만 IP가 동적으로 바뀐다면, 바뀔 때마다 DNS 레코드를 수동으로 또는 복잡한 (DDNS) 설정으로 갱신해야 하는 큰 문제가 발생합니다.</p>
</li>
<li><p><strong>서비스 간 의존성:</strong> 현대의 백엔드 아키텍처(예: 마이크로서비스)에서는 여러 서버가 서로 통신합니다. 예를 들어, 웹 서버는 데이터베이스 서버의 IP 주소를 알고 있어야 데이터를 요청할 수 있습니다. 만약 데이터베이스 서버의 IP가 동적으로 변경된다면, 웹 서버는 연결에 실패하고 전체 서비스가 중단될 것입니다.</p>
</li>
<li><p><strong>보안 및 방화벽 설정:</strong> 보안을 위해 방화벽에서는 특정 IP 주소의 접근만 허용하는 규칙(Access Control List)을 설정하는 경우가 많습니다. (예: &quot;오직 &#39;1.2.3.4&#39; IP를 가진 관리자 서버만 DB 서버에 접근할 수 있다.&quot;) 서버의 IP가 정적이어야만 이러한 IP 기반 보안 규칙을 안정적으로 운영할 수 있습니다.</p>
</li>
</ul>
<blockquote>
<p><strong>요약:</strong> 동적 IP는 네트워크에 &#39;접속&#39;하는 것이 목적인 <strong>클라이언트(사용자 PC, 스마트폰)</strong>에 적합하고, 정적 IP는 &#39;서비스를 제공&#39;하는 것이 목적인 <strong>서버</strong>에 필수적입니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[트랜잭셔널 아웃박스 패턴]]></title>
            <link>https://velog.io/@david1-p/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%94%EB%84%90-%EC%95%84%EC%9B%83%EB%B0%95%EC%8A%A4-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@david1-p/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%94%EB%84%90-%EC%95%84%EC%9B%83%EB%B0%95%EC%8A%A4-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Wed, 12 Nov 2025 10:59:52 GMT</pubDate>
            <description><![CDATA[<h2 id="분산-시스템의-데이터-정합성을-지키는-트랜잭셔널-아웃박스-패턴">분산 시스템의 데이터 정합성을 지키는 &#39;트랜잭셔널 아웃박스 패턴&#39;</h2>
<p>분산 시스템, 특히 마이크로서비스 아키텍처(MSA)에서 자주 발생하는 고질적인 문제와 그 해결책인 <strong>트랜잭셔널 아웃박스 패턴(Transactional Outbox Pattern)</strong>에 대해 알아보겠습니다.</p>
<h3 id="1-분산-시스템의-이중-쓰기-문제">1. 분산 시스템의 &#39;이중 쓰기&#39; 문제</h3>
<p>마이크로서비스 환경에서는 하나의 서비스가 자신의 데이터베이스를 변경하는 것(Write)과 동시에, 다른 서비스에게 &quot;나에게 변화가 생겼어!&quot;라고 알리기 위해 메시지 브로커(Kafka, RabbitMQ 등)에 이벤트를 발행(Write)하는 경우가 흔합니다.</p>
<p>예를 들어, <code>상품</code> 서비스가 신규 상품을 등록한 뒤, <code>검색</code> 서비스나 <code>알림</code> 서비스가 이 사실을 알 수 있도록 <code>ProductCreated</code> 이벤트를 발행하는 상황을 가정해 보겠습니다.</p>
<p>아마 가장 직관적인 코드는 다음과 같을 것입니다.</p>
<pre><code class="language-java">// 위험한 방식의 예시 코드
@Transactional
public void createProduct(ProductCreateRequest request) {
    // 1. DB에 상품 정보를 저장한다.
    Product product = new Product(request.getName(), request.getPrice());
    productRepository.save(product);

    // 2. 외부에 이벤트를 발행한다.
    eventPublisher.publish(new NewProductEvent(product.getId()));
}</code></pre>
<p>이 코드는 단순해 보이지만 심각한 문제를 내포하고 있습니다. 바로 <strong>이중 쓰기(Dual Writing)</strong> 문제입니다.</p>
<h3 id="2-왜-이-코드는-위험할까요">2. 왜 이 코드는 위험할까요?</h3>
<p>위험한 이유는 <strong>두 개의 서로 다른 시스템(데이터베이스, 메시지 브로커)에 대한 쓰기 작업을 하나의 원자적인 트랜잭션으로 묶을 수 없기 때문</strong>입니다.</p>
<p>데이터베이스의 트랜잭션은 <code>productRepository.save(product)</code> 호출 이후, <code>@Transactional</code> 어노테이션에 의해 <code>createProduct</code> 메소드가 성공적으로 종료될 때 <code>COMMIT</code>이 실행됩니다.</p>
<p>이 과정을 간단한 의사 코드로 풀어서 살펴보겠습니다.</p>
<pre><code class="language-java">public void problematicTransactionLogic() {
    try {
        // 1. DB 트랜잭션 시작
        database.transaction.begin();

        // 2. 비즈니스 로직 수행 (DB 쓰기)
        Product product = new Product(&quot;신규 상품&quot;);
        productRepository.save(product);

        // 3. 외부 시스템에 이벤트 발행
        eventPublisher.publish(new NewProductEvent(product.getId()));

        // 4. DB 트랜잭션 커밋
        database.transaction.commit();

    } catch (Exception e) {
        // 5. 문제 발생 시 DB 롤백
        database.transaction.rollback();
    }
}</code></pre>
<p>여기서 발생할 수 있는 두 가지 최악의 시나리오가 있습니다.</p>
<ol>
<li><p><strong>시나리오 A: DB 커밋 성공, 이벤트 발행 실패</strong></p>
<ul>
<li><code>productRepository.save(product)</code>는 성공했습니다.</li>
<li><code>eventPublisher.publish(...)</code>가 네트워크 문제나 브로커 장애로 <strong>실패</strong>했습니다.</li>
<li>메소드에서 예외가 발생하여 <code>database.transaction.rollback()</code>이 호출됩니다.</li>
<li><strong>결과:</strong> 실제로는 상품이 성공적으로 저장될 <em>뻔</em> 했지만, 이벤트 발행 실패 때문에 상품 등록 자체가 롤백됩니다. (혹은 예외 처리를 <code>publish</code>에서 따로 한다면, 상품은 DB에 있지만 이벤트는 발행되지 않아 아무도 모르는 유령 데이터가 됩니다.)</li>
</ul>
</li>
<li><p><strong>시나리오 B: DB 커밋 실패, 이벤트 발행 성공</strong></p>
<ul>
<li><code>productRepository.save(product)</code>는 성공했습니다.</li>
<li><code>eventPublisher.publish(...)</code>도 <strong>성공</strong>했습니다.</li>
<li>하지만 <code>database.transaction.commit()</code> 시점에 DB 장애, 락(Lock) 문제 등으로 <strong>커밋이 실패</strong>하고 롤백됩니다.</li>
<li><strong>결과:</strong> DB에는 상품 데이터가 없는데, &quot;신규 상품이 등록되었다!&quot;라는 이벤트는 이미 외부 시스템에 전파되었습니다. 다른 서비스들은 존재하지 않는 상품을 참조하려다 장애를 일으킬 것입니다.</li>
</ul>
</li>
</ol>
<p>이처럼 두 작업 중 하나만 성공하는 상황은 <strong>서비스 전체의 데이터 정합성을 심각하게 훼손</strong>시킵니다.</p>
<h3 id="3-해결책-트랜잭셔널-아웃박스-패턴-transactional-outbox-pattern">3. 해결책: 트랜잭셔널 아웃박스 패턴 (Transactional Outbox Pattern)</h3>
<p>이 문제를 해결하는 것이 바로 <strong>트랜잭셔널 아웃박스 패턴</strong>입니다.</p>
<p>핵심 아이디어는 간단합니다.</p>
<blockquote>
<p>&quot;외부 시스템(메시지 브로커)에 직접 이벤트를 발행하지 말고, &#39;발행할 이벤트&#39; 자체를 내 데이터베이스에 저장하자!&quot;</p>
</blockquote>
<p>즉, 비즈니스 데이터(상품)를 저장하는 작업과, 발행할 이벤트(상품 생성 이벤트)를 저장하는 작업을 <strong>하나의 DB 트랜잭션</strong>으로 묶어 원자성을 보장하는 것입니다.</p>
<p>이를 위해 <code>OUTBOX</code>라는 별도의 테이블을 만듭니다.</p>
<pre><code class="language-SQL">예시: 이벤트 저장을 위한 Outbox 테이블
CREATE TABLE product_outbox (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    event_type VARCHAR(255) NOT NULL,
    payload TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    processed BOOLEAN DEFAULT FALSE -- (추가) 처리 여부를 추적할 수 있습니다.
);</code></pre>
<h3 id="4-아웃박스-패턴의-구현">4. 아웃박스 패턴의 구현</h3>
<p>이제 아웃박스 패턴을 적용하여 코드를 수정해 보겠습니다.</p>
<pre><code class="language-java">// 트랜잭셔널 아웃박스 패턴이 적용된 코드
@Transactional
public void createProduct(ProductCreateRequest request) {
    // 1. 비즈니스 데이터(상품) 저장
    Product product = new Product(request.getName(), request.getPrice());
    productRepository.save(product);

    // 2. 발행할 이벤트를 Outbox 테이블에 저장
    // (아직 진짜 발행(publish)한 것이 아님!)
    ProductEvent event = new ProductEvent(product.getId(), &quot;PRODUCT_CREATED&quot;, request);
    productOutboxRepository.save(event);

    // 3. 트랜잭션 커밋
    // 이제 productRepository.save(product)와
    // productOutboxRepository.save(event)는
    // 같은 DB 트랜잭션으로 묶여 원자성을 보장받습니다.
}</code></pre>
<p>이제 어떻게 될까요?</p>
<ul>
<li><code>product</code> 저장이 성공하고 <code>event</code> 저장도 성공하면, 트랜잭션이 <strong>커밋</strong>됩니다. (성공)</li>
<li><code>product</code> 저장은 성공했지만 <code>event</code> 저장이 실패하면, 트랜잭션이 <strong>롤백</strong>됩니다. (데이터 정합성 유지)</li>
<li><code>product</code> 저장, <code>event</code> 저장이 모두 성공했지만 커밋이 실패하면, 트랜잭션이 <strong>롤백</strong>됩니다. (데이터 정합성 유지)</li>
</ul>
<p>이로써 DB와 이벤트 발행 간의 원자성(Atomicity) 문제는 해결됩니다.
다만 이후 이벤트 발행 과정에서 중복 전송이 일어날 수 있으므로, 소비자 측에서 멱등성을 보장해야 완전한 정합성이 유지됩니다.</p>
<h3 id="5-outbox의-이벤트를-진짜-발행하기-polling-vs-cdc">5. Outbox의 이벤트를 &#39;진짜&#39; 발행하기 (Polling vs. CDC)</h3>
<p>&quot;좋아, 이제 이벤트가 DB에 저장된 건 알겠어. 그럼 이걸 언제, 누가 메시지 브로커로 보내주지?&quot;</p>
<p>여기서부터는 <strong>별도의 비동기 프로세스</strong>가 필요합니다. 이 프로세스는 <code>OUTBOX</code> 테이블을 감시하다가, 새로 추가된 이벤트를 실제 메시지 브로커로 전달하는 &#39;우체부&#39; 역할을 합니다.</p>
<p>이 &#39;우체부&#39;를 구현하는 방식은 크게 두 가지입니다.</p>
<h4 id="a-폴링-polling-방식">A. 폴링 (Polling) 방식</h4>
<p>가장 구현하기 쉬운 방식입니다.</p>
<ul>
<li><strong>작동 방식:</strong> 별도의 스케줄러(예: Spring의 <code>@Scheduled</code>)가 주기적으로(예: 매 1초마다) <code>OUTBOX</code> 테이블을 <code>SELECT</code> 합니다.</li>
<li>아직 처리되지 않은(<code>processed = false</code>) 이벤트를 가져와 메시지 브로커로 발행합니다.</li>
<li>발행에 성공하면 해당 이벤트를 <code>OUTBOX</code> 테이블에서 삭제하거나, <code>processed = true</code>로 업데이트합니다.</li>
<li><strong>단점:</strong><ul>
<li>폴링 주기가 너무 짧거나 Outbox 데이터가 많아질 경우 DB 부하가 증가할 수 있습니다.
하지만 일반적인 트래픽 규모에서는 실질적인 부담이 크지 않으며, 구현이 간단하다는 장점이 있습니다.</li>
<li>이벤트 발생 시점과 실제 발행 시점 사이에 지연(Latency)이 발생합니다. (스케줄링 주기만큼)</li>
</ul>
</li>
</ul>
<h4 id="b-cdc-change-data-capture-방식">B. CDC (Change Data Capture) 방식</h4>
<p>더욱 효율적이고 세련된 방식입니다.</p>
<ul>
<li><strong>작동 방식:</strong> 애플리케이션이 DB를 직접 폴링하는 것이 아니라, <strong>데이터베이스의 트랜잭션 로그(Transaction Log)</strong>를 모니터링합니다.</li>
<li>MySQL의 <code>Binlog</code>, PostgreSQL의 <code>WAL</code> 등이 트랜잭션 로그입니다.</li>
<li><strong>Debezium</strong> 같은 CDC 도구가 이 로그를 읽다가, <code>OUTBOX</code> 테이블에 <code>INSERT</code>가 발생한 것을 &#39;캡처&#39;합니다.</li>
<li>캡처된 이벤트를 즉시 Kafka와 같은 메시지 브로커로 전달(Relay)합니다.</li>
<li><strong>장점:</strong><ul>
<li>DB에 폴링 부하를 전혀 주지 않습니다.</li>
<li>이벤트가 커밋되는 즉시 로그에 기록되므로, 거의 실시간(Near real-time)으로 이벤트를 발행할 수 있습니다.</li>
</ul>
</li>
<li><strong>단점:</strong> Debezium, Kafka Connect 등 별도의 CDC 파이프라인을 구축해야 하므로 초기 설정이 복잡합니다.</li>
<li>Debezium은 Kafka Connect 기반으로 동작하며, 데이터베이스의 트랜잭션 로그를 읽어 Kafka 토픽으로 바로 내보내는 Connector 역할을 합니다.
이후 애플리케이션은 Kafka 토픽을 구독하여 이벤트를 처리하면 됩니다.</li>
</ul>
<h3 id="6-아웃박스-패턴의-보장과-고려사항">6. 아웃박스 패턴의 보장과 고려사항</h3>
<ul>
<li><p><strong>적어도 한 번 전송 (At-least-once Delivery)</strong>
  아웃박스 패턴은 이벤트 발행 프로세스(우체부)가 브로커에게 이벤트를 &#39;성공할 때까지&#39; 재시도할 수 있게 해줍니다. (예: 발행 후 <code>OUTBOX</code>에서 삭제하기 직전에 &#39;우체부&#39; 프로세스가 죽는 경우, 재시작 시 동일 이벤트를 다시 발행함)
  따라서 이 패턴은 <strong>&quot;적어도 한 번&quot;</strong> 이벤트가 발행되는 것을 보장합니다.</p>
</li>
<li><p><strong>소비자의 멱등성 (Idempotency)</strong>
  &#39;적어도 한 번&#39;이 보장된다는 것은, 네트워크 문제 등으로 인해 <strong>&#39;중복 발행&#39;</strong>이 발생할 수 있다는 의미이기도 합니다. 따라서 이 이벤트를 수신하는 <strong>소비자(Consumer)는 반드시 멱등성(Idempotent)</strong>을 갖도록 설계해야 합니다. 즉, 같은 이벤트를 여러 번 수신하더라도 단 한 번만 처리된 것과 동일한 결과를 내도록 만들어야 합니다.</p>
</li>
</ul>
<h3 id="7-요약">7. 요약</h3>
<ol>
<li>단일 트랜잭션에서 DB 쓰기와 외부 메시지 발행을 함께 처리하면 &#39;이중 쓰기&#39; 문제로 데이터 정합성이 깨진다.</li>
<li><strong>트랜잭셔널 아웃박스 패턴</strong>은 외부 메시지를 직접 발행하는 대신, &#39;발행할 이벤트&#39;를 DB 내 <code>OUTBOX</code> 테이블에 저장한다.</li>
<li>비즈니스 로직과 이벤트 저장은 <strong>동일한 DB 트랜잭션</strong>으로 묶여 원자성을 보장받는다.</li>
<li><strong>별도의 릴레이 프로세스</strong>(Polling 또는 CDC)가 <code>OUTBOX</code> 테이블을 감시하여 실제 메시지 브로커로 이벤트를 전송한다.</li>
<li>이 패턴은 “DB 변경 → 이벤트 발행”이 반드시 일관되게 수행되도록 보장합니다.
따라서 이벤트 기반 아키텍처에서 데이터 정합성을 확보하는 핵심 패턴으로 널리 활용됩니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSRF 공격]]></title>
            <link>https://velog.io/@david1-p/CSRF-%EA%B3%B5%EA%B2%A9</link>
            <guid>https://velog.io/@david1-p/CSRF-%EA%B3%B5%EA%B2%A9</guid>
            <pubDate>Mon, 10 Nov 2025 23:47:19 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>파이널 프로젝트를 할 때, 보안 관련해서 CSRF 대비 로직을 구현한 것을 본 적이 있다. 생소한 개념이라 알아보고자 한다. </p>
</blockquote>
<h1 id="csrf-공격이란-무엇이며-어떻게-방어할까요">CSRF 공격이란 무엇이며, 어떻게 방어할까요?</h1>
<h2 id="1-csrf-cross-site-request-forgery란">1. CSRF (Cross-Site Request Forgery)란?</h2>
<p>사이트 간 요청 위조(Cross-site Request Forgery, <strong>CSRF</strong>) 공격은 사용자가 자신의 의지와 상관없이 <strong>공격자가 의도한 행위를 특정 웹사이트에 요청</strong>하도록 하는 것을 의미합니다.</p>
<p>즉, 사용자가 <code>david1p.com</code>에 로그인하여 인증 쿠키를 발급받은 상태에서, 공격자가 만든 악성 사이트에 접속했을 때, 해당 사이트가 사용자의 쿠키를 도용하여 <code>david1p.com</code>에 사용자가 원치 않는 요청(결제, 비밀번호 변경 등)을 보내는 공격입니다.</p>
<hr>
<h2 id="2-csrf-공격-시나리오">2. CSRF 공격 시나리오</h2>
<p>CSRF 공격이 어떻게 이루어지는지 <code>david1p.com</code>이라는 가상의 사이트를 예로 들어 구체적인 시나리오를 살펴보겠습니다.</p>
<ol>
<li><strong>로그인 및 쿠키 발급</strong>: 사용자가 <strong><code>david1p.com</code></strong> 서비스에 정상적으로 로그인을 수행합니다.</li>
<li><strong>쿠키 저장</strong>: 서버는 해당 사용자에 대한 인증 정보(예: 세션 ID)를 <code>Set-Cookie</code> 헤더에 담아 응답합니다. 사용자의 브라우저는 이 쿠키를 저장하고, 이후 <strong><code>david1p.com</code></strong> 에 요청을 보낼 때마다 해당 쿠키를 자동으로 함께 전달합니다.</li>
<li><strong>악성 페이지 접속 유도</strong>: 공격자는 악성 스크립트가 담긴 페이지(예: <code>attacker.com</code>)를 만들어 사용자에게 접속하도록 유도합니다. (이메일, 커뮤니티 게시글 링크 등)</li>
<li><strong>악성 요청 전송</strong>: 사용자가 <code>david1p.com</code>에 로그인한 상태(쿠키가 브라우저에 저장된 상태)에서 <code>attacker.com</code>에 접속하면, 페이지 내의 악성 스크립트가 자동으로 실행되어 <code>david1p.com</code> 서버로 강제로 요청을 전송합니다.</li>
<li><strong>서버의 오인</strong>: 이때 브라우저는 정책에 따라 <code>david1p.com</code>으로 요청을 보낼 때 <strong>저장되어 있던 쿠키(세션 ID)를 자동으로 함께</strong> 실어 보냅니다. 서버 입장에서는 이 요청이 정상적인 사용자가 보낸 것인지, <code>attacker.com</code>을 통해 위조된 것인지 구분할 수 없습니다. 쿠키가 유효하므로 서버는 이 요청을 정상적인 사용자의 요청으로 오인하고 처리하게 됩니다.</li>
</ol>
<blockquote>
<p><strong>공격 예시 1: <code>GET</code> 요청을 이용한 공격</strong></p>
<p><code>GET</code> 요청은 <code>img</code> 태그의 <code>src</code> 속성을 이용하는 것만으로도 쉽게 위조할 수 있습니다.</p>
<pre><code class="language-html">&gt; &lt;img src=&quot;[https://david1p.com/member/changePassword?newValue=1234](https://david1p.com/member/changePassword?newValue=1234)&quot; style=&quot;display:none;&quot; /&gt;</code></pre>
<p>사용자가 공격자 사이트에 방문하는 것만으로도, 브라우저는 <code>img</code> 태그를 해석하여 <code>david1p.com</code> 서버로 비밀번호 변경 요청을 (쿠키와 함께) 전송하게 됩니다.</p>
</blockquote>
<blockquote>
<p><strong>공격 예시 2: <code>POST</code> 요청을 이용한 공격 (더 위험)</strong></p>
<p>결제, 회원 탈퇴 등 더 심각한 변경을 유발하는 <code>POST</code> 요청도 위조할 수 있습니다. 공격자는 사용자가 볼 수 없는 <code>iframe</code> 내에 다음과 같은 <code>form</code>을 심어두고, 페이지가 로드될 때 JavaScript로 자동 제출(submit)시킬 수 있습니다.</p>
<pre><code class="language-html">&lt;iframe style=&quot;display:none;&quot;&gt;
  &lt;form id=&quot;csrf_form&quot; action=&quot;[https://david1p.com/payment/transfer](https://david1p.com/payment/transfer)&quot; method=&quot;POST&quot;&gt;
    &lt;input type=&quot;hidden&quot; name=&quot;to_account&quot; value=&quot;attacker_account_number&quot; /&gt;
    &lt;input type=&quot;hidden&quot; name=&quot;amount&quot; value=&quot;1000000&quot; /&gt;
  &lt;/form&gt;
  &lt;script&gt;
    document.getElementById(&quot;csrf_form&quot;).submit();
  &lt;/script&gt;
&lt;/iframe&gt;</code></pre>
<p>사용자가 이 페이지에 접속하는 순간, 자신도 모르게 100만 원을 공격자 계좌로 이체하는 <code>POST</code> 요청이 (쿠키와 함께) <code>david1p.com</code> 서버로 전송됩니다.</p>
</blockquote>
<hr>
<h2 id="3-xss-vs-csrf-간단-비교">3. XSS vs. CSRF (간단 비교)</h2>
<p>많은 입문자가 두 공격을 혼동합니다. 목적과 방식을 간단히 비교하면 다음과 같습니다.</p>
<ul>
<li><strong>XSS (Cross-Site Scripting)</strong>: 공격자가 신뢰받는 사이트(<code>david1p.com</code>)에 <strong>악성 스크립트(Script)를 삽입</strong>하는 공격입니다. 사용자의 브라우저에서 이 스크립트가 실행되어 사용자의 세션 쿠키, 개인 정보 등을 탈취합니다. (신뢰할 수 있는 <em>사이트</em>에서 <em>악성 코드</em>가 실행됨)</li>
<li><strong>CSRF (Cross-Site Request Forgery)</strong>: 공격자가 사용자의 <strong>인증(쿠키)을 도용</strong>하여, 사용자가 의도하지 않은 <strong>요청(Request)을 서버로 강제 전송</strong>하는 공격입니다. (신뢰할 수 있는 <em>사용자</em>의 브라우저에서 <em>위조된 요청</em>이 전송됨)</li>
</ul>
<p>간단히 말해, <strong>XSS는 사용자의 브라우저를 신뢰</strong>하여 코드를 실행하는 것이고, <strong>CSRF는 서버가 사용자의 요청을 신뢰</strong>한다는 점을 악용합니다.</p>
<hr>
<h2 id="4-csrf-공격-방어-방법">4. CSRF 공격 방어 방법</h2>
<p>CSRF 공격은 기본적으로 <strong>교차 출처(Cross-Origin) 상황에서의 요청을 신뢰하지 않고, 현재 요청이 정말 사용자의 의도에 의해 발생한 것인지 확인</strong>하는 방식으로 방어할 수 있습니다.</p>
<h3 id="1-referer-헤더-검증">1. Referer 헤더 검증</h3>
<ul>
<li><strong>작동 원리</strong>: <code>Referer</code> 요청 헤더는 현재 요청을 보낸 페이지의 주소(출처)를 담고 있습니다. 서버 측에서 이 <code>Referer</code> 헤더 값과 서버의 도메인(<code>Host</code> 헤더)을 비교하여, 일치하지 않으면(즉, <code>attacker.com</code>에서 보낸 요청이면) 요청을 거부(예외 발생)할 수 있습니다.</li>
<li><strong>한계</strong>: <code>Referer</code> 헤더는 브라우저 설정이나 프록시 등에 의해 누락될 수 있으며, 이론적으로 조작될 가능성도 있다는 한계가 있습니다.</li>
</ul>
<h3 id="2-anti-csrf-토큰-ssr세션-기반">2. Anti-CSRF 토큰 (SSR/세션 기반)</h3>
<p>템플릿 엔진(JSP, Thymeleaf, Pug, EJS 등)을 사용하는 SSR(서버 사이드 렌더링) 환경에서 가장 널리 쓰이는 강력한 방어책입니다.</p>
<ol>
<li><p>서버는 페이지를 생성(렌더링)하기 이전에 <strong>사용자 세션에 임의의 난수 값인 &#39;CSRF 토큰&#39;을 저장</strong>합니다.</p>
</li>
<li><p>사용자에게 페이지를 응답할 때, 중요한 요청(예: 폼 전송)을 발생시키는 <code>form</code> 태그 내부에 해당 CSRF 토큰값을 가진 <code>input</code> 태그를 숨겨서 추가합니다.</p>
<pre><code class="language-html">&lt;form action=&quot;/member/changePassword&quot; method=&quot;POST&quot;&gt;
    &lt;input type=&quot;hidden&quot; name=&quot;csrf_token&quot; value=&quot;csrf_token_12341234_random_value&quot; /&gt;
    &lt;button type=&quot;submit&quot;&gt;비밀번호 변경&lt;/button&gt;
&lt;/form&gt;</code></pre>
</li>
<li><p>사용자가 폼을 제출하여 실제 요청이 서버로 전달될 때, 서버는 <strong>폼 데이터에 포함된 <code>csrf_token</code> 값</strong>과 <strong>사용자 세션 내부에 저장된 CSRF 토큰</strong>의 일치 여부를 판단합니다.</p>
</li>
<li><p>두 값이 일치해야만 요청을 정상적으로 처리합니다.</p>
</li>
</ol>
<p>(공격자는 <code>attacker.com</code>에서 <code>david1p.com</code>으로 요청을 위조할 수는 있지만, 사용자의 세션에 저장된 이 임의의 토큰값은 알 수 없으므로 <code>csrf_token</code> 값을 맞춰서 보낼 수 없습니다.)</p>
<h3 id="3-double-submit-cookie-spa토큰-기반">3. Double Submit Cookie (SPA/토큰 기반)</h3>
<p>React, Vue, Angular와 같은 SPA(Single Page Application) 환경에서는 서버 세션을 사용하지 않고 JWT 같은 토큰을 주로 사용합니다. 이 경우 세션에 CSRF 토큰을 저장할 수 없으므로 다른 방식이 필요합니다.</p>
<ol>
<li><strong>토큰 발행</strong>: 사용자가 로그인할 때, 서버는 응답 헤더(예: <code>Authorization</code>)로 JWT 토큰을 발행하는 것과 <strong>별개</strong>로, CSRF 방어용 토큰을 <strong>쿠키</strong>로 함께 발행합니다. (이 쿠키는 <code>HttpOnly=false</code>로 설정하여 JS가 읽을 수 있게 해야 합니다.)</li>
<li><strong>클라이언트의 2중 전송</strong>: 클라이언트(JavaScript)는 API 요청 시, 브라우저가 자동으로 보내는 쿠키 외에도, <strong>JS로 쿠키에 저장된 CSRF 토큰 값을 읽어</strong> <code>HTTP Header</code> (예: <code>X-CSRF-TOKEN</code>)에 명시적으로 담아 함께 전송합니다.</li>
<li><strong>서버 검증</strong>: 서버는 요청이 도착하면 <strong>쿠키에 담긴 CSRF 토큰</strong>과 <strong>HTTP 헤더에 담긴 CSRF 토큰</strong> 값이 일치하는지 검증합니다.</li>
</ol>
<p>(SOP(동일 출처 정책)로 인해 공격자의 사이트 <code>attacker.com</code>에서는 <code>david1p.com</code>의 쿠키 값을 JS로 읽을 수 없으므로, 헤더에 토큰을 담아 보낼 수 없습니다.)</p>
<h3 id="4-samesite-쿠키-속성-사용">4. SameSite 쿠키 속성 사용</h3>
<p>쿠키 자체의 전송 정책을 제어하는 가장 강력하고 현대적인 방법입니다. 쿠키에 <code>SameSite</code> 속성을 설정하여, 크로스 사이트 요청 시 쿠키가 전송되는 것을 브라우저단에서 제어합니다.</p>
<ul>
<li><code>Set-Cookie: sessionId=...; SameSite=Strict</code><ul>
<li><strong><code>Strict</code></strong>: 같은 사이트(Same-Site)에서 보낸 요청에만 쿠키를 전송합니다. 가장 강력하지만, 다른 도메인에서 내 사이트로 링크를 타고 들어오는 경우 등에도 쿠키가 전송되지 않아 불편할 수 있습니다.</li>
<li><strong><code>Lax</code></strong>: <code>Strict</code>보다 다소 완화된 정책. <code>GET</code> 요청과 같은 일부 교차 출처 요청은 허용하지만, <code>POST</code> 폼 전송 등 CSRF 공격에 주로 사용되는 요청에서는 쿠키를 전송하지 않습니다. (최신 브라우저들의 기본값이 <code>Lax</code>인 경우가 많습니다.)</li>
</ul>
</li>
</ul>
<h3 id="5-sop와-cors-정책-활용">5. SOP와 CORS 정책 활용</h3>
<p>브라우저의 기본 보안 정책인 <strong>SOP(Same-Origin Policy, 동일 출처 정책)</strong>를 기본으로 활용하고, <strong>CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)</strong> 설정을 통해 신뢰할 수 있는 출처의 요청만 명시적으로 허용하는 방식도 CSRF를 포함한 다양한 교차 출처 공격을 방어하는 데 도움이 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커와 K8S 요약 정리]]></title>
            <link>https://velog.io/@david1-p/%EB%8F%84%EC%BB%A4%EC%99%80-K8S</link>
            <guid>https://velog.io/@david1-p/%EB%8F%84%EC%BB%A4%EC%99%80-K8S</guid>
            <pubDate>Sat, 01 Nov 2025 04:34:31 GMT</pubDate>
            <description><![CDATA[<h1 id="2025년-기준-도커docker와-쿠버네티스kubernetes-핵심-요약">[2025년 기준] 도커(Docker)와 쿠버네티스(Kubernetes) 핵심 요약</h1>
<hr>
<h2 id="1-도커-docker">1. 도커 (Docker)</h2>
<h3 id="도커란">도커란?</h3>
<p>도커는 애플리케이션을 <strong>컨테이너</strong>라는 격리된 환경에 패키징하여 실행하는 기술 및 플랫폼입니다. 컨테이너에는 애플리케이션 실행에 필요한 모든 환경(코드, 라이브러리, 설정)이 포함되어 있어, &quot;제 PC에서는 됐는데...&quot; 하는 문제를 원천적으로 해결합니다.</p>
<p>도커 사용법은 크게 <strong>도커 이미지</strong>를 다루는 것과 <strong>컨테이너</strong>를 다루는 것으로 나뉩니다.</p>
<h3 id="도커-이미지-docker-image">도커 이미지 (Docker Image)</h3>
<p>도커 이미지는 컨테이너를 실행하기 위한 <strong>읽기 전용 템플릿</strong>입니다.</p>
<ul>
<li>운영 체제의 최소한의 파일 시스템 (예: Alpine Linux)</li>
<li>애플리케이션 소스 코드 및 바이너리</li>
<li>애플리케이션 실행에 필요한 의존성(라이브러리, 도구)</li>
<li>실행 환경 설정 정보 (예: 어떤 명령어로 앱을 시작할지)</li>
</ul>
<p>이 모든 것을 포함하는 아카이브입니다. 이미지를 만드는 과정을 <strong>빌드(build)</strong>라고 하며, 컨테이너는 이 빌드된 이미지를 기반으로 실행됩니다.</p>
<h4 id="주요-이미지-명령어">주요 이미지 명령어</h4>
<p><strong>1. <code>docker image build</code> - 이미지 빌드</strong></p>
<ul>
<li><p>`-t: 이미지명과 태그(버전)를 지정합니다. &#39;.&#39;은 현재 디렉터리의 Dockerfile을 사용하라는 의미입니다.
ex) docker image build -t 내이미지명:태그명 .</p>
</li>
<li><p><code>-f</code>: 특정 파일명을 가진 Dockerfile을 지정할 수 있습니다.</p>
</li>
<li><p><code>--pull</code>: 빌드 시 캐시된 베이스 이미지를 사용하지 않고, 레지스트리에서 항상 새로 받아옵니다. (최신 보안 패치 적용 시 유용)</p>
</li>
</ul>
<p><strong>2. <code>docker image pull</code></strong> - 이미지 내려받기 (레지스트리에서 로컬로)</p>
<p><strong>3. <code>docker image ls</code></strong> - 로컬에 보유한 도커 이미지 목록 보기</p>
<p><strong>4. <code>docker image tag</code></strong> - 이미지에 추가 태그 붙이기 (예: <code>my-image:latest</code> -&gt; <code>my-image:v1.0</code>)</p>
<p><strong>5. <code>docker image push</code></strong> - 이미지를 외부 레지스트리(예: Docker Hub)에 공개(업로드)하기</p>
<p><strong>🚫 <code>docker search</code> 명령어에 대한 주의</strong></p>
<blockquote>
<p>CLI의 <code>docker search</code> 명령어는 사용이 권장되지 않습니다. 보안이 검증되지 않은 이미지가 많고 신뢰도를 파악하기 어렵습니다.</p>
<p><strong>(모범 사례)</strong>: Docker Hub, GCR, ECR 등 신뢰할 수 있는 웹 레지스트리에서 <strong>&#39;Official Image&#39;</strong> 또는 <strong>&#39;Verified Publisher&#39;</strong> 배지가 붙은 이미지를 검색하여 사용합니다.</p>
</blockquote>
<hr>
<h3 id="도커-컨테이너-docker-container">도커 컨테이너 (Docker Container)</h3>
<p>컨테이너는 도커 이미지를 <strong>실행한 인스턴스</strong>입니다. 이미지는 템플릿(설계도)이고, 컨테이너는 그 설계도로 만든 실제 동작하는 개체(집)입니다.</p>
<h4 id="컨테이너의-생애-주기-lifecycle">컨테이너의 생애 주기 (Lifecycle)</h4>
<p><strong>1) 실행 중 (Running)</strong>
<code>docker container run</code> 명령으로 이미지를 기반으로 컨테이너가 생성된 상태입니다. Dockerfile의 <code>CMD</code>나 <code>ENTRYPOINT</code>에 정의된 애플리케이션이 실행됩니다.</p>
<p><strong>2) 정지 (Stopped)</strong>
사용자가 명시적으로 정지(<code>stop</code>)시키거나, 컨테이너의 메인 애플리케이션이 종료되면 컨테이너는 &#39;정지&#39; 상태가 됩니다. 가상 환경은 동작하지 않지만, 종료 시점의 상태(데이터 변경분)가 디스크에 저장됩니다.</p>
<p><strong>3) 파기 (Destroyed)</strong>
<code>rm</code> 명령 등으로 파기된 상태입니다. 한 번 파기한 컨테이너는 다시 실행할 수 없습니다.</p>
<h4 id="주요-컨테이너-명령어">주요 컨테이너 명령어</h4>
<ul>
<li><code>docker container run</code>: 이미지로부터 컨테이너를 생성하고 실행합니다.</li>
<li><code>docker container ls</code>: 실행 중인 컨테이너 목록 보기 (<code>-a</code> 옵션으로 정지된 것 포함)</li>
<li><code>docker container stop</code>: 컨테이너를 정지합니다.</li>
<li><code>docker container restart</code>: 컨테이너를 재시작합니다.</li>
<li><code>docker container rm</code>: 컨테이너를 파기합니다.</li>
<li><code>docker container logs</code>: 컨테이너의 표준 출력을 확인합니다. (<code>-f</code> 옵션으로 실시간 추적)</li>
<li><code>docker container exec</code>: 실행 중인 컨테이너에서 추가 명령을 실행합니다. (예: <code>exec -it [이름] /bin/bash</code>)</li>
<li><code>docker container cp</code>: 호스트와 컨테이너 간 파일을 복사합니다.</li>
</ul>
<h4 id="리소스-정리-명령어">리소스 정리 명령어</h4>
<ul>
<li><code>docker container prune</code>: 실행 중이 아닌 모든 컨테이너를 삭제합니다.</li>
<li><code>docker image prune</code>: 태그가 붙지 않은(dangling) 모든 이미지를 삭제합니다.</li>
<li><code>docker system prune</code>: 사용하지 않는 컨테이너, 이미지, 볼륨, 네트워크 등 모든 도커 리소스를 일괄 삭제합니다.</li>
</ul>
<hr>
<h3 id="docker-compose-v2">Docker Compose (v2)</h3>
<p><code>docker compose</code>는 <strong>여러 컨테이너로 구성된 애플리케이션</strong>을 정의하고 실행하기 위한 도구입니다. (2019년의 <code>docker-compose</code> v1과 달리, 현재는 Docker CLI에 <code>docker compose</code> v2로 통합되었습니다.)</p>
<p><code>docker-compose.yml</code> 파일 하나로 웹 서버, 데이터베이스, 캐시 서버 등을 한꺼번에 관리할 수 있습니다.</p>
<ul>
<li><code>docker compose up</code>: <code>yml</code> 파일에 정의된 모든 컨테이너를 생성하고 시작합니다. (<code>-d</code>로 백그라운드 실행)</li>
<li><code>docker compose down</code>: <code>yml</code> 파일로 생성된 컨테이너, 네트워크, 볼륨을 중지하고 삭제합니다.</li>
<li><code>docker compose ps</code>: 현재 compose로 실행 중인 컨테이너 상태를 봅니다.</li>
<li><code>docker compose logs -f</code>: 모든 컨테이너의 로그를 실시간으로 봅니다.</li>
</ul>
<blockquote>
<p><strong>(참고)</strong>: 최신 <code>docker-compose.yml</code> 파일은 상단에 <code>version: &quot;3.x&quot;</code> 선언이 필수가 아닙니다.</p>
</blockquote>
<hr>
<hr>
<h2 id="2-쿠버네티스-kubernetes">2. 쿠버네티스 (Kubernetes)</h2>
<h3 id="쿠버네티스란">쿠버네티스란?</h3>
<p>쿠버네티스(K8s)는 <strong>컨테이너 오케스트레이션 도구</strong>입니다. &quot;오케스트레이션&quot;이란, 수십, 수백 개의 컨테이너를 대규모 프로덕션 환경에서 안정적으로 운영(배포, 스케일링, 복구)하는 작업을 자동화하는 것을 의미합니다.</p>
<ul>
<li><strong>주요 기능:</strong><ul>
<li><strong>서비스 디스커버리 및 로드 밸런싱</strong>: 컨테이너에 고유한 IP와 DNS 이름을 부여하고, 트래픽을 분산합니다.</li>
<li><strong>자동화된 롤아웃 및 롤백</strong>: 새 버전 배포 시 문제 생기면 자동으로 이전 버전으로 롤백합니다.</li>
<li><strong>자동화된 빈 패킹(Bin Packing)</strong>: 컨테이너가 필요한 리소스(CPU, Mem)를 계산하여 최적의 서버(노드)에 배치합니다.</li>
<li><strong>자가 치유 (Self-healing)</strong>: 실행 중이던 컨테이너가 죽으면 자동으로 재시작하거나 교체합니다.</li>
<li><strong>스케일링</strong>: 필요에 따라 컨테이너 개수를 자동으로 늘리거나 줄입니다.</li>
</ul>
</li>
</ul>
<h3 id="중요-쿠버네티스와-도커의-관계-2025년-기준">(중요) 쿠버네티스와 도커의 관계 (2025년 기준)</h3>
<p>과거에는 쿠버네티스가 도커 데몬을 직접 제어(Dockershim 경유)했지만, <strong>K8s v1.24 (2022년)부터 <code>Dockershim</code>이 완전히 제거되었습니다.</strong></p>
<ul>
<li><strong>현재</strong>: 쿠버네티스는 <strong>CRI(Container Runtime Interface)</strong>라는 표준을 따르는 런타임과 통신합니다. 대표적으로 <code>containerd</code>나 <code>CRI-O</code>가 있습니다.</li>
<li><strong>결론</strong>: 개발자는 <code>docker build</code>로 이미지를 만들 수 있습니다. 이 이미지는 <strong>CRI 호환 런타임이라면 어디서든 실행 가능</strong>하며, 쿠버네티스는 이 이미지를 <code>containerd</code>를 이용해 실행합니다. (참고: 도커 데스크톱도 내부적으로 <code>containerd</code>를 사용합니다.)</li>
</ul>
<h3 id="쿠버네티스-핵심-리소스">쿠버네티스 핵심 리소스</h3>
<p>쿠버네티스에서 다루는 모든 것을 &#39;리소스&#39;라고 부릅니다. 이 리소스들은 주로 <code>yaml</code> 매니페스트 파일로 정의됩니다.</p>
<table>
<thead>
<tr>
<th align="center">리소스</th>
<th align="left">용도</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>노드 (Node)</strong></td>
<td align="left">컨테이너가 배치되는 <strong>물리/가상 서버</strong> (워커 머신)</td>
</tr>
<tr>
<td align="center"><strong>네임스페이스 (Namespace)</strong></td>
<td align="left">쿠버네티스 클러스터 안의 <strong>가상 클러스터</strong> (논리적 분리)</td>
</tr>
<tr>
<td align="center"><strong>파드 (Pod)</strong></td>
<td align="left">K8s의 <strong>가장 작은 배포 단위</strong>. 하나 이상의 컨테이너 집합.</td>
</tr>
<tr>
<td align="center"><strong>레플리카세트 (ReplicaSet)</strong></td>
<td align="left">파드 복제본(Replica)의 개수를 항상 일정하게 유지.</td>
</tr>
<tr>
<td align="center"><strong>디플로이먼트 (Deployment)</strong></td>
<td align="left"><strong>(가장 중요)</strong> 배포의 기본 단위. 레플리카세트의 리비전(버전)을 관리하며 롤아웃/롤백 수행.</td>
</tr>
<tr>
<td align="center"><strong>서비스 (Service)</strong></td>
<td align="left">여러 파드에 대한 <strong>고정된 진입점(IP/DNS)</strong>을 제공. 로드 밸런싱 담당.</td>
</tr>
<tr>
<td align="center"><strong>인그레스 (Ingress)</strong></td>
<td align="left"><strong>L7 (HTTP/S)</strong>. 클러스터 외부 요청을 내부 서비스로 연결 (경로/도메인 기반 라우팅)</td>
</tr>
<tr>
<td align="center"><strong>Gateway API</strong></td>
<td align="left">인그레스의 차세대 표준. 더 강력하고 유연한 L7 라우팅 제공.</td>
</tr>
<tr>
<td align="center"><strong>컨피그맵 (ConfigMap)</strong></td>
<td align="left">설정 정보를 Key-Value 형태로 저장하여 파드에 주입.</td>
</tr>
<tr>
<td align="center"><strong>시크릿 (Secret)</strong></td>
<td align="left">API 키, DB 패워드 등 기밀 정보를 Base64 인코딩하여 저장.</td>
</tr>
<tr>
<td align="center">퍼시스턴트볼륨 (PV)</td>
<td align="left">파드가 사용할 영구 저장소(스토리지) 자체를 정의 (관리자)</td>
</tr>
<tr>
<td align="center">퍼시스턴트볼륨클레임 (PVC)</td>
<td align="left">스토리지 사용 요청 (개발자)</td>
</tr>
<tr>
<td align="center"><strong>스테이트풀세트 (StatefulSet)</strong></td>
<td align="left">상태(state)를 가지는 파드(e.g., DB)를 위한 리소스. (고유 ID, 순차적 배포)</td>
</tr>
<tr>
<td align="center"><strong>잡 (Job)</strong></td>
<td align="left">일회성 작업을 실행하고 <strong>정상적인 종료</strong>를 보장.</td>
</tr>
<tr>
<td align="center"><strong>크론잡 (CronJob)</strong></td>
<td align="left">리눅스 Cron처럼 스케줄링(예: 매일 밤 12시)되는 잡.</td>
</tr>
<tr>
<td align="center"><strong>PodSecurityAdmission (PSA)</strong></td>
<td align="left">K8s의 내장 보안 표준. 파드가 특정 보안 수준(e.g., privileged 금지)을 준수하도록 강제.</td>
</tr>
</tbody></table>
<hr>
<h3 id="파드-pod">파드 (Pod)</h3>
<p>파드는 쿠버네티스에서 생성하고 관리할 수 있는 <strong>가장 작은 배포 단위</strong>이며, 하나 이상의 컨테이너로 이루어집니다.</p>
<ul>
<li><strong>특징</strong>:<ul>
<li>같은 파드 안의 컨테이너는 <strong>같은 노드</strong>에 배치됩니다.</li>
<li><strong>네트워크와 스토리지(볼륨)를 공유</strong>합니다. (예: <code>localhost</code>로 통신 가능)</li>
</ul>
</li>
<li><strong>활용 예</strong>:<ul>
<li>(Sidecar 패턴) 메인 앱 컨테이너 + 로그 수집 컨테이너</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>(참고)</strong>: 실무에서는 <code>Pod</code>를 직접 생성(Naked Pod)하는 경우는 드물며, <code>Deployment</code>나 <code>StatefulSet</code>을 통해 관리하는 것이 표준입니다. <code>Pod</code> 매니페스트는 <code>Deployment</code>의 <code>spec.template</code> 부분에서 핵심적인 역할을 합니다.</p>
</blockquote>
<p>▼ <code>Pod</code> 매니페스트 예시 (<code>simple-pod.yaml</code>)</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: simple-nginx-pod
  labels:
    app: webserver
spec:
  containers:
    - name: nginx-container
      image: nginx:latest
      ports:
        - containerPort: 80</code></pre>
<hr>
<h3 id="배포-리소스-deployment-replicaset">배포 리소스 (Deployment, ReplicaSet)</h3>
<p><strong>레플리카세트 (ReplicaSet)</strong></p>
<blockquote>
<p>파드(kind: Pod)는 죽으면 그걸로 끝입니다. 고가용성을 위해 &#39;레플리카세트&#39;를 사용하면, 지정된 수만큼 파드의 개수를 항상 유지(self-healing)합니다.</p>
</blockquote>
<p><strong>디플로이먼트 (Deployment)</strong></p>
<blockquote>
<p><strong>(핵심)</strong> 실제 운영에서는 레플리카세트를 직접 다루기보다, 그 상위 리소스인 &#39;디플로이먼트&#39;를 사용합니다. 디플로이먼트는 <strong>애플리케이션 배포의 표준 단위</strong>입니다.</p>
<p>디플로이먼트는 내부적으로 레플리카세트를 관리하며, 앱을 <strong>업데이트(롤링 업데이트)</strong>하거나 <strong>롤백</strong>하는 등 리비전(버전) 관리를 담당합니다.</p>
</blockquote>
<p>▼ <code>Deployment</code> 매니페스트 예시 (<code>deployment.yaml</code>)</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-deployment
  labels:
    app: my-web-app
spec:
  replicas: 3 # 3개의 파드를 실행
  selector:
    matchLabels:
      app: my-web-app # 이 label을 가진 Pod를 찾아서 관리
  template: # &lt;-- Pod 템플릿 (여기서 Pod를 정의)
    metadata:
      labels:
        app: my-web-app # Service가 이 label을 보고 파드를 찾음
    spec:
      containers:
        - name: my-app-container
          image: nginx:latest # 실제 사용할 컨테이너 이미지
          ports:
            - containerPort: 80</code></pre>
<hr>
<h3 id="🌐-네트워크-리소스-service-ingress-gateway">🌐 네트워크 리소스 (Service, Ingress, Gateway)</h3>
<p><strong>서비스 (Service)</strong></p>
<blockquote>
<p>&#39;서비스&#39;는 여러 파드 그룹에 접근할 수 있는 <strong>고유한 단일 진입점(Entrypoint)</strong>을 제공하는 리소스입니다. (내부 DNS 이름 및 고정 IP 제공)</p>
</blockquote>
<p><strong>1) ClusterIP (기본값)</strong></p>
<ul>
<li>쿠버네티스 클러스터 <strong>내부</strong> IP 주소에만 서비스를 공개합니다.</li>
<li>클러스터 내의 다른 파드들이 서비스 이름(DNS)으로 접근할 때 사용됩니다.</li>
</ul>
<p>▼ <code>Service</code> (ClusterIP) 매니페스트 예시 (<code>service-clusterip.yaml</code>)</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: my-app-clusterip-svc
spec:
  type: ClusterIP # 클러스터 내부 IP만 할당 (기본값)
  selector:
    # &#39;app: my-web-app&#39; label을 가진 파드에 트래픽 전달
    # (위 Deployment 예시의 template.metadata.labels와 일치)
    app: my-web-app 
  ports:
    - protocol: TCP
      port: 80       # 서비스가 80번 포트로 노출됨
      targetPort: 80 # 파드의 80번 포트로 트래픽 전달</code></pre>
<p><strong>2) NodePort</strong></p>
<ul>
<li>ClusterIP의 기능을 포함하며, 추가로 모든 <strong>노드(Node)</strong>의 특정 포트를 개방합니다.</li>
<li>외부에서 <code>[노드 IP]:[NodePort]</code>로 접근할 수 있게 됩니다. (주로 테스트용)</li>
</ul>
<p><strong>3) LoadBalancer</strong></p>
<ul>
<li>ClusterIP, NodePort의 기능을 포함합니다.</li>
<li>클라우드 플랫폼(GCP, AWS 등)에서 제공하는 <strong>외부 로드 밸런서</strong>와 연동됩니다.</li>
</ul>
<p>▼ <code>Service</code> (LoadBalancer) 매니페스트 예시 (<code>service-loadbalancer.yaml</code>)</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: my-app-loadbalancer-svc
spec:
  type: LoadBalancer # 클라우드 로드밸런서와 연동 (외부 노출용)
  selector:
    app: my-web-app # &#39;app: my-web-app&#39; label을 가진 파드에 트래픽 전달
  ports:
    - protocol: TCP
      port: 80       # 로드밸런서의 80번 포트
      targetPort: 80 # 파드의 80번 포트로 트래픽 전달</code></pre>
<p><strong>인그레스 (Ingress) &amp; 게이트웨이 API (Gateway API)</strong></p>
<blockquote>
<p><code>Service</code>가 L4에서 동작하는 반면, <code>Ingress</code>는 L7(HTTP/S) 레벨에서 <strong>경로/도메인 기반 라우팅</strong>을 제공합니다. 최근에는 <code>Ingress</code>의 한계를 극복한 차세대 표준인 <strong><code>Gateway API</code></strong>의 사용이 증가하고 있습니다.</p>
</blockquote>
<p>▼ <code>Ingress</code> 매니페스트 예시 (<code>ingress.yaml</code>)</p>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    # (예시) Nginx Ingress Controller를 사용할 경우
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - host: &quot;myapp.example.com&quot; # 이 도메인으로 오는 요청을 처리
      http:
        paths:
          - path: / # &#39;/&#39; 경로로 오는 모든 요청
            pathType: Prefix
            backend:
              service:
                # 위에서 만든 ClusterIP 서비스로 라우팅
                name: my-app-clusterip-svc 
                port:
                  number: 80</code></pre>
<hr>
<h3 id="⚙️-기타-주요-리소스">⚙️ 기타 주요 리소스</h3>
<p><strong>컨피그맵 (ConfigMap)</strong></p>
<blockquote>
<p><code>ConfigMap</code>은 설정 정보를 Key-Value 형태로 저장하여 파드에 환경변수나 파일로 주입합니다.</p>
</blockquote>
<p>▼ <code>ConfigMap</code> 매니페스트 예시 (<code>configmap.yaml</code>)</p>
<pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
  name: my-app-config
data:
  # Key-Value 형태의 설정 데이터
  APP_COLOR: &quot;blue&quot;
  APP_MODE: &quot;production&quot;
  GREETING: &quot;Hello from ConfigMap!&quot;</code></pre>
<p><strong>시크릿 (Secret) &amp; 모범 사례</strong></p>
<blockquote>
<p><code>Secret</code> 리소스는 TLS 인증서, API 키, 패스워드 등 기밀 정보를 다룹니다. 데이터는 <strong>Base64로 인코딩</strong>되어 저장됩니다.</p>
<p><strong>(주의)</strong>: Base64는 암호화가 아닙니다. 누구나 디코딩할 수 있습니다.
<strong>(모범 사례)</strong>: 프로덕션 환경에서는 K8s <code>Secret</code>에 기밀 정보를 직접 저장하기보다, <strong><code>Vault</code></strong>나 <strong><code>AWS/GCP Secrets Manager</code></strong> 같은 외부 시크릿 관리 도구와 연동하여 파드가 실행될 때 동적으로 시크릿을 주입받는 방식을 사용합니다.</p>
</blockquote>
<p>▼ <code>Secret</code> 매니페스트 예시 (<code>secret.yaml</code>)</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Secret
metadata:
  name: my-app-secret
type: Opaque # 가장 일반적인 Secret 타입
data:
  # 값들은 반드시 Base64로 인코딩되어야 함
  # &quot;admin&quot; -&gt; base64 -&gt; &quot;YWRtaW4=&quot;
  # &quot;my-db-password123&quot; -&gt; base64 -&gt; &quot;bXktZGItcGFzc3dvcmQxMjM=&quot;
  DATABASE_USER: &quot;YWRtaW4=&quot;
  DATABASE_PASSWORD: &quot;bXktZGItcGFzc3dvcmQxMjM=&quot;</code></pre>
<p><strong>잡 (Job) &amp; 크론잡 (CronJob)</strong></p>
<ul>
<li><strong>Job</strong>: 일회성 작업을 위한 리소스. 파드가 &#39;정상 종료(Exit Code 0)&#39;될 때까지 관리합니다.</li>
<li><strong>CronJob</strong>: 잡(Job)을 스케줄(예: <code>0 5 * * *</code>)에 따라 정기적으로 실행합니다.</li>
</ul>
<p>▼ <code>Job</code> 매니페스트 예시 (<code>job.yaml</code>)</p>
<pre><code class="language-yaml">apiVersion: batch/v1
kind: Job
metadata:
  name: my-onetime-job
spec:
  template: # Job이 실행할 Pod 템플릿
    spec:
      containers:
        - name: pi-calculator
          image: perl:latest
          # 2000자리 파이(pi) 계산 후 종료하는 일회성 작업
          command: [&quot;perl&quot;,  &quot;-Mbignum=bpi&quot;, &quot;-wle&quot;, &quot;print bpi(2000)&quot;]
      restartPolicy: Never # Job은 재시작(Restart)하지 않고 실패(Failure) 시 재시도(Backoff)
  backoffLimit: 4 # 4번까지 재시도</code></pre>
<p>▼ <code>CronJob</code> 매니페스트 예시 (<code>cronjob.yaml</code>)</p>
<pre><code class="language-yaml">apiVersion: batch/v1
kind: CronJob
metadata:
  name: my-nightly-backup
spec:
  schedule: &quot;0 2 * * *&quot; # 매일 새벽 2시 0분에 실행 (Cron 표현식)
  jobTemplate: # CronJob이 생성할 Job 템플릿
    spec:
      template:
        spec:
          containers:
            - name: backup-script
              image: busybox:latest
              command: [&quot;/bin/sh&quot;, &quot;-c&quot;, &quot;echo &#39;Running nightly backup...&#39;&quot;]
          restartPolicy: OnFailure # 실패 시에만 재시작</code></pre>
<hr>
<h3 id="🚢-헬름-helm">🚢 헬름 (Helm)</h3>
<blockquote>
<p>헬름은 <strong>쿠버네티스의 패키지 관리자</strong>입니다.</p>
<p>하나의 앱을 배포하려면 <code>Deployment</code>, <code>Service</code>, <code>ConfigMap</code>, <code>Ingress</code> 등 수많은 리소스가 필요합니다. 헬름은 이 리소스들의 묶음을 <strong>&#39;차트(Chart)&#39;</strong>라는 패키지로 만들고, 환경별로 달라지는 설정값(DB 주소, 도메인 등)만 변수 처리하여 쉽게 배포/관리/업그레이드할 수 있게 도와줍니다.</p>
</blockquote>
<hr>
<h3 id="🔬-부하-테스트-locust">🔬 부하 테스트 (Locust)</h3>
<blockquote>
<p>도커는 부하 테스트에도 유용하게 사용됩니다. <strong>Locust</strong>는 파이썬으로 구현된 부하 테스트 도구로, 테스트 시나리오를 코드로 작성할 수 있습니다. 도커와 쿠버네티스를 이용해 Locust 컨테이너를 수십~수백 개로 스케일 아웃하여 대규모 분산 부하 테스트를 수행할 수 있습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java의 GC(Garbage Collection)]]></title>
            <link>https://velog.io/@david1-p/Java%EC%9D%98-GCGarbage-Collection</link>
            <guid>https://velog.io/@david1-p/Java%EC%9D%98-GCGarbage-Collection</guid>
            <pubDate>Fri, 31 Oct 2025 00:34:28 GMT</pubDate>
            <description><![CDATA[<h1 id="jvm-gc는-어떻게-동작하는가-대상-판단부터-수거-방식까지">[JVM] GC는 어떻게 동작하는가: 대상 판단부터 수거 방식까지</h1>
<p>GC(Garbage Collection)는 JVM의 핵심 기능 중 하나로, 힙(Heap) 영역에 동적으로 할당된 메모리 중 더 이상 필요 없는 객체(Garbage)를 찾아 제거하는 자동 메모리 관리 기법입니다.</p>
<p>이 포스트에서는 JVM이 어떤 객체를 &#39;쓰레기&#39;로 판단하는지, 그리고 그렇게 판단된 객체를 어떤 방식으로 &#39;수거&#39;하는지에 대해 자세히 알아보겠습니다.</p>
<hr>
<h2 id="1-gc-대상은-어떻게-판단할까요">1. GC 대상은 어떻게 판단할까요?</h2>
<p>JVM은 수많은 객체 중에서 어떤 객체를 &#39;필요 없다&#39;고 판단하고 수거 대상으로 삼을까요?</p>
<h3 id="핵심-기준-도달-가능성-reachability">핵심 기준: 도달 가능성 (Reachability)</h3>
<p>JVM은 <strong>&#39;도달 가능성(Reachability)&#39;</strong>이라는 개념을 사용해 객체가 사용 중인지 아닌지를 판단합니다.</p>
<ul>
<li><strong>도달 가능한(Reachable) 상태:</strong> 객체에 유효한 참조(Reference)가 하나라도 존재하는 경우입니다. 이 객체는 &#39;살아있는&#39; 객체로 간주합니다.</li>
<li><strong>도달 불가능한(Unreachable) 상태:</strong> 객체에 도달할 수 있는 유효한 참조가 하나도 존재하지 않는 경우입니다. 이 객체는 &#39;죽은&#39; 객체로 간주하며, <strong>GC의 수거 대상</strong>이 됩니다.</li>
</ul>
<h3 id="도달-가능성의-시작점-gc-root-root-set">&#39;도달 가능성&#39;의 시작점: GC Root (Root Set)</h3>
<p>JVM은 특정 객체가 &#39;도달 가능한지&#39;를 판단하기 위해 <strong>&#39;GC Root(Root Set)&#39;</strong>에서부터 참조 사슬을 추적하기 시작합니다. 힙 내부의 객체들끼리만 서로 참조하고 있는 것(순환 참조)만으로는 &#39;도달 가능하다&#39;고 보지 않습니다.</p>
<p>GC Root는 이 참조 사슬의 &#39;시작점&#39;이 되는 특별한 참조들입니다.</p>
<p><strong>주요 GC Root의 유형:</strong></p>
<ul>
<li><strong>1. 스택(Stack) 영역의 변수:</strong> 현재 실행 중인 메서드의 지역 변수, 파라미터 등 JRE 스택에서 참조하는 객체.</li>
<li><strong>2. 메서드(Method) 영역:</strong> 클래스의 정적(static) 변수가 참조하는 객체.</li>
<li><strong>3. 네이티브(Native) 스택:</strong> JNI(Java Native Interface) 호출로 인해 생성된 객체에 대한 참조.</li>
</ul>
<p>GC는 이 <strong>Root Set에서부터 시작</strong>하여, 참조 관계를 따라가며 도달할 수 있는 모든 객체를 표시(Mark)합니다. 이 과정이 끝난 후, <strong>표시되지 않은 모든 객체(즉, Root Set에서 도달할 수 없는 객체)가 GC 대상</strong>으로 식별됩니다.</p>
<h3 id="개발자가-gc에-관여할-수-있을까요">개발자가 GC에 관여할 수 있을까요?</h3>
<p>원칙적으로 GC는 자동으로 동작하지만, 개발자가 <code>java.lang.ref</code> 패키지의 특별한 참조 클래스를 사용하여 GC의 판단에 어느 정도 영향을 줄 수 있습니다.</p>
<ul>
<li><strong><code>SoftReference</code>:</strong> 감싸고 있는 원본 객체(Referent)에 대한 참조가 Root Set에서 끊겼을 때, <strong>힙 메모리가 부족한 경우에만</strong> GC 대상이 됩니다. (주로 캐시 구현에 사용)</li>
<li><strong><code>WeakReference</code>:</strong> 감싸고 있는 원본 객체에 대한 참조가 Root Set에서 끊겼을 때, 메모리 상태와 관계없이 <strong>다음 GC 사이클에서 바로</strong> GC 대상이 됩니다.</li>
</ul>
<hr>
<h2 id="2-gc는-실제로-어떻게-동작할까요">2. GC는 실제로 어떻게 동작할까요?</h2>
<p>GC 대상을 식별했다면, 이제 JVM은 이 객체들을 &#39;수거&#39;해야 합니다. 이 수거 작업은 여러 알고리즘을 기반으로 동작합니다.</p>
<h3 id="모든-것을-멈추는-시간-stop-the-world">모든 것을 멈추는 시간: Stop-The-World</h3>
<p>GC를 실행하기 위해 JVM은 애플리케이션 실행을 멈춥니다. 이를 <strong>&#39;Stop-The-World&#39;</strong>라고 합니다. GC가 실행되는 동안에는 GC 스레드를 제외한 모든 애플리케이션 스레드가 작업을 멈춥니다. Stop-The-World 시간이 길어질수록 애플리케이션의 응답 시간(Latency)이 길어지므로 이 시간을 최소화하는 것이 GC 튜닝의 핵심입니다.</p>
<h3 id="기본-알고리즘-mark-and-sweep">기본 알고리즘: Mark-and-Sweep</h3>
<p>가장 기본적인 GC 알고리즘입니다.</p>
<ol>
<li><strong>Mark (표시) 단계:</strong> 위에서 설명한 대로, GC Root에서 시작하여 &#39;도달 가능한&#39; 모든 객체를 식별하고 표시(Mark)합니다.</li>
<li><strong>Sweep (쓸기) 단계:</strong> 힙 전체를 스캔하면서 &#39;표시되지 않은&#39; 객체(즉, Unreachable 객체)를 찾아 메모리에서 제거합니다.</li>
</ol>
<ul>
<li><strong>단점: 메모리 파편화 (Fragmentation)</strong>
  Sweep 단계 후에 메모리 공간이 조각조각 나뉘는 &#39;파편화&#39;가 발생할 수 있습니다. 즉, 전체적인 여유 공간은 많아도, 큰 객체를 할당할 연속된 공간이 부족하여 <code>OutOfMemoryError</code>가 발생할 수 있습니다.</li>
</ul>
<h3 id="파편화-해결-mark-and-compact">파편화 해결: Mark-and-Compact</h3>
<p>Mark-and-Sweep의 파편화 문제를 해결하기 위해 고안된 방식입니다.</p>
<ol>
<li><strong>Mark (표시) 단계:</strong> Mark-and-Sweep과 동일하게 동작합니다.</li>
<li><strong>Compact (압축) 단계:</strong> Sweep 대신, &#39;살아남은&#39; 객체들을 힙의 한쪽 끝으로 차곡차곡 이동시켜(압축) 연속된 메모리 공간을 확보합니다. 그 후, 나머지 빈 공간을 &#39;Free&#39; 영역으로 만듭니다.</li>
</ol>
<ul>
<li><strong>장점:</strong> 파편화 문제가 해결됩니다.</li>
<li><strong>단점:</strong> 객체를 이동시키는 &#39;압축&#39; 작업은 Stop-The-World 시간을 증가시키는 비싼 작업입니다.</li>
</ul>
<h3 id="현대-jvm의-선택-세대별-gc-generational-gc">현대 JVM의 선택: 세대별 GC (Generational GC)</h3>
<p>현대의 JVM(HotSpot 등)은 대부분 <strong>&#39;세대별 GC&#39;</strong> 방식을 사용합니다. 이는 다음 두 가지 &#39;약한 세대 가설(Weak Generational Hypothesis)&#39;에 기반합니다.</p>
<ol>
<li><strong>&quot;대부분의 객체는 금방 죽는다(Unreachable 상태가 된다).&quot;</strong></li>
<li><strong>&quot;오래 살아남은 객체는 젊은 객체를 거의 참조하지 않는다.&quot;</strong></li>
</ol>
<p>이 가설에 따라, 힙 영역을 두 세대로 나눕니다.</p>
<ul>
<li><p><strong>Young Generation (영 세대):</strong></p>
<ul>
<li>새롭게 생성된 객체들이 위치하는 영역입니다.</li>
<li>이 영역은 다시 <strong>Eden</strong> 영역과 두 개의 <strong>Survivor</strong> 영역(S0, S1)으로 나뉩니다.</li>
<li>Young Generation에서 발생하는 GC를 <strong>&#39;Minor GC&#39;</strong>라고 부릅니다.</li>
<li><strong>동작 방식 (Minor GC):</strong><ol>
<li>새 객체는 <strong>Eden</strong>에 할당됩니다.</li>
<li>Eden이 꽉 차면 Minor GC가 발생합니다. (Stop-The-World 발생)</li>
<li>Eden과 S0(또는 S1)에 있는 &#39;살아남은&#39; 객체들을 S1(또는 S0)로 <strong>복사(Copy)</strong>합니다.</li>
<li>Eden과 S0(또는 S1) 영역을 비웁니다.</li>
<li>이 과정을 반복하며, 특정 횟수(Age) 이상 살아남은 객체는 <strong>Old Generation</strong>으로 &#39;승격(Promotion)&#39;됩니다.</li>
</ol>
</li>
<li>Minor GC는 대부분의 객체가 금방 죽는다는 가설에 따라 매우 빠르고 빈번하게 발생합니다.</li>
</ul>
</li>
<li><p><strong>Old Generation (올드 세대 / Tenured):</strong></p>
<ul>
<li>Young Generation에서 오랫동안 살아남아 승격된 객체들이 위치하는 영역입니다.</li>
<li>이 영역에서 발생하는 GC를 <strong>&#39;Major GC&#39;</strong> 또는 <strong>&#39;Full GC&#39;</strong>라고 부릅니다.</li>
<li>Old Generation은 객체들이 오랫동안 살아남는 영역이므로, GC가 덜 빈번하게 발생합니다.</li>
<li>대신 한 번 발생하면 <strong>Mark-and-Sweep</strong> 또는 <strong>Mark-and-Compact</strong> 알고리즘을 사용하여 전체 영역을 정리하므로, Stop-The-World 시간이 Minor GC보다 훨씬 깁니다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>세대별 GC는 금방 죽는 객체(Young)와 오래 사는 객체(Old)를 분리하여, GC의 효율을 극대화하고 Stop-The-World 시간을 최소화하는 전략입니다. 하지만 Old Generation이 꽉 찼을 때 발생하는 <strong>Full GC의 긴 Stop-The-World 시간</strong>은 대용량 힙(Heap) 환경에서 심각한 서비스 지연을 유발할 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="3-stw를-줄이기-위한-노력-최신-gc의-진화">3. STW를 줄이기 위한 노력: 최신 GC의 진화</h2>
<p>기존 세대별 GC의 가장 큰 숙제는 &#39;Full GC로 인한 긴 STW&#39;였습니다. 힙 크기가 수십 GB, 수백 GB로 커지면서, 한 번의 Full GC가 몇 초에서 심하면 몇 분까지 애플리케이션을 멈추게 만들었습니다.</p>
<p>이 문제를 해결하기 위해 <strong>&#39;예측 가능하고 짧은 STW&#39;</strong>를 목표로 하는 새로운 GC들이 등장했습니다.</p>
<h3 id="1-g1-garbage-first-gc">1) G1 (Garbage-First) GC</h3>
<p>Java 9부터 기본 GC로 채택된 G1은 세대별 GC의 구조를 유지하면서도, Full GC의 개념을 사실상 없앤 GC입니다.</p>
<ul>
<li><p>** 핵심 아이디어:** &quot;힙 전체를 한 번에 청소하지 말고, <strong>작은 영역(Region)으로 쪼개서</strong> 쓰레기가 가장 많은(Garbage-First) 영역부터 <strong>예측 가능하게</strong> 청소하자!&quot;</p>
</li>
<li><p><strong>특징:</strong></p>
<ol>
<li><strong>Region 기반 힙:</strong> G1은 힙을 1MB ~ 32MB 사이의 수많은 작은 <strong>Region</strong>으로 분할합니다. (기존의 Young/Old 영역처럼 물리적으로 연속되지 않습니다.)</li>
<li><strong>동적인 세대:</strong> 각 Region은 Eden, Survivor, Old의 역할을 동적으로 맡습니다.</li>
<li><strong>Pause Time Goal (일시 정지 목표):</strong> 사용자가 <code>-XX:MaxGCPauseMillis=200</code> 처럼 <strong>목표 STW 시간</strong>을 설정할 수 있습니다. G1은 이 목표 시간을 지키기 위해 &#39;청소할 Region의 수&#39;를 조절합니다.</li>
</ol>
</li>
</ul>
<hr>
<ul>
<li><p><strong>동작 방식 (Mixed GC):</strong></p>
<ol>
<li><strong>Young GC:</strong> 기존의 Minor GC와 유사하게 Eden, Survivor Region에서 살아남은 객체를 다른 Region으로 복사(Evacuation)합니다. (STW 발생)</li>
<li><strong>Concurrent Marking:</strong> Young GC가 발생하는 동안, G1은 애플리케이션과 <strong>동시에(Concurrent)</strong> 전체 힙을 스캔하여 &#39;살아있는 객체&#39;를 파악합니다. 이 정보를 통해 &#39;쓰레기만 가득 찬&#39; Old Region(Garbage-First의 대상)을 식별합니다.</li>
<li><strong>Mixed GC:</strong> G1의 핵심입니다. STW가 발생하면, <strong>모든 Young Region</strong> + <strong>가장 쓰레기가 많은 Old Region 몇 개</strong>를 함께 청소합니다.</li>
<li>살아남은 객체들을 새로운 빈 Region으로 <strong>복사(Copy)</strong>하여 자연스럽게 <strong>압축(Compaction)</strong>을 완료합니다.</li>
</ol>
</li>
<li><p><strong>결론:</strong> G1은 Full GC(Mark-Sweep-Compact)를 피하고, 짧은 STW의 &#39;Mixed GC&#39;를 여러 번 수행하여 Old Generation을 점진적으로 정리합니다. 덕분에 파편화 문제가 해결되고, STW 시간을 예측 가능한 수준(수십 ~ 수백 ms)으로 관리할 수 있게 되었습니다.</p>
</li>
</ul>
<h3 id="2-zgc-z-garbage-collector--shenandoah-gc">2) ZGC (Z Garbage Collector) &amp; Shenandoah GC</h3>
<p>G1이 STW를 &#39;짧게&#39; 만드는 데 집중했다면, ZGC와 Shenandoah는 STW를 <strong>&#39;거의 0&#39;</strong>에 가깝게 만드는 것을 목표로 합니다. 힙 크기가 수백 GB, 수 TB가 되어도 STW 시간을 <strong>1ms 미만</strong>으로 유지하는 것이 목표입니다.</p>
<ul>
<li><p>** 핵심 아이디어:** &quot;Mark(표시), Sweep(제거), 심지어 <strong>Compact(압축)까지</strong> 모든 작업을 애플리케이션과 <strong>동시에(Concurrent)</strong> 진행하자!&quot;</p>
</li>
<li><p><strong>특징:</strong></p>
<ul>
<li><strong>초저지연 (Ultra-Low-Latency):</strong> 힙 크기와 관계없이 일관되게 짧은 STW를 제공합니다.</li>
<li><strong>동시 압축 (Concurrent Compaction):</strong> GC가 객체의 주소를 &#39;A&#39;에서 &#39;B&#39;로 옮기는(압축) 순간에도, 애플리케이션 스레드는 멈추지 않고 계속 실행됩니다.</li>
</ul>
</li>
<li><p><strong>어떻게 이것이 가능할까? (Load Barriers)</strong>
  이 GC들의 핵심 기술은 <strong>&#39;로드 배리어(Load Barrier)&#39;</strong>입니다.</p>
<ol>
<li><p><strong>문제 상황:</strong> GC가 객체 <code>O</code>를 <code>A</code> 주소에서 <code>B</code> 주소로 이동시켰습니다.</p>
</li>
<li><p><strong>동시에:</strong> 애플리케이션 스레드가 <code>O</code>의 옛날 주소 <code>A</code>를 읽으려고 합니다.</p>
</li>
<li><p><strong>해결 (Load Barrier):</strong> JVM(JIT 컴파일러)이 객체 참조를 읽는 모든 코드에 &#39;배리어(방어막)&#39; 코드를 삽입합니다. 이 배리어는 객체를 읽을 때마다 &quot;이 객체가 혹시 이사(Relocation) 중인가?&quot;를 체크합니다.</p>
</li>
<li><p>만약 이사 중이거나 이사가 끝났다면, 애플리케이션 스레드에게 새 주소 <code>B</code>를 알려줍니다.</p>
<p>ZGC는 <strong>컬러드 포인터(Colored Pointers)</strong>, Shenandoah는 <strong>브룩스 포인터(Brooks Pointers)</strong>라는 각자의 방식으로 이 &#39;로드 배리어&#39;를 구현하여, GC와 애플리케이션이 동시에 힙에 접근할 수 있도록 합니다.</p>
</li>
</ol>
</li>
<li><p><strong>결론:</strong> ZGC와 Shenandoah는 STW가 거의 없는 대신, 애플리케이션의 모든 &#39;참조 읽기&#39; 작업에 약간의 오버헤드(배리어 체크)를 추가합니다. 따라서 실시간 응답이 매우 중요한 대용량(수백 GB 이상) 힙을 다루는 시스템에 가장 적합합니다.</p>
</li>
</ul>
<hr>
<h2 id="4-요약">4. 요약</h2>
<ol>
<li><strong>GC 대상 판단:</strong> <strong>GC Root</strong>에서 시작하여 <strong>&#39;도달 가능성(Reachability)&#39;</strong>을 따져, 도달할 수 없는(Unreachable) 객체를 수거 대상으로 판단합니다.</li>
<li><strong>기본 동작:</strong> <strong>Stop-The-World</strong>를 통해 애플리케이션을 멈춘 후, Mark-and-Sweep(파편화 단점)이나 Mark-and-Compact(압축 비용 단점) 같은 알고리즘으로 메모리를 정리합니다.</li>
<li><strong>세대별 GC:</strong> <strong>Young/Old</strong> 세대로 힙을 분리하여, <strong>Minor GC</strong>를 자주, <strong>Major/Full GC</strong>를 가끔 수행함으로써 효율을 높였습니다.</li>
<li><strong>최신 GC (G1):</strong> 힙을 <strong>Region</strong>으로 분할하고, <strong>Mixed GC</strong>를 통해 짧은 STW를 유지하며 점진적으로 힙 전체를 압축/정리합니다. (대부분의 환경에서 기본값)</li>
<li><strong>최신 GC (ZGC/Shenandoah):</strong> <strong>Load Barrier</strong> 기술을 사용하여 <strong>압축(Compaction)까지 동시(Concurrent)</strong>에 처리, STW를 1ms 미만으로 줄이는 것을 목표로 합니다. (초대용량 힙, 초저지연 시스템용)</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[네트워크 정리]]></title>
            <link>https://velog.io/@david1-p/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@david1-p/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 29 Oct 2025 07:00:32 GMT</pubDate>
            <description><![CDATA[<h1 id="네트워크-기본-모델-osi-7계층과-tcpip">네트워크 기본 모델 (OSI 7계층과 TCP/IP)</h1>
<h2 id="1-네트워크-참조-모델-osi-7계층과-tcpip">1. 네트워크 참조 모델: OSI 7계층과 TCP/IP</h2>
<p>네트워크 통신은 여러 단계의 복잡한 과정을 거치며, 이를 표준화하고 이해를 돕기 위해 계층형 모델을 사용합니다. 대표적인 모델로 <strong>OSI 7계층 참조 모델</strong>과 <strong>TCP/IP 모델</strong>이 있습니다.</p>
<h3 id="osi-7계층-참조-모델-osi-7-layer">OSI 7계층 참조 모델 (OSI 7 Layer)</h3>
<p>OSI(Open Systems Interconnection) 모델은 ISO(국제표준화기구)에서 개발한 <strong>개념적 참조 모델</strong>입니다. 네트워크 통신의 전 과정을 7개의 추상화된 계층으로 나누어 설명하며, 각 계층은 독립적인 기능을 수행합니다. 이는 통신 과정을 모듈화하여 문제 해결 및 표준 개발을 용이하게 합니다.</p>
<ul>
<li><p><strong>L7 (Application Layer - 응용 계층):</strong>
  사용자가 네트워크 서비스에 접근할 수 있도록 인터페이스를 제공합니다. (예: HTTP, FTP, SMTP, DNS)</p>
</li>
<li><p><strong>L6 (Presentation Layer - 표현 계층):</strong>
  데이터의 형식을 변환(Translation), 암호화(Encryption), 압축(Compression)을 수행하여 응용 계층이 데이터를 올바르게 해석할 수 있도록 보장합니다. (예: SSL/TLS, JPEG, ASCII)</p>
</li>
<li><p><strong>L5 (Session Layer - 세션 계층):</strong>
  두 응용 프로그램 간의 통신 세션을 생성, 관리 및 종료하는 역할을 합니다. 또한, 통신 중단 시 복구를 위한 동기화 지점(Checkpoint)을 설정합니다. (예: NetBIOS)</p>
</li>
<li><p><strong>L4 (Transport Layer - 전송 계층):</strong>
  송신자와 수신자 간의 <strong>End-to-End 통신</strong>을 담당합니다. 데이터 전송의 신뢰성을 보장(TCP)하거나, 신속성을 우선(UDP)합니다. 포트 번호를 사용하여 데이터를 정확한 응용 프로그램에 전달하며, 흐름 제어 및 오류 제어를 수행합니다. (예: TCP, UDP)</p>
</li>
<li><p><strong>L3 (Network Layer - 네트워크 계층):</strong>
  데이터 패킷을 송신지에서 목적지까지 전달하는 <strong>최적 경로를 결정(Routing)</strong>합니다. 논리적 주소인 IP 주소를 사용하여 장치를 식별하고, 서로 다른 네트워크 간의 통신을 중개합니다. (장비: 라우터)</p>
</li>
<li><p><strong>L2 (Data Link Layer - 데이터 링크 계층):</strong>
  <strong>동일 네트워크(Local Area Network) 내</strong>의 장치 간 데이터 전송을 담당합니다. 물리적 주소인 MAC 주소를 사용하여 프레임(Frame)을 전달하며, 물리 계층에서 발생할 수 있는 오류를 검출하고 수정합니다. (장비: 스위치)</p>
</li>
<li><p><strong>L1 (Physical Layer - 물리 계층):</strong>
  데이터를 0과 1의 비트(Bit) 신호로 변환하여 케이블, 무선 등 물리적 매체를 통해 실제로 전송합니다. (장비: 허브, 리피터, 케이블)</p>
</li>
</ul>
<h3 id="tcpip-모델-internet-protocol-suite">TCP/IP 모델 (Internet Protocol Suite)</h3>
<p>TCP/IP 모델은 현재 인터넷 표준으로 사용되는 <strong>실용적인 프로토콜 모음(Suite)</strong>입니다. OSI 모델보다 먼저 개발되었으며, 4개의 계층으로 구성됩니다.</p>
<ul>
<li><p><strong>4. Application Layer (응용 계층):</strong>
  OSI 5, 6, 7계층의 기능을 통합하여 담당합니다. (HTTP, FTP, DNS 등)</p>
</li>
<li><p><strong>3. Transport Layer (전송 계층):</strong>
  OSI 4계층과 동일한 역할을 수행합니다. (TCP, UDP)</p>
</li>
<li><p><strong>2. Internet Layer (인터넷 계층):</strong>
  OSI 3계층과 동일한 역할을 수행합니다. (IP, ICMP)</p>
</li>
<li><p><strong>1. Network Access Layer (네트워크 접근 계층):</strong>
  OSI 1, 2계층의 기능을 통합하여 담당합니다. (Ethernet, Wi-Fi)</p>
</li>
</ul>
<hr>
<h2 id="2-pdu-protocol-data-unit">2. PDU (Protocol Data Unit)</h2>
<p>PDU(프로토콜 데이터 단위)는 각 네트워크 계층에서 처리되는 데이터의 단위를 지칭합니다. 데이터는 상위 계층에서 하위 계층으로 내려가면서 각 계층의 헤더 정보가 추가되는 <strong>캡슐화(Encapsulation)</strong> 과정을 거칩니다.</p>
<ul>
<li><strong>L1 (물리 계층):</strong> Bit (비트)</li>
<li><strong>L2 (데이터 링크 계층):</strong> Frame (프레임)</li>
<li><strong>L3 (네트워크 계층):</strong> Packet (패킷)</li>
<li><strong>L4 (전송 계층):</strong> <strong>Segment</strong> (세그먼트 - TCP) / <strong>Datagram</strong> (데이터그램 - UDP)</li>
<li><strong>L7 (응용 계층):</strong> Data 또는 Message (데이터 또는 메시지)</li>
</ul>
<hr>
<h2 id="3-ip-주소-및-네트워크-계층">3. IP 주소 및 네트워크 계층</h2>
<h3 id="공인-ip-public-ip와-사설-ip-private-ip">공인 IP (Public IP)와 사설 IP (Private IP)</h3>
<ul>
<li><strong>공인 IP 주소:</strong> ISP(인터넷 서비스 제공자)가 할당하는 전 세계적으로 고유한 주소입니다. 외부 인터넷 망에서 기기를 식별하고 통신하기 위해 사용됩니다.</li>
<li><strong>사설 IP 주소:</strong> 내부 로컬 네트워크(LAN)에서만 사용되는 주소입니다. (예: 192.168.x.x). 이 주소는 인터넷에서 직접 라우팅될 수 없습니다.</li>
</ul>
<p>사설 IP를 사용하는 기기가 외부와 통신하기 위해서는 L3 장비인 <strong>라우터(Router)</strong>의 <strong>NAT(Network Address Translation)</strong> 기능을 통해, 사설 IP를 공인 IP로 변환하는 과정이 필요합니다.</p>
<h3 id="l3-장비-라우터-router">L3 장비: 라우터 (Router)</h3>
<p>라우터는 L3(네트워크 계층)에서 동작하며, IP 주소를 기반으로 <strong>서로 다른 네트워크 간</strong>의 데이터 패킷 전송 경로를 결정(Routing)합니다.</p>
<hr>
<h2 id="4-l4-전송-계층-프로토콜-및-장비">4. L4 (전송 계층) 프로토콜 및 장비</h2>
<h3 id="tcp와-udp의-차이">TCP와 UDP의 차이</h3>
<p>L4(전송 계층)는 데이터 전송 방식을 결정하며, TCP와 UDP가 대표적입니다.</p>
<ul>
<li><strong>TCP (Transmission Control Protocol):</strong>
  <strong>신뢰성</strong>을 보장하는 연결 지향형 프로토콜입니다. 3-way-handshake 과정을 통해 연결을 수립하고, 데이터의 순서 보장, 흐름 제어, 오류 제어를 수행합니다. (예: 웹 브라우징(HTTP), 이메일(SMTP), 파일 전송(FTP))</li>
<li><strong>UDP (User Datagram Protocol):</strong>
  <strong>신속성</strong>에 중점을 둔 비연결형 프로토콜입니다. 신뢰성 보장을 위한 부가 기능이 없어 오버헤드가 적고 전송 속도가 빠릅니다. (예: 실시간 스트리밍(유튜브 송출), 온라인 게임, DNS)</li>
</ul>
<h3 id="l4-스위치와-리버스-프록시-reverse-proxy">L4 스위치와 리버스 프록시 (Reverse Proxy)</h3>
<p><strong>L4 스위치</strong>는 로드 밸런서의 일종으로, L4(전송 계층)의 정보(IP 주소, 포트 번호)를 기반으로 트래픽을 여러 대의 서버로 분산시킵니다.</p>
<p>이는 L7(응용 계층)에서 동작하는 <strong>리버스 프록시</strong>와 유사한 역할을 수행하지만, 리버스 프록시는 더 상위 계층의 정보(HTTP 헤더, URL 등)를 분석하여 더 지능적인 분배(Content-Based Routing)가 가능합니다. 또한 SSL 종료(Termination), 캐싱 등의 추가 기능을 수행합니다.</p>
<hr>
<h2 id="5-l7-응용-계층-장비-및-네트워크-보안">5. L7 (응용 계층) 장비 및 네트워크 보안</h2>
<h3 id="l7-스위치-adc">L7 스위치 (ADC)</h3>
<p>L7 스위치(ADC, Application Delivery Controller)는 L7 정보를 기반으로 트래픽을 분산하는 고성능 로드 밸런서입니다.</p>
<h3 id="웹-방화벽-waf-web-application-firewall">웹 방화벽 (WAF, Web Application Firewall)</h3>
<p>웹 방화벽은 L7에서 동작하는 <strong>보안 장비</strong>입니다. 일반적인 방화벽(L3/L4)과 달리, 웹 애플리케이션에 특화된 공격(예: <strong>SQL Injection</strong>, XSS)의 패턴을 탐지하고 차단합니다.</p>
<h3 id="dmz-demilitarized-zone">DMZ (Demilitarized Zone)</h3>
<p>DMZ(비무장지대)는 내부 네트워크(LAN)와 외부 인터넷(WAN) 사이에 위치하는 <strong>격리된 네트워크 망</strong>입니다. 웹 서버와 같이 외부에 공개해야 하는 서비스를 DMZ에 배치하여, 해당 서버가 침해당하더라도 내부의 중요 자산(DB 서버 등)을 보호하는 보안 구조입니다.</p>
<hr>
<h2 id="6-스토리지-기술---raid">6. 스토리지 기술 - RAID</h2>
<p>RAID(Redundant Array of Independent Disks)는 여러 개의 물리적 디스크를 하나의 논리적 단위로 묶어 사용하는 스토리지 기술입니다. 목적에 따라 <strong>데이터 분산(Striping)</strong>을 통한 성능 향상, 또는 <strong>데이터 이중화(Redundancy)</strong>를 통한 안정성 향상을 구현합니다.</p>
<ul>
<li><strong>RAID 0 (Striping):</strong> 데이터를 여러 디스크에 분산 저장하여 입출력 속도를 향상시킵니다. (단, 디스크 1개 장애 시 전체 데이터 유실)</li>
<li><strong>RAID 1 (Mirroring):</strong> 데이터를 2개 이상의 디스크에 동일하게 복제하여 저장합니다. (안정성은 높으나 가용 용량은 절반)</li>
<li><strong>RAID 5 (Striping with Parity):</strong> 스트라이핑과 패리티(오류 검증 정보)를 함께 사용해 성능과 안정성을 절충합니다.</li>
</ul>
<hr>
<h2 id="7-nosql-데이터베이스-유형">7. NoSQL 데이터베이스 유형</h2>
<p>NoSQL은 유연한 스키마와 수평적 확장이 용이한 비관계형 데이터베이스 시스템을 통칭합니다.</p>
<ul>
<li><p><strong>Key-Value Type:</strong>
  데이터를 <strong>고유한 키(Key)와 값(Value)의 쌍</strong>으로 저장하는 단순한 구조입니다. 특정 키에 해당하는 값을 빠르게 조회하는 데 특화되어 있습니다. (예: Redis)</p>
</li>
<li><p><strong>Document Type (문서형):</strong>
  데이터를 <strong>JSON, BSON 또는 XML과 같은 문서 형식</strong>으로 저장합니다. 각 문서는 고유한 키를 가지며, 계층적인 데이터 구조를 문서 내에 저장할 수 있어 스키마 유연성이 높습니다. (예: MongoDB)</p>
</li>
<li><p><strong>Column-Family Type (컬럼 패밀리형):</strong>
  데이터를 행(Row) 단위가 아닌, <strong>컬럼 패밀리(Column Family)라는 열의 그룹</strong> 단위로 저장합니다. 대규모 데이터의 분산 저장 및 빠른 쓰기/조회에 유리합니다. (예: Apache Cassandra, Apache HBase)</p>
</li>
<li><p><strong>Graph Type (그래프형):</strong>
  데이터를 <strong>노드(Node), 엣지(Edge), 속성(Property)</strong>을 사용해 그래프 구조로 표현합니다. 데이터 간의 복잡한 관계를 효율적으로 저장하고 질의하는 데 특화되어 있습니다. (예: Neo4j, ArangoDB)</p>
</li>
</ul>
<hr>
<h2 id="8-차세대-인프라-hci와-sddc">8. 차세대 인프라: HCI와 SDDC</h2>
<h3 id="3-tier-아키텍처와-hci">3-Tier 아키텍처와 HCI</h3>
<p>전통적인 <strong>3-Tier 아키텍처</strong>는 컴퓨팅(서버), 네트워크, 스토리지(SAN)가 물리적으로 분리된 구조입니다. 이 구조는 서버가 스토리지의 데이터를 읽고 쓸 때 반드시 네트워크를 통해 스토리지 컨트롤러를 거쳐야 하므로, 이 구간에서 <strong>병목 현상(Bottleneck)</strong>이 발생할 수 있습니다.</p>
<p><strong>HCI (Hyper-Converged Infrastructure)</strong>는 이러한 문제를 해결하기 위해, 개별 장비(노드)에 <strong>컴퓨팅(서버 가상화)</strong>과 <strong>스토리지(SDS)</strong> 자원을 하나의 장비에 통합한 2-Tier 아키텍처입니다.</p>
<ul>
<li><strong>SDS (Software-Defined Storage):</strong> 스토리지 기능을 하드웨어가 아닌 소프트웨어로 구현하여, 여러 노드의 로컬 디스크를 묶어 하나의 가상 스토리지 풀로 사용합니다.</li>
<li><strong>확장성:</strong> HCI는 <strong>스케일 아웃(Scale-Out)</strong> 방식(노드 추가)으로 성능과 용량을 선형적으로 확장할 수 있습니다.</li>
</ul>
<h3 id="sddc-software-defined-data-center">SDDC (Software-Defined Data Center)</h3>
<p>SDDC(소프트웨어 정의 데이터 센터)는 HCI를 포함하는 더 상위의 개념으로, 데이터 센터의 모든 인프라 구성 요소를 가상화하고 <strong>소프트웨어로 제어 및 자동화</strong>하는 것을 의미합니다.</p>
<ul>
<li><strong>SDC (Software-Defined Computing):</strong> 서버 가상화 (예: VMware vSphere)</li>
<li><strong>SDS (Software-Defined Storage):</strong> 스토리지 가상화 (예: VMware vSAN)</li>
<li><strong>SDN (Software-Defined Networking):</strong> 네트워크 가상화 (예: VMware NSX)</li>
</ul>
<p>SDDC의 핵심은 하드웨어 종속성에서 벗어나 모든 자원을 API로 관리하고, 자동화된 프로비저닝 및 운영을 통해 민첩성과 효율성을 극대화하는 것입니다.</p>
<hr>
<h2 id="9-클라우드-컴퓨팅-서비스-모델">9. 클라우드 컴퓨팅 서비스 모델</h2>
<p>CSP(Cloud Service Provider)가 제공하는 클라우드 서비스는 관리 범위에 따라 3가지로 분류됩니다.</p>
<ul>
<li><p><strong>IaaS (Infrastructure as a Service):</strong>
  CSP가 인프라(서버, 스토리지, 네트워크) 자원만 가상화하여 제공합니다. 사용자는 OS, 미들웨어, 애플리케이션 등 모든 소프트웨어를 직접 설치하고 관리합니다. (예: AWS EC2, GCP Compute Engine)</p>
</li>
<li><p><strong>PaaS (Platform as a Service):</strong>
  CSP가 인프라뿐만 아니라 OS, 런타임(Java, Python 등)까지 포함된 플랫폼을 제공합니다. 사용자는 자신의 애플리케이션 코드와 데이터만 관리합니다. (예: Heroku, AWS Elastic Beanstalk)</p>
</li>
<li><p><strong>SaaS (Software as a Service):</strong>
  CSP가 완성된 소프트웨어 애플리케이션 자체를 서비스로 제공합니다. 사용자는 별도 설치나 관리 없이 즉시 해당 소프트웨어를 구독하여 사용합니다. (예: Google Workspace, Salesforce)</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[서버란 무엇일까? ]]></title>
            <link>https://velog.io/@david1-p/IT-%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@david1-p/IT-%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 29 Oct 2025 02:45:15 GMT</pubDate>
            <description><![CDATA[<h2 id="서버">서버</h2>
<h2 id="1-서버와-클라이언트">1. 서버와 클라이언트</h2>
<ul>
<li><strong>서버 (Server)</strong> : 클라이언트에게 네트워크를 통해 정보나 서비스를 제공하는 컴퓨터 또는 프로그램입니다.</li>
<li><strong>클라이언트 (Client)</strong> : 서버에게 정보나 서비스를 요청하는 컴퓨터 또는 프로그램입니다.</li>
</ul>
<hr>
<h2 id="2-웹-애플리케이션-운영-서버의-종류">2. 웹 애플리케이션 운영 서버의 종류</h2>
<p><img src="https://media.licdn.com/dms/image/v2/D4E12AQHKf_bBEQ4RuQ/article-cover_image-shrink_720_1280/article-cover_image-shrink_720_1280/0/1700559586204?e=1763596800&v=beta&t=DLp_r2cYlfFWaOzba1i_lhL5IYYrOkka8Liovjt_hpM" alt="웹 서버"></p>
<h3 id="1-웹-서버-web-server">1) 웹 서버 (Web Server)</h3>
<ul>
<li><strong>역할 :</strong> <strong>정적 콘텐츠</strong>(예: HTML, CSS, 이미지)를 보관하고 있다가, 클라이언트가 요청하면 그 콘텐츠를 전달합니다.</li>
<li><strong>과정 :</strong><blockquote>
<p>클라이언트가 보고 싶은 콘텐츠를 웹 서버에 <strong>요청</strong>
  &rarr; 웹 서버는 자신이 가진 <strong>정적 콘텐츠 중</strong>에서 클라이언트가 요청한 것이 있는지 검색
  &rarr; 해당 콘텐츠를 발견하면 클라이언트에게 <strong>전달</strong>
  &rarr; 클라이언트는 자신이 요청했던 콘텐츠를 웹 서버로부터 받아 봄
  &rarr; 웹 서버는 클라이언트가 요청했던 내용을 저장 <strong>(logging)</strong></p>
</blockquote>
</li>
</ul>
<hr>
<h3 id="2-애플리케이션-서버-application-server-was">2) 애플리케이션 서버 (Application Server, WAS)</h3>
<ul>
<li><strong>역할 :</strong> <strong>동적 콘텐츠</strong>(비즈니스 로직 처리, DB 조회 등이 필요한 콘텐츠)를 생성하여 클라이언트에게 전달하는 서버입니다. (보통 <strong>WAS (Web Application Server)</strong>라고 부릅니다.)</li>
<li><strong>과정 :</strong><blockquote>
<p>클라이언트가 웹 서버에 <strong>동적 콘텐츠</strong>를 요청
  &rarr; 웹 서버는 요청을 <strong>애플리케이션 서버(WAS)에 전달</strong>
  &rarr; 애플리케이션 서버는 <strong>비즈니스 로직을 실행</strong>하여 동적 콘텐츠를 <strong>생성</strong>
  &rarr; (필요시 다음 단계의 DB 서버에 데이터를 요청)
  &rarr; 생성된 동적 콘텐츠를 웹 서버로 <strong>전달</strong>
  &rarr; 애플리케이션 서버는 처리 내용을 저장 <strong>(logging)</strong>
  &rarr; 웹 서버는 애플리케이션 서버에게 받은 동적 콘텐츠를 클라이언트에게 <strong>전달</strong>
  &rarr; 웹 서버도 클라이언트가 요청했던 내용을 저장 <strong>(logging)</strong>
  &rarr; 클라이언트는 처음 웹 서버에 요청했던 동적 콘텐츠를 받아 봄</p>
</blockquote>
</li>
</ul>
<hr>
<h3 id="3-db-서버-database-server">3) DB 서버 (Database Server)</h3>
<ul>
<li><strong>역할 :</strong> 애플리케이션의 데이터를 <strong>영구적으로 저장하고 관리</strong>하며, 애플리케이션 서버의 요청에 따라 데이터를 조회, 수정, 삭제(CRUD)합니다.</li>
<li><strong>과정 :</strong><blockquote>
<p>클라이언트가 웹 서버에 <strong>동적 콘텐츠</strong>를 요청
  &rarr; 웹 서버는 이 요청을 <strong>애플리케이션 서버에 전달</strong>
  &rarr; 애플리케이션 서버는 <strong>비즈니스 로직을 실행</strong>하던 중 데이터가 필요함을 인지
  &rarr; 애플리케이션 서버는 DB 서버에 데이터 <strong>쿼리(Query)를 요청</strong>
  &rarr; DB 서버는 요청받은 쿼리를 실행하여 <strong>결과(데이터)를 반환</strong> (처리 내용 logging)
  &rarr; 애플리케이션 서버는 <strong>DB에서 받은 데이터를 가공</strong>하여 동적 콘텐츠(예: HTML, JSON)를 <strong>생성</strong>
  &rarr; 생성된 콘텐츠를 웹 서버에 <strong>전달</strong> (처리 내용 logging)
  &rarr; 웹 서버는 전달받은 콘텐츠를 클라이언트에게 <strong>제공</strong> (처리 내용 logging)</p>
</blockquote>
</li>
</ul>
<hr>
<h3 id="4-리버스-프록시-서버-reverse-proxy-server">4) 리버스 프록시 서버 (Reverse Proxy Server)</h3>
<ul>
<li><strong>역할 :</strong> 클라이언트의 요청을 <strong>대신 받아</strong> 내부 서버(웹 서버 또는 애플리케이션 서버)로 <strong>전달하는</strong> 서버입니다. 클라이언트는 리버스 프록시를 실제 서버라고 인식하며, 이를 통해 <strong>로드 밸런싱(부하 분산)</strong>, SSL 암호화, 캐싱 등의 기능을 수행합니다.</li>
<li><strong>과정 (로드 밸런싱 예시) :</strong><blockquote>
<p>(애플리케이션 서버 B, C가 리버스 프록시 뒤에 있다고 가정)</p>
<p>클라이언트가 서버에 콘텐츠를 <strong>요청</strong> (이 요청은 <strong>리버스 프록시 서버</strong>가 받음)
  &rarr; 리버스 프록시 서버는 자신이 관리하는 <strong>여러 대의 내부 서버(B, C) 상태를 확인</strong>
  &rarr; (예: B가 바쁘면) <strong>가장 한가한 C 서버</strong>에게 클라이언트의 요청을 <strong>전달(forwarding)</strong>
  &rarr; C 서버는 요청을 처리 (필요시 DB 조회)한 후 <strong>응답을 리버스 프록시 서버에게</strong> 반환
  &rarr; 리버스 프록시 서버가 이 응답을 <strong>최종적으로 클라이언트에게</strong> 전달
  &rarr; 클라이언트는 요청한 콘텐츠를 받아볼 수 있음</p>
</blockquote>
</li>
<li><strong>예시 :</strong> 수강 신청이나 티켓 예매 사이트처럼 트래픽이 몰릴 때, 리버스 프록시(로드 밸런서)가 요청을 여러 서버로 나누어 보내 시스템 다운을 방지합니다.</li>
</ul>
<hr>
<h3 id="5-캐시-서버-cache-server">5) 캐시 서버 (Cache Server)</h3>
<ul>
<li><strong>역할 :</strong> 자주 요청되는 <strong>데이터나 콘텐츠를 미리 복사해 저장</strong>(캐싱)해두고, 다음 요청 시 DB나 애플리케이션 서버를 거치지 않고 <strong>더 빠르게 응답</strong>하기 위한 서버입니다.</li>
<li><strong>과정 :</strong><blockquote>
<p>클라이언트가 이전에 요청했던 콘텐츠를 <strong>다시 요청</strong>
  &rarr; 리버스 프록시 또는 웹 서버가 요청을 받음
  &rarr; DB나 애플리케이션 서버에 요청하기 <strong>전에 캐시 서버를 먼저 확인</strong>
  &rarr; <strong>(Cache Hit)</strong> 캐시 서버에 해당 콘텐츠가 <strong>저장되어 있으면</strong>
  &rarr; DB/애플리케이션 서버를 거치지 않고 <strong>즉시 캐시에서 콘텐츠를 꺼내</strong> 응답
  &rarr; <strong>(Cache Miss)</strong> 만약 <strong>캐시에 없으면</strong>
  &rarr; 기존 방식대로 애플리케이션 서버와 DB 서버를 거쳐 콘텐츠를 가져옴
  &rarr; <strong>이 콘텐츠를 캐시 서버에 저장</strong>하고 클라이언트에게 전달 (다음 요청을 대비)</p>
</blockquote>
</li>
</ul>
<blockquote>
<p>** 참고) 리버스 프록시 vs. 포워드 프록시**</p>
<ul>
<li><strong>포워드 프록시 (Forward Proxy):</strong> <strong>클라이언트 쪽에</strong> 위치합니다. 클라이언트가 인터넷(예: google.com)에 접속할 때 대신 요청을 보내주는 서버입니다. (예: 회사 내부망에서 외부 인터넷에 접속할 때 보안이나 캐싱을 위해 사용)</li>
<li><strong>리버스 프록시 (Reverse Proxy):</strong> <strong>서버 쪽에</strong> 위치합니다. 클라이언트가 우리 서버에 접속할 때, 그 요청을 대신 받아 내부 서버로 연결해주는 서버입니다. (예: 로드 밸런싱, SSL 암호화 처리)</li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[CPU 스케줄링 ]]></title>
            <link>https://velog.io/@david1-p/OS-CPU-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81</link>
            <guid>https://velog.io/@david1-p/OS-CPU-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81</guid>
            <pubDate>Wed, 29 Oct 2025 01:17:26 GMT</pubDate>
            <description><![CDATA[<h1 id="cpu-스케줄링에-대해서">CPU 스케줄링에 대해서</h1>
<blockquote>
<p>CPU 스케줄링은 운영체제(OS)가 여러 프로세스(실행 중인 프로그램)들에게 <strong>공정하고 합리적으로 CPU 자원을 배분</strong>하는 과정을 의미합니다.</p>
</blockquote>
<blockquote>
<p>만약 CPU 스케줄링이 없다면 어떻게 될까요?
하나의 프로세스가 CPU를 무한정 독점할 수 있습니다. 당장 처리해야 할 중요한 작업(예: 사용자 로그인 요청)이, 급하지 않은 백그라운드 작업(예: 로그 파일 압축) 때문에 무한정 기다려야 할 수도 있습니다. 즉, 시스템은 무질서한 상태가 되고 응답성과 효율성이 극도로 떨어지게 됩니다.</p>
</blockquote>
<p>CPU 스케줄링은 크게 &#39;선점형&#39;과 &#39;비선점형&#39; 두 가지 방식으로 나뉩니다.</p>
<hr>
<h2 id="1-스케줄링의-두-가지-핵심-방식">1. 스케줄링의 두 가지 핵심 방식</h2>
<h3 id="1-비선점형-스케줄링-non-preemptive">1) 비선점형 스케줄링 (Non-preemptive)</h3>
<p><strong>비선점형 스케줄링</strong>은 하나의 프로세스가 CPU 자원을 사용하기 시작하면, 해당 프로세스가 스스로 종료되거나 I/O 대기 상태로 전환되기 전까지는 다른 프로세스가 CPU를 차지할 수 없는 방식입니다.</p>
<ul>
<li><strong>특징:</strong><ul>
<li><strong>장점:</strong> 흐름을 예측하기 쉽고, 문맥 교환(Context Switching) 비용이 적습니다.</li>
<li><strong>단점:</strong> 응답 시간이 길어질 수 있습니다. 실행 시간이 긴 프로세스 하나가 전체 시스템의 반응성을 저해할 수 있습니다. (예: FCFS, SJF)</li>
</ul>
</li>
</ul>
<h3 id="2-선점형-스케줄링-preemptive">2) 선점형 스케줄링 (Preemptive)</h3>
<p><strong>선점형 스케줄링</strong>은 프로세스가 CPU를 사용하고 있더라도, 운영체제가 필요에 따라(예: 더 높은 우선순위의 작업이 들어오거나, 할당된 시간이 다 되거나) 해당 프로세스로부터 자원을 <strong>강제로 빼앗아</strong> 다른 프로세스에 할당할 수 있는 방식입니다.</p>
<ul>
<li><strong>특징:</strong><ul>
<li><strong>장점:</strong> 응답 시간이 빠르고 대화형 시스템에 유리합니다.</li>
<li><strong>단점:</strong> 잦은 문맥 교환으로 인한 오버헤드가 발생할 수 있으며, 여러 프로세스가 자원을 동시에 접근하려 할 때 <strong>경쟁 상태(Race Condition)</strong>가 발생할 수 있습니다. (예: RR, SRT, MLQ)</li>
</ul>
</li>
</ul>
<blockquote>
<h4 id="💡-보충-문맥-교환-context-switching-비용이란">💡 [보충] 문맥 교환 (Context Switching) 비용이란?</h4>
<p>문맥 교환은 현재 실행 중인 프로세스의 상태(예: 레지스터 값, 프로그램 카운터)를 PCB(Process Control Block)에 저장하고, 다음에 실행할 프로세스의 상태를 PCB에서 불러와 CPU에 적재하는 과정을 말합니다.</p>
<p>이 과정 자체는 <strong>CPU가 실질적인 작업을 처리하는 시간이 아니기 때문에 &#39;오버헤드(Overhead)&#39;</strong> 즉, 비용으로 간주됩니다. 선점형 스케줄링은 이 문맥 교환이 빈번하게 발생할 수 있으므로, 이 비용을 고려해야 합니다.</p>
</blockquote>
<hr>
<h2 id="2-핵심-cpu-스케줄링-알고리즘">2. 핵심 CPU 스케줄링 알고리즘</h2>
<p>이제 가장 대표적인 CPU 스케줄링 알고리즘들을 살펴보겠습니다.</p>
<h3 id="1-fcfs-first-come-first-served">1) FCFS (First Come First Served)</h3>
<ul>
<li><strong>방식:</strong> 비선점형</li>
<li><strong>설명:</strong> 준비 큐(Ready Queue)에 도착한 순서대로 CPU를 할당받는, 가장 단순한 방식입니다. (은행 창구에서 번호표 뽑고 기다리는 것과 같습니다.)</li>
<li><strong>문제점:</strong> <strong>호위 효과 (Convoy Effect)</strong><ul>
<li>실행 시간이 매우 긴 프로세스(A)가 CPU를 먼저 점유하면, 뒤따르는 실행 시간이 짧은 프로세스들(B, C)이 A가 끝날 때까지 하염없이 기다려야 하는 현상입니다.</li>
<li><em>예시:</em> A(30초), B(2초), C(1초) 순으로 도착 시, B는 30초, C는 32초를 기다려야 합니다. B와 C의 평균 대기 시간은 매우 길어집니다.</li>
</ul>
</li>
</ul>
<h3 id="2-sjf-shortest-job-first">2) SJF (Shortest Job First)</h3>
<ul>
<li><strong>방식:</strong> 비선점형</li>
<li><strong>설명:</strong> 준비 큐에 있는 프로세스 중 <strong>CPU 실행 시간이 가장 짧은 프로세스</strong>에게 CPU를 먼저 할당합니다.</li>
<li><strong>장점:</strong> 호위 효과를 해결하고, 시스템의 평균 대기 시간을 최소화할 수 있습니다.</li>
<li><strong>문제점:</strong><ol>
<li><strong>기아 현상 (Starvation):</strong> 실행 시간이 긴 프로세스는, 실행 시간이 짧은 프로세스들이 계속 큐에 도착할 경우 무한정 대기하게 되어 CPU를 할당받지 못할 수 있습니다.</li>
<li><strong>실행 시간 예측의 어려움:</strong> 가장 큰 현실적 한계입니다. OS는 프로세스가 <strong>미래에 얼마나 오래 실행될지</strong> 정확히 알 수 없습니다. (보통 과거의 실행 기록을 바탕으로 <em>예측</em>합니다.)</li>
</ol>
</li>
</ul>
<h3 id="3-srt-shortest-remaining-time">3) SRT (Shortest Remaining Time)</h3>
<ul>
<li><strong>방식:</strong> 선점형</li>
<li><strong>설명:</strong> SJF 알고리즘의 선점형 버전입니다. 현재 실행 중인 프로세스의 <strong>남은 실행 시간</strong>보다 더 짧은 실행 시간을 가진 프로세스가 준비 큐에 도착하면, 즉시 CPU를 빼앗아 새 프로세스에 할당합니다.</li>
<li><strong>특징:</strong> SJF의 장점을 가지면서 응답성을 높였습니다. 하지만 SJF와 동일하게 &#39;기아 현상&#39;과 &#39;실행 시간 예측&#39;의 문제를 안고 있습니다.</li>
</ul>
<h3 id="4-라운드-로빈-round-robin-rr">4) 라운드 로빈 (Round Robin, RR)</h3>
<ul>
<li><strong>방식:</strong> 선점형</li>
<li><strong>설명:</strong> FCFS에 <strong>타임 슬라이스 (Time Slice) 또는 타임 퀀텀 (Time Quantum)</strong> 개념을 도입한 방식입니다. 모든 프로세스는 정해진 타임 슬라이스만큼 CPU를 사용하고, 시간이 만료되면 준비 큐의 맨 뒤로 이동합니다.</li>
<li><strong>특징:</strong><ul>
<li>모든 프로세스가 공평하게 CPU 시간을 보장받아 응답 시간이 빠릅니다. 현대 시분할 시스템의 근간이 됩니다.</li>
<li><strong>타임 슬라이스 크기</strong>가 중요합니다.<ul>
<li><strong>너무 크면:</strong> FCFS와 비슷해져 호위 효과가 발생하고 응답성이 떨어집니다.</li>
<li><strong>너무 작으면:</strong> 문맥 교환이 너무 빈번하게 발생하여 오버헤드가 커집니다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="3-고급-스케줄링과-기아-현상-해결">3. 고급 스케줄링과 &#39;기아 현상&#39; 해결</h2>
<p>SJF, SRT 등 우선순위를 기반으로 하는 스케줄링은 &#39;기아 현상&#39;이라는 고질적인 문제를 안고 있습니다. 이를 해결하기 위한 고급 기법들을 알아봅니다.</p>
<h3 id="1-다단계-큐-multilevel-queue">1) 다단계 큐 (Multilevel Queue)</h3>
<ul>
<li><strong>설명:</strong> 우선순위별로 준비 큐를 여러 개 두는 방식입니다.</li>
<li><strong>작동 방식:</strong><ol>
<li>우선순위가 가장 높은 큐(예: 시스템 프로세스 큐)에 있는 프로세스들을 먼저 처리합니다.</li>
<li>가장 높은 큐가 비어있을 때만, 그 다음 우선순위 큐(예: 대화형 작업 큐)의 프로세스를 처리합니다.</li>
</ol>
</li>
<li><strong>장점:</strong> 프로세스 유형별(예: 대화형 vs. 배치)로 다른 스케줄링 정책(예: 상위 큐는 RR, 하위 큐는 FCFS)을 적용할 수 있습니다.</li>
<li><strong>문제점:</strong> 여전히 하위 큐의 프로세스들은 상위 큐에 작업이 계속 들어오면 기아 현상을 겪을 수 있습니다.</li>
</ul>
<h3 id="2-기아-현상의-해결책-에이징-aging">2) &#39;기아 현상&#39;의 해결책: 에이징 (Aging)</h3>
<p>SJF, SRT, 다단계 큐 등에서 발생하는 기아 현상을 해결하기 위한 대표적인 기법이 바로 <strong>에이징(Aging)</strong>입니다.</p>
<blockquote>
<p><strong>에이징 (Aging):</strong> 오랫동안 준비 큐에서 대기한 프로세스의 우선순위를 <strong>시간이 지남에 따라 점차 높여주는</strong> 기법입니다.</p>
</blockquote>
<p>아무리 우선순위가 낮은 작업이라도, 충분히 오래 기다리면 결국 우선순위가 높아져 언젠가는 실행될 기회를 보장받게 됩니다.</p>
<h3 id="3-다단계-피드백-큐-multilevel-feedback-queue-mlfq">3) 다단계 피드백 큐 (Multilevel Feedback Queue, MLFQ)</h3>
<p>다단계 큐의 기아 현상 문제를 에이징 기법을 적용해 해결한, 가장 정교하고 현대적인 스케줄링 방식 중 하나입니다.</p>
<ul>
<li><strong>작동 방식:</strong><ol>
<li>새 프로세스는 일단 <strong>가장 높은 우선순위 큐</strong>에 들어갑니다. (빠른 응답 기대)</li>
<li>해당 큐의 타임 슬라이스 내에 작업이 끝나지 않으면, 한 단계 <strong>낮은 우선순위 큐</strong>로 강등됩니다. (CPU를 오래 쓰는 작업은 우선순위가 낮아짐)</li>
<li>낮은 우선순위 큐일수록 보통 더 긴 타임 슬라이스를 할당받습니다.</li>
<li><strong>(에이징 적용)</strong> 만약 가장 낮은 큐에서 너무 오래 대기한 프로세스가 있다면, 다시 <strong>상위 큐로 승급</strong>시켜 기아 현상을 방지합니다.</li>
</ol>
</li>
</ul>
<p>MLFQ는 CPU 실행 시간을 예측할 필요 없이, 실행 패턴(짧은 작업/긴 작업)에 따라 동적으로 프로세스의 우선순위가 조정되므로 매우 효율적이고 유연합니다.</p>
<hr>
<h2 id="4-마치며-개발자가-스케줄링을-알아야-하는-이유">4. 마치며: 개발자가 스케줄링을 알아야 하는 이유</h2>
<p>CPU 스케줄링은 운영체제 깊숙한 곳에서 일어나지만, 백엔드 애플리케이션의 <strong>응답 시간(Latency)</strong>과 <strong>처리량(Throughput)</strong>에 직접적인 영향을 줍니다.</p>
<ul>
<li><strong>SJF</strong> 계열은 <strong>처리량</strong>을 극대화하려 하지만 기아 현상이 발생할 수 있습니다.</li>
<li><strong>RR</strong> 계열은 <strong>응답성</strong>을 보장하려 하지만 문맥 교환 비용이 발생합니다.</li>
<li><strong>MLFQ</strong>는 이 둘의 균형을 맞추려는 현대적인 해법입니다.</li>
</ul>
<blockquote>
<p>어떤 스케줄링 알고리즘이 사용되느냐에 따라, 애플리케이션의 특정 요청이 왜 늦게 응답하는지, 혹은 서버 리소스가 특정 작업에 몰리는 이유를 어렴풋이나마 짐작할 수 있게 됩니다. 이는 시스템의 성능 병목을 파악하고 최적화하는 데 중요한 기초 지식이 되어줄 것입니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[디스크 접근 시간]]></title>
            <link>https://velog.io/@david1-p/CS-%EC%A7%80%EC%8B%9D-%EB%94%94%EC%8A%A4%ED%81%AC-%EC%A0%91%EA%B7%BC-%EC%8B%9C%EA%B0%84</link>
            <guid>https://velog.io/@david1-p/CS-%EC%A7%80%EC%8B%9D-%EB%94%94%EC%8A%A4%ED%81%AC-%EC%A0%91%EA%B7%BC-%EC%8B%9C%EA%B0%84</guid>
            <pubDate>Mon, 27 Oct 2025 02:42:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.maeil-mail.kr/">매일메일</a>에서 백엔드와 연관된 CS 면접질문들을 보내주는데 오늘은 흥미로운 내용이 있어서 정리하려고 한다. 
내 코드나 쿼리가 느려지는 원인이 CPU, 메모리, 네트워크, 디스크I/O 때문일 수도 있다는 것이다.</p>
</blockquote>
<h2 id="1-디스크-접근-시간-disk-access-time---hdd-기준">1. 디스크 접근 시간 (Disk Access Time) - HDD 기준</h2>
<p>하드 디스크(HDD)에서 특정 데이터 블록을 읽거나 쓰는 데 걸리는 총 시간을 의미합니다. 
이 시간은 기계적인 동작에 드는 시간이 큰 비중을 차지하며, 세 가지 주요 구성 요소의 합입니다.</p>
<blockquote>
<p>디스크 접근 시간 = 탐색 시간 + 회전 지연 시간 + 데이터 전송 시간</p>
</blockquote>
<h3 id="1-탐색-시간-seek-time">1) 탐색 시간 (Seek Time)</h3>
<ul>
<li><p>정의: 디스크의 헤드(Head)를 데이터가 있는 <strong>트랙(Track) / 실린더(Cylinder)</strong>까지 이동시키는 데 걸리는 시간입니다.</p>
</li>
<li><p>특징: 기계적인 팔(Arm)이 물리적으로 움직이는 과정이라, 디스크 접근 시간 중 가장 오래 걸릴 수 있는 병목 지점입니다.</p>
</li>
</ul>
<h3 id="2-회전-지연-시간-rotational-latency">2) 회전 지연 시간 (Rotational Latency)</h3>
<ul>
<li><p>정의: 헤드가 목표 트랙에 도착한 후, 디스크 원판(Platter)이 회전하여 원하는 <strong>섹터(Sector)</strong>가 헤드 바로 아래에 올 때까지 기다리는 시간입니다.</p>
</li>
<li><p>특징: 디스크의 분당 회전수(RPM)가 높을수록 이 지연 시간은 줄어듭니다. (예: 7,200 RPM이 5,400 RPM보다 빠름)</p>
</li>
</ul>
<h3 id="3-데이터-전송-시간-data-transfer-time">3) 데이터 전송 시간 (Data Transfer Time)</h3>
<ul>
<li><p>정의: 헤드 아래에 위치한 섹터에서 실제 데이터를 읽어(Read) 버퍼로 옮기거나, 버퍼의 데이터를 쓰는(Write) 데 걸리는 시간입니다.</p>
</li>
<li><p>특징: 전송할 데이터 블록의 크기, 트랙의 저장 밀도 등에 영향을 받습니다.</p>
</li>
</ul>
<hr>
<h2 id="2-순차-접근이-랜덤-접근보다-빠른-이유-hdd">2. 순차 접근이 랜덤 접근보다 빠른 이유 (HDD)</h2>
<p>핵심은 탐색 시간과 회전 지연 시간이라는 <strong>&#39;기계적 지연(Mechanical Delay)&#39;</strong>을 얼마나 자주 겪느냐에 있습니다.</p>
<h3 id="1-순차-접근-sequential-access">1) 순차 접근 (Sequential Access)</h3>
<ul>
<li><p>데이터가 디스크에 물리적으로 연속된 블록에 저장되어 있습니다. (예: 큰 동영상 파일, 로그 파일)</p>
</li>
<li><p>최초 1회의 탐색 시간과 회전 지연 시간만 발생하면, 그 후로는 헤드를 거의 움직이지 않고 데이터를 연속으로 쭉 읽어 들입니다.</p>
</li>
<li><p><span style="color:red">접근 시간 = (1 x 탐색) + (1 x 회전) + (N x 전송)</span></p>
</li>
<li><p>기계적 지연이 최소화되므로 속도가 매우 빠릅니다.</p>
</li>
</ul>
<h3 id="2-랜덤-접근-random-access">2) 랜덤 접근 (Random Access)</h3>
<ul>
<li><p>데이터가 디스크 여러 곳에 흩어져 저장되어 있습니다. 
(예: DB 인덱스를 타지 않는 쿼리, 여기저기 흩어진 작은 파일들)</p>
</li>
<li><p>각각의 데이터 블록을 읽을 때마다 헤드가 새 위치로 이동(탐색 시간)하고, 섹터가 돌아오길 기다려야(회전 지연 시간) 합니다.</p>
</li>
<li><p><span style="color:red">접근 시간 = (N x 탐색) + (N x 회전) + (N x 전송)</span></p>
</li>
<li><p>데이터 조각이 100개라면, 이 기계적 지연이 100번 반복됩니다.</p>
</li>
</ul>
<blockquote>
<h3 id="한줄-요약">한줄 요약</h3>
<p>랜덤 접근은 데이터를 찾기 위한 기계적인 이동(탐색 + 회전)을 계속 반복해야 하므로, 한 번의 이동으로 쭉 읽는 순차 접근보다 훨씬 느릴 수밖에 없습니다.</p>
</blockquote>
<hr>
<h2 id="3-현대의-스토리지-ssd-solid-state-drive">3. 현대의 스토리지: SSD (Solid State Drive)</h2>
<p>위의 모든 설명은 기계식 HDD에 해당합니다. 하지만 현대의 서버와 PC는 대부분 SSD를 사용하며, SSD는 이 문제를 근본적으로 다르게 해결합니다.</p>
<ul>
<li><p>작동 원리: SSD는 HDD처럼 움직이는 팔이나 회전하는 원판이 없습니다. 낸드 플래시(NAND Flash) 메모리 반도체를 이용해 전자적으로 데이터를 읽고 씁니다.</p>
</li>
<li><p>탐색 시간과 회전 지연의 제거: 물리적인 이동이 없으므로, 탐색 시간과 회전 지연 시간이 사실상 0에 가깝습니다.</p>
</li>
<li><p>성능: 
   HDD가 치명적으로 느렸던 랜덤 접근 속도가 SSD에서는 극적으로 향상되었습니다.
   (참고: SSD도 여전히 순차 접근이 랜덤 접근보다 조금 더 빠르긴 하지만, HDD만큼 그 격차가 압도적이지 않음.)</p>
</li>
</ul>
<hr>
<h2 id="4-개발자가-알아야-할-연관-지식-소프트웨어">4. 개발자가 알아야 할 연관 지식 (소프트웨어)</h2>
<p>이러한 하드웨어의 한계를 극복하기 위해 소프트웨어(운영체제, DB 등)는 다양한 전략을 사용합니다.</p>
<h3 id="1-운영체제os의-페이지-캐시-page-cache">1) 운영체제(OS)의 페이지 캐시 (Page Cache)</h3>
<ul>
<li>OS는 디스크 I/O가 RAM 접근보다 수천~수만 배 느리다는 것을 압니다.</li>
<li>따라서 RAM의 일부를 디스크 데이터의 &#39;캐시&#39; 공간으로 사용합니다. (이것이 &#39;페이지 캐시&#39; 또는 &#39;버퍼 캐시&#39;입니다.)</li>
<li>읽기: 애플리케이션이 파일 읽기를 요청하면, OS는 디스크를 보기 전에 먼저 RAM 캐시를 확인합니다. 데이터가 캐시에 있으면(Cache Hit), 디스크 접근 없이 즉시 반환합니다. (매우 빠름)</li>
<li>쓰기: 애플리케이션이 파일 쓰기를 요청하면, OS는 일단 RAM 캐시에만 쓰고 &quot;완료&quot;라고 응답합니다. 실제 디스크에 쓰는 작업은 나중에 백그라운드에서 모아서 처리합니다. (Write-Back 전략)</li>
<li>Takeaway: 어제 10초 걸린 쿼리가 오늘 0.1초 만에 실행된다면, 해당 데이터가 DB나 OS의 페이지 캐시에 적재되었기 때문일 확률이 높습니다.</li>
</ul>
<h3 id="2-데이터베이스-인덱스-b-tree">2) 데이터베이스 인덱스 (B-Tree)</h3>
<ul>
<li>인덱스의 목적: 수억 개의 데이터(HDD의 여러 트랙에 흩어진) 중에서 &#39;단 하나의 데이터&#39;를 찾을 때, 모든 데이터를 순차적으로 읽는(Full Table Scan) 비효율을 막는 것입니다.</li>
<li>B-Tree 자료구조: B-Tree는 &#39;디스크 접근 횟수(I/O)&#39;를 최소화하도록 특별히 설계된 자료구조입니다.<ul>
<li>하나의 노드(블록)에 많은 데이터를 저장하여 트리의 높이(Depth)를 극단적으로 낮춥니다.</li>
<li>트리의 높이가 3<del>4 정도만 되어도 수백만</del>수천만 건의 데이터를 처리할 수 있습니다.</li>
<li>즉, 원하는 데이터를 찾기 위해 디스크 &#39;탐색(Seek)&#39;을 3~4번만 하면 되도록 만들어줍니다.</li>
</ul>
</li>
</ul>
<h3 id="3-애플리케이션의-로깅-버퍼링">3) 애플리케이션의 로깅 (버퍼링)</h3>
<ul>
<li>만약 log.write(&quot;에러 발생&quot;)이라는 코드가 호출될 때마다 즉시 디스크 파일에 쓴다면, 이는 수많은 <strong>&#39;작은 랜덤 쓰기(Small Random Write)&#39;</strong>를 유발합니다.</li>
<li>이는 HDD에서 최악의 성능을 보이며, 애플리케이션 전체의 응답 속도를 저하시킵니다.</li>
<li>해결책 (Buffering): 대부분의 로깅 라이브러리는 로그 메시지를 즉시 쓰지 않고, 일단 메모리 버퍼에 모아 둡니다.</li>
<li>버퍼가 가득 차거나 특정 시간이 되면, 모아둔 로그 전체를 <strong>&#39;하나의 큰 순차 쓰기(Large Sequential Write)&#39;</strong>로 디스크에 기록합니다.</li>
<li><strong>Takeaway</strong>: 수천 번의 랜덤 쓰기를 한 번의 순차 쓰기로 바꿔, 디스크의 기계적 지연을 최소화하는 핵심 최적화 기법입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[해시 충돌(Hash Collision)]]></title>
            <link>https://velog.io/@david1-p/%ED%95%B4%EC%8B%9C-%EC%B6%A9%EB%8F%8CHash-Collision</link>
            <guid>https://velog.io/@david1-p/%ED%95%B4%EC%8B%9C-%EC%B6%A9%EB%8F%8CHash-Collision</guid>
            <pubDate>Thu, 23 Oct 2025 23:38:27 GMT</pubDate>
            <description><![CDATA[<h1 id="해시-충돌hash-collision이란-무엇이며-어떻게-해결할까">해시 충돌(Hash Collision)이란 무엇이며, 어떻게 해결할까?</h1>
<p>백엔드 개발에서 성능은 매우 중요하며, 데이터를 빠르게 조회하기 위해 해시(Hash) 기반의 자료 구조(해시 테이블, 해시맵 등)를 빈번하게 사용합니다. 해시 자료 구조의 핵심과 성능 저하의 주범인 &#39;해시 충돌&#39;에 대해 알아보겠습니다.</p>
<hr>
<h2 id="해시hash-자료-구조란">해시(Hash) 자료 구조란?</h2>
<p>해시 자료 구조는 <strong>키-값 쌍(Key-Value pair)</strong>으로 이루어진 데이터 구조입니다.</p>
<p>가장 큰 특징은 키(Key)를 이용해 값(Value)을 평균 <strong>$O(1)$</strong>의 매우 빠른 시간 복잡도로 찾을 수 있다는 것입니다.</p>
<p>이것이 가능한 이유는 내부적으로 키를 <strong>해시 함수(Hash Function)</strong>에 통과시켜 반환된 &#39;해시 값&#39;을 배열의 인덱스로 사용하여 값을 관리하기 때문입니다.</p>
<h2 id="해시-충돌-hash-collision-이란">해시 충돌 (Hash Collision) 이란?</h2>
<p>해시 자료 구조는 키를 해시 함수에 넣어서 나오는 결과를 기반으로 값을 관리합니다. 하지만 해시 함수는 종종 <strong>서로 다른 키를 사용해도 같은 해시 값을 반환</strong>하는 경우가 존재합니다.</p>
<p>이처럼 서로 다른 키가 같은 인덱스(버킷)로 매핑되는 상황을 <strong>해시 충돌(Hash Collision)</strong>이라고 합니다.</p>
<p>해시 충돌이 발생하면 $O(1)$의 시간 복잡도를 보장할 수 없게 되며, 충돌이 많아질수록 성능은 $O(n)$에 가까워질 수 있습니다. 따라서 이 충돌을 효율적으로 관리하고 완화하는 것이 핵심입니다.</p>
<hr>
<h2 id="해시-충돌-완화-전략">해시 충돌 완화 전략</h2>
<p>해시 충돌을 완화하기 위한 접근 방법으로 크게 두 가지가 대표적입니다.</p>
<ol>
<li><strong>분리 연결법 (Separate Chaining)</strong></li>
<li><strong>개방 주소법 (Open Addressing)</strong></li>
</ol>
<h3 id="1-분리-연결법-separate-chaining">1. 분리 연결법 (Separate Chaining)</h3>
<p><strong>분리 연결법</strong>은 이름 그대로 충돌이 발생한 데이터를 기존 데이터에 &#39;연결&#39;시키는 방식입니다.</p>
<p>즉, 각 버킷을 단순한 값이 아닌 <strong>연결 리스트(Linked List)</strong>나 <strong>트리(Tree)</strong> 형태로 관리하여, 충돌이 발생하더라도 해당 버킷에 데이터를 계속해서 추가할 수 있도록 합니다.</p>
<ul>
<li><strong>장점</strong>: 구현이 비교적 간단하고, 데이터가 많아져도(적재율이 높아져도) 개방 주소법에 비해 성능 저하가 완만한 편입니다.</li>
<li><strong>단점</strong>: 연결 리스트나 트리를 위한 추가적인 메모리 공간이 필요합니다.</li>
</ul>
<h4 id="추가-분리-연결법의-성능-최적화-list-to-tree">(추가) 분리 연결법의 성능 최적화 (List to Tree)</h4>
<p>하나의 버킷에 데이터가 계속 쌓여 연결 리스트가 길어지면, 해당 버킷을 탐색하는 시간은 $O(n)$이 되어 성능이 저하됩니다.</p>
<p>많은 현대 언어의 해시맵 구현체(예: <strong>Java의 <code>HashMap</code></strong>)는 이 문제를 해결하기 위해, 특정 버킷의 연결 리스트 길이가 일정 임계값(예: 8개)을 초과하면, 해당 버킷의 자료 구조를 연결 리스트에서 <strong>레드-블랙 트리(Red-Black Tree)</strong>와 같은 균형 잡힌 이진 탐색 트리로 변환합니다.</p>
<ul>
<li><strong>연결 리스트</strong>: 탐색 시간 $O(n)$</li>
<li><strong>트리</strong>: 탐색 시간 $O(\log n)$</li>
</ul>
<p>이를 통해 최악의 경우에도 탐색 성능을 $O(\log n)$으로 보장할 수 있습니다.</p>
<h3 id="2-개방-주소법-open-addressing">2. 개방 주소법 (Open Addressing)</h3>
<p><strong>개방 주소법</strong>은 충돌이 발생했을 때, 해당 버킷이 이미 사용 중이라면 <strong>다른 비어있는 해시 버킷</strong>을 찾아 데이터를 삽입하는 방식입니다.</p>
<ul>
<li><strong>장점</strong>: 추가적인 메모리 공간이 필요 없으며, 데이터가 적을 때는 분리 연결법보다 빠를 수 있습니다. (캐시 효율성)</li>
<li><strong>단점</strong>: 데이터가 많아질수록(적재율이 높아질수록) 빈 버킷을 찾는 과정이 길어져 성능이 저하될 수 있으며, 데이터 삭제가 까다롭습니다.</li>
</ul>
<hr>
<h2 id="개방-주소법의-다른-버킷-찾는-방법-probing">개방 주소법의 &#39;다른 버킷&#39; 찾는 방법 (Probing)</h2>
<p>개방 주소법에서 빈 버킷을 찾기 위한 탐사(Probing) 방법에는 여러 가지가 존재합니다.</p>
<h3 id="1-선형-탐사법-linear-probing">1. 선형 탐사법 (Linear Probing)</h3>
<p>임의의 고정된 크기(예: 1)만큼 <strong>한 칸씩</strong> 순차적으로 이동하며 빈 버킷을 찾는 가장 간단한 방법입니다.</p>
<ul>
<li><strong>단점</strong>: 특정 버킷 주변이 모두 채워져 있는 <strong>1차 군집 현상(Primary Clustering)</strong>이 발생하기 쉽고, 이 경우 해시 성능이 크게 저하될 수 있습니다.</li>
</ul>
<h3 id="2-제곱-탐사법-quadratic-probing">2. 제곱 탐사법 (Quadratic Probing)</h3>
<p>선형 탐사법처럼 한 칸씩 찾는 것이 아닌, $1^2, 2^2, 3^2, ...$ 만큼의 보폭으로 <strong>제곱으로 늘려가며</strong> 빈 버킷을 찾습니다.</p>
<ul>
<li><strong>장점</strong>: 보폭이 점점 늘어나기 때문에 1차 군집 현상을 완화하여 특정 영역을 빠르게 벗어날 수 있습니다.</li>
<li><strong>단점</strong>: 여러 키가 해시 함수로 같은 값을 갖게 될 경우, 모두 같은 순서로 탐사하게 되어 비효율적인 상황(<strong>2차 군집 현상, Secondary Clustering</strong>)이 발생할 수 있습니다.</li>
</ul>
<h3 id="3-이중-해싱-double-hashing">3. 이중 해싱 (Double Hashing)</h3>
<p>해시 충돌이 발생하는 경우, <strong>두 번째 보조 해시 함수(Auxiliary Hash Function)</strong>를 사용하는 방법입니다.</p>
<p>첫 번째 해시 값으로는 초기 위치를 정하고, 충돌 시 두 번째 해시 함수의 결과값만큼 일정하게 이동하며 빈 버킷을 찾습니다.</p>
<ul>
<li><strong>장점</strong>: 키마다 이동하는 보폭이 달라지므로, 군집 현상(Clustering)이 발생할 가능성이 가장 작습니다.</li>
<li><strong>단점</strong>: 추가적인 보조 해시 함수 연산이 필요하므로 다른 방식에 비해 연산량이 많습니다.</li>
</ul>
<hr>
<h2 id="추가-해시-성능-유지를-위한-핵심-적재율과-리해싱">(추가) 해시 성능 유지를 위한 핵심: 적재율과 리해싱</h2>
<p>해시 충돌은 피할 수 없지만, 충돌이 &#39;너무 자주&#39; 일어나지 않도록 관리하는 것이 중요합니다. 이때 사용되는 개념이 <strong>적재율(Load Factor)</strong>입니다.</p>
<ul>
<li><strong>적재율 (Load Factor)</strong>: 해시 테이블의 전체 버킷 수 대비 현재 얼마나 많은 데이터가 저장되어 있는지를 나타내는 비율입니다.<ul>
<li><code>적재율 = (저장된 데이터 개수) / (전체 버킷 수)</code></li>
</ul>
</li>
</ul>
<p>적재율이 너무 높아지면(예: 0.75 이상) 빈 공간이 줄어들어 해시 충돌이 급격하게 증가하고 성능이 저하됩니다.</p>
<h3 id="리해싱-rehashing">리해싱 (Rehashing)</h3>
<p>이 문제를 해결하기 위해 해시 테이블은 적재율이 특정 임계값(예: Java <code>HashMap</code>의 기본값 0.75)을 초과하면 <strong>리해싱(Rehashing)</strong>을 수행합니다.</p>
<p><strong>리해싱</strong>이란, 기존보다 더 큰 크기(보통 2배)의 새로운 버킷 배열을 생성한 뒤, 기존의 모든 데이터를 새로운 해시 함수(또는 변경된 인덱스 계산)에 따라 새 배열에 다시 삽입하는 과정을 말합니다.</p>
<p>리해싱은 비용이 많이 드는 작업이지만, 적재율을 낮춰 다시 $O(1)$의 평균 시간 복잡도를 유지할 수 있도록 해주는 필수적인 작업입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[커널아카데미] 백엔드 12기 26주차 회고(파이널 프로젝트 5주차)]]></title>
            <link>https://velog.io/@david1-p/%EC%BB%A4%EB%84%90%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EB%B0%B1%EC%97%94%EB%93%9C-12%EA%B8%B0-26%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%ED%8C%8C%EC%9D%B4%EB%84%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-5%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@david1-p/%EC%BB%A4%EB%84%90%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EB%B0%B1%EC%97%94%EB%93%9C-12%EA%B8%B0-26%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%ED%8C%8C%EC%9D%B4%EB%84%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-5%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 21 Sep 2025 01:48:18 GMT</pubDate>
            <description><![CDATA[<ol>
<li><p>시스템 아키텍처 분석 및 설계</p>
<ul>
<li>JOBER_AI 템플릿 생성 시스템의 전체 구조 파악</li>
<li>Frontend ↔ Backend (Spring Boot) ↔ AI part (FastAPI) 3계층 아키텍처 분석</li>
<li>실시간 채팅 기능을 위한 WebSocket 통신 설계</li>
</ul>
<ol start="2">
<li>AI 파트 실시간 채팅 기능 구현</li>
</ol>
<ul>
<li>FastAPI WebSocket 지원 추가<ul>
<li>ConnectionManager 클래스로 세션 관리</li>
<li>Thread-safe 동시 연결 처리</li>
<li>실시간 메시지 송수신 기능</li>
</ul>
</li>
<li>백엔드 연동 API 개발<ul>
<li>/ai/sessions/start 엔드포인트 구현</li>
<li>/ai/chat/{session_id} WebSocket 엔드포인트</li>
<li>백엔드 호환 모델 (BackendAiTemplateRequest/Response) 추가</li>
</ul>
</li>
</ul>
<ol start="3">
<li>API 문서화 및 체계화</li>
</ol>
<ul>
<li>Swagger UI 기능별 분류<ul>
<li>Template Generation, Real-time Chat, Session Management</li>
<li>Backend Integration, System, Debug 태그로 구분</li>
<li>개발자 친화적 API 문서 완성</li>
</ul>
</li>
<li>엔드포인트 정리<ul>
<li>기존 14개 + 신규 WebSocket 기능 통합</li>
<li>각 API별 요청/응답 형식 상세 문서화</li>
</ul>
</li>
</ul>
<ol start="4">
<li>템플릿 생성 로직 심화 분석</li>
</ol>
<ul>
<li>3단계 템플릿 선택 시스템 분석<ul>
<li>BasicTemplateMatcher (기존 템플릿)</li>
<li>PublicTemplateManager (공용 템플릿 105개)</li>
<li>Agent2 (새 템플릿 생성)</li>
</ul>
</li>
<li>핵심 컴포넌트 상세 분석<ul>
<li>Agent1: 6W 변수 추출 + 의도 분석</li>
<li>VariableMapper: LLM 기반 의미적 변수 매핑</li>
<li>TemplateValidator: 품질 검증 시스템</li>
</ul>
</li>
</ul>
<ol start="5">
<li>백엔드 요구사항 문서화</li>
</ol>
<ul>
<li>Spring Boot WebSocket 구현 가이드<ul>
<li>pom.xml 의존성 정의</li>
<li>WebSocketConfig, TemplateWebSocketHandler 구현 명세</li>
<li>AiServiceClient WebClient 연동 방안</li>
</ul>
</li>
<li>완전한 동작 플로우 설계<ul>
<li>세션 시작 → WebSocket 연결 → 실시간 통신 전체 과정</li>
<li>에러 처리 및 연결 해제 로직 포함</li>
</ul>
</li>
</ul>
<p>기술적 도전과 해결</p>
<p>문제점</p>
<ul>
<li>기존 단방향 API 방식에서 실시간 양방향 통신으로 전환 필요</li>
<li>3개 분리된 시스템(Frontend/Backend/AI) 간 연동 복잡성</li>
<li>기존 코드 호환성 유지하면서 새 기능 추가</li>
</ul>
<p>해결 방안</p>
<ul>
<li>WebSocket 기반 실시간 통신 아키텍처 설계</li>
<li>기존 API 유지하면서 새 엔드포인트 병행 운영</li>
<li>세션 관리 시스템으로 상태 일관성 보장</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[커널아카데미] 백엔드 12기 25주차 회고(파이널 프로젝트 4주차)
]]></title>
            <link>https://velog.io/@david1-p/%EC%BB%A4%EB%84%90%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EB%B0%B1%EC%97%94%EB%93%9C-12%EA%B8%B0-25%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%ED%8C%8C%EC%9D%B4%EB%84%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-4%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@david1-p/%EC%BB%A4%EB%84%90%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EB%B0%B1%EC%97%94%EB%93%9C-12%EA%B8%B0-25%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%ED%8C%8C%EC%9D%B4%EB%84%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-4%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 14 Sep 2025 07:09:04 GMT</pubDate>
            <description><![CDATA[<p>힘든 일주일이었다. 중간에 번아웃이 왔는데, 나혼자만 열심히 하는게 아니라 팀원 모든 분들이 열심히 하는데 어느날 갑자기 하루종일 내가 뭘하고 있는지, 약간 현타 비슷하게 왔다.</p>
<p>중간 발표를 하였는데, 같은 RFP를 받은 다른 조들의 프로젝트를 보면, 이미 챗봇 구현도 마무리 되었고, 템플릿 생성도 수월하게 마무리 된것 같았다.</p>
<p>앞으로 나는 고도화를 진행해야 할 예정이다.
만약에 사용자들이 한번에 여러명이 동시에 작업을 한다면, 프리징이 걸려서 에러가 난다는 다른 팀원분들의 말을 듣고, 이런 동시성 문제를 해결하기 위해 병렬 처리(Parallel Processing)나 비동기 처리(Asynchronous Processing) 적용을 생각을 해보고 있다.</p>
<p>그리고 먼저 top_k, top_p 같은 파라미터를 사용해 Gemini, OpenAI 두 LLM에서 다양한 결과물을 생성하게 한 뒤, 그중에서 가장 좋은 템플릿을 골라내는 리랭크(Re-rank) 로직을 짤 예정이다.</p>
<p>물론 한쪽 Gemini의 하루 제한량 토큰을 다 사용하면, 서비스가 멈추지 않도록 다른 OpenAI API를 사용하는 폴백(Fallback) 로직도 짤 예정이다.</p>
]]></description>
        </item>
    </channel>
</rss>