<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>lil_young.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sat, 14 Mar 2026 13:37:41 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>lil_young.log</title>
            <url>https://images.velog.io/images/lil_young/profile/151f9d49-9834-45a5-8dbe-a39424e7c208/social.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. lil_young.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/lil_young" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[RabbitMQ] 메시지 브로커 RabbitMQ가 필요한 이유]]></title>
            <link>https://velog.io/@lil_young/RabbitMQ-%EB%A9%94%EC%8B%9C%EC%A7%80-%EB%B8%8C%EB%A1%9C%EC%BB%A4-RabbitMQ%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@lil_young/RabbitMQ-%EB%A9%94%EC%8B%9C%EC%A7%80-%EB%B8%8C%EB%A1%9C%EC%BB%A4-RabbitMQ%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sat, 14 Mar 2026 13:37:41 GMT</pubDate>
            <description><![CDATA[<p>현재 진행 중인 프로젝트의 막바지 단계에 투입되어 시나리오별 테스트를 수행하고, API 생성 및 수정, 모니터링 시스템 구축 등의 업무를 수행하고 있다. </p>
<p>프로젝트를 간단하게 요약하자면, 메세지 발송 플랫폼이다. 이 얘기를 하는 이유는 오늘 주제인 RabbitMQ를 설명하기 위함이다.</p>
<p>테스트를 하면서, 메세지를 1명부터 50만건까지 보냈을 때, 이 많은 데이터를 어떻게 안정적으로 보낼 수 있지? 라는 궁금증이 생겼고, 프로젝트 문서를 봤을 때 RabbitMQ를 사용하고 있었다.</p>
<p>먼저, 대량 메세지가 어떤 프로세스를 통해 전송이 되는지 알아보자.
50만건의 대량 메세지를 보냈을 경우 다음과 같은 플로우가 진행된다.</p>
<blockquote>
<p>FRONTEND -&gt; BACKEND(API) -&gt; RabbitMQ -&gt; BACKEND(EGN) -&gt; 메세지 관련 외부 API</p>
</blockquote>
<p>여기서 RabbitMQ가 하는 역할은 50만건의 대량 메세지를 Queue에 넣고, 순차적으로 발송을 할 수 있게 해준다.</p>
<p>이제 RabbitMQ가 무엇이고, 어떤 역할을 하는지 알아보자.</p>
<hr>
<h3 id="rabbitmq란">RabbitMQ란?</h3>
<p>RabbitMQ는 <strong>메시지 브로커(Message Broker)</strong> 이다. 메시지를 보내는 쪽(Producer)과 받는 쪽(Consumer) 사이에서 메시지를 <strong>안전하게 전달해주는 중간 매개체</strong> 역할을 한다.</p>
<p>쉽게 말하면, 택배 물류센터와 같다. 보내는 사람이 물류센터에 택배를 맡기면, 물류센터가 분류해서 배달원에게 전달하는 것처럼, Producer가 RabbitMQ에 메시지를 넣으면, RabbitMQ가 알맞은 Queue로 분류해서 Consumer에게 전달한다.</p>
<hr>
<h3 id="amqp란">AMQP란?</h3>
<p>RabbitMQ는 <strong>AMQP(Advanced Message Queuing Protocol)</strong> 프로토콜을 사용한다.</p>
<p>AMQP는 메시지 지향 미들웨어를 위한 <strong>오픈 표준 프로토콜</strong>로, 클라이언트와 메시지 브로커 간의 통신 규약이다. HTTP가 웹 통신의 표준인 것처럼, AMQP는 메시지 큐 통신의 표준이라고 보면 된다.</p>
<p>AMQP의 주요 특징은 다음과 같다.</p>
<ul>
<li><strong>신뢰성</strong>: 메시지가 유실되지 않도록 보장 (ACK 메커니즘)</li>
<li><strong>라우팅</strong>: Exchange와 Binding을 통한 유연한 메시지 분류</li>
<li><strong>큐잉</strong>: 메시지를 큐에 저장하고 순서대로 처리</li>
<li><strong>플랫폼 독립적</strong>: Java, Python, Node.js 등 언어에 상관없이 사용 가능</li>
</ul>
<p>전체 통신 흐름은 다음과 같다.</p>
<pre><code>Producer → (AMQP 프로토콜) → Broker(RabbitMQ) → (AMQP 프로토콜) → Consumer</code></pre><hr>
<h3 id="rabbitmq의-핵심-구성-요소">RabbitMQ의 핵심 구성 요소</h3>
<p>RabbitMQ는 5가지 핵심 구성 요소로 이루어져 있다.</p>
<pre><code>Producer → Exchange → Binding → Queue → Consumer</code></pre><p>하나씩 알아보자.</p>
<hr>
<h4 id="1-producer-생산자">1. Producer (생산자)</h4>
<p>메시지를 <strong>만들어서 보내는 쪽</strong>이다. Producer는 메시지를 Queue에 직접 넣는 것이 아니라, <strong>Exchange에게 전달</strong>한다.</p>
<pre><code>Producer: &quot;이메일 50만건 보내줘&quot;
    ↓ 메시지 전달 (routing key 포함)
Exchange</code></pre><p>이 프로젝트에서는 채널별 QueueSender 클래스들이 Producer 역할을 한다.</p>
<hr>
<h4 id="2-exchange-교환기">2. Exchange (교환기)</h4>
<p>Producer로부터 메시지를 받아서 <strong>어떤 Queue에 보낼지 결정</strong>하는 라우터 역할을 한다. 우체국의 분류센터와 같다. 편지를 받으면 주소를 보고 어느 배달함에 넣을지 결정하는 것처럼, Exchange는 Routing Key를 보고 어느 Queue에 넣을지 결정한다.</p>
<p>Exchange에는 4가지 타입이 있다.</p>
<table>
<thead>
<tr>
<th>타입</th>
<th>설명</th>
<th>라우팅 방식</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Direct</strong></td>
<td>Routing Key가 <strong>정확히 일치</strong>하는 Queue에 전달</td>
<td>1:1 매칭</td>
</tr>
<tr>
<td><strong>Topic</strong></td>
<td>Routing Key의 <strong>패턴이 일치</strong>하는 Queue에 전달</td>
<td>와일드카드 매칭 (<code>*</code>, <code>#</code>)</td>
</tr>
<tr>
<td><strong>Fanout</strong></td>
<td>연결된 <strong>모든 Queue에 전달</strong> (Routing Key 무시)</td>
<td>브로드캐스트</td>
</tr>
<tr>
<td><strong>Headers</strong></td>
<td>메시지 <strong>헤더 속성</strong>을 기준으로 전달</td>
<td>헤더 매칭</td>
</tr>
</tbody></table>
<p>이 프로젝트에서는 <strong>Topic Exchange</strong>를 사용한다. Topic 타입에서 사용하는 와일드카드는 다음과 같다.</p>
<ul>
<li><code>*</code> : 단어 1개를 대체 (예: <code>email.*.send</code> → <code>email.system1.send</code> 매칭)</li>
<li><code>#</code> : 0개 이상의 단어를 대체 (예: <code>message.#</code> → <code>message.email.system1.send</code> 매칭)</li>
</ul>
<hr>
<h4 id="3-binding-바인딩">3. Binding (바인딩)</h4>
<p>Exchange와 Queue를 <strong>연결해주는 규칙</strong>이다. &quot;이 패턴의 메시지는 이 Queue로 보내라&quot;라는 라우팅 규칙을 정의한다.</p>
<p>예를 들어, 메시지 발송 플랫폼에서 채널별로 Binding을 설정하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>Routing Key 패턴</th>
<th>→ 연결된 Queue</th>
</tr>
</thead>
<tbody><tr>
<td><code>email.*.send</code></td>
<td>email.send.queue</td>
</tr>
<tr>
<td><code>sms.*.send</code></td>
<td>sms.send.queue</td>
</tr>
<tr>
<td><code>push.*.send</code></td>
<td>push.send.queue</td>
</tr>
</tbody></table>
<p>Producer가 Routing Key를 <code>email.system1.send</code>로 보내면, Exchange가 Binding 규칙을 확인하고 <code>email.*.send</code> 패턴에 매칭되는 <code>email.send.queue</code>에 메시지를 넣는다.</p>
<hr>
<h4 id="4-queue-큐">4. Queue (큐)</h4>
<p>메시지가 <strong>순서대로 쌓여서 대기하는 공간</strong>이다. Consumer가 처리할 때까지 메시지를 안전하게 보관한다. FIFO(First In, First Out) 구조로, 먼저 들어온 메시지가 먼저 처리된다.</p>
<p>이 프로젝트에서는 채널별로 Queue가 분리되어 있다. 채널별로 Queue를 분리한 이유는 <strong>독립성</strong> 때문이다. 만약 Queue가 하나라면, 이메일 발송이 밀릴 때 SMS 발송도 같이 밀린다. Queue를 분리하면 각 채널이 서로 영향을 주지 않고 독립적으로 처리할 수 있다.</p>
<hr>
<h4 id="5-consumer-소비자">5. Consumer (소비자)</h4>
<p>Queue에서 메시지를 <strong>꺼내서 실제로 처리하는 쪽</strong>이다.</p>
<p>이 프로젝트에서는 채널별 Consumer 클래스들이 각 Queue를 감시하고 있다가, 메시지가 들어오면 꺼내서 외부 API를 통해 실제 발송을 실행한다.</p>
<hr>
<h4 id="6-전체적인-구조">6. 전체적인 구조</h4>
<p><img src="https://velog.velcdn.com/images/lil_young/post/9d3f387b-0083-4017-bf04-588da764b211/image.png" alt=""></p>
<hr>
<h3 id="ack-acknowledgment">ACK (Acknowledgment)</h3>
<p>Consumer가 메시지를 성공적으로 처리했는지 RabbitMQ에게 알려주는 메커니즘이다.</p>
<ul>
<li><strong>ACK (Acknowledgment)</strong>: &quot;처리 완료했어&quot; → Queue에서 메시지 제거</li>
<li><strong>NACK (Negative Acknowledgment)</strong>: &quot;처리 실패했어&quot; → Dead Letter Queue로 이동</li>
</ul>
<pre><code>Queue에서 메시지 꺼냄
    ↓
Consumer가 처리 시도
    ↓
성공 → ACK 전송 → Queue에서 메시지 삭제
실패 → NACK 전송 → Dead Letter Queue로 이동</code></pre><p>ACK 모드에는 두 가지가 있다.</p>
<table>
<thead>
<tr>
<th>모드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Auto ACK</strong></td>
<td>Consumer가 메시지를 받으면 자동으로 ACK 전송. 처리 도중 서버가 죽으면 메시지 유실</td>
</tr>
<tr>
<td><strong>Manual ACK</strong></td>
<td>코드에서 명시적으로 ACK/NACK를 전송. 처리 완료 후에만 ACK를 보내므로 메시지 유실 방지</td>
</tr>
</tbody></table>
<p>이 프로젝트에서는 <strong>Manual ACK</strong> 모드를 사용한다. Consumer가 메시지를 꺼냈지만 처리 도중 서버가 죽어도, ACK를 보내지 않았기 때문에 메시지가 Queue에 남아있어서 <strong>유실되지 않는다.</strong></p>
<hr>
<h3 id="dead-letter-queue-dlq">Dead Letter Queue (DLQ)</h3>
<p>처리에 실패한 메시지가 모이는 <strong>실패 전용 Queue</strong>이다.</p>
<p>메시지가 DLQ로 가는 경우는 다음과 같다.</p>
<ul>
<li>Consumer가 NACK를 보낸 경우 (처리 실패)</li>
<li>메시지 TTL(Time-To-Live)이 만료된 경우</li>
<li>Queue가 가득 찬 경우</li>
</ul>
<p>DLQ에 쌓인 메시지는 나중에 확인하고 원인을 분석하거나, 재처리할 수 있다.</p>
<pre><code>정상 흐름:  Exchange → Queue → Consumer → ACK → 완료
실패 흐름:  Exchange → Queue → Consumer → NACK → Dead Letter Exchange → DLQ</code></pre><hr>
<h3 id="전체-흐름-정리">전체 흐름 정리</h3>
<p>이메일 50만건을 발송하는 경우의 전체 흐름을 정리하면 다음과 같다.</p>
<pre><code>1. Producer
   &quot;이메일 발송해줘&quot; (routing key: email.*.send)
        ↓
2. Exchange (Topic Exchange)
   routing key 확인 → email.*.send 패턴 매칭
        ↓
3. Binding
   email.*.send → email.send.queue 규칙 적용
        ↓
4. Queue (email.send.queue)
   메시지 대기 (FIFO)
        ↓
5. Consumer
   Queue에서 메시지를 꺼내서 처리
        ↓
6. 외부 API
   실제 이메일 발송
        ↓
7. 결과 처리
   성공 → ACK → Queue에서 제거
   실패 → NACK → DLQ로 이동</code></pre><hr>
<h3 id="rabbitmq를-사용하는-이유">RabbitMQ를 사용하는 이유</h3>
<p>그렇다면 RabbitMQ 없이 API에서 바로 발송하면 안될까?</p>
<table>
<thead>
<tr>
<th></th>
<th>RabbitMQ 없이</th>
<th>RabbitMQ 사용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>대량 발송</strong></td>
<td>50만건 요청 시 API 서버 과부하</td>
<td>Queue에 넣어두고 순차적으로 처리</td>
</tr>
<tr>
<td><strong>서버 장애</strong></td>
<td>발송 중 서버가 죽으면 데이터 유실</td>
<td>Queue에 남아있어서 복구 후 재처리</td>
</tr>
<tr>
<td><strong>속도 조절</strong></td>
<td>한번에 몰려서 외부 API 차단 위험</td>
<td>속도를 조절하며 순차 처리</td>
</tr>
<tr>
<td><strong>채널 분리</strong></td>
<td>이메일이 밀리면 SMS도 같이 밀림</td>
<td>채널별 Queue가 독립적으로 동작</td>
</tr>
<tr>
<td><strong>재시도</strong></td>
<td>실패 시 재시도 로직을 직접 구현</td>
<td>DLQ에 쌓아두고 나중에 재처리</td>
</tr>
</tbody></table>
<p>결국 RabbitMQ는 <strong>&quot;지금 당장 처리할 수 없는 대량의 작업을 안전하게 쌓아두고, 유실 없이 순서대로 처리할 수 있게 해주는 시스템&quot;</strong> 이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[단위 테스트와 통합 테스트]]></title>
            <link>https://velog.io/@lil_young/%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%99%80-%ED%86%B5%ED%95%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@lil_young/%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%99%80-%ED%86%B5%ED%95%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Thu, 25 Dec 2025 12:30:59 GMT</pubDate>
            <description><![CDATA[<p>현재 진행 중인 프로젝트에서 단위 테스트와 통합 테스트를 엑셀로 정리하며 테스트케이스를 작성하다보니, 두 테스트의 차이를 정확하게 이해하고 정리할 필요성을 느꼈다.
이번 글에서는 단위 테스트와 통합 테스트의 개념, 특징, 장단점을 비교해 정리해본다.</p>
<h3 id="단위-테스트unit-test">단위 테스트(Unit Test)</h3>
<p>단위 테스트는 소프트웨어의 <strong>가장 작은 단위가 예상한대로 동작하는지</strong> 검증하는 테스트이다.
여기서 말하는 단위는 보통 <strong>메서드</strong>, <strong>함수</strong>, <strong>클래스</strong>를 말한다.</p>
<p><strong>1. 단위 테스트의 특징</strong></p>
<ul>
<li><strong>독립성</strong><ol>
<li>각 테스트는 서로 독립적으로 실행됨.</li>
<li>다른 테스트 결과에 의존하지 않음</li>
</ol>
</li>
<li><strong>빠른 실행</strong><ol>
<li>작은 코드 단위를 테스트하기 때문에 실행 속도가 빠름</li>
</ol>
</li>
<li><strong>모의 객체(Mock) 사용</strong><ol>
<li>DB, 외부 API, 파일 시스템 등 외부 의존성을 모의 객체(Mock Object)로 대체하여 테스트</li>
</ol>
</li>
<li><strong>세밀한 검증</strong><ol>
<li>특정 메서드나 로직의 내부 동작을 정밀하게 검증 가능</li>
</ol>
</li>
</ul>
<p><strong>2. 단위 테스트의 장점</strong></p>
<ul>
<li><strong>빠른 피드백</strong><ul>
<li>일반적으로 빠르게 실행되어 즉시 결과를 확인할 수 있음</li>
</ul>
</li>
<li><strong>높은 독립성</strong><ul>
<li>각 테스트는 독립적으로 실행되며, 다른 테스트에 영향을 받지 않음</li>
</ul>
</li>
<li><strong>코드 커버리지 향상</strong><ul>
<li>개별 구성 요소를 세밀하게 테스트하여 코드 커버리지를 높일 수 있음</li>
</ul>
</li>
<li><strong>디버깅 용이성</strong><ul>
<li>특정 기능이나 메서드의 문제를 쉽게 찾아내고 수정 가능</li>
</ul>
</li>
</ul>
<p><strong>3. 단위 테스트의 단점</strong></p>
<ul>
<li><strong>제한된 검증 범위</strong><ul>
<li>개별 로직만 검증하므로 시스템 전체 동작은 보장하지 못함</li>
</ul>
</li>
<li><strong>모킹 복잡성</strong><ul>
<li>복잡한 Mock 설정이 필요할 수 있으며, Mock이 잘못되면 테스트 신뢰성이 떨어질 수 있음</li>
</ul>
</li>
</ul>
<h3 id="통합-테스트integration-test">통합 테스트(Integration Test)</h3>
<p>통합 테스트는 <strong>여러 개의 모듈이 서로 연동되어 동작할 때, 문제없이 작동하는지</strong>를 검증하는 테스트이다.</p>
<p>단위 테스트가 하나의 기능에 집중한다면, 통합 테스트는 기능과 기능 사이의 연결에 초점을 둔다.</p>
<p><strong>1. 통합 테스트의 특징</strong></p>
<ul>
<li><strong>모듈 간 상호작용 검증</strong><ul>
<li>개별 모듈이 통합되어 동작할 때, 발생할 수 있는 문제를 검증</li>
</ul>
</li>
<li><strong>실제 환경과 유사한 테스트</strong><ul>
<li>실제 운영 호나경과 비슷한 조건에서 테스트하여 시스템의 신뢰성을 높임</li>
</ul>
</li>
<li><strong>데이터 흐름 검증</strong><ul>
<li>모듈 간의 데이터 흐름과 의존성을 테스트하여 데이터의 일관성과 정확성을 확인</li>
</ul>
</li>
<li><strong>종단 간(E2E) 시나리오 테스트</strong><ul>
<li>사용자 시나리오를 기반으로 시스템의 종단 간 기능을 테스트</li>
</ul>
</li>
</ul>
<p><strong>2. 통합 테스트의 장점</strong></p>
<ul>
<li><strong>실제 운영 환경에서의 신뢰성</strong><ul>
<li>실제 운영 환경과 비슷한 조건에서 테스트하여 시스템의 신뢰성을 높임</li>
</ul>
</li>
<li><strong>종단 간 검증</strong><ul>
<li>시스템의 여러 부분이 함께 올바르게 작동하는지 검증하여 전체적인 기능을 확인</li>
</ul>
</li>
<li><strong>단위 테스트로 찾기 어려운 문제 발견</strong><ul>
<li>트랜잭션, 설정 누락, 연동 오류 등</li>
</ul>
</li>
<li><strong>릴리즈 전 품질 보장</strong><ul>
<li>실 서비스 장애 가능성을 사전에 발견하여 수정 가능</li>
</ul>
</li>
</ul>
<p><strong>3. 통합 테스트의 단점</strong></p>
<ul>
<li><strong>느린 실행 속도</strong><ul>
<li>여러 모듈을 포함하여 테스트하므로 단위 테스트보다 실행 속도가 느림</li>
</ul>
</li>
<li><strong>복잡한 설정</strong><ul>
<li>실제 환경과 유사한 조건을 설정하는 것이 복잡하고 시간이 많이 소요될 수 있음</li>
</ul>
</li>
<li><strong>디버깅 어려움</strong><ul>
<li>문제가 발생했을 때, 단위 테스트보다 원인을 찾기가 어려움</li>
</ul>
</li>
</ul>
<p><strong>마무리</strong>
단위 테스트는 개별 로직의 안정성을, 통합 테스트는 시스템 전체 흐름의 안정성을 보장한다.</p>
<p>엑셀로 테스트케이스를 정리할 때도
<strong>“이건 로직 검증인가?”</strong>
<strong>“아니면 흐름 검증인가?”</strong>
를 기준으로 구분하면 테스트 목적이 훨씬 명확해진다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백업] 백업 방식]]></title>
            <link>https://velog.io/@lil_young/%EB%B0%B1%EC%97%85-%EB%B0%B1%EC%97%85-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@lil_young/%EB%B0%B1%EC%97%85-%EB%B0%B1%EC%97%85-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Sun, 21 Dec 2025 12:49:21 GMT</pubDate>
            <description><![CDATA[<p>실무를 경험하며 백업 방식에도 여러 종류가 있다는 사실을 알게 되었다.
이 글에서는 <strong>전체 백업</strong>, <strong>차등 백업</strong>, <strong>증분 백업</strong>의 개념을 살펴보고,
현재 회사에서 사용 중인 증분 백업을 중심으로 차등 백업과의 차이점을 정리해 보고자 한다.</p>
<h3 id="백업이란">백업이란?</h3>
<blockquote>
<p>원본 파일이 손상되거나 유실될 경우를 대비해, 원본 데이터를 미리 복제하여 어떠한 문제가 일어나도 데이터를 복구할 수 있도록 준비해두는 것을 말한다.</p>
</blockquote>
<p>앞에서 말했듯, 백업 방식에는 크게 <strong>전체 백업</strong>, <strong>차등 백업</strong>, <strong>증분 백업</strong>이 있다.</p>
<hr>
<h3 id="전체-백업">전체 백업</h3>
<p>전체 백업은 <strong>데이터 변경 여부와 관계없이</strong>,  
<strong>백업 시점에 존재하는 모든 데이터</strong>를 그대로 복사해 저장하는 방식이다.<br>다시 말해, <strong>이전 백업의 존재 여부나 파일 변경 여부와 상관없이</strong>,  
<strong>해당 시점의 전체 상태를 그대로 저장</strong>한다.</p>
<p>복구 시 <strong>전체 백업 파일 하나만으로 데이터 복원이 가능</strong>하다는 장점이 있지만,<br><strong>백업 시간과 저장 공간을 많이 필요로 한다</strong>는 단점이 있다.</p>
<hr>
<h3 id="차등-백업">차등 백업</h3>
<p>차등 백업은 <strong>마지막 전체 백업 이후 변경되거나 추가된 모든 데이터</strong>를 백업하는 방식이다.<br>즉, 전체 백업 이후 발생한 데이터 변경 사항을 누적하여 백업한다.</p>
<p>데이터 복구 시에는 <strong>전체 백업과 가장 최근의 차등 백업만 복원</strong>하면 되므로,<br>여러 백업 파일을 순차적으로 복원해야 하는 증분 백업에 비해 <strong>복구 시간이 상대적으로 짧다</strong>.</p>
<hr>
<h3 id="증분-백업">증분 백업</h3>
<p>증분 백업은 <strong>마지막 전체 백업 이후 변경되거나 추가된 데이터만</strong> 백업하는 방식이다.<br>백업 시에는 <strong>마지막 증분 백업 이후의 변경 내용만 백업</strong>되므로,<br><strong>백업 시간과 저장 공간 사용이 가장 적다</strong>는 특징이 있다.</p>
<p>단, 데이터 복구 시에는 <strong>전체 백업과 모든 증분 백업 파일이 필요</strong>하므로,<br>다른 백업 방식에 비해 <strong>복구 시간이 가장 오래 걸리며</strong>,  
<strong>전체 백업에 종속적</strong>이라는 단점이 있다.</p>
<h3 id="예시로-알아보는-차등-백업과-증분-백업의-차이점">예시로 알아보는 차등 백업과 증분 백업의 차이점</h3>
<p>전체 백업을 수행한 이후, A라는 데이터가 추가되어 백업을 수행하고,<br>이후 다시 B라는 데이터가 추가된 상황을 가정해 보자.</p>
<h4 id="차등-백업의-경우">차등 백업의 경우</h4>
<ul>
<li>전체 백업 이후 A 데이터가 추가되어 차등 백업을 수행하면,<br><strong>A 데이터가 백업된다.</strong></li>
<li>이후 B 데이터가 추가된 상태에서 다시 차등 백업을 수행하면,<br><strong>전체 백업 이후에 변경된 모든 데이터가 백업되므로, A와 B 데이터가 함께 백업된다.</strong></li>
</ul>
<p>즉, 차등 백업은 <strong>마지막 전체 백업 이후의 변경 사항을 누적하여 백업</strong>하는 방식이다.</p>
<h4 id="증분-백업의-경우">증분 백업의 경우</h4>
<ul>
<li>전체 백업 이후 A 데이터가 추가되어 증분 백업을 수행하면,<br><strong>A 데이터만 백업된다.</strong></li>
<li>이후 B 데이터가 추가된 상태에서 다시 증분 백업을 수행하면,<br><strong>직전 백업 이후에 변경된 데이터만 백업되므로, B 데이터만 백업된다.</strong></li>
</ul>
<p>즉, 증분 백업은 <strong>마지막 백업 이후의 변경 사항만 백업</strong>하는 방식이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[OAuth] 토큰 관리 방식]]></title>
            <link>https://velog.io/@lil_young/OAuth-%ED%86%A0%ED%81%B0-%EA%B4%80%EB%A6%AC-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@lil_young/OAuth-%ED%86%A0%ED%81%B0-%EA%B4%80%EB%A6%AC-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Fri, 28 Nov 2025 00:28:57 GMT</pubDate>
            <description><![CDATA[<p>OAuth를 이용해 로그인 기능을 구현하다보면, 자연스럽게 생기는 의문이 있다.</p>
<blockquote>
<p>Access Token은 어디에, Refresh Token은 어디에 저장할까?</p>
</blockquote>
<p>이 내용을 알기 전에, 먼저 OAuth 정의부터 알아보자.</p>
<h3 id="oauth-정의">OAuth 정의</h3>
<p><strong>OAuth</strong>는 Open Authorization의 약자로, <strong>신뢰할 수 있는 제 3자 인증 제공자(구글, 카카오, 네이버 등)</strong>를 통해 <strong>프로필 정보, 이메일 등</strong>에 대한 접근 권한을 위임받을 수 있도록 하는 <strong><em>인증 및 권한 부여 프로토콜</em></strong>이다.
<strong>SNS 로그인</strong>은 사용자가 구글, 네이버 등 SNS 서비스에 로그인을 하고, 이 로그인과 OAuth 기술을 통해 사용자 정보를 해당 SNS로 부터 받아와 웹 서비스에 회원가입과 로그인을 하는 것을 말한다.</p>
<p><strong><em>정의에 대해 알아봤으니, 이제 어떻게 흘러가는지 알아보자.예시는 구글 OAuth를 기준으로 설명하겠다.</em></strong></p>
<h3 id="oauth-동작-과정">OAuth 동작 과정</h3>
<ol>
<li>사용자가 구글 로그인 버튼을 누른다.</li>
<li>우리 서버에서 구글에서 제공하는 로그인 화면으로 Redirect한다.<ul>
<li>구글 서버에 제어권이 넘어간다.</li>
</ul>
</li>
<li>구글 로그인 화면에서 사용자가 로그인을 한다.</li>
<li>구글에서 인가 코드를 우리 서버에게 전달한다.<ul>
<li>구글에서 우리 서버에게 무언가를 전달하기 위해서는 Redirect 방식으로 전달해야 한다.</li>
<li>HTTP Redirect(302)는 GET 요청을 발생시키므로 Body를 포함할 수 없다. 그래서 구글은 Redirect 응답(302)을 통해 우리 서버의 redirect_uri로 이동시키면서, 인가 코드와 함께 state 값을 쿼리 파라미터(URL)에 전달한다.</li>
<li>Access Token이나 사용자 정보를 직접 전달하지 않는 이유는 URL 노출 위험(로그, 캐시 등)이 있기 때문이고, 대신 짧은 수명의 인가 코드를 주고 서버에서 안전하게 토큰으로 교환하도록 설계되어 있다.</li>
</ul>
</li>
<li>우리 서버에서 인가 코드를 받으면 다음 두 가지 방식 중 하나로 처리할 수 있다.
 5-1. 인가 코드를 그대로 사용해 구글에 사용자 정보를 요청한다.
 5-2. 인가 코드를 사용해 Access Token과 Refresh Token을 발급받고, 필요한 경우 Refresh Token을 서버에 저장한 뒤, Access Token을 이용해 사용자 정보를 요청한다.</li>
</ol>
<p>구글에서는 후자의 방식을 권장한다. 왜냐하면 인가 코드를 가지고 Access Token을 요청할 때는, 구글 서버가 아닌 우리 서버에서 요청을 보내는 것이므로 HTTP POST 요청을 해서, HTTP Response으로 데이터를 받을 수 있기 때문에, 인가 코드를 Body에 담아서 보낼 수 있으므로 보안상 안전하기 때문이다.</p>
<p><strong><em>그럼 &quot;인가 코드를 누군가 중간에 가로채면 위험한 거 아닌가?&quot;</em></strong>
이런 의문이 들 수 있지만, 실제로는 크게 걱정할 필요가 없다.</p>
<p>예를 들어, 우리 서버 주소가 <strong><a href="https://myserver.com">https://myserver.com</a></strong>이라고
 가정해 보자.
사용자가 구글 로그인을 완료하면, 구글은 사전에 등록된 redirect_uri로만 인가 코드를 전송한다.
즉, 인가 코드는 등록된 서버가 아니면 절대 전달되지 않는다.</p>
<p>또한 우리 서버가 이 인가 코드를 들고 Google Token Endpoint에 토큰 발급 요청을 보내면, 구글은
<strong>1. 인가 코드가 유효한지
2. redirect_uri가 정확히 일치하는지
3. client_id가 맞는지</strong>
를 모두 검증한 뒤에만 Access Token을 발급해 준다.</p>
<p>따라서 인가 코드를 단순히 탈취하더라도,
redirect_uri·client_id가 맞지 않으면 토큰을 발급받을 수 없기 때문에 실제 보안 위협은 거의 없다.</p>
<p>이런 안전성이 가능한 이유는 인가 코드가 내부적으로 client_id, redirect_uri 등 검증용 정보와 강하게 묶여 있기 때문이다.</p>
<p>이처럼 인가 코드 흐름은 보안적으로 안전하게 설계되어 있다.
그렇다면 이제 우리 서버가 구글이나 카카오로부터 어떤 사용자 정보를 받아올 수 있는지,
즉 정보의 범위(scope) 가 무엇인지 살펴볼 필요가 있다.</p>
<h3 id="scope">Scope</h3>
<p>Scope란 자사 서비스가 구글, 카카오 등 OAuth 제공자에게 요청할 수 있는 사용자 정보의 범위를 의미한다.
예를 들어 이메일, 프로필, 닉네임 등의 항목이 여기에 해당한다.
이 Scope는 인가 코드를 요청할 때 함께 지정하며, OAuth 제공자는 이 Scope에 따라 사용자에게 “어떤 정보를 제공해도 되는지”에 대한 동의를 받는다.</p>
<hr>
<h2 id="oauth-구현-방식">OAuth 구현 방식</h2>
<p>이제 OAuth 구현 방식에 대해 알아보자.</p>
<h3 id="프론트인가코드-수신--서버access-token-교환-방식">프론트(인가코드 수신) + 서버(Access Token 교환) 방식</h3>
<p>가장 일반적으로 사용하는 구조로, 프론트엔드에서 인가 코드를 받고, 실제 토큰 교환과 인증 처리(Access Token 발급, 사용자 정보 요청)는 백엔드 서버에서 담당하는 방식이다.</p>
<p><strong>동작 방식</strong></p>
<ol>
<li>프론트엔드에서 구글 로그인 화면을 구글에 요청해 구글 로그인 페이지로 사용자를 Redirect 한다.</li>
<li>사용자가 구글 로그인 화면에서 로그인을 완료한다.</li>
<li>구글에서 내 프론트화면으로 인가코드를 URL에 담아 Redirect한다.</li>
<li>프론트엔드가 받은 인가코드를 Spring(백엔드) 서버로 전달한다.</li>
<li>서버에서 인가코드를 사용해 구글 서버에 Access Token.(필요 시 Refresh Token 포함)을 요청한다.</li>
<li>발급받은 Access Token을 통해 구글 API에서 사용자 정보(id, email, name 등)를 구글 서버에 요청 후 인증하여 조회한다.</li>
<li>사용자 정보가 확인되면 서버는 자체 로그인 처리 후 JWT 토큰을 클라이언트에게 발급한다.
<img src="https://velog.velcdn.com/images/lil_young/post/2ae4f5a8-c84e-463a-a5f8-9776bbfdd2eb/image.png" alt=""></li>
</ol>
<p><strong>장점</strong></p>
<ul>
<li>JWT를 서버에서 직접 발급하므로, 클라이언트는 안전하게 Body를 통해 토큰을 전달받을 수 있다.</li>
<li>인가 코드 → 토큰 교환 → 사용자 정보 요청까지 모든 단계가 서버에서 이루어지므로
흐름이 명확하며, 디버깅과 로깅에 매우 유리하다.</li>
<li>프론트에는 인가 코드만 잠시 노출되고, Access/Refresh Token은 모두 서버에서 처리되어 보안성이 높다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>프론트엔드에 client_id가 노출되는 구조이므로 완전한 보안 환경은 아니다. (여기서 말하는 client_id는 구글 API를 사용하기 위해 발급받는 공개용 키이며, 단독으로는 큰 위험 요소는 아니다. redirect_uri 검증이 있기 때문에 악용도 어렵다.)</li>
<li>인증 로직 대부분이 백엔드에 집중되므로 서버 구현이 상대적으로 더 복잡해진다.</li>
</ul>
<h3 id="서버에서-인가코드-access-token-모두-처리하는-방식">서버에서 인가코드, Access Token 모두 처리하는 방식</h3>
<p>Spring의 oauth2-client 의존성을 활용해, 서버에서 인가코드, AccessToken 발급, 사용자 요청까지 모든 절차를 서버에서 자동 처리하는 방식이다.</p>
<p><strong>장점</strong></p>
<ul>
<li>Spring Security OAuth2가 전체 흐름을 자동으로 처리하기 때문에 구현 코드가 간결하다.</li>
<li>client_id, client_secret 등 민감한 키 값을 서버 내부에서만 보관하므로 노출 위험이 적다.</li>
<li>인증 과정 대부분이 백엔드 내부에서 이루어져, 프론트 노출 요소가 적다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>최종적으로 사용자에게 JWT 토큰을 줄 때, Redirect 방식을 취할 수 밖에 없어서 보안상 취약점이 존재한다.<ul>
<li>이유는, 사용자가 구글 로그인 버튼을 누르면 클라이언트에서 Spring 서버로 이동하라고 Redirect를 하고, 서버에서 구글 로그인 페이지로 이동하기 위해 다시 Redirect를 한다. 이후 인가 코드를 받고, 이 코드를 이용해 서버에서 Access Token 발급과 사용자 정보 요청을 처리한 뒤 클라이언트로 Access Token을 전달해야 하는데, 이때도 Redirect 방식으로 전달할 수밖에 없기 때문에 body에 넣어 보낼 수 없다.</li>
</ul>
</li>
<li>모든 절차가 라이브러리를 통해 통합되고 자동화되어 있어 코드 파악과 디버깅이 어렵다.</li>
<li>인가코드가 Redirect될 때, 인가코드 요청을 했던 동일 서버가 아닌 경우 문제가 발생한다.
<img src="https://velog.velcdn.com/images/lil_young/post/013ae67e-a4ba-42da-9ff4-3443bb7c0015/image.png" alt="">
여러 서버를 띄운다는 가정하에, 동일 서버로 인식하기 위해서는 로드밸런서에서 별도로 sticky 옵션 지정이 필요하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS] Call By Value / Call By Reference]]></title>
            <link>https://velog.io/@lil_young/CS-Call-By-Value-Call-By-Reference</link>
            <guid>https://velog.io/@lil_young/CS-Call-By-Value-Call-By-Reference</guid>
            <pubDate>Thu, 20 Nov 2025 10:34:40 GMT</pubDate>
            <description><![CDATA[<p>분명 아는 내용인데도, 막상 질문을 받으면 갑자기 기억이 안 나는 개념들이 있다.
이 ‘CS’ 카테고리에서는 그런 개념들을 다시 정리해볼 예정이다.</p>
<h3 id="오늘은-call-by-value와-call-by-reference에-대해-복습해보자">오늘은 Call By Value와 Call By Reference에 대해 복습해보자.</h3>
<hr>
<h1 id="call-by-value">Call By Value</h1>
<p>Call By Value는 말 그대로 값(value)을 복사해서 전달하는 방식이다.
즉, 함수에 인자를 넘길 때 원본 값이 아닌 ‘복사본’이 전달된다.</p>
<blockquote>
<p>원본 값은 절대 변경되지 않는다.
함수 내부에서 값을 바꿔도, 바뀌는 건 복사본뿐.</p>
</blockquote>
<p>Java의 기본 타입(int, double 등)이 대표적인 Call By Value 방식이다.</p>
<h1 id="call-by-reference">Call By Reference</h1>
<p>Call By Reference는 값의 주소(reference)를 전달하는 방식이다.
즉, 변수에 들어있는 메모리 주소를 넘기기 때문에, 함수 내부에서 값을 바꾸면 원본 값도 함께 변경된다.</p>
<p>C/C++에서 포인터를 사용한 전달 방식이 대표적이다.</p>
<h3 id="그러면-java는-call-by-value-인가-call-by-reference-인가">그러면 Java는 &#39;Call By Value&#39; 인가? &#39;Call By Reference&#39; 인가?</h3>
<p>결론부터 말하면, <strong>Java</strong>는 기본적으로 <strong>Call By Value</strong>이다.</p>
<p>필자는 알고리즘 문제를 풀 때, 아래와 같이 얕은 복사때문에 헷갈린 적이 있다.</p>
<pre><code class="language-java">int[] a = {1, 2, 3};
int[] b = a;

System.out.println(Arrays.toString(b)); // [1, 2, 3]
b[0] = 4;
System.out.println(Arrays.toString(a)); // [4, 2, 3]</code></pre>
<p>그래서 Java에서도 Call By Reference를 사용하는 줄 알았는데, 위와 같은 상황은 메모리 주소를 넘긴 것이 아니라, 메모리 주소 값 자체를 복사해서 b라는 변수에 할당한 것이기 때문에 Call By Value 이다.
<img src="https://velog.velcdn.com/images/lil_young/post/1c9e37ef-c9e8-42b1-a162-772c95dc92a3/image.png" alt=""></p>
<p>그래서 보통 아래와 같이 Arrays 클래스의 copyof() 메서드를 사용해 깊은 복사를 사용한다.</p>
<pre><code class="language-java">int[] a = {1, 2, 3};
int[] b = Arrays.copyOf(a, a.length);

System.out.println(Arrays.toString(b)); // [1, 2, 3]
b[0] = 4;
System.out.println(Arrays.toString(a)); // [1, 2, 3]
System.out.println(Arrays.toString(b)); // [4, 2, 3]</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BOJ 2573] 빙산]]></title>
            <link>https://velog.io/@lil_young/BOJ-2573-%EB%B9%99%EC%82%B0</link>
            <guid>https://velog.io/@lil_young/BOJ-2573-%EB%B9%99%EC%82%B0</guid>
            <pubDate>Wed, 19 Nov 2025 14:51:47 GMT</pubDate>
            <description><![CDATA[<h3 id="풀이-코드">[풀이 코드]</h3>
<pre><code class="language-java">
import java.util.*;
import java.io.*;

public class 빙산 {
    static int N, M;
    static int[][] arr;
    static boolean[][] v;
    static List&lt;Point&gt; iceList;
    static int year, iceCount;
    static boolean isDivide;
    static int[] dr = {0, 0, 1, -1};
    static int[] dc = {1, -1, 0, 0};
    static int[][] zeroNumber;


    static class Point {
        int x, y;
        Point(int x, int y) {
            this.x=x;
            this.y=y;
        }
    }
    public static void main(String[] args) throws Exception {
        // 빙산 1, 바다 0
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine().trim());
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        arr = new int[N][M];
        iceList = new ArrayList&lt;Point&gt;();
        for (int i = 0; i &lt; N; i++) {
            st = new StringTokenizer(br.readLine().trim());
            for (int j = 0; j &lt; M; j++) {
                arr[i][j] = Integer.parseInt(st.nextToken());
                if(arr[i][j] &gt; 0) iceList.add(new Point(i, j));
            }
        }

        year = 0;
        isDivide = false;
        zeroNumber = new int[N][M];
        while(!isDivide &amp;&amp; !iceList.isEmpty()) {
            // 각 빙산 위치마다 0의 개수를 센다
            for(Point p : iceList) {
                int cnt = zeroCount(p);
                zeroNumber[p.x][p.y] = cnt; 
            }

            // 각 빙산을 녹인다.
            for(int i=0; i&lt;iceList.size(); i++) {
                Point p = iceList.get(i);
                if(arr[p.x][p.y] - zeroNumber[p.x][p.y] &lt;= 0) {
                    arr[p.x][p.y] = 0;
                    iceList.remove(i--);
                }else {
                    arr[p.x][p.y] -= zeroNumber[p.x][p.y]; 
                }
                zeroNumber[p.x][p.y] = 0;
            }

            // 빙산 덩어리 갯수를 센다.
            iceCount = 0;
            v = new boolean[N][M];
            for(Point p : iceList) {
                if(!v[p.x][p.y]) {
                    dfs(p.x, p.y);
                    iceCount++;
                }
            }

            if(iceCount &gt;= 2) isDivide = true;
            year++;
        }

        if(iceList.isEmpty()) {
            System.out.println(0); return;
        }
        System.out.println(year);

    }

    private static int zeroCount(Point p) {
        int num = 0;
        for(int d=0; d&lt;4; d++) {
            int nr = p.x + dr[d];
            int nc = p.y + dc[d];
            if(nr&lt;0 || nr&gt;=N || nc&lt;0 || nc&gt;=M) continue;
            if(arr[nr][nc] == 0) num++;
        }

        return num;
    }

    private static void dfs(int r, int c) {
        v[r][c] = true;
        for (int d = 0; d &lt; 4; d++) {
            int nr = r + dr[d];
            int nc = c + dc[d];
            if(nr&lt;0 || nr&gt;=N || nc&lt;0 || nc&gt;=M || v[nr][nc] || arr[nr][nc] &lt;= 0) continue;
            dfs(nr, nc);
        }
    }

}</code></pre>
<p>시뮬레이션 + 4방탐색 문제다.</p>
<p>풀이시간은 53분이 걸렸다.</p>
<p>56%에서 시간초과가 나서, 계속 문제를 다시 읽어본 다음 두 덩어리 이상으로 나누어지지 않는 경우의 수를 놓쳤다는 걸 알게됐다.
그래서 while문 탈출 조건에 iceList가 다 비어있을 경우를 추가하고, 결과 값도 수정했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 숫자 문자열과 영단어]]></title>
            <link>https://velog.io/@lil_young/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%88%AB%EC%9E%90-%EB%AC%B8%EC%9E%90%EC%97%B4%EA%B3%BC-%EC%98%81%EB%8B%A8%EC%96%B4</link>
            <guid>https://velog.io/@lil_young/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%88%AB%EC%9E%90-%EB%AC%B8%EC%9E%90%EC%97%B4%EA%B3%BC-%EC%98%81%EB%8B%A8%EC%96%B4</guid>
            <pubDate>Tue, 04 Nov 2025 11:12:59 GMT</pubDate>
            <description><![CDATA[<h3 id="풀이-코드">[풀이 코드]</h3>
<pre><code class="language-java">import java.util.*;
import java.io.*;

class Solution {
    static Map&lt;Integer, String&gt; map;
    public int solution(String s) {
        String answer = &quot;&quot;;
        map = new HashMap&lt;Integer, String&gt;();
        map.put(0, &quot;zero&quot;);
        map.put(1, &quot;one&quot;);
        map.put(2, &quot;two&quot;);
        map.put(3, &quot;three&quot;);
        map.put(4, &quot;four&quot;);
        map.put(5, &quot;five&quot;);
        map.put(6, &quot;six&quot;);
        map.put(7, &quot;seven&quot;);
        map.put(8, &quot;eight&quot;);
        map.put(9, &quot;nine&quot;);

        String str = &quot;&quot;;
        for (int i = 0; i &lt; s.length(); i++) {
            // 숫자일 때
            if(Character.isDigit(s.charAt(i))) {
                answer += s.substring(i, i+1);
            }else {
                str+=s.charAt(i);

                if(map.containsValue(str)) {
                    for(Map.Entry&lt;Integer, String&gt; a : map.entrySet()) {
                        if(a.getValue().equals(str)) {
                            answer += a.getKey();
                        }
                    }
                    str = &quot;&quot;;
                }

            }
        }
        int num = Integer.parseInt(answer);
        return num;
    }
}</code></pre>
<p>최근에 알고리즘 문제를 풀지 않아가지고, 알고리즘 접근 방법과 자바에서 사용하는 주요 메서드들에 대해 기억하는데 굉장히 오래걸렸다. ㅠㅠ
결국 이런 쉬운 문제도 30분이 걸리다니,, 오늘부터 다시 1일 1코테를 시작해보겠다.</p>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/81301?language=java">https://school.programmers.co.kr/learn/courses/30/lessons/81301?language=java</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 자바 특징]]></title>
            <link>https://velog.io/@lil_young/Java-%EC%9E%90%EB%B0%94-%ED%8A%B9%EC%A7%95</link>
            <guid>https://velog.io/@lil_young/Java-%EC%9E%90%EB%B0%94-%ED%8A%B9%EC%A7%95</guid>
            <pubDate>Sat, 01 Nov 2025 01:14:22 GMT</pubDate>
            <description><![CDATA[<h3 id="1-객체-지향-언어">1. 객체 지향 언어</h3>
<ul>
<li>절차 지향 언어와 다르게 하나의 기능을 객체형태로 만들어 객체들을 결합하여 하나의 프로그램을 만드는 것</li>
</ul>
<blockquote>
<h3 id="객체-지향-4대-특성">객체 지향 4대 특성</h3>
</blockquote>
<h4 id="1-캡슐화encapsulation">1. 캡슐화(Encapsulation)</h4>
<ul>
<li>객체의 속성과 기능을 하나로 묶고 외부에서 직접 접근하지 못하게 하여 데이터의 안정성과 일관성을 보장하는 것<blockquote>
<blockquote>
<p>접근제어자 (정보 은닉을 코드로 구현하는 도구)</p>
<ul>
<li>public (+) : 클래스 내부/외부에서 접근이 가능하다.</li>
<li>protected (#) : 해당 객체를 상속받은 객체 내부에서 접근할 수 있다. 단순 인스턴스에선 접근할 수 없다.</li>
<li>default : 동일한 패키지 내의 객체에서 인스턴스를 생성하여 접근할 수 있다.</li>
<li>private (-) : 클래스 내부에서 접근이 가능하나 외부에서 접근할 수 없다.</li>
</ul>
</blockquote>
</blockquote>
<h4 id="2-상속inheritance">2. 상속(Inheritance)</h4>
</li>
<li>재사용 (extends), 부모 클래스에서 선언한 변수나 메서드를 자식 클래스에서 직접 만들지 않고 상속을 받음으로써, 자식 클래스가 부모 클래스의 변수나 메서드를 사용할 수 있는 기능</li>
<li>자바의 모든 class는 Object 클래스의 후손이다.</li>
<li>부모 클래스의 생성자, 초기화 블록은 상속이 되지 않는다.</li>
<li>부모의 private 멤버는 상속은 되지만, 직접 접근은 불가하다.</li>
<li>자바는 단일 상속만 지원한다.</li>
<li>Is a (상속관계) : 자식 클래스는 (하나의) 부모 클래스이다.</li>
<li>Has a (연관관계) : 한 클래스 멤버변수로 다른 클래스 타입의 참조 변수를 선언한다.<h4 id="3-추상화abstraction">3. 추상화(Abstraction)</h4>
</li>
<li>객체에서 공통된 속성과 행위를 추출한다.</li>
<li>유연성을 확보하기 위해 구체적인 것은 제거한다는 의미이다.</li>
<li>프로그램에서 필요한 공통점을 추출하고, 불필요한 공통점을 제거하는 과정이다.<h4 id="4-다형성polymorphism">4. 다형성(Polymorphism)</h4>
하나의 객체가 여러 형태로 동작할 수 있는 성질</li>
<li>오버라이딩 (Overriding)<ul>
<li>부모 클래스의 메서드를 자식 클래스에서 재정의(덮어쓰기) 하는 것</li>
<li>메서드 헤드라인 위에 반드시 Annotation, @Override 표시</li>
<li>부모 클래스와 자식 클래스의 메서드를 비교하여 메서드 이름이 동일하거나 매개변수의 개수, 타입, 순서가 동일하다면 메서드를 재정의 할 수 있다. (단, 접근제어자 private 또는 final, static 메서드는 오버라이딩이 불가능함)</li>
</ul>
</li>
<li>오버로딩 (Overloading)<ul>
<li>한 클래스 내에서 같은 이름의 메서드를 여러 개 정의하는 것</li>
<li>같은 메서드 이름, 다른 매개변수 선언 조건이어야 사용 가능</li>
</ul>
</li>
</ul>
<h3 id="2-컴파일--인터프리터-언어">2. 컴파일 + 인터프리터 언어</h3>
<ul>
<li>자바는 컴파일 언어인 동시에 인터프리터 언어이다.</li>
<li>자바는 텍스트 소스를 컴파일하여 클래스파일로 만든 다음 자바 런타임이 클래스 파일을 인터프리터 하면서 실행된다.</li>
</ul>
<blockquote>
<h4 id="컴파일">컴파일</h4>
<p>사람이 작성한 소스코드를 기계가 이해할 수 있는 코드(클래스파일)로 변환하는 과정</p>
</blockquote>
<ul>
<li>개발자가 작성한 자바 코드는 Hello.java 같은 텍스트 파일이다.</li>
<li>이걸 바로 컴퓨터가 실행할 수 없기 때문에, 컴파일러(javac)가 바이트코드(.class)로 바꿔준다. (바이트코드는 JVM이 이해할 수 있는 코드)<h4 id="인터프리터">인터프리터</h4>
컴파일된 바이트코드(.class)를 한 줄씩 읽고 실행하는 과정</li>
<li>실제 실행 시에는 JVM이 이 .class 파일을 한 줄씩 읽고 실행한다.</li>
</ul>
<h3 id="3-독립적인-플랫폼">3. 독립적인 플랫폼</h3>
<ul>
<li>어떠한 운영체제라도 독립적으로 자바언어를 사용할 수 있다.</li>
<li>그 이유는 JVM에 의해서 실행되기 때문이다.</li>
</ul>
<blockquote>
<h4 id="jvm-java-virtual-machine">JVM (Java Virtual Machine)</h4>
<p>자바 프로그램을 실행하기 위한 가상의 컴퓨터</p>
</blockquote>
<ul>
<li>개발자가 만든 코드를 컴파일러가 바이트코드(.class)로 변환한다.</li>
<li>여기서 만들어진 바이트코드는 운영체제에 직접 종속되지 않는 중간 코드이다.
각 운영체제에는 그 OS에 맞는 JVM이 설치되어 있어서, JVM이 이 바이트코드를 해석(인터프리트)하여 실행시켜 준다.</li>
</ul>
<h3 id="4-자동-메모리-관리gc-garbage-collection">4. 자동 메모리 관리(GC, Garbage Collection)</h3>
<ul>
<li>자바는 개발자가 직접 메모리에 접근할 수 없으며 자바가 직접 관리한다.</li>
<li>객체 생성시 자동적으로 메모리 영역을 찾아서 할당하고, 사용하지 않는 객체는 제거해준다.</li>
<li>C언어는 개발자가 직접 코드를 작성해야 했지만, 자바는 이러한 작업을 자동으로 해주기 때문에 메모리 관리에 신경쓰지 않아도 된다.</li>
</ul>
<blockquote>
<h4 id="jvm의-gcgarbage-collection">JVM의 GC(Garbage Collection)</h4>
</blockquote>
<ul>
<li><strong>작동방식</strong></li>
</ul>
<ol>
<li>개발자가 new 키워드로 객체를 생성하면, JVM이 자동으로 Heap 영역에 메모리를 할당한다.</li>
<li>더 이상 해당 객체를 참조하는 변수가 없으면, GC가 이를 &quot;쓰레기(Garbage)&quot; 객체로 판단한다.</li>
<li>일정 시점에 GC가 동작하여 이러한 객체를 메모리에서 제거하고, 메모리를 자동으로 회수한다.</li>
</ol>
<ul>
<li>참고</li>
</ul>
<ol>
<li>GC는 JVM 의 Heap 메모리를 감시하면서 사용되지 않는 객체를 Mark(표시)하고, Sweep(제거)하는 구조로 동작한다.</li>
<li>GC는 백그라운드에서 자동 실행되며, 개발자는 필요 시 System.gc()로 수동 요청할 수도 있지만, 권장되지는 않는다.</li>
</ol>
<h3 id="5-멀티-쓰레딩-지원">5. 멀티 쓰레딩 지원</h3>
<ul>
<li>하나의 프로그램 단위가 동일한 쓰레드를 동시에 수행할 수 있다.</li>
<li>운영체제마다 멀티 쓰레드를 이용하는 API가 다르나 자바의 경우는 자바 API를 사용하기 때문에 쉽게 구현 가능하다.</li>
</ul>
<blockquote>
<h4 id="멀티스레딩">멀티스레딩</h4>
<p>하나의 프로그램(프로세스) 안에서 여러 작업(스레드)을 동시에 실행할 수 있게 하는 기술</p>
</blockquote>
<h3 id="6-동적이다">6. 동적이다.</h3>
<ul>
<li>객체간의 상호작용을 정의하기 때문에 필요하지 않는 객체는 생성되지 않고, 필요한 객체만 생성하여 사용한다.</li>
<li>오류가 발생하면 발생한 오류의 클래스만 수정하면 되므로 전체를 수정할 필요가 없다. 즉, 유지보수를 쉽고, 빠르게 진행할 수 있다.</li>
</ul>
<h3 id="7-안전하고-강력하다">7. 안전하고 강력하다.</h3>
<ul>
<li>모든 메모리 접근을 자바 시스템이 관리하기 때문에 시스템 붕괴의 우려가 없다.</li>
<li>자바는 포인터 개념이 없고 유형 정의가 강력하여 실행 전에 클래스 파일을 이용한 프로그램 검사가 가능하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링] JDBC / MyBaits / JPA]]></title>
            <link>https://velog.io/@lil_young/%EC%8A%A4%ED%94%84%EB%A7%81-JDBC-MyBaits-JPA</link>
            <guid>https://velog.io/@lil_young/%EC%8A%A4%ED%94%84%EB%A7%81-JDBC-MyBaits-JPA</guid>
            <pubDate>Fri, 31 Oct 2025 01:27:04 GMT</pubDate>
            <description><![CDATA[<h3 id="jdbcjava-database-connectivity">JDBC(Java Database Connectivity)</h3>
<p>JDBC는 DB에 접근할 수 있도록 Java에서 제공하는 API이다.
모든 Java의 Data Access 기술(MyBatis, JPA 등)의 근간이 되는 기술이다.</p>
<p><strong>JDBC 역할</strong></p>
<ul>
<li>DB 연결(Connection)</li>
<li>SQL 실행</li>
<li>결과 처리</li>
<li>리소스 해제</li>
</ul>
<h3 id="jdbc-driver">JDBC Driver</h3>
<p>JDBC Driver는 자바 프로그램의 요청을 DBMS가 이해할 수 있는 프로토콜로 변환해주는 클라이언트 사이드 어댑터이다.</p>
<p>DB마다 Driver가 존재하므로, 자신이 사용하는 DB에 맞는 JDBC Driver를 사용한다.</p>
<h3 id="mybatis">MyBatis</h3>
<p>MyBatis는 JDBC의 반복적이고 번거로운 코드를 줄이기 위해 만들어진 SQL 매퍼 기반 데이터 접근 프레임워크이다.</p>
<p>개발자가 작성한 SQL 문을 Java 객체로 자동으로 매핑 시켜준다.</p>
<p><strong>특징</strong></p>
<ul>
<li>Java 코드와 SQL 매핑하기 때문에, SQL 문을 직접 작성하고 최적화할 수 있다.</li>
<li>동적 SQL 생성이 가능하다.</li>
</ul>
<h3 id="jpajava-persistence-api">JPA(Java Persistence API)</h3>
<p>JPA는 자바 진영의 표준 ORM(Object Relational Mapping) 기술로, 객체(Entity)와 데이터베이스 테이블 간 매핑을 자동으로 처리해 개발자가 SQL을 직접 작성하지 않고도 데이터를 관리할 수 있게 해준다.</p>
<p><strong>특징</strong></p>
<ul>
<li>자바에서의 ORM을 위한 표준 인터페이스를 제공한다.</li>
<li>객체와 데이터베이스 간의 자동 매핑을 지원한다.</li>
<li>객체 지향적인 개발에 중점을 둔다.</li>
<li>DB가 변경되어도 SQL 문을 다시 작성할 필요가 없다.</li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th><strong>MyBatis</strong></th>
<th><strong>JPA</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>개념</strong></td>
<td>SQL Mapper 기반 프레임워크</td>
<td>ORM (Object Relational Mapping) 기반 표준 프레임워크</td>
</tr>
<tr>
<td><strong>동작 방식</strong></td>
<td>SQL을 직접 작성하고 XML 또는 어노테이션으로 매핑</td>
<td>엔티티 객체를 통해 테이블과 매핑, SQL은 내부적으로 자동 생성</td>
</tr>
<tr>
<td><strong>개발 중심</strong></td>
<td><strong>SQL 중심 개발</strong> (쿼리 제어 중점)</td>
<td><strong>객체 중심 개발</strong> (엔티티 중심 도메인 설계)</td>
</tr>
<tr>
<td><strong>자동화 수준</strong></td>
<td>JDBC의 반복 코드 제거 정도 (반자동)</td>
<td>SQL 생성, 매핑, 트랜잭션 관리까지 자동 (완전 자동화)</td>
</tr>
<tr>
<td><strong>SQL 작성 여부</strong></td>
<td>개발자가 직접 작성</td>
<td>직접 작성 필요 없음 (단, 복잡한 경우 @Query 가능)</td>
</tr>
<tr>
<td><strong>동적 SQL 지원</strong></td>
<td><code>&lt;if&gt;</code>, <code>&lt;where&gt;</code>, <code>&lt;foreach&gt;</code> 등으로 동적 쿼리 작성 가능</td>
<td>Criteria API 또는 QueryDSL 등 별도 도구 필요</td>
</tr>
<tr>
<td><strong>성능 튜닝</strong></td>
<td>세밀한 쿼리 제어 가능 → <strong>튜닝 용이</strong></td>
<td>내부 SQL 자동 생성 → 튜닝 어려움 (쿼리 최적화 필요)</td>
</tr>
<tr>
<td><strong>DB 독립성</strong></td>
<td>낮음 (DB별 SQL 수정 필요)</td>
<td>높음 (DBMS 변경에도 코드 영향 적음)</td>
</tr>
<tr>
<td><strong>학습 난이도</strong></td>
<td>낮음 (SQL 친숙한 개발자에게 쉬움)</td>
<td>높음 (ORM 개념과 영속성 이해 필요)</td>
</tr>
<tr>
<td><strong>대표 어노테이션</strong></td>
<td><code>@Mapper</code>, <code>@Select</code>, <code>@Insert</code></td>
<td><code>@Entity</code>, <code>@Id</code>, <code>@GeneratedValue</code>, <code>@OneToMany</code> 등</td>
</tr>
<tr>
<td><strong>사용 목적</strong></td>
<td>SQL 제어와 튜닝이 중요한 서비스</td>
<td>객체지향적인 설계와 생산성이 중요한 서비스</td>
</tr>
<tr>
<td><strong>장점</strong></td>
<td>SQL 자유도 높음, 직관적인 로직 제어</td>
<td>생산성 높음, 반복 코드 최소화, DB 독립성</td>
</tr>
<tr>
<td><strong>단점</strong></td>
<td>SQL 관리 부담, 유지보수 어려움</td>
<td>성능 예측 어려움, 학습 비용 높음</td>
</tr>
<tr>
<td><strong>대표 사용 예시</strong></td>
<td>고성능 대형 트래픽 서비스, 복잡한 쿼리 다루는 시스템</td>
<td>CRUD 중심 웹 서비스, 객체지향 설계 기반 시스템</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링] Spring MVC]]></title>
            <link>https://velog.io/@lil_young/%EC%8A%A4%ED%94%84%EB%A7%81-Spring-MVC</link>
            <guid>https://velog.io/@lil_young/%EC%8A%A4%ED%94%84%EB%A7%81-Spring-MVC</guid>
            <pubDate>Thu, 30 Oct 2025 10:20:26 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-mvc-구조">Spring MVC 구조</h1>
<p><img src="https://velog.velcdn.com/images/lil_young/post/ab0a2795-2542-446b-8790-841d3a031818/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/lil_young/post/8f70a9c6-272d-401e-9e81-8fd9861004db/image.png" alt=""></p>
<h3 id="동작-과정">동작 과정</h3>
<ol>
<li>Client 요청을 DispatcherServlet이 받는다.</li>
<li>DispatcherServlet은 HandlerMapping를 호출하여 요청 정보를 전달한다. 요청 정보(URL)를 분석하여 적합한 Controller를 선택한다.</li>
<li>DispatcherServlet이 다음으로 HandlerAdapter를 호출한다. HandlerAdapter는 요청한 URL에 적합한 Method를 찾는다.</li>
<li>HandlerAdapter가 Controller로 요청을 위임한다. Controller는 Business Logic을 처리하고, View에 전달할 결과를 Model 객체에 저장한다.</li>
<li>Controller는 view name을 DispatcherServlet에 리턴한다.</li>
<li>DispatcherServlet이 ViewResolver를 호출하여 Controller가 리턴한 view name을 기반으로 적합한 View를 찾는다.</li>
<li>DispatcherServlet이 View 객체에 처리 결과를 전달하여 보여준다.</li>
<li>View 객체는 해당하는 View를 호출한다. View는 Model 객체에 화면을 표시하는 데 필요한 객체를 가져와 화면에 처리하고, Client에 넘겨준다.</li>
</ol>
<h3 id="dispatcherservlet"><strong><code>DispatcherServlet</code></strong></h3>
<p>DispatcherServlet은 Spring MVC에서 사용자의 모든 요청을 받아 처리하는 프론트 컨트롤러로, 웹 애플리케이션의 진입점 역할을 수행한다.</p>
<p>이 서블릿의 설정은 주로 web.xml 파일이나 WebApplicationInitializer를 통해 이루어진다. DispatcherServlet은 클라이언트로부터 받은 요청을 적절한 컨트롤러에게 전달하여 처리한다.</p>
<h3 id="handlermapping"><strong><code>HandlerMapping</code></strong></h3>
<p>HandlerMapping은 DispatcherServlet이 요청을 처리할 컨트롤러를 찾는 역할을 한다. 이것은 사용자의 요청 URL을 분석하여 어떤 컨트롤러가 이를 처리할지를 결정한다.</p>
<p>XML 파일이나 Java config 어노테이션을 사용하여 설정할 수 있다.</p>
<h3 id="handleradapter"><strong><code>HandlerAdapter</code></strong></h3>
<p>HandlerAdapter는 매핑된 컨트롤러의 실행을 요청하고, 그 결과를 ModelAndView 객체로 변환한다. 이는 다양한 종류의 컨트롤러를 유연하게 지원하기 위한 역할을 수행한다. 즉, 각각의 컨트롤러가 어떻게 실행되고 그 결과를 어떻게 처리해야 하는지를 다양한 방식으로 다룰 수 있게 해준다.</p>
<h3 id="controller"><strong><code>Controller</code></strong></h3>
<p>Controller는 사용자의 요청을 받아 해당 요청에 대한 비즈니스 로직을 실행하고, 그 결과를 Model에 저장한 후, 적절한 View로 전달하는 역할을 한다.</p>
<p>주로 Java 클래스로 구현되며, @Controller 어노테이션을 통해 Spring에 등록한다.</p>
<h3 id="viewresolver"><strong><code>ViewResolver</code></strong></h3>
<p>ViewResolver는 Controller에서 리턴된 뷰 이름을 기반으로 실제 View 객체를 찾는 역할을 한다. 이를 통해 View 객체는 실제 화면을 생성하고 클라이언트에게 응답한다.</p>
<p>Spring은 다양한 View 템플릿을 지원하며, ViewResolver는 해당 템플릿을 찾아준다.</p>
<h3 id="view"><strong><code>View</code></strong></h3>
<p>View는 화면을 생성하고 클라이언트에게 응답하는 역할을 한다. Controller에서 전달받은 데이터를 이용하여 사용자에게 시각적으로 표현되는 부분을 담당한다.</p>
<p>주로 HTML, JSP, Thymeleaf 등의 템플릿 엔진을 사용하여 구현한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링] AOP / PSA]]></title>
            <link>https://velog.io/@lil_young/%EC%8A%A4%ED%94%84%EB%A7%81-AOP-PSA</link>
            <guid>https://velog.io/@lil_young/%EC%8A%A4%ED%94%84%EB%A7%81-AOP-PSA</guid>
            <pubDate>Wed, 29 Oct 2025 01:11:06 GMT</pubDate>
            <description><![CDATA[<h1 id="aop-aspect-oriented-programming">AOP (Aspect Oriented Programming)</h1>
<p>로깅, 보안, 트랜잭션과 같은 공통 기능을 분리하여 하나의 책임을 가지게 하는 프로그래밍 기법</p>
<p>코드의 재사용으로 생산성을 높이고, 유지보수를 용이하게 만든다.</p>
<p><img src="https://velog.velcdn.com/images/lil_young/post/cd9d7e3b-334e-4fa2-9491-66c3976d101a/image.png" alt=""></p>
<p>해당 사진처럼 횡단관심 기능이 핵심관심 기능에 공통으로 들어가는데, 이것을 분리하여 핵심관심에 재사용하는 것이 AOP다.</p>
<h3 id="aop-용어">AOP 용어</h3>
<p><strong><code>Advice</code></strong></p>
<ul>
<li>AOP 관점에서 분리된 횡단관심 기능, 실제로 Joinpoint에서 사용될 코드</li>
</ul>
<p><strong><code>Target</code></strong></p>
<ul>
<li>AOP 관점에서 핵심관심 기능, Advice가 적용될 대상</li>
</ul>
<p><strong><code>Joinpoint</code></strong></p>
<ul>
<li>Advice를 적용 가능한 지점, 특정 작업이 실행되는 시점</li>
<li>프링은 메소드에 대한 Joinpoint만 가능하다 : 프록시를 이용해 AOP를 구현하기 때문.</li>
</ul>
<p><strong><code>Pointcut</code></strong></p>
<ul>
<li>실제로 Advice가 적용되는 Joinpoint를 나타냄</li>
</ul>
<p><strong><code>Weaving</code></strong></p>
<ul>
<li>Joinpoint를 Advice로 감싸는 과정</li>
</ul>
<h3 id="aspect를-사용하는-예시">Aspect를 사용하는 예시</h3>
<pre><code class="language-java">// Aspect 어노테이션 사용
@Aspect
     class A{        // 이곳에서 타겟은 &quot;b()&quot; 메소드다.
      // Around: 타켓의 실행 전후로 처리 됨
      @Around(&quot;b()&quot;)
    public void test(){
            // b() 메소드의 적용될 코드
      }
      // Before: 타켓이 실행 전 처리 됨
      @Before(&quot;b()&quot;)
      public void test2(){
            // 생략
      }
      // After Returning: 타겟이 실행 후 처리 됨
      @After Returning(&quot;b()&quot;)
      public void test3(){
        // 생략
      }
      // After Throwing: 타겟이 예외를 발생시키면 처리 됨
      @After Throwing(&quot;b()&quot;)
      public void test4(){
        // 생략
      }
}</code></pre>
<p>이와 같이 Pointcut을 활용하여 특정지점, 또는 시점에 적용할 수 있는 코드를 작성할 수 있다.</p>
<hr>
<h1 id="psa-portable-service-abastraction">PSA (Portable Service Abastraction)</h1>
<aside>

<p>SpringFramework에서 제공하는 서비스 추상화 계층</p>
<p>다양한 서비스 기술을 추상화하여 코드를 특정 기술에 종속시켜 구현하는 것이 아닌 변경에 용이하게 편의성을 제공해준다.</p>
</aside>

<p>해당 프로젝트에는 SpringWeb을 dependency로 가지고 있고, WAS로 Tomcat이 사용되고 있다.</p>
<p><img src="https://velog.velcdn.com/images/lil_young/post/2fcad334-f444-43f6-8e7e-55592f8fb2b2/image.png" alt=""></p>
<p>하지만, build.gradle에서 spring-boot-starter-Web을 spring-boot-starter-webflux로 변경하면 내부 코드 변경 없이</p>
<p><img src="https://velog.velcdn.com/images/lil_young/post/6424a87a-e589-4594-b287-77ae5582ba5b/image.png" alt=""></p>
<p>WAS가 Netty로 변경된다.</p>
<p>Spring 내부에는 이미 추상화 서비스가 구현되어 있기 때문에 같은 서비스내에서는 내부 코드 변경없이 간단하게 서비스를 변경할 수 있다.</p>
<p>이밖에도 트랜잭션, JDBC, 캐시와 관련된 여러가지 부분이 서비스 추상화 계층을 통해 특정 기술에 의존하지 않고, 일관된 코드로 구현할 수 있게 해주는 핵심이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링] IoC / DI]]></title>
            <link>https://velog.io/@lil_young/%EC%8A%A4%ED%94%84%EB%A7%81-IoC-DI</link>
            <guid>https://velog.io/@lil_young/%EC%8A%A4%ED%94%84%EB%A7%81-IoC-DI</guid>
            <pubDate>Mon, 27 Oct 2025 22:34:55 GMT</pubDate>
            <description><![CDATA[<h3 id="ioc-inversion-of-control-제어의-역전"><strong>IoC (Inversion of Control), 제어의 역전</strong></h3>
<pre><code class="language-java">// 개발자가 제어
public class A {}
public class B {
    private A a;
    public B(){
        this.a = new A();
    }
}

// IoC Container(Spring) 제어
@Component
public class A {}
public class B{
    private A a;
    @Autowired
    public B(A a){
        this.a = a;
    }
}</code></pre>
<p>1번 코드의 경우 new 키워드를 사용하여 직접 객체를 생성 했지만, 2번 코드는 @Component와 @Autowired를 사용하여 new 키워드가 없음에도 객체가 생성되는 코드이다.
이처럼 개발자가 직접 객체를 관리 하는게 아닌 프레임워크에게 객체 생성 및 생명주기를 맡기는것이 제어의 역전이라 할 수 있다.</p>
<h3 id="di-dependency-injection-의존성-주입"><strong>DI (Dependency Injection), 의존성 주입</strong></h3>
<p>객체간의 의존관계를 직접 생성하는것이 아닌 IoC 컨테이너가 관리하는 Bean을 외부에서 주입받아 의존 관계를 형성하는 것을 DI, &quot;의존성 주입&quot;이라고 한다.</p>
<p>의존성 주입 방법에는 3가지가 있다.</p>
<ul>
<li>DI되는 객체는 반드시 Bean 등록이 되어있어야한다. 아래 코드는 @Component 어노테이션을 이용해 Bean을 등록한 상태이다.</li>
</ul>
<pre><code class="language-java">@Component
public class A {}

// 생성자 주입
@Component
public class B {
    private final A a;

    @Autowired
    public B(A a) {
        this.a = a;
    }
}

// Setter 주입
@Component
public class B {
    private A a;

    @Autowired
    public void setA(A a) {
        this.a = a;
    }
}

// 필드주입
@Component
public class B {
    @Autowired
    private A a;
}</code></pre>
<p>이 3가지 방법중 스프링에서 권장하는 방법은 <strong>생성자 주입</strong>이다.</p>
<p><strong>생성자 주입</strong></p>
<p><strong><code>순환 참조 예방</code></strong></p>
<ul>
<li>Application 작동시점에서 순환 참조를 확인할 수 있다.</li>
<li>Spring Boot 2.6 부터는 순환 참조가 기본적으로 허용되지 않기 때문에 이하 버전에서만 효과가 있다.</li>
</ul>
<p><strong><code>불변성 보장</code></strong></p>
<ul>
<li>Application 실행 시점에서 의존성이 주입되고 final 키워드를 사용하여 다른 방법과 달리 객체의 불변성을 보장할 수 있다.</li>
</ul>
<p><strong><code>테스트의 편리함</code></strong></p>
<ul>
<li>컴파일 시점의 객체를 주입받아 테스트 코드 작성이 가능하고, 누락 객체가 있을 경우, 컴파일 시점에서 확인되기 때문에 테스트를 보다 쉽게 할 수 있다.</li>
</ul>
<p><strong>Setter 주입</strong></p>
<ol>
<li>객체 변경이 필요한 경우 사용된다.</li>
</ol>
<p><strong>필드 주입</strong></p>
<ol>
<li>코드가 간결하다.</li>
<li>프레임워크에 지나치게 의존적이고, 외부에서 변경이 불가능하다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링] Spring 핵심 개념]]></title>
            <link>https://velog.io/@lil_young/%EC%8A%A4%ED%94%84%EB%A7%81-Spring-%ED%95%B5%EC%8B%AC-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@lil_young/%EC%8A%A4%ED%94%84%EB%A7%81-Spring-%ED%95%B5%EC%8B%AC-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Thu, 23 Oct 2025 02:51:47 GMT</pubDate>
            <description><![CDATA[<h3 id="spring-이란">Spring 이란?</h3>
<ul>
<li>자바 기반의 엔터프라이즈 애플리케이션 개발을 쉽게 해주는 오픈소스 프레임워크</li>
<li>목표는 <code>객체지향적 설계</code> + <code>개발 생산성</code> + <code>유지보수성 향상</code></li>
</ul>
<h3 id="spring이-등장한-이유">Spring이 등장한 이유</h3>
<ul>
<li>과거 EJB(Enterprise Java Bean)의 복잡한 설정, 무거운 실행 구조를 대체하기 위해 등장</li>
<li>POJO 기반 경량 컨테이너로, 단순한 객체 관리 및 DI 지원을 핵심으로 함</li>
</ul>
<h3 id="spring의-핵심-개념">Spring의 핵심 개념</h3>
<ol>
<li><strong>POJO</strong><ul>
<li>특별한 상속/규약 없이 순수 자바 객체로 구성</li>
</ul>
</li>
<li><strong>IoC/DI</strong><ul>
<li>객체 생성과 의존성 관리를 프레임워크가 담당</li>
</ul>
</li>
<li><strong>AOP</strong><ul>
<li>로깅, 보안, 트랜잭션과 같은 공통 기능을 모듈화하여 하나의 책임을 가지게 하는 기법</li>
</ul>
</li>
<li>PSA<ul>
<li>스프링이 제공하는 일관된 추상화 계층을 통해 개발자가 특정 기술(JDBC, JMS, 트랜잭션 등)에 종속되지 않고 하나의 통일된 방식으로 서비스에 접근할 수 있도록 한 개념</li>
</ul>
</li>
</ol>
<h3 id="spring-container-개념">Spring Container 개념</h3>
<ol>
<li>스프링에서 의존관계 주입을 이용하여 애플리케이션을 구성하는 여러 빈(Bean)들의 생명주기(Lifecycle)와 애플리케이션의 서비스 실행 등을 관리하며 생성된 인스턴스들에게 기능을 제공하는 것을 부르는 말이다.</li>
<li>컨테이너에 적절한 설정만 있다면 프로그래머의 개입 없이도 작성된 코드를 컨테이너가 빈을 스스로 참조한 뒤 알아서 관리한다.</li>
<li>스프링 컨테이너를 언급할때는 빈 팩토리(BeanFactory) 와 애플리케이션 컨텍스트(ApplicationContext) 두 가지로 다룬다.</li>
</ol>
<ul>
<li><strong><code>BeanFactory</code></strong><ul>
<li>스프링 컨테이너의 기본적인 형태</li>
<li>Bean의 생성 및 의존성 주입만 담당</li>
<li>지연 로딩 방식 사용 → 요청이 들어올 때 Bean 생성</li>
<li>기능이 단순해서 거의 사용하지 않음</li>
</ul>
</li>
<li><strong><code>ApplicationContext</code></strong><ul>
<li>BeanFactory를 확장한 상위 컨테이너</li>
<li>Bean 관리뿐 아니라 다음 기능을 추가로 제공<ul>
<li>국제화(i18n)</li>
<li>이벤트 리스너</li>
<li>환경 설정(Profile)</li>
<li>AOP / 트랜잭션 관리</li>
</ul>
</li>
<li>즉시 로딩 방식 사용 → 컨테이너 초기화 시 모든 Bean 생성</li>
<li>대부분의 프로젝트에서 사용하는 표준 컨테이너</li>
</ul>
</li>
</ul>
<p>스프링 공식 문서에서는 두 컨테이너 중 특별한 이유가 없다면 ApplicationContext를 사용해야 한다고 나와있다.</p>
<p>이유는 BeanFactory의 모든 기능을 포함하는 것은 물론이고 추가 기능들을 제공하기 때문이다.</p>
<ul>
<li>작동 과정
```</li>
</ul>
<ol>
<li>설정 파일(@Configuration or XML) → Bean 정의 읽음</li>
<li>Container 생성 (ApplicationContext)</li>
<li>Bean 등록 및 의존성 주입 (DI)</li>
<li>Bean 초기화 → 서비스 실행</li>
<li>애플리케이션 종료 시 Bean 소멸
```<h3 id="스프링-프레임워크의-주요-모듈들">스프링 프레임워크의 주요 모듈들</h3>
<img src="https://velog.velcdn.com/images/lil_young/post/5fc9f007-c231-4833-afe8-1a93ecb65b32/image.png" alt=""></li>
</ol>
<ul>
<li>코어 : <code>DI</code>, <code>IoC</code>에 대한 핵심 기능 제공 (<code>Bean</code>, <code>Bean Factory</code>, <code>Application Context</code>)</li>
<li>횡단 관심 : <code>AOP</code>에서 핵심기능 외에 중간중간 삽입되어야 할 기능들</li>
<li>웹 : 웹 서비스를 위한 기능들</li>
<li>비즈니스 : 비즈니스 로직을 실행, 선언적 트랜잭션 관리</li>
<li>데이터 : 데이터베이스 및 외부 인터페이스와의 상호작용<h3 id="흐름">흐름</h3>
<pre><code>개발자 코드(POJO)
      ↓
Spring Container (ApplicationContext)
      ↓
Bean 등록 &amp; 관리 (IoC)
      ↓
의존성 주입 (DI)
      ↓
공통 관심사 처리 (AOP)
      ↓
애플리케이션 실행 (MVC 구조 기반)</code></pre></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] HTTP]]></title>
            <link>https://velog.io/@lil_young/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-HTTP</link>
            <guid>https://velog.io/@lil_young/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-HTTP</guid>
            <pubDate>Fri, 19 Sep 2025 00:42:48 GMT</pubDate>
            <description><![CDATA[<h1 id="http">HTTP</h1>
<ul>
<li>웹에서 클라이언트와 서버가 데이터를 주고받을 때 사용하는 프로토콜<ul>
<li>클라이언트 - 서버 구조</li>
</ul>
</li>
<li>최상단 계층인 <code>응용(애플리케이션) 계층</code>에서 사용<ul>
<li>물-데-네-전-세-표-<strong>응</strong></li>
</ul>
</li>
<li>HTTP는 HTTP/1.0부터 HTTP/3.0까지 있음</li>
<li><code>텍스트 기반 프로토콜</code>(요청과 응답 모두 사람이 읽을 수 있는 텍스트 형식으로 주고받음)</li>
<li><code>비연결성</code>으로 요청과 응답이 끝나면 연결 상태를 유지하지 않음<ul>
<li>Stateless</li>
</ul>
</li>
<li>웹 브라우저에서 페이지, 이미지, 동영상 같은 리소스를 가져오기 위한 규약</li>
</ul>
<p>요약하면 <code>웹에서 클라이언트와 서버가 어떻게 대화할지 정해놓은 약속</code>이라고 생각하면 된다.</p>
<hr>
<h2 id="http10">HTTP/1.0</h2>
<p>HTTP/1.0은 기본적으로 <code>한 연결당 하나의 요청을 처리</code>하도록 설계되었다. 즉, 요청마다 TCP 연결을 해줘야 한다.
이는 <code>RTT 증가</code>를 불러오게 되었다.</p>
<blockquote>
<p><strong>RTT란?</strong>
  패킷이 목적지에 도달하고 나서 다시 출발지로 돌아오기까지 걸리는 시간이다. </p>
</blockquote>
<p>즉, <code>패킷 왕복 시간</code>이다.
<img src="https://velog.velcdn.com/images/lil_young/post/0b3446f3-adf8-48d6-a302-fea7a856acc3/image.png" alt="">
위 그림을 보면 RTT에 대해 확실하게 이해가 된다.</p>
<h3 id="rtt의-증가를-해결하기-위한-방법">RTT의 증가를 해결하기 위한 방법</h3>
<p>매번 연결할 때마다 RTT가 증가하므로 서버 부담이 많이 가고 응답 시간이 길어지는 문제점이 있다. 이를 해결 하기 위해 아래와 같은 방법을 사용했다.</p>
<ol>
<li><p>이미지 스플리팅</p>
<p> C 이미지는 A와 B 이미지를 하나로 합쳐 놓은 큰 이미지 파일이라고 가정한다.
 그러면 C 이미지 하나로 특정 영역만 잘라서 A 이미지, B 이미지 둘 다 보여줄 수 있다.</p>
<pre><code class="language-css"> .icon {
   width: 10px;
   height: 10px;
   background-image: url(&quot;C.png&quot;);
 }

 .icon-a {
   background-position: 0px 0px;   /* A 이미지 부분 */
 }

 .icon-b {
   background-position: -10px 0px; /* B 이미지 부분 */
 }
</code></pre>
</li>
<li><p>코드 압축<br> 코드 압축은 코드를 압축해서 개행 문자, 빈칸을 없애서 코드의 크기를 최소화하는 방법이다.</p>
<pre><code class="language-java"> String a = 10;
 String b = 20;
 System.out.println(a + b);</code></pre>
<p> <em>코드 압축 후</em></p>
<pre><code class="language-java"> String a = &quot;10&quot;, b = &quot;20&quot;;
 System.out.println(a + b);</code></pre>
</li>
<li><p>이미지 Base64 인코딩</p>
<pre><code class="language-css"> .icon {
   background-image: url(&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...&quot;);
 }</code></pre>
<p> 여러 개의 작은 이미지를 각각 요청하는 대신, HTML이나 CSS 안에 Base64 문자열로 인코딩해서 삽입할 수 있다. 이렇게 하면 별도의 네트워크 요청이 줄어들어 RTT 감소 효과가 생긴다. 하지만 Base64 문자열로 변환할 경우 코드가 37% 정도 용량이 커지는 단점(Base64 인코딩 특성)이 있다.</p>
</li>
</ol>
<hr>
<h2 id="http11">HTTP/1.1</h2>
<p>HTTP/1.0에서 발전한 것이 HTTP/1.1이다.</p>
<p>매번 TCP 연결을 하는 것이 아니라 <code>한 번 TCP 초기화를 한 이후에 keep-alive라는 옵션으로 여러 개의 파일을 송수신</code>할 수 있게 바뀌었다.</p>
<ul>
<li>HTTP/1.0에서도 keep-alive가 있었지만 표준화가 되어있지 않았고, <code>HTTP/1.1부터 표준화</code> 되어 기본 옵션으로 설정되었다.
<img src="https://velog.velcdn.com/images/lil_young/post/1091a9db-67a2-404b-af58-756d8a93fae3/image.png" alt="">
위 그림을 보면 한 번 TCP 3-웨이-핸드셰이크가 발생하면 그 다음부터 발생하지 않는 것을 볼 수 있다.
하지만 문서 안에 포함된 다수의 리소스를 처리하려면 요청할 리소스 개수에 비례해서 대기 시간이 길어지는 단점이 있다.</li>
</ul>
<p>다음은 HTTP/1.1의 특징이다.</p>
<h3 id="1-hol-blocking">1. HOL Blocking</h3>
<p>HOL Blocking(Head Of Line Blocking)은 네트워크에서 같은 큐에 있는 패킷이 그 첫 번째 패킷에 의해 지연될 때 발생하는 성능 저하 현상을 말한다.
<img src="https://velog.velcdn.com/images/lil_young/post/659d4ed7-8965-4848-a982-df2c31744133/image.png" alt=""></p>
<p>예를 들어 앞의 그림처럼 image.jpg와 styles.css, data.xml을 다운로드 받을 때 보통은 순차적으로 잘 받아지지만 image.jpg가 느리게 받아진다면 그 뒤에 있는 것들이 대기하게 되며 다운로드가 지연되는 상태가 된다.</p>
<h3 id="2-무거운-헤더-구조">2. 무거운 헤더 구조</h3>
<p>HTTP/1.1의 헤더에는 쿠키 등 많은 메타데이터가 들어 있고 압축이 되지 않아 무거웠다.</p>
<hr>
<h2 id="http20">HTTP/2.0</h2>
<p>HTTP/2.0은 SPDY 프로토콜에서 파생된 HTTP/1.x 보다 지연 시간을 줄이고 응답 시간을 더 빠르게 할 수 있으며 멀티플렉싱, 헤더 압축, 서버 푸시, 요청의 우선순위 처리를 지원하는 프로토콜이다.</p>
<h3 id="1-멀티플렉싱">1. 멀티플렉싱</h3>
<p>멀티플렉싱이란 여러 개의 스트림을 사용하여 송수신한다는 것이다.</p>
<p>이를 통해 특정 스트림의 패킷이 손실되었다고 하더라도 해당 스트림에만 영향을 미치고 나머지 스트림은 정상적으로 동작할 수 있다.</p>
<blockquote>
<p><strong>스트림이란?</strong> 
시간이 지남에 따라 사용할 수 있게 되는 일련의 데이터 요소를 가리키는 데이터 흐름</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/lil_young/post/c83c400c-4145-4ecd-8491-c1d641054856/image.png" alt=""></p>
<p>앞의 그림은 하나의 연결 내 여러 스트림을 캡쳐한 모습이다. 병렬적인 스트림들을 통해 데이터를 서빙하고 있다. 또한, 스트림 내의 데이터들도 쪼개져 있다. 애플리케이션에서 받아온 메시지를 독립된 프레임으로 조각내어 서로 송수신한 이후 다시 조립하며 데이터를 주고받는다.</p>
<p><img src="https://velog.velcdn.com/images/lil_young/post/7e05b98b-c1ac-413f-9664-75b37a72d0a3/image.png" alt=""></p>
<p>이를 통해 단일 연결을 사용하여 병렬로 여러 요청을 받을 수 있고 응답을 줄 수 있다. 이렇게 되면 HTTP/1.x에서 발생하는 문제인 HOL Blocking을 해결할 수 있다.</p>
<h3 id="2-헤더-압축">2. 헤더 압축</h3>
<p>HTJTP/1.x에는 크기가 큰 헤더라는 문제가 있었다.
이를 HTTP/2.0에서는 헤더 압축을 써서 해결하는데, 허프만 코딩 압축 알고리즘을 사용하는 HPACK 압축 형식을 가진다.</p>
<blockquote>
<p><strong>허프만 코딩이란?</strong>
문자열을 문자 단위로 쪼개 빈도수를 세어 빈도가 높은 정보는 적은 비트 수를 사용하여 표현하고, 빈도가 낮은 정보는 비트 수를 많이 사용하여 표현해서 전체 데이터의 표현에 필요한 비트양을 줄이는 원리이다.</p>
</blockquote>
<h3 id="3-서버-푸시">3. 서버 푸시</h3>
<p>HTTP/1.1에서는 클라이언트가 서버에 요청을 해야 파일을 다운로드 받을 수 있었다면,
HTTP/2.0은 클라이언트 요청 없이 서버가 바로 리소스를 푸시할 수 있다.
<img src="https://velog.velcdn.com/images/lil_young/post/d5f34b4f-c084-42e7-bd4a-1d88819158fa/image.png" alt=""></p>
<p>html에는 css나 js 파일이 포함되기 마련인데 html을 읽으면서 그 안에 들어 있던 css 파일을 서버에서 푸시하여 클라이언트에 먼저 줄 수 있다.</p>
<p>장점으로는 RTT를 줄일 수 있어 페이지 로딩 속도가 빠른 대신에, 불필요한 리소스일 경우 대역폭 낭비다.</p>
<hr>
<h2 id="https">HTTPS</h2>
<p>HTTP/2.0은 HTTPS 위에서 동작한다.</p>
<p>HTTPS는 애플리케이션 계층과 전송 계층 사이에 보안 계층인 SSL/TLS 계층을 넣은 신뢰할 수 있는 HTTP 요청을 말한다. 이를 통해 <code>통신을 암호화</code>한다.</p>
<ul>
<li>OSI 7 계층에서는 전송 계층과 응용(애플리케이션) 계층 사이에 보안 계층은 존재하지 않고, 데이터 인코딩, 암호화, 압축 등을 담당하는 표현 계층이 있는데 개념적으로 보면 SSL/TLS는 표현 계층에 있다고 보면 된다.</li>
<li>실제 인터넷에선 네트워크 액세스 계층 - 인터넷 계층(IP) - 전송 계층(TCP/UDP) - 애플리케이션 계층(HTTP 등) 이 있는데, 여기서는 전송 계층 위, 애플리케이션 계층 아래에 별도로 보안 계층이 있다고 설명한다.</li>
<li>즉, 이론적(OSI 모델)에서 SSL/TLS는 표현 계층에 해당, 실제 구현에서는 전송 계층과 애플리케이션 사이에 낀 별도의 보안 계층으로 취급한다.</li>
</ul>
<h3 id="ssltls">SSL/TLS</h3>
<p>SSL(Secure Socket Layer)은 SSL 1.0, SSL 2.0, SSL 3.0, TLS 1.0, TLS 1.3까지 버전이 올라가며 마지막으로 TLS로 명칭이 변경되었으나, 보통 이를 합쳐 <code>SSL/TLS</code>로 많이 부른다.</p>
<p>SSL/TLS은 전송 계층에서 보안을 제공하는 프로토콜이다. 클라이언트와 서버가 통신할 때 SSL/TLS를 통해 제 3자가 메시지를 도청하거나 변조하지 못하도록 한다.</p>
<ul>
<li>여기서 전송 계층이라 표현한 이유는 전송 계층 바로 위에 별도의 보안 계층에서 제공하므로 실제로 있는 계층인 전송 계층이라 표현함.</li>
</ul>
<p>SSL/TLS를 통해 공격자가 서버인 척하며 사용자 정보를 가로채는 네트워크상의 <code>인터셉터</code>를 <code>방지</code>할 수 있다.</p>
<p>SSL/TLS는 보안 세션을 기반으로 데이터를 암호화하며 보안 세션이 만들어질 때</p>
<ol>
<li><strong>인증 메커니즘</strong> → 서버 인증서 확인 (CA 서명 검증, 필요 시 클라이언트 인증서도 포함)</li>
<li><strong>키 교환 암호화 알고리즘</strong> → 세션 키를 안전하게 교환(RSA, ECDHE), 세션 내 데이터 암호화(AES, Chacha20)</li>
<li><strong>해싱 알고리즘</strong> → 무결성 검증 및 메시지 인증 (SHA-256)</li>
</ol>
<p>이 사용된다. 이 조합이 바로 사이퍼 슈트다.</p>
<blockquote>
<p><strong>보안 세션</strong>
보안이 시작되고 끝나는 동안 유지되는 세션을 말하고, SSL/TLS는 핸드셰이크를 통해 보안 세션을 생성하고 이를 기반으로 상태 정보 등을 공유한다.</p>
</blockquote>
<p>TLS의 핸드 셰이크
<img src="https://velog.velcdn.com/images/lil_young/post/b8993d65-9165-4de9-837d-ec85d78467f4/image.png" alt=""></p>
<p>위 그림을 보면 클라이언트와 서버가 키를 공유하고 이를 기반으로 인증, 인증 확인 등의 작업이 일어나는 단 한번의 1-RTT가 생긴 후 데이터를 송수신한다.</p>
<p>클라이언트에서 사이퍼 슈트를 서버에 전달하면 서버는 받은 사이퍼 슈트의 암호화 알고리즘 리스트를 제공할 수 있는지 확인한다. 제공할 수 있다면 서버에서 클라이언트로 인증서를 보내는 인증 메커니즘이 시작되고 이후 해싱 알고리즘 등으로 암호화된 데이터의 송수신이 시작된다.</p>
<p>여기서 말하는 사이퍼 슈트는 프로토콜, AEAD 사이퍼 모드, 해싱 알고리즘이 나열된 규약을 말하며 5개가 있다.</p>
<ul>
<li>TLS_AES_128_GCM_SHA256</li>
<li>TLS_AES_256_GCM_SHA384</li>
<li>TLS_CHACHA20_POLY1305_SHA256</li>
<li>TLS_AES_128_CCM_SHA256</li>
<li>TLS_AES_128_CCM_8_SHA256</li>
</ul>
<p>예를 들어 TLS_AES_128_GCM_SHA256에서 <code>TLS는 프로토콜</code>, <code>AES_128_GCM은 AEAD 사이퍼 모드</code>, <code>SHA256은 해싱 알고리즘</code>을 뜻한다.</p>
<blockquote>
<p><strong>AEAD 사이퍼 모드</strong>
데이터 암호화 알고리즘이며 AES_128_GCM, CHACHA20_POLY1305 등이 있다. 
<strong>AES_128_GCM의 뜻</strong></p>
</blockquote>
<ol>
<li>AES 대칭키 암호화 알고리즘을 사용</li>
<li>128 비트의 키를 사용하는 표준 블록 암호화 기술</li>
<li>병렬 계산에 용이한 암호화 알고리즘 GCM
이 결합된 알고리즘을 뜻한다.</li>
</ol>
<h3 id="인증-메커니즘">인증 메커니즘</h3>
<p>인증 메커니즘은 CA(Certificate Authorities)에서 발급한 인증서를 기반으로 이루어진다.</p>
<p>CA에서 발급한 인증서는 안전한 연결을 시작하는  데 있어 필요한 <code>공개키</code>를 <code>클라이언트에 제공</code>하고, 사용자가 접속한 서버가 <code>신뢰할 수 있는 서버</code>임을 <code>보장</code>한다.</p>
<p>인증서는 서비스 정보, 공개키, 지문, 디지털 서명 등으로 이루어진다.</p>
<p>CA는 아무 기업이나 할 수 없고 신뢰성이 보장된 공인된 기업들만 참여할 수 있으며 대표적으로 Comodo, GoDaddy, GlobalSign, 아마존 등이 있다.</p>
<p><strong>CA 발급 과정</strong>
자신의 서비스가 CA 인증서를 발급 받으려면</p>
<pre><code>1. 사이트 운영자가 자신의 공개키와 사이트 정보(도메인명, 조직명 등)를 포함한 CSR을 만든다.
    - 여기서 공개키는 ‘내 서비스와 통신할 때 이 키를 써달라’는 의미
2. CSR 제출 (운영자 → CA)
3. CA 검증 (도메인 소유 확인)
    - CA는 제출된 사이트가 실제로 신청자 소유인지 확인한다.
        - DNS 레코드 검증, 이메일 확인 등등
4. 인증서 데이터 구성 (CA)
    - CA는 신청자의 공개키 + 사이트 정보를 모아 인증서 데이터 구조를 만든다.
5. 인증서 해시 생성 (CA)
    - 이 데이터 전체를 해싱하여 지문을 생성한다.
        - 여기서 말하는 지문은 인증서 전체 내용을 요약한 값이다.
6. 전자서명 생성 (CA)
    - 생성된 지문을 CA의 비밀키로 암호화 → 전자서명 생성
    - 이 전자서명을 인증서에 포함시킨다.
7. 인증서 발급 (CA → 운영자)
    - CA의 전자서명이 붙은 인증서를 운영자에게 전달
8. 배포 (운영자)
    - 받은 인증서를 서버에 설치한다.
    - 이후 클라이언트는 HTTPS 연결 시 이 인증서를 받아보고, 내장된 CA 공개키를 이용해 서명을 검증한다.</code></pre><h3 id="암호화-알고리즘">암호화 알고리즘</h3>
<p>키 교환 암호화 알고리즘으로는</p>
<ol>
<li>대수곡선 기반의 ECDHE</li>
<li>모듈식 기반의 DHE</li>
</ol>
<p>를 사용한다. 둘 다 디피-헬만 방식을 근간으로 만들어졌다.</p>
<p>*<em>디피-헬만 키 교환 암호화 알고리즘
*</em></p>
<blockquote>
<p>암호키를 교환하는 하나의 방법</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/lil_young/post/4f8d6fc1-afd0-4412-bf07-b811f0d84b0b/image.png" alt=""></p>
<p>g, x, p를 알면 y는 구하기 쉽지만, g와 y와 p만 안다면 x를 구하기는 어렵다는 원리에 기반한 알고리즘</p>
<p><img src="https://velog.velcdn.com/images/lil_young/post/3944e117-67b2-4a28-95c1-c81fb390499c/image.png" alt=""></p>
<p><em>아래의 예시를 보면 쉽게 이해가 간다.</em></p>
<p>p = 23, g =5</p>
<p>A의 비밀 a = 6, B의 비밀 b = 15</p>
<p>A = 5^6 mod 23 = 8</p>
<p>B = 5^15 mod 23 = 19</p>
<p>교환 후</p>
<p>A가 계산: B^a mod 23 = 19^6 mod 23 = 2</p>
<p>B가 계산: A^b mod 23 = 8^15 mod 23 = 2</p>
<p>공격자는 공개 값 A = g^a 와 B = g^b를 볼 수 있지만, a 또는 b를 알아내려면 이산로그 문제를 풀어야 한다. 큰 소수 p와 충분히 큰 a, b를 쓰면 이 문제는 계산상 불가능하므로 공격자가 g^ab를 직접 계산할 수 없다.</p>
<h3 id="해싱-알고리즘">해싱 알고리즘</h3>
<p>해싱 알고리즘은 데이터를 추정하기 힘든 더 작고, 섞여있는 조각으로 만드는 알고리즘이다.
SSL/TLS는 해싱 알고리즘으로 SHA-256, SHA-384 알고리즘을 쓰며 SHA-256 알고리즘을 많이 쓴다.</p>
<blockquote>
<p>SHA-256 알고리즘
해시 함수의 결과값이 256비트인 알고리즘이며 단방향이다. 비트코인을 비롯한 많은 블록체인 시스템에서도 쓰여진다.
SHA-256 알고리즘은 해싱을 해야 할 메시지에 1을 추가하는 등 전처리를 하고 전처리된 메시지를 기반으로 해시를 반환한다.</p>
</blockquote>
<h3 id="seo에도-도움이-되는-https">SEO에도 도움이 되는 HTTPS</h3>
<p>SEO(Search Engine Optimization)는 검색엔진 최적화를 뜻하며 사용자들이 구글, 네이버 같은 검색엔진으로 웹 사이트를 검색했을 때 그 결과를 페이지 상단에 노출시켜 많은 사람이 볼 수 있도록 최적화하는 방법을 의미한다.</p>
<p>이를 위한 방법으로는</p>
<ol>
<li>캐노니컬 설정</li>
<li>메타 설정</li>
<li>페이지 속도 개선</li>
<li>사이트맵 관리</li>
<li>모바일 최적화</li>
</ol>
<p>등이 있다.</p>
<h3 id="https-구축-방법">HTTPS 구축 방법</h3>
<ol>
<li>직접 CA에서 구매한 인증키를 기반으로 HTTPS 서비스 구축</li>
<li>서버 앞단의 HTTPS를 제공하는 로드밸런서를 두기</li>
<li>서버 앞단에 HTTPS를 제공하는 CDN을 두기</li>
</ol>
<h2 id="http30">HTTP/3.0</h2>
<p>HTTP/3.0은 HTTP/1.1 및 HTTP/2.0 과 함께 World Wide Web에서 정보를 교환하는 데 사용되는 세 번째 버전이다.</p>
<p>TCP 위에서 돌아가는 HTTP/2.0 과는 달리 HTTP/3.0 은 QUIC 이라는 계층 위에서 돌아가며, TCP 기반이 아닌 UDP 기반으로 돌아간다.</p>
<p><img src="https://velog.velcdn.com/images/lil_young/post/e4469db8-0cfa-461e-875f-2e33103f26d2/image.png" alt=""></p>
<p>또한, HTTP/2.0의 장점인 멀티플렉싱을 가지고 있으며 초기 연결 설정 시 지연 시간 감소라는 장점이 있다.</p>
<h3 id="초기-연결-설정-시-지연-시간-감소">초기 연결 설정 시 지연 시간 감소</h3>
<p>QUIC는 TCP를 사용하지 않기 때문에 통신을 시작할 때 번거로운 3-웨이-핸드셰이크 과정을 거치지 않아도 된다.</p>
<p>QUIC는 첫 연결 설정에 1-RTT만 소요된다. 클라이언트가 서버에 어떤 신호를 한 번 주고, 서버도 거기에 응답하기만 하면 바로 본 통신을 시작할 수 있다.</p>
<p>참고로 QUIC는 순방향 오류 수정 메커니즘이 적용되었다. </p>
<p>이는 전송한 패킷이 손실되었다면 수신 측에서 에러를 검출하고 수정하는 방식이며 열약한 네트워크 환경에서도 낮은 패킷 손식률을 자랑한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BOJ 8972] 미친 아두이노]]></title>
            <link>https://velog.io/@lil_young/BOJ-8972-%EB%AF%B8%EC%B9%9C-%EC%95%84%EB%91%90%EC%9D%B4%EB%85%B8</link>
            <guid>https://velog.io/@lil_young/BOJ-8972-%EB%AF%B8%EC%B9%9C-%EC%95%84%EB%91%90%EC%9D%B4%EB%85%B8</guid>
            <pubDate>Mon, 08 Sep 2025 19:43:58 GMT</pubDate>
            <description><![CDATA[<h3 id="풀이-코드">[풀이 코드]</h3>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class 미친아두이노 {
    static int R, C;
    static int jr, jc;
    static String s;
    static char[][] arr;
    static int[] dr = {0, 1, 1, 1, 0, 0, 0, -1, -1, -1};
    static int[] dc = {0, -1, 0, 1, -1, 0, 1, -1, 0, 1};
    static List&lt;Point&gt; list;
    static int cnt;
    static boolean isEnd;
    static class Point {
        int r, c;
        Point(int r, int c) {
            this.r=r;
            this.c=c;
        }
    }
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        R = Integer.parseInt(st.nextToken());
        C = Integer.parseInt(st.nextToken());
        arr = new char[R][C];
        list = new ArrayList&lt;Point&gt;();
        for (int i = 0; i &lt; R; i++) {
            String str = br.readLine();
            for (int j = 0; j &lt; C; j++) {
                arr[i][j] = str.charAt(j);
                if(arr[i][j] == &#39;I&#39;) {
                    jr = i;
                    jc = j;
                }
                if(arr[i][j] == &#39;R&#39;) {
                    list.add(new Point(i, j));
                }
            }
        }
        s = br.readLine();

        cnt = 0;
        isEnd = false;
        L:for(int d=0; d&lt;s.length(); d++) {
            int move = s.charAt(d) - &#39;0&#39;;

            // 1. 종수의 아두이노가 8가지 방향으로 이동시키거나, 그 위치에 그대로 둔다.
            jr += dr[move];
            jc += dc[move];
            cnt++;
            // 2. 종수의 아두이노가 미친 아두이노가 있는 칸으로 이동한 경우 게임이 끝난다.
            for(int i=0; i&lt;list.size(); i++) {
                Point p = list.get(i);
                if(jr==p.r &amp;&amp; jc==p.c) {
                    isEnd = true;
                    break L; 
                }
            }
            // 3. 미친 아두이노는 8가지 방향 중에서 종수의 아두이노와 가장 가까워 지는 방향으로 한 칸 이동한다.
            for (int i = 0; i &lt; list.size(); i++) {
                Point p = list.get(i);
                int bestDist = Integer.MAX_VALUE;
                int bestD = 0;
                for (int b = 1; b &lt;= 9; b++) {
                    if(b==5) continue;
                    int r = p.r + dr[b];
                    int c = p.c + dc[b];
                    int dist = Math.abs(jr - r) + Math.abs(jc - c);
                    if(dist &lt; bestDist) {
                        bestDist = dist;
                        bestD = b;
                    }
                }
                p.r+=dr[bestD];
                p.c+=dc[bestD];
            }

            // 4. 미친 아두이노가 종수의 아두이노가 있는 칸으로 이동한 경우에는 게임이 끝난다.
            for (int i = 0; i &lt; list.size(); i++) {
                Point p = list.get(i);
                if(p.r==jr &amp;&amp; p.c==jc) {
                    isEnd = true;
                    break L; 
                }
            }

            // 5. 2개 또는 그 이상의 미친 아두이노가 같은 칸에 있는 경우에는 큰 폭발이 일어나고, 그 칸에 있는 아두이노는 모두 파괴된다.
            Map&lt;String, Integer&gt; map = new HashMap&lt;&gt;();
            for (int i = 0; i &lt; list.size(); i++) {
                Point p = list.get(i);
                map.put(p.r + &quot;,&quot; + p.c, map.getOrDefault(p.r + &quot;,&quot; + p.c, 0) + 1);
            }
            for(Map.Entry&lt;String, Integer&gt; entry : map.entrySet()) {
                if(entry.getValue() &gt;= 2) {
                    String[] f = entry.getKey().split(&quot;,&quot;);
                    int f_r = Integer.parseInt(f[0]);
                    int f_c = Integer.parseInt(f[1]);

                    for (int i = 0; i &lt; list.size(); i++) {
                        Point p = list.get(i);
                        if(p.r==f_r &amp;&amp; p.c==f_c) {
                            list.remove(i--);
                        }
                    }
                }
            }
        }
        if(isEnd) {
            System.out.println(&quot;kraj &quot; + cnt);
        }else {
            char[][] result = new char[R][C];
            for(int i=0; i&lt;R; i++) {
                for(int j=0; j&lt;C; j++) {
                    result[i][j] = &#39;.&#39;;
                }
            }


            result[jr][jc] = &#39;I&#39;;
            for (int i = 0; i &lt; list.size(); i++) {
                Point p = list.get(i);
                result[p.r][p.c] = &#39;R&#39;; 
            }

            for(int i=0; i&lt;R; i++) {
                for(int j=0; j&lt;C; j++) {
                    System.out.print(result[i][j]);
                }
                System.out.println();
            }
        }

    }
}</code></pre>
<p>이 문제에서 가장 중요한 내용은
시뮬레이션 중, 3번인 미친 아두이노가 종수의 아두이노와 가장 가까워 지는 방향으로 한 칸 이동하는 코드를 작성하는 것이라고 생각한다.</p>
<pre><code class="language-java">            for (int i = 0; i &lt; list.size(); i++) {
                Point p = list.get(i);
                int bestDist = Integer.MAX_VALUE;
                int bestD = 0;
                for (int b = 1; b &lt;= 9; b++) {
                    if(b==5) continue;
                    int r = p.r + dr[b];
                    int c = p.c + dc[b];
                    int dist = Math.abs(jr - r) + Math.abs(jc - c);
                    if(dist &lt; bestDist) {
                        bestDist = dist;
                        bestD = b;
                    }
                }
                p.r+=dr[bestD];
                p.c+=dc[bestD];
            }</code></pre>
<p>list에서 미친 아두이노의 위치를 하나하나씩 꺼낸 후, 8방 탐색을 돌린다.
b가 5인 경우, 제자리이므로 continue를 하고, 나머지 값들에 대해 맨해튼 거리를 구하고 거리가 가장 짧은 거리의 b를 구한다.
그리고 미친 아두이노에서 맨해튼 거리가 가장 짧은 위치로 한칸 이동한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BOJ 14718] 용감한 용사 진수]]></title>
            <link>https://velog.io/@lil_young/BOJ-14718-%EC%9A%A9%EA%B0%90%ED%95%9C-%EC%9A%A9%EC%82%AC-%EC%A7%84%EC%88%98</link>
            <guid>https://velog.io/@lil_young/BOJ-14718-%EC%9A%A9%EA%B0%90%ED%95%9C-%EC%9A%A9%EC%82%AC-%EC%A7%84%EC%88%98</guid>
            <pubDate>Fri, 05 Sep 2025 13:54:05 GMT</pubDate>
            <description><![CDATA[<h3 id="풀이-코드">[풀이 코드]</h3>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {
    static int N, K;
    static int[][] arr;
    static int p, d, t;
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        K = Integer.parseInt(st.nextToken());
        arr = new int[N][3];
        for (int i = 0; i &lt; N; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j &lt; 3; j++) {
                arr[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        int result = Integer.MAX_VALUE;
        for (int i = 0; i &lt; N; i++) {
            p = arr[i][0];
            for (int j = 0; j &lt; N; j++) {
                d = arr[j][1];
                PriorityQueue&lt;Integer&gt; pq = new PriorityQueue&lt;&gt;(Collections.reverseOrder());
                for (int k = 0; k &lt; N; k++) {
                    t = arr[k][2];
                    if(arr[k][0] &lt;= p &amp;&amp; arr[k][1] &lt;= d) {
                        pq.offer(t);
                        if(pq.size()&gt;K) {
                            pq.poll();
                        }
                    }

                    if(pq.size()==K) {
                        int min = pq.peek();
                        result = Math.min(result, p+d+min);
                    }
                }

            }

        }
        System.out.println(result);
    }
}</code></pre>
<p>p는 힘이고, d는 민첩, t는 지능이다.</p>
<p>3중 for문을 이용해 모든 경우의 수를 확인한다.</p>
<p>예를 들어, 
입력이 아래와 같이 주어졌다면,</p>
<blockquote>
</blockquote>
<p>3 1
234 23 342
35 4634 34
46334 6 789</p>
<p>234 23 342
234 23 34
234 23 789
234 4634 342
234 4634 34
...
이런 규칙대로, p, d, t에 값이 새로 갱신된다.</p>
<p>이 과정에서 3중 for문 중 마지막 for문에서 k 위치에 있는 힘, 민첩이 i 위치에 있는 힘인 p, j 위치에 있는 민첩인 d보다 작거나 같으면 t를 pq에 넣는다.
그리고 pq의 size가 이겨야 할 병사 수인 K보다 크면 pq.poll()을 통해 인트가 가장 큰 값을 pq에서 빼내고, K랑 같아지면 result 값을 갱신한다.
p + d + pq.peek() 로 갱신하는 이유는 이겨야 할 병사 수가 K이고, 최소의 스탯 포인트를 구해야 하므로 p와 d 그리고 pq에서 인트가 가장 큰 값을 더한 값을 result와 비교해서 최솟값으로 갱신한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BOJ 16234] 인구 이동]]></title>
            <link>https://velog.io/@lil_young/BOJ-16234-%EC%9D%B8%EA%B5%AC-%EC%9D%B4%EB%8F%99</link>
            <guid>https://velog.io/@lil_young/BOJ-16234-%EC%9D%B8%EA%B5%AC-%EC%9D%B4%EB%8F%99</guid>
            <pubDate>Tue, 02 Sep 2025 07:45:55 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<hr>
<p>N×N크기의 땅이 있고, 땅은 1×1개의 칸으로 나누어져 있다. 각각의 땅에는 나라가 하나씩 존재하며, r행 c열에 있는 나라에는 A[r][c]명이 살고 있다. 인접한 나라 사이에는 국경선이 존재한다. 모든 나라는 1×1 크기이기 때문에, 모든 국경선은 정사각형 형태이다.</p>
<p>오늘부터 인구 이동이 시작되는 날이다.</p>
<p>인구 이동은 하루 동안 다음과 같이 진행되고, 더 이상 아래 방법에 의해 인구 이동이 없을 때까지 지속된다.</p>
<p>국경선을 공유하는 두 나라의 인구 차이가 L명 이상, R명 이하라면, 두 나라가 공유하는 국경선을 오늘 하루 동안 연다.
위의 조건에 의해 열어야하는 국경선이 모두 열렸다면, 인구 이동을 시작한다.
국경선이 열려있어 인접한 칸만을 이용해 이동할 수 있으면, 그 나라를 오늘 하루 동안은 연합이라고 한다.
연합을 이루고 있는 각 칸의 인구수는 (연합의 인구수) / (연합을 이루고 있는 칸의 개수)가 된다. 편의상 소수점은 버린다.
연합을 해체하고, 모든 국경선을 닫는다.
각 나라의 인구수가 주어졌을 때, 인구 이동이 며칠 동안 발생하는지 구하는 프로그램을 작성하시오.</p>
<p>[입력]
첫째 줄에 N, L, R이 주어진다. (1 ≤ N ≤ 50, 1 ≤ L ≤ R ≤ 100)</p>
<p>둘째 줄부터 N개의 줄에 각 나라의 인구수가 주어진다. r행 c열에 주어지는 정수는 A[r][c]의 값이다. (0 ≤ A[r][c] ≤ 100)</p>
<p>인구 이동이 발생하는 일수가 2,000번 보다 작거나 같은 입력만 주어진다.</p>
<p>[출력]
인구 이동이 며칠 동안 발생하는지 첫째 줄에 출력한다.</p>
<h3 id="풀이-코드">[풀이 코드]</h3>
<pre><code>import java.io.*;
import java.util.*;

public class Main {
    static int N, L, R, sum;
    static boolean isMove;
    static int[][] arr;
    static boolean[][] v;
    static int[] dr = {0, 0, 1, -1};
    static int[] dc = {1, -1, 0, 0};
    static List&lt;Point&gt; list;

    static class Point{
        int r, c;
        Point(int r, int c){
            this.r=r;
            this.c=c;
        }
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        L = Integer.parseInt(st.nextToken());
        R = Integer.parseInt(st.nextToken());
        arr = new int[N][N];
        for (int i = 0; i &lt; N; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j &lt; N; j++) {
                arr[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        int result = 0;
        while(true) {
            isMove = false;

            v = new boolean[N][N];
            for (int i = 0; i &lt; N; i++) {
                for (int j = 0; j &lt; N; j++) {
                    if(!v[i][j]) {
                        boolean isPossible = check(i, j);
                        if(!isPossible) continue;
                        isMove = true;
                        list = new ArrayList&lt;&gt;();
                        sum=arr[i][j];
                        // list에 공유하는 좌표를 넣는다.
                        bfs(i, j);

                        int avg = sum/list.size();
                        // list에 있는 값을 평균으로 변경한다.
                        for(Point p : list) {
                            arr[p.r][p.c] = avg;
                        }
                    }
                }
            }
            if(!isMove) break;
            result++;
        }
        System.out.println(result);

    }
    private static boolean check(int r, int c) {
        // 먼저 사방에 L이상 R이하인 좌표가 있는지 확인한다.
        for (int d = 0; d &lt; 4; d++) {
            int nr = r + dr[d];
            int nc = c + dc[d];
            if(nr&lt;0 || nr&gt;=N || nc&lt;0 || nc&gt;=N) continue;
            int gap = Math.abs(arr[r][c] - arr[nr][nc]);
            if(gap&gt;=L &amp;&amp; gap&lt;=R) {
                return true;
            }
        }
        return false;
    }

    private static void bfs(int r, int c) {
        Queue&lt;Point&gt; queue = new ArrayDeque&lt;&gt;();
        queue.offer(new Point(r, c));
        v[r][c] = true;
        list.add(new Point(r, c));

        while(!queue.isEmpty()) {
            Point p = queue.poll();
            for (int d = 0; d &lt; 4; d++) {
                int nr = p.r + dr[d];
                int nc = p.c + dc[d];
                if(nr&lt;0 || nr&gt;=N || nc&lt;0 || nc&gt;=N || v[nr][nc]) continue;
                int gap = Math.abs(arr[p.r][p.c] - arr[nr][nc]);
                if(gap&gt;=L &amp;&amp; gap&lt;=R) {
                    queue.offer(new Point(nr, nc));
                    v[nr][nc] = true;
                    list.add(new Point(nr, nc));
                    sum+=arr[nr][nc];
                }
            }
        }
    }
}</code></pre><p>BFS를 이용해 시뮬레이션을 돌린 코드다.
먼저, 2중 반복문을 돌면서 방문되지 않은 좌표일 경우 해당 좌표에서 사방 탐색할 경우 L이상 R이하인지 확인한다.
확인이 되면 isMove를 True로 변경하고 BFS를 돌면서 열린 국경선을 리스트 변수 list 안에 Point 클래스로 좌표 객체를 생성 후 넣으면서 sum 변수에 값을 더한다.
이 작업이 끝나면 list 에는 열린 국경선의 모든 객체가 있고, sum 변수는 각 연합 크기의 합이 저장된다.
그리고 평균을 구하고 list 에 있는 좌표에 평균 값을 갱신한다.
만약 isMove가 False 일 경우, 인구 이동이 없으므로 break 문으로 while 문을 종료한다.</p>
<p>아래는 DFS 코드다.</p>
<pre><code>import java.util.*;
import java.io.*;

public class Main {
    static int N, L, R;
    static int[][] arr;
    static boolean[][] v;
    static int[] dr = {0, 0, 1, -1};
    static int[] dc = {1, -1, 0, 0};

    static int sum, count;
    static List&lt;int[]&gt; list;

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        L = Integer.parseInt(st.nextToken());
        R = Integer.parseInt(st.nextToken());
        arr = new int[N][N];
        for(int i=0; i&lt;N; i++) {
            st = new StringTokenizer(br.readLine());
            for(int j=0; j&lt;N; j++) {
                arr[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        int result = 0;
        while(true) {
            v = new boolean[N][N];
            boolean moved = false;

            for (int i = 0; i &lt; N; i++) {
                for (int j = 0; j &lt; N; j++) {
                    if(v[i][j]) continue;

                    list = new ArrayList&lt;&gt;();
                    sum=0;
                    count=0;

                    dfs(i, j);

                    if(list.size() &gt;= 2) {
                        int avg = sum / count;
                        for(int[] a : list) {
                            arr[a[0]][a[1]] = avg;
                        }
                        moved = true;
                    }
                }
            }

            if(!moved) {
                break;
            }
            result++;
        }
        System.out.println(result);
    }
    private static void dfs(int r, int c) {
        v[r][c] = true;
        list.add(new int[] {r, c});
        sum+=arr[r][c];
        count++;

        for (int d = 0; d &lt; 4; d++) {
            int nr = r+dr[d];
            int nc = c+dc[d];
            if(nr&lt;0 || nr&gt;=N || nc&lt;0 || nc&gt;=N || v[nr][nc]) continue;

            int gap = Math.abs(arr[r][c] - arr[nr][nc]);
            if(gap &gt;= L &amp;&amp; gap &lt;= R) {
                dfs(nr, nc);
            }
        }
    }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[BOJ 13335] 트럭]]></title>
            <link>https://velog.io/@lil_young/BOJ-13335-%ED%8A%B8%EB%9F%AD</link>
            <guid>https://velog.io/@lil_young/BOJ-13335-%ED%8A%B8%EB%9F%AD</guid>
            <pubDate>Sun, 31 Aug 2025 14:11:51 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<hr>
<p>강을 가로지르는 하나의 차선으로 된 다리가 하나 있다. 이 다리를 n 개의 트럭이 건너가려고 한다. 트럭의 순서는 바꿀 수 없으며, 트럭의 무게는 서로 같지 않을 수 있다. 다리 위에는 단지 w 대의 트럭만 동시에 올라갈 수 있다. 다리의 길이는 w 단위길이(unit distance)이며, 각 트럭들은 하나의 단위시간(unit time)에 하나의 단위길이만큼만 이동할 수 있다고 가정한다. 동시에 다리 위에 올라가 있는 트럭들의 무게의 합은 다리의 최대하중인 L보다 작거나 같아야 한다. 참고로, 다리 위에 완전히 올라가지 못한 트럭의 무게는 다리 위의 트럭들의 무게의 합을 계산할 때 포함하지 않는다고 가정한다.</p>
<p>예를 들어, 다리의 길이 w는 2, 다리의 최대하중 L은 10, 다리를 건너려는 트럭이 트럭의 무게가 [7, 4, 5, 6]인 순서대로 다리를 오른쪽에서 왼쪽으로 건넌다고 하자. 이 경우 모든 트럭이 다리를 건너는 최단시간은 아래의 그림에서 보는 것과 같이 8 이다.
<img src="https://velog.velcdn.com/images/lil_young/post/72c99bed-f341-4e84-9c9d-d39e7ea270d0/image.png" alt=""></p>
<p>다리의 길이와 다리의 최대하중, 그리고 다리를 건너려는 트럭들의 무게가 순서대로 주어졌을 때, 모든 트럭이 다리를 건너는 최단시간을 구하는 프로그램을 작성하라.</p>
<p>[입력]
입력 데이터는 표준입력을 사용한다. 입력은 두 줄로 이루어진다. 입력의 첫 번째 줄에는 세 개의 정수 n (1 ≤ n ≤ 1,000) , w (1 ≤ w ≤ 100) and L (10 ≤ L ≤ 1,000)이 주어지는데, n은 다리를 건너는 트럭의 수, w는 다리의 길이, 그리고 L은 다리의 최대하중을 나타낸다. 입력의 두 번째 줄에는 n개의 정수 a1, a2, ⋯ , an (1 ≤ ai ≤ 10)가 주어지는데, ai는 i번째 트럭의 무게를 나타낸다.</p>
<p>[출력]
출력은 표준출력을 사용한다. 모든 트럭들이 다리를 건너는 최단시간을 출력하라.</p>
<h3 id="풀이-코드">[풀이 코드]</h3>
<hr>
<pre><code>
import java.util.*;
import java.io.*;

public class Main {
    static int n, w, L;
    static int[] arr;
    static class Point {
        int w, value;
        Point(int w, int value) {
            this.w=w;
            this.value=value;
        }
    }
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        n = Integer.parseInt(st.nextToken()); // 트럭 수
        w = Integer.parseInt(st.nextToken()); // 다리 길이
        L = Integer.parseInt(st.nextToken()); // 다리 최대하중
        st = new StringTokenizer(br.readLine());
        arr = new int[n];
        for (int i = 0; i &lt; n; i++) {
            arr[i] = Integer.parseInt(st.nextToken());
        }
        int cnt=0;
        int idx=0;
        List&lt;Point&gt; list = new ArrayList&lt;&gt;();
        while(true) {
            cnt++;
            for(int i=0; i&lt;list.size(); i++) {
                list.get(i).w--;
                if(list.get(i).w == 0) {
                    list.remove(i--);
                }
            }

            int sum=0;
            for(int i=0; i&lt;list.size(); i++) {
                sum+=list.get(i).value;
            }

            if(idx &lt; n &amp;&amp; sum + arr[idx] &lt;= L) {
                list.add(new Point(w, arr[idx++]));    
            }

            if(idx==n &amp;&amp; list.isEmpty()) break;
        }![](https://velog.velcdn.com/images/lil_young/post/a72c55d4-6e79-4f79-8f2f-582653cc1478/image.png)

        System.out.println(cnt);
    }
}</code></pre><p>전형적인 시뮬레이션 문제다.
list는 현재 다리 위에 올라간 트럭을 나타내고, while 문을 돌면서 시뮬레이션을 시작한다.</p>
<p>첫 번째로 list의 갯수만큼 반복문을 돌면서 각 트럭마다 가지고 있는 다리 길이인 w를 1씩 감소한다. 이 과정에서 w가 0이되면 다리를 다 건넜다고 판단하므로 list에서 트럭을 제거한다.</p>
<p>두 번째로 list에 있는 트럭의 값을 sum 이라는 변수에 더한다.</p>
<p>세 번째로 이전에 구한 sum과 현재 idx에 있는 트럭을 더했을 때 L 이하이면 list에 트럭을 추가한다.</p>
<p>그리고 idx가 n이고 list가 비었을 경우 모든 트럭이 다리 위를 지나간 경우이기 때문에 break 문으로 while 문을 탈출한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BOJ 1654] 랜선 자르기]]></title>
            <link>https://velog.io/@lil_young/BOJ-1654-%EB%9E%9C%EC%84%A0-%EC%9E%90%EB%A5%B4%EA%B8%B0</link>
            <guid>https://velog.io/@lil_young/BOJ-1654-%EB%9E%9C%EC%84%A0-%EC%9E%90%EB%A5%B4%EA%B8%B0</guid>
            <pubDate>Tue, 12 Aug 2025 04:15:41 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<hr>
<p>집에서 시간을 보내던 오영식은 박성원의 부름을 받고 급히 달려왔다. 박성원이 캠프 때 쓸 N개의 랜선을 만들어야 하는데 너무 바빠서 영식이에게 도움을 청했다.</p>
<p>이미 오영식은 자체적으로 K개의 랜선을 가지고 있다. 그러나 K개의 랜선은 길이가 제각각이다. 박성원은 랜선을 모두 N개의 같은 길이의 랜선으로 만들고 싶었기 때문에 K개의 랜선을 잘라서 만들어야 한다. 예를 들어 300cm 짜리 랜선에서 140cm 짜리 랜선을 두 개 잘라내면 20cm는 버려야 한다. (이미 자른 랜선은 붙일 수 없다.)</p>
<p>편의를 위해 랜선을 자르거나 만들 때 손실되는 길이는 없다고 가정하며, 기존의 K개의 랜선으로 N개의 랜선을 만들 수 없는 경우는 없다고 가정하자. 그리고 자를 때는 항상 센티미터 단위로 정수길이만큼 자른다고 가정하자. N개보다 많이 만드는 것도 N개를 만드는 것에 포함된다. 이때 만들 수 있는 최대 랜선의 길이를 구하는 프로그램을 작성하시오.</p>
<p>[입력]
첫째 줄에는 오영식이 이미 가지고 있는 랜선의 개수 K, 그리고 필요한 랜선의 개수 N이 입력된다. K는 1이상 10,000이하의 정수이고, N은 1이상 1,000,000이하의 정수이다. 그리고 항상 K ≦ N 이다. 그 후 K줄에 걸쳐 이미 가지고 있는 각 랜선의 길이가 센티미터 단위의 정수로 입력된다. 랜선의 길이는 231-1보다 작거나 같은 자연수이다.</p>
<p>[출력]
첫째 줄에 N개를 만들 수 있는 랜선의 최대 길이를 센티미터 단위의 정수로 출력한다.</p>
<h3 id="풀이-코드">[풀이 코드]</h3>
<hr>
<pre><code>import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;

public class Main {
    static long K, N, max;
    static long[] arr;

    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        K = Long.parseLong(st.nextToken());
        N = Long.parseLong(st.nextToken());
        arr = new long[(int) K];
        max = Integer.MIN_VALUE;
        for (int i = 0; i &lt; K; i++) {
            arr[i] = Long.parseLong(br.readLine());
            max = Math.max(max, arr[i]);
        }

        long result = bs();
        System.out.println(result);
    }

    private static long bs() {
        long start=1;
        long end=max;

        while(start&lt;=end) {
            long mid=(start+end)/2;
            long length=0;
            for(long a : arr) {
                length+=(a / mid);
            }

            if(length&lt;N) {
                // 너무 길게 잘라 필요한 개수보다 적게 잘렸으므로 더 짧게 잘라야 함
                end = mid-1;
            }else {
                // 너무 짧게 잘라 필요한 개수 이상 잘렸으므로 더 길게 잘라도 됨
                start = mid+1;
            }
        }

        return start-1;
    }
}</code></pre><p>바이너리 서치 문제다.</p>
<p>start를 1로, end를 배열 중 가장 큰 값으로 선언한 후 mid를 구한다.
그리고 배열에 있는 모든 값을 mid로 나눈 몫을 length에 더하면 현재 랜선 길이를 알 수 있다.</p>
<p>만약 랜선 길이가 구하려고 하는 길이보다 작을 경우, mid가 크기때문에 랜선 길이가 적게 나온 것이므로 end = mid-1 로 하고,
랜선 길이가 구하려고 하는 길이보다 클 경우, mid가 작기때문에 랜선 길이가 크게 나온 것이므로 start = mid+1로 한다.
이렇게 계속 반복하다 보면 start는 계속해서 증가하고, end는 감소할 것이다. 결국에는 start와 end가 교차하는 시점에서 탐색이 종료되고 start-1 또는 end가 만들 수 있는 최대 랜선 길이가 된다.</p>
<p>필자는 처음에 start를 0으로 두어 divide zero exception 오류가 났었다. 반복문을 돌면서 어처피 start는 0이 될리가 없다고 생각했지만 start가 0이고 end가 1이면 mid는 (0+1)/2 가 되어서 0이 된다. 그리고 이후에 length+=(a / mid); 로직에서 오류가 발생한다는 걸 알게 되었다.</p>
<p>또한 바이너리 서치 문제를 풀 때는 모든 변수를 long으로 선언하는 습관을 들어야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BOJ 2805] 나무 자르기]]></title>
            <link>https://velog.io/@lil_young/BOJ-2805-%EB%82%98%EB%AC%B4-%EC%9E%90%EB%A5%B4%EA%B8%B0</link>
            <guid>https://velog.io/@lil_young/BOJ-2805-%EB%82%98%EB%AC%B4-%EC%9E%90%EB%A5%B4%EA%B8%B0</guid>
            <pubDate>Mon, 11 Aug 2025 10:00:52 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<hr>
<p>상근이는 나무 M미터가 필요하다. 근처에 나무를 구입할 곳이 모두 망해버렸기 때문에, 정부에 벌목 허가를 요청했다. 정부는 상근이네 집 근처의 나무 한 줄에 대한 벌목 허가를 내주었고, 상근이는 새로 구입한 목재절단기를 이용해서 나무를 구할것이다.</p>
<p>목재절단기는 다음과 같이 동작한다. 먼저, 상근이는 절단기에 높이 H를 지정해야 한다. 높이를 지정하면 톱날이 땅으로부터 H미터 위로 올라간다. 그 다음, 한 줄에 연속해있는 나무를 모두 절단해버린다. 따라서, 높이가 H보다 큰 나무는 H 위의 부분이 잘릴 것이고, 낮은 나무는 잘리지 않을 것이다. 예를 들어, 한 줄에 연속해있는 나무의 높이가 20, 15, 10, 17이라고 하자. 상근이가 높이를 15로 지정했다면, 나무를 자른 뒤의 높이는 15, 15, 10, 15가 될 것이고, 상근이는 길이가 5인 나무와 2인 나무를 들고 집에 갈 것이다. (총 7미터를 집에 들고 간다) 절단기에 설정할 수 있는 높이는 양의 정수 또는 0이다.</p>
<p>상근이는 환경에 매우 관심이 많기 때문에, 나무를 필요한 만큼만 집으로 가져가려고 한다. 이때, 적어도 M미터의 나무를 집에 가져가기 위해서 절단기에 설정할 수 있는 높이의 최댓값을 구하는 프로그램을 작성하시오.</p>
<p>[입력]
첫째 줄에 나무의 수 N과 상근이가 집으로 가져가려고 하는 나무의 길이 M이 주어진다. (1 ≤ N ≤ 1,000,000, 1 ≤ M ≤ 2,000,000,000)</p>
<p>둘째 줄에는 나무의 높이가 주어진다. 나무의 높이의 합은 항상 M보다 크거나 같기 때문에, 상근이는 집에 필요한 나무를 항상 가져갈 수 있다. 높이는 1,000,000,000보다 작거나 같은 양의 정수 또는 0이다.</p>
<p>[출력]
적어도 M미터의 나무를 집에 가져가기 위해서 절단기에 설정할 수 있는 높이의 최댓값을 출력한다.</p>
<h3 id="풀이-코드">[풀이 코드]</h3>
<hr>
<pre><code>import java.io.*;
import java.util.*;

public class Main {
    static int N, M;
    static long min, max;
    static long[] arr;
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        arr = new long[N];
        st = new StringTokenizer(br.readLine());
        max = Integer.MIN_VALUE;
        min = Integer.MAX_VALUE;
        for (int i = 0; i &lt; N; i++) {
            arr[i] = Integer.parseInt(st.nextToken());
            max = Math.max(max, arr[i]);
            min = Math.min(min, arr[i]);
        }
        long result = bs();
        System.out.println(result);
    }
    private static long bs() {
        long start = 0;
        long end = max;
        long result = 0;

        while(start&lt;=end) {
            long mid = (start+end)/2;

            long sum = 0;
            for (int i = 0; i &lt; N; i++) {
                if(arr[i]-mid &gt;= 0) {
                    sum+=(arr[i]-mid);    
                }
            }
            if(sum &gt;= M ) {
                result = mid;
                start = mid+1;
            }else {
                end = mid-1;
            }
        }
        return result;
    }
}</code></pre><p>start를 0으로, end를 배열 중 가장 큰 값으로 선언 후, start가 end보다 작거나 같을 때 while문을 계속 반복한다.
이 과정에서 mid는 (start+end)/2로 구하고 배열에 있는 모든 값에 mid를 뺀 값을 더하면 자른 나무 길이를 알 수 있다.</p>
<p>여기에서 자른 나무 길이를 나타내는 sum이 M보다 크거나 같을 경우 result = mid 로 갱신하는 이유는 자를 수 있는 나무 높이 중 가장 큰 값을 return 해야 하기 때문이다.
sum이 M보다 작을 경우 mid를 줄여야 하기때문에 end = mid-1 를 한다.</p>
<p>요즘 바이너리 서치 문제를 풀고 있는데 나무 자르기 유형의 문제가 2개 있었다.
원래는 이 문제를 풀려고 했으나, 실수로 먼저 14247번 나무 자르기 문제를 접했고 바이너리 서치가 아니여서 당황했다..</p>
]]></description>
        </item>
    </channel>
</rss>