<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>siha_014.log</title>
        <link>https://velog.io/</link>
        <description>뭐라도 해보자</description>
        <lastBuildDate>Tue, 29 Sep 2026 13:08:58 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>siha_014.log</title>
            <url>https://velog.velcdn.com/images/siha_014/profile/4d39fac2-ff1c-4ab8-ab2f-225fe69a3252/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. siha_014.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/siha_014" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[멀티 서버 환경에서의 동시성 제어]]></title>
            <link>https://velog.io/@siha_014/%EB%A9%80%ED%8B%B0-%EC%84%9C%EB%B2%84-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</link>
            <guid>https://velog.io/@siha_014/%EB%A9%80%ED%8B%B0-%EC%84%9C%EB%B2%84-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</guid>
            <pubDate>Tue, 29 Sep 2026 13:08:58 GMT</pubDate>
            <description><![CDATA[<h1 id="분산락이란">분산락이란</h1>
<p>여러 대의 서버나 프로세스가 동시에 같은 공유 자원에 접근할 때, 데이터 충돌을 막기 위한 방법이다.</p>
<p>낙관적 락과 비관적 락은 DB에서, <code>synchronized</code>는 JVM에서 동작한다면, 분산락은 여러 서버가 공유할 수 있는 외부 저장소를 두고 여기서 락을 관리한다.</p>
<h2 id="왜-필요한가">왜 필요한가</h2>
<p>규모가 작은 프로그램의 경우에는 한 대의 서버만으로도 충분하지만, 규모가 커지면 서버의 수를 늘려야 할 수도 있다.</p>
<p>이때 여러 대의 서버가 존재하고, 서버들이 하나의 공유 자원에 동시에 접근한다면 서버 간의 동시성도 제어해야 한다.</p>
<p><code>synchronized</code>를 사용할 경우 각 서버의 JVM에서 각각 동작하기 때문에, 한 서버에서 획득한 락을 다른 서버에서는 알 수 없다.</p>
<p>반면 비관적 락은 DB에서 동작하기 때문에, 여러 서버가 같은 DB를 공유하고 있다면 멀티 서버 환경에서도 동시성을 제어할 수 있다.</p>
<p>그렇다면 굳이 분산락을 사용하는 이유는 무엇일까?</p>
<p>비관적 락은 DB의 row에 락을 걸지만, 분산락은 Redis와 같은 외부 저장소에서 별도로 락을 관리할 수 있다.</p>
<p>일반적으로 분산락을 사용하는 이유로는 DB의 row에 직접 락을 걸지 않고 별도의 저장소에서 락 경합을 관리할 수 있다는 점, DB 커넥션을 잡은 상태에서 락을 기다리는 구조를 피할 수 있다는 점 등이 있다.</p>
<p>다만 이번 실험에서는 이러한 성능 차이까지 측정하지는 않고, 멀티 서버 환경에서 각각의 방식이 동시성을 어떻게 제어하는지 확인해보았다.</p>
<h2 id="redis의-redisson">Redis의 Redisson</h2>
<p>분산락을 구현할 수 있는 방법은 여러 가지가 있으나, 이번 프로젝트에서는 Redis의 Redisson을 사용했다.</p>
<p>Redis는 key-value 형태의 메모리 기반 저장소로 빠르게 데이터를 읽고 쓸 수 있으며, Redisson은 Redis를 이용해 분산락을 비롯한 여러 분산 자료구조를 사용할 수 있도록 제공하는 Java 라이브러리이다.</p>
<h1 id="spring에서-분산락-구현">Spring에서 분산락 구현</h1>
<h2 id="실험-환경">실험 환경</h2>
<p><img src="https://velog.velcdn.com/images/siha_014/post/63bb5354-a0f0-4de9-891b-46f8d1b63e40/image.png" alt=""></p>
<p>IntelliJ에서 서버를 8080, 8081 두 개를 동시에 띄워 멀티 서버 환경을 만들었다.</p>
<p><img src="https://velog.velcdn.com/images/siha_014/post/05094e0a-39f9-4fc8-8a41-03d0be69872e/image.png" alt=""></p>
<p>또 테스트를 위해 수량(quantity)이 5인 쿠폰을 생성했고, User 50명을 <code>data.sql</code>을 통해 만들어, 50명이 수량이 5인 쿠폰을 동시에 두 개의 서버를 통해 발급(issue)하도록 했다.</p>
<h2 id="테스트">테스트</h2>
<p>테스트는 JMeter로 진행했다.</p>
<p>Server 8080에는 User 1부터 25, Server 8081에는 User 26부터 50이 요청을 보내도록 설정했다.</p>
<p>즉, 두 서버에 동시에 요청이 들어오는 상황을 만들어 테스트했다.</p>
<h1 id="before-락-없이-구현">Before: 락 없이 구현</h1>
<p><img src="https://velog.velcdn.com/images/siha_014/post/031b843f-aab2-45a5-9d40-631e160286b5/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/siha_014/post/a5ef00e3-bcef-4090-8fba-192b9b00cffb/image.png" alt=""></p>
<h4 id="성공-케이스">성공 케이스</h4>
<pre><code class="language-json">{&quot;id&quot;:1,&quot;serialCode&quot;:&quot;D7D4565A-ADA&quot;,&quot;couponName&quot;:&quot;선착순 테스트 쿠폰&quot;,&quot;username&quot;:&quot;user01&quot;,&quot;status&quot;:&quot;UNUSED&quot;,&quot;startAt&quot;:&quot;2026-09-29T00:00:00&quot;,&quot;endAt&quot;:&quot;2026-12-31T23:59:59&quot;,&quot;createdAt&quot;:&quot;2026-09-29T21:17:48.77676&quot;}</code></pre>
<h4 id="에러-케이스">에러 케이스</h4>
<pre><code class="language-json">{&quot;timestamp&quot;:&quot;2026-09-29T12:17:48.838Z&quot;,&quot;status&quot;:500,&quot;error&quot;:&quot;Internal Server Error&quot;,&quot;path&quot;:&quot;/api/coupons/1/issued-coupons&quot;}</code></pre>
<p><img src="https://velog.velcdn.com/images/siha_014/post/ee35e566-897c-4115-8f17-a550c6c07b73/image.png" alt=""></p>
<h4 id="실패-케이스">실패 케이스</h4>
<pre><code class="language-json">{&quot;message&quot;:&quot;쿠폰 수량이 소진되었습니다. couponId=1&quot;}</code></pre>
<p>결과는 다음과 같았다.</p>
<p><strong>5건보다 초과된 9건이 성공했고, 3건은 500 에러가 발생했으며, 나머지 38건은 쿠폰 수량 소진으로 실패했다.</strong></p>
<p>쿠폰의 수량은 5개였기 때문에 정상적인 상황이라면 5건만 성공해야 한다.</p>
<p>그런데 실제로는 9건이 성공했다.</p>
<h2 id="lost-update">Lost Update</h2>
<pre><code class="language-java">public void issue() {
    // 기간/수량 체크...
    quantity--;  // 메모리 상의 값 -1
}</code></pre>
<p>여러 스레드가 거의 동시에 같은 Coupon row를 조회하면, 각자 자기 트랜잭션 안에서 &quot;그 시점의 quantity 값&quot;을 메모리에 들고 있게 된다.</p>
<p>예를 들어 스레드 A, B, C가 모두 <code>quantity=3</code>인 시점에 조회했다면,</p>
<pre><code class="language-text">A: 3 → 2
B: 3 → 2
C: 3 → 2</code></pre>
<p>셋 다 각자 <code>3 - 1 = 2</code>로 계산해서 <code>UPDATE</code>를 날린다.</p>
<p>때문에 세 번의 감소가 일어나야 하는데, 최종적으로는 딱 한 번 감소한 것처럼 덮어써지게 된다.</p>
<p>결과적으로 9번의 성공(9번의 <code>quantity--</code> 시도)이 있었지만, 실제로 반영된 감소분은 5번(<code>5 → 0</code>)뿐이다.</p>
<p>나머지 4번의 감소는 다른 트랜잭션에 덮어써져서 유실되었다.</p>
<p>이 현상이 <strong>Lost Update</strong>이다.</p>
<h2 id="dead-lock">Dead Lock</h2>
<pre><code class="language-text">ErrorCode: 1213, SQLState: 40001
Deadlock found when trying to get lock; try restarting transaction</code></pre>
<p>서버에서의 로그를 확인한 결과는 위와 같았다.</p>
<p><strong>애플리케이션 레벨에 락을 하나도 걸지 않았는데, MySQL(InnoDB) 자체에서 데드락이 발생한 것이다.</strong></p>
<p><code>synchronized</code>나 <code>ReentrantLock</code> 같은 애플리케이션 레벨 락이 없어도, InnoDB는 <code>UPDATE</code>문을 실행하는 과정에서 해당 row에 대한 락을 사용한다.</p>
<p>이것은 DB 엔진이 데이터의 동시 변경을 처리하기 위해 사용하는 락이기 때문에, 우리가 명시적으로 <code>SELECT ... FOR UPDATE</code>를 사용하지 않았더라도 락 경합이 발생할 수 있다.</p>
<p>다만 <strong><code>UPDATE</code>가 row lock을 사용한다는 사실만으로 이번 Deadlock의 정확한 원인까지 알 수 있는 것은 아니다.</strong></p>
<p>이번 테스트에서는 <code>issued_coupons</code>를 INSERT한 뒤 <code>coupons</code>를 UPDATE하는 과정이 있었기 때문에, 외래 키(FK)와 관련된 락이 Deadlock에 영향을 주었을 가능성도 있다.</p>
<p>정확한 원인은 다음과 같이 InnoDB의 Deadlock 정보를 확인해야 한다.</p>
<pre><code class="language-sql">SHOW ENGINE INNODB STATUS\G</code></pre>
<p>따라서 이번 테스트에서는 특정 원인이라고 단정하기보다는, <strong>애플리케이션에서 별도의 락을 사용하지 않아도 DB 내부의 락 경합으로 Deadlock이 발생할 수 있다는 것</strong>을 확인했다.</p>
<h1 id="before-비관적-락-적용">Before: 비관적 락 적용</h1>
<p>다음으로 비관적 락을 적용했다.</p>
<pre><code class="language-java">public interface CouponRepository extends JpaRepository&lt;Coupon, Long&gt; {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;SELECT c FROM Coupon c WHERE c.id = :id&quot;)
    Optional&lt;Coupon&gt; findByIdForUpdate(@Param(&quot;id&quot;) Long id);
}</code></pre>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class IssuedCouponService {

    private final IssuedCouponRepository issuedCouponRepository;
    private final CouponRepository couponRepository;
    private final UserService userService;

    @Transactional
    public IssuedCouponResponse issue(Long couponId, IssuedCouponRequest dto) {
        User user = userService.findUser(dto.getUserId());

        Coupon coupon = couponRepository.findByIdForUpdate(couponId)
                        .orElseThrow(() -&gt; new CouponNotFoundException(couponId));

        coupon.issue();

        IssuedCoupon issuedCoupon = IssuedCoupon.builder()
                .coupon(coupon)
                .user(user)
                .startAt(coupon.getStartAt())
                .endAt(coupon.getEndAt())
                .build();

        issuedCouponRepository.save(issuedCoupon);
        return IssuedCouponResponse.from(issuedCoupon);
    }
}</code></pre>
<p><code>PESSIMISTIC_WRITE</code>를 사용하면 쿠폰 row를 조회하는 시점에 쓰기 락을 획득한다.</p>
<p>따라서 먼저 락을 획득한 트랜잭션이 쿠폰 발급을 처리하는 동안, 다른 트랜잭션은 해당 row의 락이 풀릴 때까지 기다리게 된다.</p>
<p><img src="https://velog.velcdn.com/images/siha_014/post/a0f2a36d-1c60-449a-9925-28e6c58995de/image.png" alt=""></p>
<p>두 번 테스트해본 결과, 두 번 모두 <strong>5명 성공 &amp; 45명 실패(쿠폰 수량 소진)</strong>&#xC73C;로 시나리오대로의 결과가 나왔다.</p>
<p>쿠폰 수량이 5개이므로 정상적으로 5건만 성공했다.</p>
<p>또한 테스트할 때마다 성공하는 사용자가 달라지는 것을 확인할 수 있었다.</p>
<p>비관적 락은 요청의 순서를 보장하는 것이 아니라, 동시에 접근하는 트랜잭션이 락을 획득할 때까지 기다리도록 하여 동시 변경을 제어하기 때문이다.</p>
<h1 id="after-redisson-분산락-적용">After: Redisson 분산락 적용</h1>
<p>다음으로 Redis의 Redisson을 이용해 분산락을 적용했다.</p>
<pre><code class="language-gradle">implementation &#39;org.redisson:redisson-spring-boot-starter:4.7.0&#39;</code></pre>
<p><code>redisson-spring-boot-starter</code>를 사용하면 <code>RedissonClient</code> 빈이 자동 등록되므로 <code>@Configuration</code> 클래스를 따로 만들어 <code>RedissonClient</code>를 등록하지 않아도 된다.</p>
<p>Redis의 host, port 등의 연결 정보는 별도의 설정을 통해 구성했다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class IssuedCouponService {

    private final IssuedCouponRepository issuedCouponRepository;
    private final CouponRepository couponRepository;
    private final UserService userService;
    private final IssuedCouponExecutor issuedCouponExecutor;
    private final RedissonClient redissonClient;

    public IssuedCouponResponse issue(Long couponId, IssuedCouponRequest dto) {
        String lockKey = &quot;lock:coupon:&quot; + couponId;
        RLock lock = redissonClient.getLock(lockKey);

        boolean acquired = false;

        try {
            acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);

            if (!acquired) {
                throw new LockAcquisitionException(lockKey);
            }

            return issuedCouponExecutor.issue(couponId, dto);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new LockAcquisitionException(lockKey);

        } finally {
            if (acquired &amp;&amp; lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    ...
}</code></pre>
<p><code>IssuedCouponService</code>에 <code>RedissonClient</code>를 주입하고 <code>RLock</code>을 적용했다.</p>
<p><code>tryLock(3, 10, TimeUnit.SECONDS)</code>에서 첫 번째 <code>3</code>은 락을 획득하기 위해 최대 3초 동안 기다리는 시간이고, 두 번째 <code>10</code>은 락을 획득했을 때 최대 10초 동안 락을 점유하는 시간이다.</p>
<p>락을 획득하지 못한 경우에는 <code>LockAcquisitionException</code>을 발생시키도록 했다.</p>
<p>그리고 락을 획득한 경우 <code>finally</code>에서 락을 해제한다.</p>
<h2 id="왜-issuedcouponexecutor를-분리했는가">왜 <code>IssuedCouponExecutor</code>를 분리했는가?</h2>
<p>여기서 실제 쿠폰 발급 로직을 <code>IssuedCouponExecutor</code>라는 별도의 Bean으로 분리했다.</p>
<pre><code class="language-text">IssuedCouponService
    ↓
분산락 획득
    ↓
IssuedCouponExecutor
    ↓
@Transactional
    ↓
쿠폰 발급</code></pre>
<p>분산락을 획득하는 메서드와 실제 DB 트랜잭션을 처리하는 메서드를 분리하기 위해서다.</p>
<p>Spring의 <code>@Transactional</code>은 프록시를 통해 동작하기 때문에 같은 클래스 안에서 자신의 메서드를 호출하는 self-invocation에서는 프록시를 거치지 않는다.</p>
<p>예를 들어 다음과 같은 구조라면,</p>
<pre><code class="language-java">public void issue() {
    // 분산락 획득

    issueInternal();
}

@Transactional
public void issueInternal() {
    // 실제 발급
}</code></pre>
<p><code>issueInternal()</code>의 <code>@Transactional</code>이 의도대로 적용되지 않을 수 있다.</p>
<p>따라서 실제 트랜잭션 로직을 별도의 Bean으로 분리해서 프록시를 거치도록 했다.</p>
<h1 id="비교">비교</h1>
<p>세 가지 방식을 비교하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th></th>
<th align="right">락 없음</th>
<th align="right">비관적 락</th>
<th align="right">Redisson 분산락</th>
</tr>
</thead>
<tbody><tr>
<td>성공 건수</td>
<td align="right">9</td>
<td align="right">5</td>
<td align="right">5</td>
</tr>
<tr>
<td>Lost Update</td>
<td align="right">발생</td>
<td align="right">발생하지 않음</td>
<td align="right">발생하지 않음</td>
</tr>
<tr>
<td>Deadlock</td>
<td align="right">3건</td>
<td align="right">0건</td>
<td align="right">0건</td>
</tr>
<tr>
<td>정상적인 수량 제어</td>
<td align="right">실패</td>
<td align="right">성공</td>
<td align="right">성공</td>
</tr>
</tbody></table>
<p>먼저 락이 없는 경우에는 5개인 쿠폰에서 9건이 성공하면서 Lost Update가 발생했다.</p>
<p>또한 3건의 Deadlock으로 인해 500 에러도 발생했다.</p>
<p>비관적 락을 적용한 경우에는 DB에서 쿠폰 row를 잠그고 다른 트랜잭션이 기다리도록 하여, 정확히 5건만 성공했다.</p>
<p>Redisson 분산락을 적용한 경우에도 두 서버가 Redis를 통해 동일한 락을 공유하기 때문에 정확히 5건만 성공했다.</p>
<p>이번 실험에서는 <strong>비관적 락과 분산락 모두 멀티 서버 환경에서 동시성을 정상적으로 제어할 수 있었다.</strong></p>
<p>다만 이번 테스트만으로 비관적 락과 분산락 중 어느 방식이 더 빠르거나 효율적인지를 판단할 수는 없다.</p>
<h1 id="마무리">마무리</h1>
<p>이번 글에서는 서버를 2대로 구성하여 멀티 서버 환경에서 동시성 제어를 테스트해보았다.</p>
<p>지금까지 살펴본 동시성 제어 방법을 정리하면 다음과 같다.</p>
<ul>
<li><strong>낙관적 락</strong> → 충돌을 가정하지 않고, 변경 시점에 충돌을 확인한다.</li>
<li><strong>비관적 락</strong> → DB에서 row를 잠가 동시 변경을 제어한다.</li>
<li><strong><code>synchronized</code></strong> → 하나의 JVM 안에서 공유 자원에 대한 동시 접근을 제어한다.</li>
<li><strong>분산락</strong> → 여러 서버가 공유할 수 있는 외부 저장소를 통해 락을 관리한다.</li>
</ul>
<p>처음에는 서버가 여러 대가 되면 DB에서 사용하는 비관적 락도 사용할 수 없는 것이 아닌가 생각했다.</p>
<p>하지만 실제로 테스트해보니, 여러 서버가 <strong>같은 DB를 공유하고 있다면 비관적 락 역시 서버 간의 동시성을 제어할 수 있었다.</strong></p>
<p>반면 <code>synchronized</code>는 JVM 내부에서 동작하기 때문에 서버가 여러 대가 되면 서버 간의 동시성까지 제어할 수 없다.</p>
<p>결국 중요한 것은 단순히 서버의 개수가 아니라 <strong>어떤 범위의 공유 자원을 제어해야 하는가</strong>였다.</p>
<p>하나의 JVM 안에서 공유되는 자원이라면 <code>synchronized</code>를 사용할 수 있고, 여러 서버가 같은 DB의 데이터를 변경한다면 DB의 락을 사용할 수 있다.</p>
<p>그리고 DB 외부에서 여러 서버가 공유해야 하는 락이 필요하다면 Redis와 같은 외부 저장소를 이용한 분산락을 사용할 수 있다.</p>
<p>이번 동시성 제어 시리즈에서는 낙관적 락과 비관적 락을 시작으로 Java의 <code>synchronized</code>, 그리고 멀티 서버 환경에서의 분산락까지 직접 테스트해보았다.</p>
<p>각 락의 사용법만 공부했을 때보다 직접 같은 상황에서 적용하고 결과를 비교해보면서, <strong>동시성 문제를 어떤 범위에서 해결해야 하는지</strong> 생각해볼 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java의 동시성 제어]]></title>
            <link>https://velog.io/@siha_014/Java%EC%9D%98-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</link>
            <guid>https://velog.io/@siha_014/Java%EC%9D%98-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</guid>
            <pubDate>Fri, 25 Sep 2026 14:59:03 GMT</pubDate>
            <description><![CDATA[<p>앞서 살펴본 낙관적 락과 비관적 락은 DB에서의 동시성 제어 방법이다. Java 애플리케이션에서도 여러 Thread가 하나의 자원을 동시에 접근하면서 동시성 문제가 발생할 수 있는데, Java에서는 <code>synchronized</code> 키워드를 통해 이러한 동시성 문제를 제어할 수 있다.</p>
<h1 id="synchronized란">synchronized란</h1>
<p>멀티스레드 환경에서 여러 Thread가 공유하는 자원에 동시에 접근하는 것을 제어하기 위한 Java의 키워드이다.
즉, 여러 Thread가 하나의 공유 자원을 동시에 건드려서 문제가 생기는 것을 막기 위해 사용하는 것이다.</p>
<h2 id="왜-synchronized가-필요한가">왜 synchronized가 필요한가</h2>
<p>프로그램을 실행하면 하나의 작업만 순서대로 처리하는 것이 아니라 여러 Thread가 동시에 동작할 수 있다.
문제는 여러 Thread가 하나의 공유 자원을 함께 사용할 때 발생한다.
특히 여러 Thread가 같은 객체의 상태를 읽고 변경하는 경우, 실행 순서에 따라 결과가 달라질 수 있다.</p>
<pre><code class="language-java">public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized void decrement() {
        counter--;
    }

    public synchronized int value() {
        return counter;
    }
}</code></pre>
<p>위의 코드에서, <code>increment()</code>와 <code>decrement()</code> 함수는 값을 읽고, 증가 또는 감소시키고, 다시 저장하는 여러 과정을 거친다.</p>
<p>그런데 만약 Thread A와 Thread B가 <code>increment()</code>을 동시에 수행한다면,</p>
<pre><code class="language-text">Thread A: stock 읽기
Thread B: stock 읽기
Thread A: stock 증가
Thread B: stock 증가
Thread A: stock 저장
Thread B: stock 저장</code></pre>
<p>처럼 서로의 실행이 끼어들 수 있다.
이렇게 여러 Thread의 실행이 서로 끼어들어 실행되는 것을 <strong>interleaving</strong>이라고 한다.</p>
<p>결국 여러 Thread가 공유 자원에 동시에 접근하면서 예상하지 못한 결과가 발생할 수 있고, 이런 문제가 발생할 수 있는 구간을 <strong>Critical Section(임계 영역)</strong>이라고 한다.</p>
<p>따라서 Critical Section에 여러 Thread가 동시에 접근하지 못하도록 동시성 제어가 필요하다.
Java에서는 이를 위해 <code>synchronized</code>를 사용할 수 있다.</p>
<h2 id="synchronized-method">synchronized method</h2>
<p><code>synchronized</code>를 메서드에 붙이면 해당 메서드는 <code>synchronized method</code>가 된다.</p>
<pre><code class="language-java">public synchronized void decrement() {
    stock--;
}</code></pre>
<p>이 메서드를 여러 Thread가 동시에 호출한다고 해서 여러 Thread가 동시에 메서드 내부를 실행할 수 있는 것은 아니다.</p>
<p>같은 객체의 <code>synchronized</code> 메서드에 접근하려는 Thread 중 하나가 먼저 락을 획득하면 해당 Thread가 메서드를 실행하고, 다른 Thread들은 락을 획득할 때까지 기다린다.</p>
<p>즉,</p>
<pre><code class="language-text">Thread A → Lock 획득 → 메서드 실행 → Lock 해제
Thread B →            대기
                         ↓
                     Lock 획득 → 메서드 실행</code></pre>
<p>과 같은 식으로 동작한다.</p>
<h2 id="synchronized-statement">synchronized statement</h2>
<p>메서드 전체가 아니라 특정 코드 영역에만 동기화를 적용하고 싶다면 <code>synchronized statement</code>를 사용할 수 있다.</p>
<pre><code class="language-java">public void decrease() {
    synchronized (this) {
        stock--;
    }
}</code></pre>
<p>여기서 <code>this</code>는 현재 객체를 의미하며, <code>synchronized</code> 뒤에 있는 객체를 기준으로 락을 획득한다.</p>
<p>즉 <code>synchronized</code>는 단순히 &quot;메서드에 락을 건다&quot;라기보다는 <strong>특정 객체를 기준으로 동기화를 수행한다</strong>고 이해하는 것이 더 정확하다.</p>
<h2 id="monitor-lock">Monitor Lock</h2>
<p>그렇다면 여기서 한 가지 의문이 생긴다.</p>
<p><code>Thread</code>가 어떻게 서로 기다리고, 누가 먼저 실행할지를 어떻게 정하는 것일까?</p>
<p><code>synchronized</code>는 JVM이 관리하는 <strong>Intrinsic Lock(내재적 락)</strong> 또는 <strong>Monitor Lock</strong>을 이용한다.</p>
<p>개발자가 직접 Monitor 객체를 만들어 사용하는 것은 아니고, Java 객체를 동기화의 대상으로 사용하면 JVM이 그에 필요한 락 상태를 관리한다.</p>
<pre><code class="language-java">public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}</code></pre>
<p>위 코드에서는 <code>this</code> 객체를 기준으로 Monitor Lock을 획득한다.</p>
<p>한 Thread가 해당 락을 획득하고 Critical Section을 실행하는 동안 다른 Thread가 같은 락을 획득하려고 하면 기다리게 된다.</p>
<p>그리고 먼저 락을 획득한 Thread가 작업을 끝내고 락을 해제하면 다른 Thread가 락을 획득하여 실행할 수 있다.</p>
<p>결국 우리가 <code>synchronized</code>라는 키워드 하나를 사용하면 JVM이 뒤에서 이런 동기화 과정을 처리해주는 것이다.</p>
<h2 id="synchronized가-보장하는-것">synchronized가 보장하는 것</h2>
<p>여기서 <code>synchronized</code>가 단순히 &quot;한 명씩 실행시킨다&quot;만 보장하는 것은 아니다.</p>
<h3 id="mutual-exclusion">Mutual Exclusion</h3>
<p>가장 먼저 생각할 수 있는 것은 <strong>상호 배제(Mutual Exclusion)</strong>이다.</p>
<p>같은 Monitor Lock을 사용하는 여러 Thread가 있을 때 한 번에 하나의 Thread만 해당 Critical Section을 실행할 수 있다.</p>
<p>따라서 여러 Thread가 공유 자원의 상태를 동시에 변경하는 것을 막을 수 있다.</p>
<h3 id="happens-before-relationship">Happens-Before Relationship</h3>
<p>그리고 <code>synchronized</code>는 Thread 간의 <strong>메모리 가시성(Visibility)</strong>도 보장한다.</p>
<p>한 Thread가 <code>synchronized</code> 영역에서 공유 상태를 변경하고 락을 해제한 뒤, 다른 Thread가 같은 Monitor의 락을 획득하면 이전 Thread의 변경 사항을 볼 수 있도록 보장된다.</p>
<p>이러한 실행 순서와 메모리 가시성에 대한 관계를 <strong>Happens-Before Relationship</strong>이라고 한다.</p>
<p>즉 <code>synchronized</code>는 단순히 Thread를 줄 세우는 것뿐만 아니라, Thread 간의 메모리 가시성까지 보장하는 역할을 한다.</p>
<h2 id="synchronized의-범위">synchronized의 범위</h2>
<p>여기서 중요한 점이 하나 있다.</p>
<p><code>synchronized</code>는 <strong>JVM 내부에서 동작한다.</strong></p>
<p>따라서 하나의 JVM 안에서 여러 Thread가 같은 객체를 공유하는 상황에서는 사용할 수 있지만, 여러 서버가 각각 별도의 JVM으로 실행되는 환경에서는 서버 간의 동시성을 제어할 수 없다.</p>
<pre><code class="language-text">Server A                         Server B

JVM A                            JVM B
 ├─ Thread A                     ├─ Thread B
 └─ Thread C                     └─ Thread D
       ↓                               ↓
 synchronized                     synchronized
       ↓                               ↓
     서로 다른 JVM이므로 서로의 Lock을 모름</code></pre>
<p>따라서 멀티 서버 환경에서 동일한 DB 데이터를 여러 서버가 동시에 변경하는 상황이라면 <code>synchronized</code>만으로는 충분하지 않다.</p>
<p>이런 경우 DB Lock이나 Distributed Lock과 같은 다른 동시성 제어 방법을 고려할 수 있다.</p>
<h1 id="다른-동시성-제어-방법과의-비교">다른 동시성 제어 방법과의 비교</h1>
<p>여기까지 공부하고 나니 낙관적 락, 비관적 락, <code>synchronized</code>, Distributed Lock이 왜 자꾸 같이 등장하는지도 조금 이해할 수 있었다.</p>
<p>결국 모두 동시성 문제를 해결하기 위한 방법이지만, <strong>어디에서 무엇을 보호하는지</strong>가 다르다.</p>
<ul>
<li><strong>Optimistic Lock</strong> → DB 데이터의 충돌을 감지하고 처리</li>
<li><strong>Pessimistic Lock</strong> → DB 데이터를 미리 잠그고 충돌을 방지</li>
<li><strong><code>synchronized</code></strong> → 하나의 JVM 안에서 Thread 간 공유 상태를 제어</li>
<li><strong>Distributed Lock</strong> → 여러 서버/JVM에서 공유되는 작업을 제어</li>
</ul>
<p>따라서 어떤 락이 무조건 더 좋은 것이 아니라, 현재 어떤 자원을 공유하고 있고 어떤 환경에서 동작하는지를 보고 적절한 방법을 선택해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[낙관적 락과 비관적 락]]></title>
            <link>https://velog.io/@siha_014/%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD%EA%B3%BC-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD</link>
            <guid>https://velog.io/@siha_014/%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD%EA%B3%BC-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD</guid>
            <pubDate>Thu, 24 Sep 2026 13:30:18 GMT</pubDate>
            <description><![CDATA[<h1 id="낙관적-락과-비관적-락">낙관적 락과 비관적 락</h1>
<p>앞선 글에서 낙관적 락과 비관적 락을 각각 적용해 보았다.</p>
<p>낙관적 락은 <strong>충돌이 드물 것이라고 가정하고, 일단 작업을 진행한 뒤 저장하는 시점에 충돌 여부를 확인하는 방식</strong>이다.</p>
<p>반대로 비관적 락은 <strong>충돌이 발생할 가능성이 높다고 가정하고, 데이터를 조회하는 시점부터 다른 트랜잭션의 동시 변경을 제한하는 방식</strong>이다.</p>
<p>결국 두 락 모두 <strong>&quot;여러 요청이 동시에 같은 데이터를 변경할 때 어떻게 데이터의 정합성을 지킬 것인가?&quot;</strong>라는 같은 문제를 해결하기 위한 방법이다.</p>
<p>다만 그 문제에 접근하는 방식은 정반대였다.</p>
<ul>
<li>낙관적 락 → 일단 실행하고, 충돌이 발생하면 처리한다.</li>
<li>비관적 락 → 충돌하기 전에 락을 획득하고, 다른 트랜잭션을 대기시킨다.</li>
</ul>
<h1 id="동작-방식-비교">동작 방식 비교</h1>
<table>
<thead>
<tr>
<th></th>
<th>낙관적 락</th>
<th>비관적 락</th>
</tr>
</thead>
<tbody><tr>
<td>전제</td>
<td>충돌은 드물다</td>
<td>충돌이 잦을 수 있다</td>
</tr>
<tr>
<td>방식</td>
<td>물리적인 락 없이 작업하고 저장 시점에 version 검사</td>
<td>조회 시점에 DB row에 락을 획득</td>
</tr>
<tr>
<td>충돌 시</td>
<td>version 불일치 → 예외 발생</td>
<td>다른 트랜잭션의 락이 해제될 때까지 대기</td>
</tr>
<tr>
<td>대표적인 구현</td>
<td><code>@Version</code></td>
<td><code>@Lock(PESSIMISTIC_WRITE)</code></td>
</tr>
<tr>
<td>비용</td>
<td>충돌 시 재조회/재시도 비용</td>
<td>락 대기 및 DB 자원 점유 비용</td>
</tr>
</tbody></table>
<p>낙관적 락은 <code>@Version</code>을 이용하여 데이터를 읽을 당시의 version과 실제 UPDATE 시점의 version을 비교한다.</p>
<pre><code class="language-sql">UPDATE post
SET like_count = ?,
    version = version + 1
WHERE id = ?
  AND version = ?</code></pre>
<p>다른 트랜잭션이 먼저 데이터를 수정하여 version이 변경되었다면 UPDATE 조건을 만족하지 못하고, 낙관적 락 충돌이 발생한다.</p>
<p>반면 비관적 락은 조회 시점에 락을 획득한다.</p>
<pre><code class="language-sql">SELECT *
FROM product
WHERE id = ?
FOR UPDATE</code></pre>
<p>다른 트랜잭션이 이미 해당 row에 대한 쓰기 락을 가지고 있다면, 락이 해제될 때까지 대기한다.</p>
<p>즉, 낙관적 락은 <strong>충돌을 나중에 발견하는 방식</strong>이고, 비관적 락은 <strong>충돌 자체가 발생하지 않도록 동시 변경을 제한하는 방식</strong>이라고 볼 수 있다.</p>
<h1 id="handler의-필요성--왜-차이가-나는가">Handler의 필요성 — 왜 차이가 나는가</h1>
<p>두 락을 직접 구현해보면서 흥미로웠던 부분은 <strong>예외 처리의 필요성이 다르게 보였다는 점</strong>이었다.</p>
<h2 id="낙관적-락">낙관적 락</h2>
<p>낙관적 락에서는 충돌이 발생하면 version 불일치가 예외로 드러난다.</p>
<p>내가 좋아요 기능에 <code>@Version</code>을 적용했을 때도 동시에 같은 글을 수정하면 <code>ObjectOptimisticLockingFailureException</code>이 발생했다.</p>
<p>따라서 충돌을 성공으로 처리하고 싶다면 이 예외를 잡아서 재시도하는 로직이 필요하다.</p>
<p>앞선 테스트에서도 <code>Spring Retry</code>를 이용하여 재시도 횟수와 <code>backoff</code>를 설정했고, 재시도 횟수를 늘릴수록 최종적으로 성공하는 요청의 수가 증가하는 것을 확인했다.</p>
<p>반대로 충돌을 성공으로 처리할 필요가 없다면, 예외를 그대로 상위 계층으로 전달하여 실패 응답으로 처리하는 것도 가능하다.</p>
<p>즉, 낙관적 락에서 중요한 것은 <strong>충돌이 예외로 드러난다는 것</strong>이고, 그 충돌을 재시도할 것인지 실패로 처리할 것인지는 애플리케이션의 요구사항에 따라 결정해야 한다.</p>
<h2 id="비관적-락">비관적 락</h2>
<p>비관적 락은 조금 다르다.</p>
<p>기본적으로 다른 트랜잭션이 락을 가지고 있다면 바로 예외를 발생시키는 것이 아니라, 해당 락이 해제될 때까지 대기한다.</p>
<pre><code class="language-text">T1 → Product 조회 + 락 획득
             ↓
         재고 차감
             ↓
          COMMIT
             ↓
         락 해제

T2 → Product 조회 + 락 획득 시도
             ↓
            대기
             ↓
         T1의 락 해제
             ↓
         락 획득 후 진행</code></pre>
<p>그래서 앞선 재고 테스트에서는 별도의 <code>PessimisticLockException</code> 처리가 없어도 정상적으로 요청이 처리되었다.</p>
<p>하지만 락을 무한정 기다리게 둘 수는 없다.</p>
<p>그래서 다음과 같이 락 타임아웃을 설정할 수 있다.</p>
<pre><code class="language-java">@QueryHints({
    @QueryHint(
        name = &quot;jakarta.persistence.lock.timeout&quot;,
        value = &quot;3000&quot;
    )
})</code></pre>
<p>이 경우 락을 일정 시간 동안 획득하지 못하면 timeout이 발생하고, 환경에 따라 <code>PessimisticLockException</code>이나 Spring에서 변환된 <code>CannotAcquireLockException</code> 등의 예외가 발생할 수 있다.</p>
<p>따라서 비관적 락도 결국 <strong>실패 가능성을 고려한 예외 처리가 필요하다.</strong></p>
<p>결국 Handler의 필요 여부를 단순히</p>
<blockquote>
<p>낙관적 락은 Handler가 필요하고, 비관적 락은 필요 없다.</p>
</blockquote>
<p>라고 구분할 수는 없다.</p>
<p><strong>락 획득이나 충돌 실패를 애플리케이션에서 어떻게 처리할 것인지에 따라 달라진다.</strong></p>
<p>낙관적 락은 충돌을 발견하면 재시도할지 실패시킬지를 결정해야 하고, 비관적 락은 락 대기를 허용하더라도 timeout이나 Deadlock 등의 실패 상황을 고려해야 한다.</p>
<h1 id="그래서-언제-뭘-쓰는가">그래서 언제 뭘 쓰는가</h1>
<p>결국 어떤 락이 더 좋은지는 정해져 있지 않다.</p>
<p>데이터의 특성과 충돌 빈도, 정합성 요구사항에 따라 선택해야 한다.</p>
<h3 id="낙관적-락을-고려할-수-있는-경우">낙관적 락을 고려할 수 있는 경우</h3>
<ul>
<li>동일한 데이터를 동시에 수정할 가능성이 낮은 경우</li>
<li>충돌이 발생하더라도 재시도로 해결할 수 있는 경우</li>
<li>다른 트랜잭션을 기다리게 하지 않고 작업을 진행하고 싶은 경우</li>
<li>락을 오래 유지하는 비용을 피하고 싶은 경우</li>
</ul>
<p>앞선 테스트에서는 게시글의 좋아요에 낙관적 락을 적용했다.</p>
<p>다만 좋아요나 조회수라고 해서 항상 낙관적 락이 정답이라는 의미는 아니다.</p>
<p>서비스에서 좋아요 수가 반드시 정확하게 유지되어야 하는지, 충돌 발생 시 재시도할 것인지, 또는 일부 요청의 유실을 허용할 것인지에 따라 구현 방법은 달라질 수 있다.</p>
<h3 id="비관적-락을-고려할-수-있는-경우">비관적 락을 고려할 수 있는 경우</h3>
<ul>
<li>동일한 데이터를 동시에 변경할 가능성이 높은 경우</li>
<li>충돌이 발생했을 때 재시도하는 것보다 처음부터 동시 변경을 제한하는 것이 적절한 경우</li>
<li>데이터의 정합성이 중요한 경우</li>
<li>재고처럼 한정된 자원을 여러 요청이 동시에 차감하는 경우</li>
</ul>
<p>앞선 테스트에서는 상품 재고 차감에 비관적 락을 적용했다.</p>
<p>재고가 10개인데 여러 사용자가 동시에 구매한다면, 여러 트랜잭션이 같은 재고를 읽고 각각 차감하는 상황이 발생할 수 있다.</p>
<p>이 경우 재고보다 많은 주문이 생성되는 문제가 발생할 수 있기 때문에, 상품 row를 조회하는 시점에 비관적 락을 획득하여 동시 변경을 제한했다.</p>
<p>결국 두 락 모두 정답이라기보다는 <strong>데이터의 성격에 맞는 트레이드오프를 선택하는 것</strong>이라고 생각한다.</p>
<p>낙관적 락은 충돌을 허용하고 충돌이 발생했을 때 처리하는 대신, 락을 오래 유지하지 않는다는 장점이 있다.</p>
<p>비관적 락은 충돌 자체를 줄이는 대신, 락을 획득하고 있는 동안 다른 트랜잭션이 대기할 수 있고, 락 대기 시간이 길어지거나 Deadlock이 발생할 가능성도 고려해야 한다.</p>
<h1 id="마무리">마무리</h1>
<p>이번에 낙관적 락과 비관적 락을 직접 구현하면서 알게 된 것은, 두 방법 모두 결국 <strong>동시에 여러 요청이 같은 데이터를 변경할 때 발생하는 문제를 해결하기 위한 방법</strong>이라는 것이다.</p>
<p>다만 낙관적 락은</p>
<blockquote>
<p><strong>&quot;충돌이 발생할 수도 있지만 일단 진행하고, 나중에 확인하자.&quot;</strong></p>
</blockquote>
<p>비관적 락은</p>
<blockquote>
<p><strong>&quot;충돌할 것 같으니 먼저 잠그고 진행하자.&quot;</strong></p>
</blockquote>
<p>라는 차이가 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[비관적 락 (Pessimistic lock)]]></title>
            <link>https://velog.io/@siha_014/%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD-Pessimistic-lock</link>
            <guid>https://velog.io/@siha_014/%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD-Pessimistic-lock</guid>
            <pubDate>Wed, 23 Sep 2026 05:37:54 GMT</pubDate>
            <description><![CDATA[<h1 id="비관적-락이란">비관적 락이란,</h1>
<p>충돌이 잦을 거라고 가정하고, 다른 트랜잭션이 해당 데이터를 동시에 변경하지 못하도록 락을 획득하고, 필요한 경우 다른 트랜잭션이 락이 해제될 때까지 대기하게 한다.
<code>SELECT ... FOR UPDATE</code>처럼 조회 시점에 row를 잠근다.
이는 낙관적 락과 반대로 &quot;선 차단, 후 처리&quot;하는 방식이다.</p>
<p>이번에는 상품을 구매하면 재고가 차감되는 상황에서의 동시성을 구현해봤다.</p>
<h1 id="spring에서-비관적-락-구현">Spring에서 비관적 락 구현</h1>
<pre><code class="language-java">   @Lock(LockModeType.PESSIMISTIC_WRITE)
   @Query(&quot;select p from Product p where p.id = :id&quot;)
   Optional&lt;Product&gt; findByIdForUpdate(@Param(&quot;id&quot;) Long id);</code></pre>
<p>적용하고자 하는 객체의 조회 메서드에 <code>@Lock(LockModeType.PESSIMISTIC_WRITE)</code>를 지정하면 JPA/Hibernate가 해당 조회를 비관적 쓰기 락을 획득하는 쿼리로 처리하며, MySQL에서는 일반적으로 <code>SELECT ... FOR UPDATE</code> 형태로 실행된다.</p>
<p>구현하고자 한 것은 상품을 구매(purchase)하면 주문을 생성하고, 재고를 차감하는 상황이다. </p>
<h2 id="before-락-없이findbyid-돌려본-결과--lost-update">Before: 락 없이(<code>findById</code>) 돌려본 결과 — Lost Update</h2>
<pre><code class="language-java">// 락 없이 조회 (동시성 문제 재현용)
    @Transactional
    public OrderResponse purchaseWithoutLock(Long productId, CreateOrderRequest dto) {
        Product product = productRepository.findById(productId)
                .orElseThrow(() -&gt; new EntityNotFoundException(&quot;Product not found: &quot; + productId));

        product.purchase(dto.getQuantity());

        Order order = Order.builder()
                .product(product)
                .username(dto.getUsername())
                .quantity(dto.getQuantity())
                .build();

        orderRepository.save(order);
        return OrderResponse.from(order);
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/siha_014/post/425534ed-a461-4e3b-a09c-8feccc0cdc1d/image.png" alt=""></p>
<p><code>findById</code>로 조회할 경우, 즉 비관적 락이 적용하지 않았을 때, 재고 10개, 스레드 100개로 테스트한 결과, 최종 재고는 0으로 &quot;그럴듯하게&quot; 나왔다. 
알고보니 이는 구매 메서드의 애플리케이션 레벨 검증이 있어서, <code>IllegalStateException: 재고 부족</code> 이 발생한 것이었다.</p>
<pre><code class="language-java">    public void purchase(int quantity) {
        if (stock &lt; quantity) {
            throw new IllegalStateException(&quot;재고 부족&quot;);
        }
        this.stock -= quantity;
    }</code></pre>
<p>하지만 <code>Order</code> 테이블을 확인해보니 실제 성공 건수는 18건으로 재고보다 초과된 결과나 나왔다. 즉, 동시성 제어에 실패했음을 알 수 있다.</p>
<p>재고가 10인 상태에서 여러 트랜잭션이 동시에 조회하면, 여러 트랜잭션이 모두 &quot;재고가 10&quot;이라고 읽을 수 있다.
각 트랜잭션이 자신의 계산 결과를 DB에 반영하면서 서로의 변경을 덮어쓸 수 있기 때문에 Lost Update가 발생한다.</p>
<h3 id="deadlock-발생">Deadlock 발생</h3>
<p>이때 Deadlock도 발생했다. 비관적 락을 명시적으로 사용하지 않았더라도 InnoDB는 UPDATE를 수행할 때 필요한 행에 락을 획득한다. 여러 트랜잭션이 동일한 데이터를 동시에 변경하려는 과정에서 서로 락을 기다리는 교착 상태가 발생할 수 있다.</p>
<p>실제 테스트에서도 다음과 같은 예외가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/siha_014/post/66f986f1-7ffb-4fe8-a769-9c3219d36330/image.png" alt=""></p>
<p>MySQL의 InnoDB는 Deadlock을 감지하면 관련 트랜잭션 중 하나를 rollback하여 교착 상태를 해소한다. 따라서 이 경우 애플리케이션에서는 해당 예외를 전달받게 된다.</p>
<h2 id="after-비관적-락-적용findbyidforupdate-결과">After: 비관적 락 적용(<code>findByIdForUpdate</code>) 결과</h2>
<pre><code class="language-java">@Transactional
    public OrderResponse purchase(Long productId, CreateOrderRequest dto) {
        Product product = productRepository.findByIdForUpdate(productId)
                .orElseThrow(() -&gt; new EntityNotFoundException(&quot;Product not found: &quot; + productId));

        product.purchase(dto.getQuantity());

        Order order = Order.builder()
                .product(product)
                .username(dto.getUsername())
                .quantity(dto.getQuantity())
                .build();

        orderRepository.save(order);
        return OrderResponse.from(order);
    }</code></pre>
<p>비관적 락을 적용한 <code>findByIdForUpdate</code> 메서드를 사용해 같은 조건에서 테스트한 결과 정확히 재고 0, 주문 10건으로 성공할 수 있었다.</p>
<p>또, 예외(<code>PessimisticLockException</code>, <code>CannotAcquireLockException</code>) 처리가 없을 때도 문제 없이 실행되었었는데, 비관적 락은 다른 트랜잭션이 락을 보유하고 있는 경우 즉시 충돌 예외를 발생시키기보다는, 기본적으로 락이 해제될 때까지 대기한다. 다만 락 획득에 제한 시간을 설정한 경우 timeout이 발생하면 예외가 발생할 수 있다. 비관적 락은 충돌이 발생한 뒤 데이터를 되돌리는 방식이 아니라, 락을 획득한 트랜잭션이 작업을 끝낼 때까지 다른 트랜잭션의 해당 row에 대한 락 획득을 대기시켜 동시 변경을 직렬화한다.</p>
<h3 id="락-타임아웃을-건-이유">락 타임아웃을 건 이유</h3>
<pre><code class="language-java">@QueryHints({@QueryHint(name = &quot;jakarta.persistence.lock.timeout&quot;, value = &quot;3000&quot;)})</code></pre>
<p>이후에는 락에 타임아웃을 걸었는데, 기본값인 무한 대기로 두면, 극단적인 상황에서 요청이 지나치게 오랫동안 대기할 수 있기 때문에 위험하다.
타임아웃이 걸리면 <code>PessimisticLockException</code> / <code>CannotAcquireLockException</code> 발생하게 되어 이때부터 핸들러가 필요해진다.</p>
<h1 id="마무리--그래서-왜-재고엔-비관락을-썼는가">마무리 — 그래서 왜 재고엔 비관락을 썼는가</h1>
<p>이번 프로젝트에서는 선착순 구매 상황을 가정했기 때문에 동일한 상품의 재고를 여러 요청이 동시에 변경할 가능성이 높았고, 재고와 주문 수량의 정합성이 중요했다. 따라서 충돌 발생 후 재시도하는 낙관적 락보다, 조회 시점에 해당 상품을 잠그고 동시 변경을 직렬화하는 비관적 락을 적용했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[낙관적 락 (Optimistic Lock)]]></title>
            <link>https://velog.io/@siha_014/%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD-Optimistic-Lock</link>
            <guid>https://velog.io/@siha_014/%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD-Optimistic-Lock</guid>
            <pubDate>Tue, 22 Sep 2026 05:48:38 GMT</pubDate>
            <description><![CDATA[<h1 id="낙관적-락이란">낙관적 락이란,</h1>
<blockquote>
<p>If data conflicts are rare, a strategy where data is read without placing any physical locks.</p>
</blockquote>
<p>&quot;충돌은 드물게 일어날 것&quot;이라는 가정을 전제로 하는 전략이다. 그래서 미리 락을 걸어 접근을 막는 대신, 일단 읽고 실행한 뒤 저장하는 시점에만 충돌 여부를 검사한다.</p>
<ul>
<li>READ &gt; 그냥 읽기</li>
<li>UPDATE/DELETE &gt; version 체크하고 적용하기</li>
</ul>
<p>즉, 선 실행 후 검사(optimistic) 방식이다.</p>
<p>같은 글에 대한 &quot;좋아요&quot;를 여러 사람이 동시에 누르는 상황을 가정하며 낙관락을 적용해 보았다.</p>
<h1 id="spring에서-낙관적-락-구현-version">Spring에서 낙관적 락 구현, @Version</h1>
<pre><code class="language-java">public class Post {
    ...

    @Version
    private Long version;

    ...
}</code></pre>
<p>이처럼 낙관적 락이 필요한 엔티티에 <code>@Version</code>을 붙인 필드를 생성한다.
그러면 객체의 인스턴트가 생성될 때, 즉, INSERT 구문이 생성될 때, 이 값은 초기화된다.
또, UPDATE나 DELETE의 변경 구문의 실행 시에 <code>WHERE id = ? AND version= ?</code>로 version의 값의 조건이 자동으로 붙으므로써, version 값을 체크하여 충돌을 방지한다.</p>
<p>충돌이 감지될 경우, 해당 로직을 재시도한다. 재시도 로직이 없을 경우, 충돌이 감지되면 JPA가 낙관적 락 예외를 발생시키며, 해당 트랜잭션에서 수행한 변경 사항은 커밋되지 않고 rollback된다.</p>
<h2 id="before-재시도-로직-없이-구현할-경우">Before: 재시도 로직 없이 구현할 경우</h2>
<p><img src="https://velog.velcdn.com/images/siha_014/post/155234df-6ce3-46b9-ab7b-6ad839575cfb/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/siha_014/post/d0a77388-fc20-46a0-8586-23642398e931/image.png" alt=""></p>
<p>같은 글에 100명이 동시에 &quot;좋아요&quot;를 누르도록 한 상황의 결과는 12건 성공이었다.</p>
<p>또, 여러 번 시도해도 결과가 12건으로 같았는데, 이는 세팅된 Connection Pool의 값에 의한 결과였다.
(<code>hikari: maximum-pool-size: 50</code>로 값을 변경했을 때, 7로 결과값이 바뀌었지만, 성공에 유의미한 결과를 주진 않았다.)</p>
<h3 id="executor로-만든-동시-요청이-얼마나-동시였는가">Executor로 만든 동시 요청이 얼마나 동시였는가</h3>
<p><code>ExecutorService</code>로 스레드 100개를 띄웠다고 해서, 실제로 DB에 100개가 동시에 요청을 보내는 건 아니다.</p>
<p>Spring Boot는 기본적으로 HikariCP 커넥션 풀을 최대 10개로 잡는다. 즉 스레드는 100개를 띄웠어도, 실제로 DB 커넥션을 얻어 요청을 보낼 수 있는 건 최대 10개뿐이고, 나머지는 커넥션이 반납될 때까지 대기열에서 기다린다.</p>
<p>그래서 이 테스트가 검증하는 동시성의 실체는 &quot;스레드 100개의 경쟁&quot;이 아니라, &quot;커넥션 풀 크기만큼의 경쟁 + 대기열&quot;에 더 가깝다. 풀 크기를 10 → 50으로 늘렸을 때 결과가 12에서 7로 바뀐 것도 이 때문이다 — 이번 테스트에서는 커넥션 풀이 커지면서 더 많은 요청이 동시에 DB 작업을 진행할 수 있었고, 그 결과 동일한 version을 읽고 경쟁하는 요청이 늘어나 충돌 양상이 달라진 것으로 볼 수 있다.</p>
<h2 id="after-재시도-로직-포함하여-구현할-경우">After: 재시도 로직 포함하여 구현할 경우</h2>
<p>수동으로 재시도 로직을 만드는 방법도 있지만(for문), Spring의 Spring Retry를 사용하여 구현해봤다.</p>
<pre><code class="language-java">@Retryable(
            retryFor = ObjectOptimisticLockingFailureException.class,
            maxAttempts = 20,
            backoff = @Backoff(delay = 10)
    )
    @Transactional
    public void likePost(Long id) {
        Post post = postRepository.findById(id)
                .orElseThrow(() -&gt; new EntityNotFoundException(&quot;Post not found: &quot; + id));
        post.increaseLikeCount();
    }</code></pre>
<p>결과는 final likeCount 값이 90이상으로 재시도 로직이 없을 때에 비해 높은 성공률을 보였다.</p>
<p>그리고, maxAttepts의 값에 비례해 성공률의 차이를 보였는데, 5일때 보다 20일때 더 높은 값을 얻을 수 있었으나, 그만큼 실행 시간도 늘어났다.</p>
<h3 id="maxattempts와-backoff가-미치는-영향">maxAttempts와 backoff가 미치는 영향</h3>
<h4 id="trade-off">trade-off</h4>
<table>
<thead>
<tr>
<th>maxAttempts</th>
<th>성공(likeCount)</th>
<th>소요시간</th>
</tr>
</thead>
<tbody><tr>
<td>5</td>
<td>49</td>
<td>609ms</td>
</tr>
<tr>
<td>10</td>
<td>73</td>
<td>863ms</td>
</tr>
<tr>
<td>20</td>
<td>92</td>
<td>1s 11ms</td>
</tr>
</tbody></table>
<p>재시도 횟수를 늘릴수록 성공률은 올라가지만, 그만큼 시간도 늘어난다. 재시도할 때마다 &quot;실패 → 재조회 → 재시도&quot; 사이클이 도니, 시도 횟수만큼 시간이 누적되는 구조다.</p>
<p><code>backoff</code>는 이 사이클 사이에 짧은 대기(delay)를 주는 설정이다. 재시도가 실패하자마자 곧바로 다시 시도하면, 방금 실패한 것과 같은 타이밍에 또 충돌할 가능성이 높다. <code>@Backoff(delay = 10)</code>처럼 짧은 텀을 주면 재충돌 확률을 어느 정도 낮출 수 있다.</p>
<p>다만 <code>maxAttempts</code>를 무한정 늘리는 게 답은 아니다. 재시도가 많아질수록 응답 시간이 늘어나 사용자 체감 지연으로 이어지고, 경쟁이 극심한 상황에서는 재시도만으로 100%를 보장하기도 어렵다. 그래서 실무에서는 재시도 한도를 합리적인 선에서 정하고, 좋아요처럼 약간의 유실이 허용되는 데이터는 그 선에서 타협하는 편이 현실적이다.</p>
<h1 id="마무리---왜-좋아요-기능에-낙관락을-사용했는가">마무리 - 왜 &#39;좋아요&#39; 기능에 낙관락을 사용했는가</h1>
<p>이번 프로젝트에서는 좋아요 요청이 동시에 발생하는 상황을 가정해 낙관적 락을 적용해 보았다. 낙관적 락은 충돌이 발생하더라도 사전에 다른 트랜잭션의 접근을 막지 않고, 충돌이 발생한 경우에만 재시도하는 방식이다. 따라서 충돌이 지속적으로 발생하는 상황에서는 재시도 비용이 증가하지만, 충돌이 빈번하지 않은 상황에서는 비관적 락처럼 사전에 DB 자원을 점유하지 않고 처리할 수 있다는 장점이 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring batch]]></title>
            <link>https://velog.io/@siha_014/Spring-batch</link>
            <guid>https://velog.io/@siha_014/Spring-batch</guid>
            <pubDate>Sun, 17 May 2026 12:21:03 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-batch의-개념">Spring Batch의 개념</h1>
<h2 id="왜-spring-batch를-쓰는가">왜 Spring Batch를 쓰는가?</h2>
<p>매일 새벽 수백만 건의 결제 정산, 대량 이메일 발송, 로그 집계처럼 <strong>정해진 시간에 대량의 데이터를 처리해야 할 때</strong> REST API로는 감당이 안 된다. 이런 상황을 위한 게 Spring Batch다.</p>
<p>Spring Batch는 <strong>대용량 데이터를 일괄 처리(Batch Processing)</strong> 하기 위한 Spring 기반의 경량 프레임워크다.</p>
<hr>
<h2 id="핵심-아키텍처">핵심 아키텍처</h2>
<pre><code>Job
└── Step
      ├── chunk 1  →  Read → Process → Write → 커밋
      ├── chunk 2  →  Read → Process → Write → 커밋
      └── chunk 3  →  Read → Process → Write → 커밋</code></pre><h3 id="주요-구성-요소">주요 구성 요소</h3>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Job</strong></td>
<td>배치 작업의 최상위 단위</td>
</tr>
<tr>
<td><strong>Step</strong></td>
<td>Job을 구성하는 독립적인 단계</td>
</tr>
<tr>
<td><strong>ItemReader</strong></td>
<td>DB, 파일, API 등에서 데이터를 읽음</td>
</tr>
<tr>
<td><strong>ItemProcessor</strong></td>
<td>읽은 데이터를 변환/필터링</td>
</tr>
<tr>
<td><strong>ItemWriter</strong></td>
<td>처리된 데이터를 저장/출력</td>
</tr>
<tr>
<td><strong>JobRepository</strong></td>
<td>Job/Step 실행 정보를 DB에 저장</td>
</tr>
<tr>
<td><strong>JobLauncher</strong></td>
<td>Job을 실행시키는 인터페이스</td>
</tr>
</tbody></table>
<hr>
<h2 id="step의-두-가지-방식-chunk-vs-tasklet">Step의 두 가지 방식: Chunk vs Tasklet</h2>
<p>Spring Batch의 Step은 두 가지 방식 중 하나를 선택한다.</p>
<h3 id="chunk-방식">Chunk 방식</h3>
<p>대용량 데이터 처리에 사용한다. <strong>chunk-size만큼 읽고, 처리하고, 한 번에 저장</strong>하는 방식이다.</p>
<p>내부 동작을 정확히 보면:</p>
<ul>
<li><code>read()</code> 가 chunk-size만큼 <strong>반복 호출</strong>됨</li>
<li><code>process()</code> 도 그만큼 <strong>반복 호출</strong>됨</li>
<li><code>write()</code> 가 처리된 데이터를 <strong>List로 한 번에</strong> 받는 구조</li>
</ul>
<pre><code>read()   → process()
read()   → process()
read()   → process()
→ write(List [1, 2, 3]) → 커밋 ✅</code></pre><p>트랜잭션은 <strong>chunk 단위</strong>로 걸린다.</p>
<pre><code>Step 시작
  chunk 1 → read x3 → process x3 → write(List) → 커밋 ✅
  chunk 2 → read x3 → process x3 → write(List) → 커밋 ✅
  chunk 3 → read x3 → process x3 → write(List) → 💥 실패
  → chunk 3만 롤백! chunk 1, 2는 이미 커밋됐으니 안전 ✅
Step 종료</code></pre><h3 id="tasklet-방식">Tasklet 방식</h3>
<p>파일 삭제, 알림 발송처럼 <strong>단순하고 가벼운 작업</strong>에 사용한다.
트랜잭션이 <strong>Step 전체</strong>에 걸린다.</p>
<pre><code class="language-java">@Bean
public Step cleanStep() {
    return stepBuilderFactory.get(&quot;cleanStep&quot;)
        .tasklet((contribution, chunkContext) -&gt; {
            tempFileService.deleteAll();
            return RepeatStatus.FINISHED;
        })
        .build();
}</code></pre>
<p>Tasklet으로 대용량 데이터를 처리하면 위험하다.</p>
<pre><code>30만 건 처리 중...
29만 9999건 완료
마지막 1건 💥 실패
→ 30만 건 전부 롤백 ❌
→ 처음부터 다시 30만 건</code></pre><h3 id="언제-뭘-쓰나">언제 뭘 쓰나?</h3>
<table>
<thead>
<tr>
<th>상황</th>
<th>방식</th>
</tr>
</thead>
<tbody><tr>
<td>대량 데이터 읽고 → 저장</td>
<td>Chunk</td>
</tr>
<tr>
<td>파일 삭제, 알림 발송, 초기화</td>
<td>Tasklet</td>
</tr>
<tr>
<td>API 한 번 호출하고 끝</td>
<td>Tasklet</td>
</tr>
<tr>
<td>DB 100만 건 마이그레이션</td>
<td>Chunk</td>
</tr>
</tbody></table>
<p>한 Job 안에서 Tasklet과 Chunk를 자유롭게 조합할 수도 있다.</p>
<pre><code class="language-java">@Bean
public Job myJob() {
    return jobBuilderFactory.get(&quot;myJob&quot;)
        .start(cleanStep())       // Tasklet: 임시 파일 삭제
        .next(processStep())      // Chunk: 대량 데이터 처리
        .next(notifyStep())       // Tasklet: 완료 알림 발송
        .build();
}</code></pre>
<hr>
<h2 id="jobrepository가-db에-저장하는-이유">JobRepository가 DB에 저장하는 이유</h2>
<p>JobRepository는 Job/Step 실행 정보를 DB에 저장한다. 이유는 <strong>배치 작업은 실패했을 때 어떻게 복구하느냐</strong>가 가장 중요하기 때문이다.</p>
<h3 id="1-재시작restart-지원">1. 재시작(Restart) 지원</h3>
<p>Spring Batch는 실패한 Job을 중단 지점부터 재시작할 수 있다.
단, <strong>Reader가 <code>ExecutionContext</code>에 상태를 저장해야</strong> 중간부터 재시작이 가능하다.</p>
<pre><code>100만 건 처리 중...
70만 건 완료 ✅ (ExecutionContext에 진행 상태 저장)
30만 건 처리 중 → 💥 서버 다운

→ Restart 시 70만 1건부터 재시작 가능 ✅ (상태 저장된 경우)
→ Stateless Reader라면 처음부터 다시 시작 ❌</code></pre><p><code>JpaPagingItemReader</code> 같은 Spring 제공 Reader는 기본적으로 상태 저장이 되지만,
커스텀 Reader를 직접 구현하는 경우에는 <code>ItemStreamReader</code>를 구현해서 상태 저장 로직을 직접 작성해야 한다.</p>
<h3 id="2-중복-실행-방지">2. 중복 실행 방지</h3>
<p>Spring Batch는 <strong>JobName + JobParameters 조합</strong>으로 <code>JobInstance</code>를 결정한다.
같은 파라미터로 실행하면 상태와 관계없이 <strong>실행 자체가 불가능</strong>하다.</p>
<pre><code class="language-java">// 매번 다른 파라미터를 넣어야 새로운 JobInstance로 인식
jobLauncher.run(settlementJob, new JobParametersBuilder()
    .addLong(&quot;time&quot;, System.currentTimeMillis()) // 이게 없으면 두 번째 실행 불가
    .toJobParameters());</code></pre>
<p>실무에서 <code>addLong(&quot;time&quot;, ...)</code> 으로 타임스탬프를 넣는 이유가 바로 이것이다.</p>
<h3 id="3-실행-이력-모니터링">3. 실행 이력 모니터링</h3>
<pre><code>실행일시          | Job명          | 상태      | 처리건수
2026-05-17 02:00 | settlementJob  | COMPLETED | 980,000건
2026-05-16 02:00 | settlementJob  | FAILED    | 450,000건</code></pre><p>Spring Batch는 자동으로 아래 메타 테이블들을 생성한다.</p>
<table>
<thead>
<tr>
<th>테이블</th>
<th>저장 내용</th>
</tr>
</thead>
<tbody><tr>
<td><code>BATCH_JOB_INSTANCE</code></td>
<td>JobName + JobParameters 조합 (논리적 실행 단위)</td>
</tr>
<tr>
<td><code>BATCH_JOB_EXECUTION</code></td>
<td>Job의 실제 실행 기록 (시작/종료시간, 상태)</td>
</tr>
<tr>
<td><code>BATCH_STEP_EXECUTION</code></td>
<td>Step별 실행 기록 (읽은 건수, 처리 건수, 실패 건수)</td>
</tr>
<tr>
<td><code>BATCH_JOB_EXECUTION_PARAMS</code></td>
<td>Job 실행 시 넘긴 파라미터</td>
</tr>
</tbody></table>
<hr>
<h2 id="내결함성-skip-retry-restart">내결함성: Skip, Retry, Restart</h2>
<pre><code class="language-java">.&lt;ViewLog, AuthorSettlement&gt;chunk(1000)
.reader(reader())
.processor(processor())
.writer(writer())
.faultTolerant()
    .skip(DataAccessException.class) // 이 예외는 skip
    .skipLimit(10)                   // 최대 10건까지 skip 허용
    .retry(DeadlockLoserDataAccessException.class) // 이건 재시도
    .retryLimit(3)
.build();</code></pre>
<ul>
<li><strong>Skip</strong>: 특정 예외 발생 시 해당 건 건너뜀</li>
<li><strong>Retry</strong>: 일시적 오류 시 재시도</li>
<li><strong>Restart</strong>: 실패한 Job을 중단 지점부터 재시작 (Reader 상태 저장 필요)</li>
</ul>
<p>Skip/Retry/Restart는 <strong>&quot;다시 실행할 수 있게 해주는 것&quot;</strong> 이고,
트랜잭션 롤백은 <strong>&quot;데이터를 어디까지 되돌리냐&quot;</strong> 의 문제다. 레이어가 다르다.</p>
<hr>
<h2 id="실제-구현-예시">실제 구현 예시</h2>
<p>웹툰 플랫폼의 작가 정산 배치를 예시로 보자.</p>
<p><strong>시나리오</strong>: 매일 새벽 2시, 전날 열람 데이터를 집계해서 작가 정산 테이블에 저장</p>
<pre><code>[서비스 운영 중 - 낮]
사용자가 웹툰 클릭 → 결제 완료 → ViewLog 테이블에 자동 저장

[새벽 2시 배치]
ViewLog에서 전날 데이터 읽기 (Reader)
    ↓
수수료 30% 제외하고 작가 수익 계산 (Processor)
    ↓
AuthorSettlement 테이블에 저장 (Writer)</code></pre><pre><code class="language-java">@Configuration
@RequiredArgsConstructor
public class SettlementJobConfig {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final EntityManagerFactory entityManagerFactory;

    // Job
    @Bean
    public Job settlementJob() {
        return jobBuilderFactory.get(&quot;settlementJob&quot;)
            .start(settlementStep())
            .build();
    }

    // Step
    @Bean
    public Step settlementStep() {
        return stepBuilderFactory.get(&quot;settlementStep&quot;)
            .&lt;ViewLog, AuthorSettlement&gt;chunk(1000)
            .reader(viewLogReader())
            .processor(settlementProcessor())
            .writer(settlementWriter())
            .build();
    }

    // Reader: 전날 열람 로그 읽기
    @Bean
    public JpaPagingItemReader&lt;ViewLog&gt; viewLogReader() {
        return new JpaPagingItemReaderBuilder&lt;ViewLog&gt;()
            .name(&quot;viewLogReader&quot;)
            .entityManagerFactory(entityManagerFactory)
            .pageSize(1000)
            .queryString(&quot;SELECT v FROM ViewLog v WHERE v.createdAt &gt;= :yesterday&quot;)
            .parameterValues(Map.of(&quot;yesterday&quot;, LocalDate.now().minusDays(1)))
            .build();
    }

    // Processor: 수수료 계산
    @Bean
    public ItemProcessor&lt;ViewLog, AuthorSettlement&gt; settlementProcessor() {
        return viewLog -&gt; {
            int authorRevenue = (int) (viewLog.getPrice() * 0.7); // 30% 수수료
            return new AuthorSettlement(viewLog.getWebtoonId(), authorRevenue);
        };
    }

    // Writer: 정산 테이블에 저장
    @Bean
    public JpaItemWriter&lt;AuthorSettlement&gt; settlementWriter() {
        return new JpaItemWriterBuilder&lt;AuthorSettlement&gt;()
            .entityManagerFactory(entityManagerFactory)
            .build();
    }
}</code></pre>
<p>제네릭 타입의 흐름을 정리하면:</p>
<pre><code>JpaPagingItemReader&lt;ViewLog&gt;              // Reader: 읽을 타입
ItemProcessor&lt;ViewLog, AuthorSettlement&gt;  // Processor: 입력 → 출력 타입
JpaItemWriter&lt;AuthorSettlement&gt;           // Writer: 쓸 타입

.&lt;ViewLog, AuthorSettlement&gt;chunk(1000)   // &lt;Reader 타입, Writer 타입&gt;</code></pre><p>타입이 체인처럼 연결되며, 중간에 타입이 맞지 않으면 컴파일 에러가 나기 때문에 <strong>타입 안정성</strong>도 보장된다.</p>
<p>Job 하나당 Config 파일 하나로 관리하는 게 일반적인 패턴이다.</p>
<pre><code>batch
├── SettlementJobConfig.java     // 작가 정산
├── StatisticsJobConfig.java     // 열람 통계
└── CleanUpJobConfig.java        // 오래된 로그 삭제</code></pre><hr>
<h2 id="스케줄링">스케줄링</h2>
<p>Spring Batch 자체는 &quot;어떻게 처리하냐&quot;만 담당하고, &quot;언제 실행하냐&quot;는 별개다.</p>
<pre><code class="language-java">// 새벽 2시에 자동 실행
@Scheduled(cron = &quot;0 0 2 * * *&quot;)
public void run() {
    jobLauncher.run(settlementJob, new JobParametersBuilder()
        .addLong(&quot;time&quot;, System.currentTimeMillis())
        .toJobParameters());
}</code></pre>
<p>보통 새벽에 배치를 실행하는 이유는 <strong>트래픽이 없는 시간대</strong>여야 하기 때문이다. 배치는 DB를 대량으로 읽고 쓰기 때문에 서비스 운영 중에 돌리면 DB 부하 급증, 테이블 Lock 등 장애로 이어질 수 있다.</p>
<p>스케줄링 외에도 다양한 실행 방법이 있다.</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Scheduled</code></td>
<td>정해진 시간 자동 실행</td>
</tr>
<tr>
<td>REST API 트리거</td>
<td>관리자 페이지 버튼으로 수동 실행</td>
</tr>
<tr>
<td>앱 시작 시 실행</td>
<td><code>spring.batch.job.enabled=true</code></td>
</tr>
<tr>
<td>외부 스케줄러</td>
<td>Jenkins, Kubernetes CronJob 등</td>
</tr>
</tbody></table>
<hr>
<h2 id="내-프로젝트에서의-활용">내 프로젝트에서의 활용</h2>
<p>현재 개발 중인 AI 영어 학습 프로젝트에서는 실시간 피드백은 API로 처리하고, Spring Batch는 아래 두 가지 작업에 활용할 예정이다.</p>
<p><strong>1. 주간 리포트 생성</strong></p>
<pre><code>일주일치 Feedback 데이터 읽기 (Reader)
    ↓
주간 평균 점수, 약점 패턴 집계 (Processor)
    ↓
WeeklyReport 테이블에 저장 (Writer)</code></pre><p><strong>2. 학습 목표 달성 여부 판단</strong></p>
<pre><code>user_goals 테이블 읽기 (Reader)
    ↓
current_value &gt;= target_value 체크 (Processor)
    ↓
is_achieved 업데이트 (Writer)</code></pre><p>실시간 처리가 필요한 건 API로, 주기적인 집계가 필요한 건 Spring Batch로 분리한 구조다.</p>
<hr>
<h2 id="정리">정리</h2>
<ul>
<li>Spring Batch는 <strong>대용량 데이터 일괄 처리</strong> 프레임워크</li>
<li><strong>Chunk 방식</strong>: <code>read()</code> / <code>process()</code> 는 반복 호출, <code>write()</code> 는 List로 한 번에 처리</li>
<li><strong>Chunk vs Tasklet</strong>: 트랜잭션 범위가 다름 (chunk 단위 vs Step 전체)</li>
<li><strong>JobRepository</strong>: JobName + JobParameters로 JobInstance 결정, 실행 이력 저장</li>
<li><strong>Restart</strong>: Reader가 ExecutionContext에 상태를 저장해야 중간부터 재시작 가능</li>
<li><strong>스케줄링</strong>: Spring Batch와 별개, 새벽에 실행하는 게 일반적</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring SSE구현]]></title>
            <link>https://velog.io/@siha_014/Spring-SSE%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@siha_014/Spring-SSE%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sat, 18 Apr 2026 12:08:41 GMT</pubDate>
            <description><![CDATA[<h1 id="sseserver-sent-events란">SSE(Server-Sent Events)란</h1>
<p>프로젝트를 구현하는 중, OpenAI API와의 채팅 기능을 개발하게 되었다. 실시간으로 AI 응답을 스트리밍해야 했고, 자연스럽게 WebSocket과 SSE 중 하나를 선택해야 하는 상황에 놓였다. 결론적으로 SSE를 선택했다. 이 글에서는 WebSocket과 SSE를 비교하고, 왜 SSE를 선택하게 되었는지 정리하고자 한다.</p>
<hr>
<h2 id="1-웹의-실시간-통신">1. 웹의 실시간 통신</h2>
<p>HTTP는 기본적으로 클라이언트와 서버 간의 <strong>요청(Request) / 응답(Response)</strong> 모델로 동작한다. 클라이언트가 요청을 보내면, 서버는 그에 대한 응답을 반환한다. 응답이 완료되면 연결은 종료된다.</p>
<p>이 구조는 단순하고 명확하지만, <strong>실시간성이 필요한 상황</strong>에서는 한계가 있다.</p>
<h3 id="polling">Polling</h3>
<p>이를 해결하기 위한 가장 단순한 방법이 <strong>Polling</strong>이다. Polling은 클라이언트가 일정 주기로 서버에 요청을 보내 상태를 확인하는 방식이다.</p>
<pre><code>클라이언트 → 서버: &quot;new data?&quot;  (매 N초마다 반복)
서버 → 클라이언트: &quot;yes&quot; or &quot;no&quot;</code></pre><p>구현 방식이 가장 간단하지만, 명확한 단점이 있다.</p>
<ul>
<li><strong>서버 리소스 낭비</strong>: 변화가 없어도 주기적으로 요청이 발생한다.</li>
<li><strong>실시간성 부족</strong>: 이벤트 발생 시점과 클라이언트가 데이터를 받는 시점이 일치하지 않는다.</li>
</ul>
<p>이러한 단점을 해결하기 위해 등장한 것이 <strong>WebSocket</strong>과 <strong>SSE</strong>다.</p>
<hr>
<h2 id="2-websocket--sse">2. WebSocket &amp; SSE</h2>
<h3 id="websocket">WebSocket</h3>
<p>WebSocket은 <strong>양방향(Full-Duplex) 통신</strong> 기술이다. 일반 HTTP와 달리 <strong>Stateful</strong>하며, 한 번 연결이 수립되면 클라이언트와 서버가 서로 필요할 때마다 자유롭게 메시지를 주고받을 수 있다.</p>
<p>연결 수립 시 HTTP <strong>Handshake</strong>를 통해 WebSocket 프로토콜로 업그레이드한다.</p>
<pre><code>클라이언트 → 서버: HTTP Upgrade 요청
서버 → 클라이언트: 101 Switching Protocols
--- 이후 WebSocket 연결 유지 ---
클라이언트 ↔ 서버: 자유로운 양방향 메시지</code></pre><p><strong>주요 특징:</strong></p>
<ul>
<li>양방향 통신</li>
<li>지속적인 연결 유지 (Stateful)</li>
<li>실시간 채팅, 게임, 협업 도구에 적합</li>
</ul>
<h3 id="sse-server-sent-events">SSE (Server-Sent Events)</h3>
<p>SSE는 <strong>서버 → 클라이언트 단방향 스트리밍</strong> 기술이다. HTTP 연결을 유지한 채로, 서버가 클라이언트에게 데이터를 지속적으로 흘려보낼 수 있다.</p>
<pre><code>클라이언트 → 서버: 연결 요청 (한 번)
서버 → 클라이언트: 데이터 스트리밍 (지속적)</code></pre><p>응답 형식은 <code>data:</code> prefix를 가진 텍스트 스트림이다.</p>
<pre><code>data: {&quot;content&quot;: &quot;Hello&quot;}
data: {&quot;content&quot;: &quot; world&quot;}
data: [DONE]</code></pre><p><strong>주요 특징:</strong></p>
<ul>
<li>서버 → 클라이언트 단방향</li>
<li>HTTP 기반 (별도 프로토콜 업그레이드 불필요)</li>
<li>자동 재연결 지원</li>
<li>구현이 WebSocket보다 단순</li>
</ul>
<hr>
<h2 id="3-websocket-vs-sse">3. WebSocket vs SSE</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>WebSocket</th>
<th>SSE</th>
</tr>
</thead>
<tbody><tr>
<td>통신 방향</td>
<td>양방향</td>
<td>단방향 (서버 → 클라이언트)</td>
</tr>
<tr>
<td>프로토콜</td>
<td>ws:// / wss://</td>
<td>HTTP</td>
</tr>
<tr>
<td>연결 방식</td>
<td>Handshake 후 업그레이드</td>
<td>일반 HTTP 연결 유지</td>
</tr>
<tr>
<td>상태</td>
<td>Stateful</td>
<td>HTTP 기반</td>
</tr>
<tr>
<td>자동 재연결</td>
<td>직접 구현</td>
<td>브라우저 내장 지원</td>
</tr>
<tr>
<td>구현 복잡도</td>
<td>높음</td>
<td>낮음</td>
</tr>
<tr>
<td>적합한 상황</td>
<td>실시간 채팅, 게임, 협업</td>
<td>알림, 피드, AI 스트리밍</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-sse를-선택한-이유">4. SSE를 선택한 이유</h2>
<p>이 프로젝트의 통신 구조를 분석해보면 다음과 같다.</p>
<pre><code>클라이언트 → 서버: 사용자 메시지 전송 (HTTP POST)
서버 → 클라이언트: AI 응답 스트리밍 (실시간)</code></pre><p>클라이언트 → 서버 방향은 단순한 HTTP POST로 충분하다. 서버가 클라이언트에게 먼저 메시지를 <strong>자발적으로</strong> 보내야 하는 상황이 없기 때문이다.</p>
<p>결국 <strong>서버 → 클라이언트 방향의 스트리밍만 필요</strong>하다. WebSocket의 양방향 기능은 이 프로젝트에서 불필요한 복잡도를 추가할 뿐이다.</p>
<blockquote>
<p>단방향 스트리밍 특성상 WebSocket보다 SSE가 더 적합하다.</p>
</blockquote>
<hr>
<h2 id="5-spring-webflux에서-sse-구현">5. Spring WebFlux에서 SSE 구현</h2>
<p>SSE 스트리밍 응답을 처리하기 위해 Spring WebFlux의 <code>Flux</code>를 활용했다.</p>
<h3 id="의존성-추가">의존성 추가</h3>
<pre><code class="language-groovy">implementation &#39;org.springframework.boot:spring-boot-starter-webflux&#39;</code></pre>
<h3 id="controller">Controller</h3>
<p><code>produces = MediaType.TEXT_EVENT_STREAM_VALUE</code>를 설정하면 Spring이 자동으로 SSE 형식으로 응답한다.</p>
<pre><code class="language-java">@PostMapping(value = &quot;/{sessionId}/messages&quot;, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux&lt;String&gt; sendMessage(
        @AuthenticationPrincipal CustomOAuth2User principal,
        @PathVariable Long sessionId,
        @RequestBody SendMessageRequest dto
) {
    return conversationService.sendMessage(principal.getUserId(), sessionId, dto);
}</code></pre>
<h3 id="service---openai-스트리밍">Service - OpenAI 스트리밍</h3>
<p>OpenAI API에 <code>&quot;stream&quot;: true</code>를 설정하고, <code>bodyToFlux(String.class)</code>로 응답을 스트림으로 받는다.</p>
<pre><code class="language-java">public Flux&lt;String&gt; stream(List&lt;ChatMessage&gt; conversationHistory, String userMessage) {
    Map&lt;String, Object&gt; requestBody = Map.of(
            &quot;model&quot;, model,
            &quot;max_tokens&quot;, maxTokens,
            &quot;stream&quot;, true,
            &quot;messages&quot;, buildMessages(PromptConstants.CHAT_PROMPT, conversationHistory, userMessage)
    );

    return openAiWebClient.post()
            .uri(&quot;/chat/completions&quot;)
            .bodyValue(requestBody)
            .retrieve()
            .bodyToFlux(String.class)
            .filter(chunk -&gt; !chunk.equals(&quot;[DONE]&quot;))
            .mapNotNull(this::extractStreamContent);
}</code></pre>
<h3 id="스트리밍-응답-수집-및-저장">스트리밍 응답 수집 및 저장</h3>
<p>스트리밍으로 오는 조각들을 <code>doOnNext</code>로 모으고, 완료 시점에 <code>doOnComplete</code>로 DB에 저장한다.</p>
<pre><code class="language-java">StringBuilder fullResponse = new StringBuilder();

return openAiService.stream(history, dto.getContent())
        .doOnNext(fullResponse::append)
        .doOnComplete(() -&gt; {
            saveAssistantMessage(sessionId, fullResponse.toString());
            conversationRedisService.addMessage(sessionId, ChatMessage.ofAssistant(fullResponse.toString()));
        });</code></pre>
<p>클라이언트 입장에서는 조각조각 실시간으로 받고, 서버는 모든 조각이 도착했을 때 전체 응답을 저장하는 구조다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OAuth2로 구글로그인 구현하기]]></title>
            <link>https://velog.io/@siha_014/OAuth2%EB%A1%9C-%EA%B5%AC%EA%B8%80%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@siha_014/OAuth2%EB%A1%9C-%EA%B5%AC%EA%B8%80%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 15 Apr 2026 05:10:10 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-security-oauth2-구글-로그인-구현하기">Spring Security OAuth2 구글 로그인 구현하기</h1>
<p>현재 프로젝트에서 구글 로그인과 일반 로그인을 같이 사용하려고 한다.
또, 해당 프로젝트에서는 Access Token + Refresh Token의 JWT 인증 방식을 사용하고 있어, 이에 맞춰야 했다.</p>
<hr>
<h2 id="google-로그인-흐름">Google 로그인 흐름</h2>
<p><img src="https://velog.velcdn.com/images/siha_014/post/354c28ef-1eb2-4c42-8166-4ba1032db7ff/image.png" alt=""></p>
<ol>
<li>클라이언트가 구글에 로그인을 요청한다.</li>
<li>구글이 인가 코드를 반환한다.</li>
<li>클라이언트가 인가 코드를 서버에 전달한다.<ul>
<li>서버가 직접 구글에 Access Token을 요청하기 위함이다. 브라우저에 Access Token을 직접 노출하지 않아 보안상 안전하다.</li>
</ul>
</li>
<li>인가 코드를 전달받은 서버가 인가 코드와 함께 Access Token을 요청한다.</li>
<li>구글은 서버가 안전하다고 판단할 시 Access Token을 반환한다.</li>
<li>서버가 Access Token으로 사용자 정보를 요청한다.</li>
<li>구글이 사용자 정보를 반환한다. (<code>email</code>, <code>name</code>, <code>sub</code> 등)</li>
<li>이를 기반으로 회원을 등록하거나 조회하고 JWT 로직을 실행한다.</li>
<li>Access Token과 Refresh Token과 함께 클라이언트로 리다이렉트한다.</li>
</ol>
<blockquote>
<p><strong>참고</strong>: 4~7번 과정은 Spring Security가 자동으로 처리한다.
<code>CustomOAuth2UserService.loadUser()</code>가 호출될 때 이미 완료된 상태이다.</p>
</blockquote>
<hr>
<h2 id="엔티티">엔티티</h2>
<h3 id="user">User</h3>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = &quot;users&quot;)
public class User extends Timestamped {

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

    @Column(nullable = false, length = 50)
    private String nickname;

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

    @Column
    private String password;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    @Column(nullable = false)
    private Integer streakDays = 0;

    @Column
    private LocalDate lastStudiedAt;

    @Builder
    public User(String nickname, String email, String password, Role role) {
        this.nickname = nickname;
        this.email = email;
        this.password = password;
        this.role = role;
    }

    public void updateNickname(String nickname) { this.nickname = nickname; }
}</code></pre>
<h3 id="oauthaccount">OAuthAccount</h3>
<pre><code class="language-java">@Entity
@Table(name = &quot;oauth_accounts&quot;,
        uniqueConstraints = @UniqueConstraint(columnNames = {&quot;provider&quot;, &quot;provider_id&quot;}))
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OAuthAccount {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;user_id&quot;, nullable = false)
    private User user;

    @Column(nullable = false, length = 20)
    private String provider;

    @Column(name = &quot;provider_id&quot;, nullable = false, length = 1000)
    private String providerId;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Builder
    public OAuthAccount(User user, String provider, String providerId) {
        this.user = user;
        this.provider = provider;
        this.providerId = providerId;
    }
}</code></pre>
<p><code>User</code> 엔티티에 <code>provider</code>와 <code>providerId</code>를 nullable로 넣지 않고, <code>OAuthAccount</code>로 분리한 이유는 확장성 때문이다.
한 유저가 여러 OAuth 계정(구글, 카카오 등)을 연결할 수 있도록 고려했고, 일반 로그인과 확실히 구분하기 위해 이러한 선택을 했다.</p>
<hr>
<h2 id="customoauth2user">CustomOAuth2User</h2>
<p>인증된 사용자 정보(principal)를 담는 객체이다. <code>SecurityContext</code>의 <code>Authentication</code>에서 principal로 사용된다.</p>
<p>이 프로젝트는 구글 로그인과 일반 로그인을 모두 지원하기 때문에, 두 방식 모두 동일한 principal 타입을 사용해 일관성을 유지했다.</p>
<pre><code class="language-java">@Getter
public class CustomOAuth2User implements OAuth2User {

    private final User user;
    private final Map&lt;String, Object&gt; attributes;

    public CustomOAuth2User(User user, Map&lt;String, Object&gt; attributes) {
        this.user = user;
        this.attributes = attributes;
    }

    @Override
    public Map&lt;String, Object&gt; getAttributes() {
        return attributes;
    }

    @Override
    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
        return List.of(new SimpleGrantedAuthority(&quot;ROLE_&quot; + user.getRole().name()));
    }

    @Override
    public String getName() {
        return String.valueOf(user.getId());
    }
}</code></pre>
<ul>
<li><code>user</code>: 우리 서비스 DB에 저장된 사용자 정보</li>
<li><code>attributes</code>: 구글로부터 받은 원본 사용자 정보 (OAuth2 로그인 시 채워짐, JWT 인증 시 빈 Map)</li>
</ul>
<hr>
<h2 id="customoauth2userservice">CustomOAuth2UserService</h2>
<p><code>DefaultOAuth2UserService</code>를 상속받아 구글 로그인 시 사용자 정보를 처리하는 서비스이다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    private final UserRepository userRepository;
    private final OAuthAccountRepository oAuthAccountRepository;

    @Override
    @Transactional
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);

        String provider = userRequest.getClientRegistration().getRegistrationId();
        Map&lt;String, Object&gt; attributes = oAuth2User.getAttributes();

        String providerId = (String) attributes.get(&quot;sub&quot;);
        String email = (String) attributes.get(&quot;email&quot;);
        String nickname = (String) attributes.get(&quot;name&quot;);

        User user = oAuthAccountRepository.findByProviderAndProviderId(provider, providerId)
                .map(OAuthAccount::getUser)
                .orElseGet(() -&gt; registerNewUser(email, nickname, provider, providerId));

        return new CustomOAuth2User(user, attributes);
    }

    private User registerNewUser(String email, String nickname, String provider, String providerId) {
        User user = userRepository.findByEmail(email)
                .orElseGet(() -&gt; userRepository.save(
                        User.builder()
                                .email(email)
                                .nickname(nickname)
                                .role(Role.USER)
                                .build()
                ));

        oAuthAccountRepository.save(OAuthAccount.builder()
                .user(user)
                .provider(provider)
                .providerId(providerId)
                .build());

        return user;
    }
}</code></pre>
<h3 id="superloaduser가-하는-일">super.loadUser()가 하는 일</h3>
<p><code>super.loadUser()</code>는 <code>DefaultOAuth2UserService</code>의 구현으로, 내부적으로 다음을 수행한다.</p>
<pre><code class="language-java">@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    // ...
    OAuth2AccessToken token = userRequest.getAccessToken();
    Map&lt;String, Object&gt; attributes = this.attributesConverter.convert(userRequest).convert(response.getBody());
    // ...
    return new DefaultOAuth2User(authorities, attributes, userNameAttributeName);
}</code></pre>
<p><code>userRequest</code> 안에 구글 Access Token이 담겨 있고, 이를 이용해 구글 사용자 정보 API를 호출한다.
즉, <code>super.loadUser()</code> 한 줄로 구글 API 호출과 사용자 정보 파싱이 완료된다.</p>
<hr>
<h2 id="oauth2successhandler">OAuth2SuccessHandler</h2>
<p>구글 로그인이 성공했을 때 동작하는 핸들러이다.
<code>CustomOAuth2UserService.loadUser()</code>가 성공적으로 완료된 후 호출되며, JWT를 발급하고 클라이언트로 리다이렉트한다.</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtProvider jwtProvider;
    private final RefreshTokenService refreshTokenService;

    @Value(&quot;${app.oauth2.redirect-uri}&quot;)
    private String redirectUri;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();
        Long userId = oAuth2User.getUser().getId();

        String accessToken = jwtProvider.generateAccessToken(userId);
        String refreshToken = jwtProvider.generateRefreshToken(userId);

        refreshTokenService.save(userId, refreshToken);

        String targetUrl = UriComponentsBuilder.fromUriString(redirectUri)
                .queryParam(&quot;accessToken&quot;, accessToken)
                .queryParam(&quot;refreshToken&quot;, refreshToken)
                .build().toUriString();

        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
}</code></pre>
<p><code>authentication.getPrincipal()</code>로 <code>CustomOAuth2User</code>를 꺼내 <code>userId</code>를 가져온다.
이후 JWT를 발급하고, Refresh Token은 Redis에 저장한 뒤 클라이언트로 리다이렉트한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CustomOAuth2User 구현하기]]></title>
            <link>https://velog.io/@siha_014/CustomOAuth2User-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@siha_014/CustomOAuth2User-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 13 Apr 2026 07:56:32 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 구현하면서 OAuth2를 사용하기로 결정한 후, 어떻게 인증을 구현할 것인지 고민했다.
우선 이 방식에서는 기존에 사용하던 UserDetails 구현체인 CustomUserDetails를 그대로 사용하는 것이 적절한지 고민이 되었다.</p>
<p>결론적으로,
OAuth2 로그인 과정에서는 UserDetails보다는 OAuth2User를 사용하는 것이 더 적절하다고 판단했다.</p>
<p>그 이유는 다음과 같다.</p>
<h2 id="왜-userdetails가-아니라-oauth2user인가">왜 UserDetails가 아니라 OAuth2User인가?</h2>
<p>UserDetails는 username/password 기반의 인증을 전제로 설계된 인터페이스이다.
즉, 서버가 직접 사용자의 인증 정보를 검증하는 일반 로그인 방식에 적합하다.</p>
<p>반면 OAuth2 로그인은 다음과 같은 흐름을 가진다.</p>
<blockquote>
<p>사용자 → Google 로그인 → Google이 인증 → 사용자 정보 반환</p>
</blockquote>
<p>이 과정에서 서버는 인증을 직접 수행하지 않고,
외부 Provider(Google)로부터 사용자 정보를 전달받는다.</p>
<p>이때 전달되는 데이터는 다음과 같은 형태이다.</p>
<pre><code>{
  &quot;email&quot;: &quot;...&quot;,
  &quot;name&quot;: &quot;...&quot;,
  &quot;picture&quot;: &quot;...&quot;
}</code></pre><p>=&gt; 즉, 정형화된 필드가 아니라 Map 형태의 attributes 데이터</p>
<p>따라서 이러한 구조를 다루기 위해 Spring Security에서는 <code>getAttributes()</code>를 제공하는 <code>OAuth2User</code> 인터페이스를 사용한다.</p>
<h2 id="customoauth2user">CustomOAuth2User</h2>
<p>이러한 이유로, 본 프로젝트에서는 OAuth2 로그인 시 전달받은 사용자 정보와 DB의 User 엔티티를 함께 다루기 위해 <code>CustomOAuth2User</code>를 구현하였다.</p>
<pre><code class="language-java">@Getter
public class CustomOAuth2User implements OAuth2User {

    private final User user;
    private final Map&lt;String, Object&gt; attributes;

    public CustomOAuth2User(User user, Map&lt;String, Object&gt; attributes) {
        this.user = user;
        this.attributes = attributes;
    }

    @Override
    public Map&lt;String, Object&gt; getAttributes() {
        return attributes;
    }

    @Override
    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
        return List.of(new SimpleGrantedAuthority(&quot;ROLE_&quot; + user.getRole().name()));
    }

    @Override
    public String getName() {
        return String.valueOf(user.getId());
    }
}</code></pre>
<ul>
<li><code>user</code> -&gt; 우리 서비스의 DB에 저장된 사용자 정보</li>
<li><code>attributes</code> -&gt; OAuth2 Provider(Google)로부터 받은 원본 사용자 정보</li>
</ul>
<p>=&gt; 내부 사용자 정보 + 외부 인증 정보를 하나의 객체로 통합</p>
<h2 id="jwtauthenticationfilter에서의-처리">JwtAuthenticationFilter에서의 처리</h2>
<p>OAuth2 로그인이 완료된 이후에는 JWT 기반 인증을 사용하므로,
요청마다 JWT 토큰을 검증하고 사용자 정보를 복원해야 한다.</p>
<pre><code>CustomOAuth2User oAuth2User = new CustomOAuth2User(user, Map.of());</code></pre><p>여기서 attributes를 Map.of()로 비워두는 이유는 다음과 같다.</p>
<p>=&gt; JWT 인증 과정에서는 이미 로그인 과정이 끝난 상태이기 때문에
=&gt; 더 이상 OAuth2 Provider로부터 받은 원본 데이터는 필요하지 않기 때문이다.</p>
<p>즉,</p>
<ul>
<li>OAuth 로그인 시 → attributes 필요</li>
<li>JWT 인증 시 → user 정보만 필요</li>
</ul>
<p>이후 <code>CustomOAuth2User</code>를 principal로 갖는 Authentication 객체를 생성하여
SecurityContext에 저장한다.</p>
<pre><code class="language-java">Authentication authentication = new UsernamePasswordAuthenticationToken(
        oAuth2User, null, oAuth2User.getAuthorities()
);</code></pre>
<p>이를 통해 Controller에서는 다음과 같이 사용자 정보를 사용할 수 있다.</p>
<pre><code class="language-java">@AuthenticationPrincipal CustomOAuth2User user</code></pre>
<h2 id="정리">정리</h2>
<ul>
<li><code>UserDetails</code>는 서버가 직접 인증하는 일반 로그인 방식에 적합하다.</li>
<li><code>OAuth2User</code>는 외부 Provider로부터 전달받은 사용자 정보를 처리하기 위한 인터페이스이다.</li>
<li>JWT 인증 단계에서는 두 인터페이스 중 어떤 것을 사용해도 무방하지만,
본 프로젝트에서는 OAuth2 로그인과의 일관성을 유지하기 위해 <code>OAuth2User</code>를 사용하였다.
한 줄 결론</li>
</ul>
<p>=&gt; <strong>OAuth2라서 OAuth2User를 써야 하는 것이 아니라, 외부 인증 데이터(attributes)를 다루고 구조 일관성을 유지하기 위해 선택했다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로컬 MySQL 포트 충돌 해결기]]></title>
            <link>https://velog.io/@siha_014/%EB%A1%9C%EC%BB%AC-MySQL-%ED%8F%AC%ED%8A%B8-%EC%B6%A9%EB%8F%8C-%ED%95%B4%EA%B2%B0%EA%B8%B0</link>
            <guid>https://velog.io/@siha_014/%EB%A1%9C%EC%BB%AC-MySQL-%ED%8F%AC%ED%8A%B8-%EC%B6%A9%EB%8F%8C-%ED%95%B4%EA%B2%B0%EA%B8%B0</guid>
            <pubDate>Wed, 25 Feb 2026 13:03:03 GMT</pubDate>
            <description><![CDATA[<h1 id="mac에서-docker-mysql-연결-안-될-때--로컬-mysql-포트-충돌-해결기">Mac에서 Docker MySQL 연결 안 될 때 — 로컬 MySQL 포트 충돌 해결기</h1>
<h2 id="문제-상황">문제 상황</h2>
<p>Spring Boot 프로젝트에서 Docker Compose로 MySQL을 띄우고 연결하려 했는데, 아래 에러가 계속 발생했다.</p>
<pre><code>Caused by: java.sql.SQLSyntaxErrorException: Access denied for user &#39;dev&#39;@&#39;localhost&#39; to database &#39;ai_english&#39;</code></pre><pre><code>Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure</code></pre><p>권한 부여(<code>GRANT ALL PRIVILEGES</code>)도 해보고, IP를 <code>172.18.0.2</code>로 직접 지정도 해봤지만 근본적으로 해결되지 않았다.</p>
<hr>
<h2 id="원인">원인</h2>
<pre><code class="language-bash">docker-compose up -d</code></pre>
<p>를 실행했을 때 아래 에러가 나왔다.</p>
<pre><code>Error response from daemon: ports are not available: exposing port TCP 127.0.0.1:3306 -&gt; 127.0.0.1:0: listen tcp4 127.0.0.1:3306: bind: address already in use</code></pre><p><strong>Mac에 MySQL이 직접 설치되어 있었고, 이미 3306 포트를 점유하고 있었다.</strong></p>
<p>Docker MySQL도 3306 포트를 사용하려 했기 때문에 충돌이 발생했고, Docker 컨테이너가 제대로 뜨지 않았던 것이다.</p>
<pre><code class="language-bash">brew services list | grep mysql
# mysql started ...</code></pre>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<h3 id="방법-1-로컬-mysql-중지-권장">방법 1. 로컬 MySQL 중지 (권장)</h3>
<pre><code class="language-bash">brew services stop mysql
docker-compose up -d</code></pre>
<p>Docker로 MySQL을 관리할 것이라면 로컬 MySQL은 꺼두는 것이 깔끔하다.</p>
<h3 id="방법-2-docker-mysql-포트-변경">방법 2. Docker MySQL 포트 변경</h3>
<p><code>docker-compose.yml</code>에서 포트를 변경한다.</p>
<pre><code class="language-yaml">mysql:
  ports:
    - &quot;3307:3306&quot;</code></pre>
<p><code>application.yml</code>도 맞춰서 변경한다.</p>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:mysql://localhost:3307/ai_english</code></pre>
<p>나는 방법1, 로컬 MySQL을 중지해서 해결했다.</p>
<hr>
<h2 id="교훈">교훈</h2>
<ul>
<li>Mac에 MySQL이 설치되어 있다면 Docker MySQL과 <strong>3306 포트 충돌</strong>이 발생한다.</li>
<li><code>Access denied</code>, <code>Communications link failure</code> 에러가 권한 문제처럼 보여도 <strong>실제로는 컨테이너 자체가 제대로 안 뜬 것</strong>일 수 있다.</li>
<li>문제 해결 시 에러 메시지만 보지 말고 <strong><code>docker ps</code>로 컨테이너 상태부터 확인</strong>하는 것이 중요하다.</li>
</ul>
<p>당연하게 MySQL을 설치, 실행한 것이 문제가 될 줄은 몰랐다.</p>
<pre><code class="language-bash"># 컨테이너 상태 확인 습관 들이기
docker ps
docker-compose up -d</code></pre>
<hr>
<h2 id="개발-환경">개발 환경</h2>
<ul>
<li>macOS</li>
<li>Docker Desktop</li>
<li>Spring Boot 3.5.x</li>
<li>MySQL 8.0 (Docker)</li>
<li><code>brew</code> 로 로컬 MySQL 설치되어 있던 상태</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Board 프로젝트 - Image 업로드 및 콘텐츠 관리 구조]]></title>
            <link>https://velog.io/@siha_014/Board-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Image-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%B0%8F-%EC%BD%98%ED%85%90%EC%B8%A0-%EA%B4%80%EB%A6%AC-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@siha_014/Board-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Image-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%B0%8F-%EC%BD%98%ED%85%90%EC%B8%A0-%EA%B4%80%EB%A6%AC-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Thu, 19 Feb 2026 06:48:26 GMT</pubDate>
            <description><![CDATA[<h1 id="이미지-업로드와-게시글-저장을-분리한-이유">이미지 업로드와 게시글 저장을 분리한 이유</h1>
<p>WYSIWYG 에디터를 프론트엔드에서 사용한다는 가정하에 백엔드를 구현하고자 하니 고민이 생겼다.</p>
<p>이미지를 어떻게 처리할까?</p>
<p>가장 단순한 방법은 게시글 저장 시 이미지를 함께 전송하는 것이다. 하지만 WYSIWYG 에디터 특성상 사용자는 글을 쓰는 도중에 이미지를 첨부하고, 미리보기로 확인하면서 작성을 이어간다. 이 흐름을 자연스럽게 지원하려면 <strong>이미지 업로드와 게시글 저장을 분리</strong>하는 것이 맞다고 판단했다.</p>
<hr>
<h2 id="실제-동작-흐름">실제 동작 흐름</h2>
<p>사용자 입장에서는 저장 버튼 하나로 모든 게 업로드되는 것처럼 보이지만, 실제로는 두 단계로 나뉜다.</p>
<p><strong>1단계 — 이미지 업로드</strong></p>
<p>WYSIWYG 에디터에 이미지를 첨부하는 순간, 백그라운드에서 먼저 파일이 서버로 전송된다.</p>
<pre><code>POST /api/posts/media/images
Content-Type: multipart/form-data</code></pre><p>서버는 파일을 로컬에 저장하고 UUID 기반의 key와 접근 URL을 반환한다.</p>
<pre><code class="language-json">{
  &quot;key&quot;: &quot;posts/2026/02/11/abc.jpeg&quot;,
  &quot;url&quot;: &quot;/files/posts/2026/02/11/abc.jpeg&quot;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/siha_014/post/47e2c133-1bd7-4aaa-8137-dc60fd63c2f2/image.png" alt=""></p>
<p>에디터는 이 URL을 받아서 HTML에 <code>&lt;img&gt;</code> 태그로 즉시 삽입한다. 덕분에 사용자는 이미지가 본문에 렌더링되는 걸 바로 확인할 수 있다.</p>
<p><strong>2단계 — 게시글 저장</strong></p>
<p>저장 버튼을 누르면 에디터의 HTML 문자열이 JSON으로 전송된다.</p>
<pre><code>POST /api/posts
Content-Type: application/json</code></pre><pre><code class="language-json">{
  &quot;title&quot;: &quot;이미지 테스트&quot;,
  &quot;contents&quot;: &quot;&lt;p&gt;본문 내용&lt;/p&gt;&lt;img src=\&quot;/files/posts/2026/02/11/abc.jpeg\&quot;&gt;&quot;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/siha_014/post/164ee284-c8c9-4862-87c3-b8f6fc5bf4ad/image.png" alt=""></p>
<p>이미지는 이미 업로드가 끝난 상태이므로, 게시글 저장 단계에서는 HTML 문자열만 저장하면 된다.</p>
<hr>
<h2 id="이미지-메타데이터는-어떻게-관리하나">이미지 메타데이터는 어떻게 관리하나</h2>
<p>게시글 HTML 안에 이미지 URL이 포함되어 있긴 하지만, 이것만으로는 &quot;이 게시글에 어떤 이미지가 사용됐는지&quot;를 서버가 파악하기 어렵다. 그래서 <code>PostImage</code>라는 별도 엔티티에 이미지 정보를 저장한다.</p>
<pre><code class="language-java">@Entity
public class PostImage {

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

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = &quot;post_id&quot;, nullable = false)
    private Post post;

    @Column(nullable = false, length = 500)
    private String storageKey;

    @Column(nullable = false)
    private int sortOrder;
}</code></pre>
<p>몇 가지 설계 기준이 있었다.</p>
<p>URL 대신 <code>storageKey</code>만 저장한다. URL은 스토리지 구현에 따라 바뀔 수 있다. 지금은 로컬 파일 서버를 쓰지만 나중에 S3로 전환하면 URL 형식이 달라진다. key만 저장해두면 URL은 런타임에 조립하면 그만이다.</p>
<p><code>sortOrder</code>로 순서를 보장한다. WYSIWYG 에디터에서는 이미지 순서가 의미를 가질 수 있다. HTML 파싱 순서대로 index를 부여해서 저장한다.</p>
<hr>
<h2 id="html에서-이미지를-추출하는-방법">HTML에서 이미지를 추출하는 방법</h2>
<p>게시글이 저장될 때, 서버는 HTML을 분석해서 사용된 이미지 목록을 뽑아낸다.</p>
<pre><code class="language-java">private static final Pattern IMG_SRC_PATTERN =
    Pattern.compile(&quot;&lt;img[^&gt;]*\\s+src=[\&quot;&#39;](/files/[^\&quot;&#39;]+)[\&quot;&#39;][^&gt;]*&gt;&quot;, Pattern.CASE_INSENSITIVE);</code></pre>
<p><code>/files/</code>로 시작하는 URL만 인식하도록 제한했다. 외부 이미지 URL(예: 다른 사이트에서 복붙한 이미지)은 무시한다. 이렇게 하면 서버가 관리하는 이미지만 <code>PostImage</code>에 기록된다.</p>
<hr>
<h2 id="게시글-수정-시-동기화">게시글 수정 시 동기화</h2>
<p>수정이 까다로운 부분이었다. 기존 이미지 중 일부는 삭제되고, 새 이미지가 추가될 수 있다. 순서도 바뀔 수 있다.</p>
<p>여러 방식을 고민했는데, 결국 <strong>전체 삭제 후 재생성</strong> 방식을 택했다.</p>
<ol>
<li>기존 <code>PostImage</code> 전부 삭제</li>
<li>수정된 HTML에서 이미지 추출</li>
<li>새 목록으로 재생성</li>
</ol>
<p>diff 방식도 고려했지만, WYSIWYG 에디터에서는 사용자가 이미지를 자유롭게 이동/삭제/추가하기 때문에 변경분을 추적하는 게 생각보다 복잡해진다. 전체 재생성이 단순하고 버그 여지도 적었다.</p>
<hr>
<h2 id="업로드를-분리한-이유">업로드를 분리한 이유</h2>
<p>WYSIWYG 에디터는 사용자가 이미지를 붙이는 즉시 본문에서 미리보기를 보여줘야 한다. 이걸 구현하려면 이미지가 에디터 조작 시점에 이미 서버에 올라가 있어야 한다. 게시글 저장 시점까지 이미지를 들고 있을 수가 없다.</p>
<p>그리고 이미지 업로드 실패와 게시글 저장 실패를 분리할 수 있다는 것도 장점이다. 이미지는 잘 올라갔는데 게시글 저장에서 실패했다면, 이미지를 다시 올릴 필요 없이 게시글만 재시도하면 된다.</p>
<hr>
<h2 id="확장-가능성">확장 가능성</h2>
<p>현재는 로컬 파일 서버에 저장하지만, <code>FileStorage</code> 인터페이스를 구현체만 교체하면 S3로 전환할 수 있다. <code>storageKey</code> 기반으로 설계한 이유가 여기 있다.</p>
<p>추후 고아 이미지(게시글에 참조되지 않는 이미지) 정리 배치, CDN 적용, XSS 필터링 강화 등도 자연스럽게 붙일 수 있는 구조다.</p>
<hr>
<pre><code>[Client]
   │
   ├─ POST /media/images (file)
   │        ↓
   │   LocalFileStorage
   │        ↓
   │   key + url 반환
   │
   └─ POST /posts (JSON)
            ↓
       PostService
            ↓
   HtmlImageExtractor
            ↓
      PostImage 저장</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 보안]]></title>
            <link>https://velog.io/@siha_014/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EB%B3%B4%EC%95%88</link>
            <guid>https://velog.io/@siha_014/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EB%B3%B4%EC%95%88</guid>
            <pubDate>Thu, 29 Jan 2026 11:36:35 GMT</pubDate>
            <description><![CDATA[<h2 id="보안---사용자-권한-관리">보안 - 사용자 권한 관리</h2>
<p>데이터베이스 시스템에서는 다양한 사용자가 존재하며, 각 사용자에게 적절한 권한을 부여해야 보안을 유지할 수 있음</p>
<ul>
<li>GRANT: 사용자에게 권한 부여
GRANT 명령어를 사용하여 특정 사용자에게 데이터베이스에 대한 접근 권한을 부여할 수 있음
각 권한은 SEELCT, INSERT, UPDATE, DELETE등으로 세분화할 수 있음</li>
<li>REVOKE - 사용자 권한 회수
기존에 부여한 특정 권한을 회수할 때 사용
잘못된 권한을 부여했거나, 특정 사용자가 더 이상 해당 권한을 가질 필요가 없을 때 사용</li>
</ul>
<h2 id="보안---sql-인젝션-방어">보안 - SQL 인젝션 방어</h2>
<p>SQL 인젝션(SQL Injection): 공격자가 SQL문을 조작하여 데이터베이스를 무단으로 조작하는 해킹 방법</p>
<h3 id="prepared-statement준비된-쿼리를-사용한-sql-인젝션-방어">Prepared Statement(준비된 쿼리)를 사용한 SQL 인젝션 방어</h3>
<ul>
<li>미리 SQL문을 준비한 후, 사용자 입력값을 안전하게 바인딩하여 실행하는 방식</li>
<li>사용자가 입력한 값이 SQL 문에 직접 삽입되지 않으므로, SQL 인젝션 공격을 방어할 수 있음</li>
<li>SQL문을 미리 준비하고, 사용자 입력값을 ? 자리에 안전하게 바인딩</li>
<li>입력값이 자동으로 인코딩되므로 SQL 문법이 깨지지 않음 -&gt; 인젝션 공격 차단</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[인덱스 및 데이터 최적화]]></title>
            <link>https://velog.io/@siha_014/%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EB%B0%8F-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@siha_014/%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EB%B0%8F-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Wed, 28 Jan 2026 12:46:53 GMT</pubDate>
            <description><![CDATA[<h1 id="인덱스">인덱스</h1>
<p>데이터베이스에서 검색 속도를 향상시키기 위한 자료구조
책의 목차처럼, 원하는 데이터를 빠르게 찾을 수 있도록 도와줌
특정 컬럼에 인덱스를 생성하면 해당 컬럼을 검색할 때 속도가 크게 향상됨</p>
<h2 id="클러스터형-인덱스-vs-비클러스터형-인덱스">클러스터형 인덱스 vs 비클러스터형 인덱스</h2>
<p>기본적으로 모든 인덱스는 이 두 개념 중 하나에 속함
데이터 저장 방식과 검색 방식에 차이가 있음</p>
<h3 id="클러스터형-인덱스-clustered-index">클러스터형 인덱스 (Clustered Index)</h3>
<p><strong>데이터 자체가 인덱스 순서대로 저장됨</strong>
따라서 검색이 빠르지만, 데이터의 삽입/삭제 시 재정렬이 필요할 수 있음
PK에 자동으로 생성되며, 테이블당 하나만 존재 가능</p>
<h3 id="비클러스터형-인덱스-non-clustered-index">비클러스터형 인덱스 (Non-Clustered Index)</h3>
<p>PK 이외의 컬럼에서 검색 속도를 높이기 위해 사용됨
<strong>인덱스와 실제 데이터가 따로 저장</strong>되므로, 조회 시 한 단계 더 검색해야 함
인덱스 전용 테이블이 별도로 생성
한 테이블에 여러 개의 비클러스터형 인덱스를 생성할 수 있음
보조 인덱스(Secondary Index)라고도 불림</p>
<h2 id="인덱스의-자료구조">인덱스의 자료구조</h2>
<h3 id="b-tree구조">B-Tree구조</h3>
<p>모든 노드에 데이터가 저장됨
전체 데이터를 여러 블록(노드)로 분할해 저장
검색, 삽입, 삭제 속도 O(log N) -&gt; 빠르게 탐색 가능</p>
<h3 id="btree">B+Tree</h3>
<p>B-Tree의 단점을 보완한 자료구조
리프노드가 연결 리스트 형태로 구성되고, 리프 노드에만 데이터를 저장하여 범위 검색이 빠름
루트 노드, 중간 노드는 키 값만 저장</p>
<h4 id="클러스터형-인덱스의-자료구조btree를-사용하는-경우">클러스터형 인덱스의 자료구조(B+Tree를 사용하는 경우)</h4>
<p>클러스터형인덱스는 데이터 자체가 리프 노드에 저장됨
리프 노드에 실제 데이터가 저장되며, 키 값(id) 순서대로 정렬됨
즉, 인덱스를 따라가면서 데이터를 찾으면 추가적인 참조 없이 바로 데이터를 읽을 수 있음</p>
<h4 id="비클러스터형-인덱스의-자료구조btree를-사용하는-경우">비클러스터형 인덱스의 자료구조(B+Tree를 사용하는 경우)</h4>
<p>비클러스터형 인덱스는 인덱스와 실제 데이터가 별로도 저장됨
리프 노드에는 실제 데이터가 아닌 PK를 참조하는 값이 저장됨
검색을 수행하면 먼저 인덱스를 검색하고, 다시 PK를 참조하여 실제 데이터를 가져와야 함</p>
<h2 id="인덱스-생성-방법">인덱스 생성 방법</h2>
<h3 id="클러스터형-인덱스-생성">클러스터형 인덱스 생성</h3>
<p>테이블의 실제 데이터 저장 순서를 결정하는 인덱스
PK를 설정하면 자동으로 생성됨
테이블마다 하나만 생성 가능함
인덱스를 따라가면 바로 데이터에 접근할 수 있음(추가 탐색 불필요)</p>
<h3 id="기본-인덱스-생성비클러스터형-인덱스">기본 인덱스 생성(비클러스터형 인덱스)</h3>
<p>하나 또는 여러 컬럼에 대해 인덱스를 생성하는 방법
CREATE INDEX 구문으로 생성됨<br>비클러스터형 인덱스가 생성됨(이미 클러스터형 인덱스가 존재하므로)
인덱스를 따라가서 다시 테이블을 참조해야 데이터에 접근 가능함</p>
<h3 id="고유-인덱스unique-index-생성">고유 인덱스(Unique Index) 생성</h3>
<p>중복되지 않는 값을 저장해야 하는 컬럼에 사용
UNIQUE 제약 조건이 포함된 인덱스를 생성하면 중복값 입력이 불가능함
중복 방지를 위한 제약 조건이 주요 목적이며, 인덱스 자체는 보통 비클러스터형 인덱스로 생성됨
단, PK로 지정되는 경우에는 클러스터형 인덱스로 생성됨</p>
<h2 id="복합-인덱스-composite-index">복합 인덱스 (Composite Index)</h2>
<p>여러 개의 컬럼을 하나의 인덱스로 묶어서 생성하는 인덱스
하나의 인덱스로 여러 컬럼을 동시에 검색할 때 성능을 최적화함</p>
<p><strong>사용 이유</strong></p>
<ul>
<li>WHERE 조건에서 여러 개의 컬럼을 함께 검색하는 경우 속도를 최적화하기 위해</li>
<li>데이터베이스가 여러 개의 컬럼을 하나의 트리 구조에서 관리하도록 하여, 검색 효율을 높일 수 있음</li>
<li>하나의 인덱스를 사용하여여러 조건을 동시에 만족하는 데이터를 빠르게 찾을 수 있음</li>
</ul>
<p><strong>주의할 점</strong>
CREATE INDEX idx(col1, col2, col3)처럼 복합 인덱스를 만들면 인덱스를 활용하려면 col1부터 순서대로 WHERE 절에 포함되어야 함
예를 들어, col2, col3만 조건에서 사용하면 인덱스를 사용할 수 없음
즉, 인덱스는 지정한 컬럼 순서대로 조건이 걸려 있어야 성능이 최적화됨</p>
<h2 id="인덱스와-성능">인덱스와 성능</h2>
<p>인덱스는 데이터베이스에서 검색 성능을 향상시키지만, 잘못 사용하면 오히려 성능을 저하시킬 수도 있음
인덱스의 장점과 단점을 이해하고, 언제 인덱스를 활용해야 하는지 판단하는 것이 중요</p>
<p><strong>인덱스가 성능을 향상시키는 경우</strong>
데이터 조회(SELECT) 속도 향상
WHERE, JOIN, ORDER BY, GROUP BY 같은 연산이 포함된 쿼리 최적화
대량의 데이터를 검색할 때, 특정 컬럼을 기준으로 빠르게 탐색 가능</p>
<p><strong>인덱스가 성능을 저하시킬 수 있는 경우</strong>
크기가 작은 데이블에서 WHERE 검색(FULL SCAN이 빠를 수 있음)
INSERT, UPDATE, DELETE 성능 저하
(자주 변경되는 컬럼에 인덱스를 생성하면, 인덱스 유지 비용 증가)</p>
<h1 id="데이터-최적화">데이터 최적화</h1>
<p>데이터가 많아질수록 데이터베이스의 성능을 유지하기 위해 데이터를 분할하는 기법이 필요함</p>
<h2 id="샤딩sharding">샤딩(Sharding)</h2>
<p>데이터를 여러 개의 독립적인 데이터베이스(샤드)로 나누어 저장</p>
<p>샤딩이 필요한 경우</p>
<ul>
<li>데이터베이스가 너무 커서 하나의 DB서버에 저장할 수 없는 경우</li>
<li>전 세계 여러 지역에서 서비스를 운영하여 데이터 지역성을 최적화해야 하는 경우</li>
</ul>
<h2 id="파티셔닝partitioning">파티셔닝(Partitioning)</h2>
<p>하나의 데이터베이스 내에서 데이터를 여러 개의 파티션으로 분할
샤딩과 다르게 물리적으로 데이터베이스를 나누는 것이 아닌, 논리적으로 하나의 테이블을 여러 개의 파티션으로 나누는 방식
(수평 파티셔닝 / 수직 파티셔닝)</p>
<p>피티셔닝이 필요한 경우
특정 데이터가 너무 많아서 관리가 어려운 경우
특정 조건으로 자주 조회하는 데이터가 많아서, 검색 성능을 높이기 위해 데이터를 논리적으로 분리할 필요가 있는 경우</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[트랜잭션]]></title>
            <link>https://velog.io/@siha_014/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98</link>
            <guid>https://velog.io/@siha_014/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98</guid>
            <pubDate>Tue, 27 Jan 2026 12:26:33 GMT</pubDate>
            <description><![CDATA[<h1 id="트랜잭션">트랜잭션</h1>
<p>데이터베이스에서 하나의 논리적 작업 단위
모든 작업이 성공해야만 최종적으로 반영되고, 하나라도 실패하면 전체 작업이 취소(Rollback)됨
데이터의 일관성을 유지하기 위해 사용됨</p>
<h2 id="트랜잭션의-특성">트랜잭션의 특성</h2>
<ul>
<li><p>Atomicity (원자성)
: 트랜잭션 내의 모든 작업은 모두 성공 또는 모두 실패해야 함</p>
</li>
<li><p>Consistency (일관성)
: 트랜잭션이 실행되기 전과 후의 데이터가 일관성을 유지해야 함</p>
</li>
<li><p>Isolation (고립성)
: 여러 트랜잭션이 동시에 실행될 때, 서로 영향을 주지 않아야 함</p>
</li>
<li><p>Durability (지속성)
: 트랜잭션이 완료되면 그 결과가 영구적으로 반영되어야 함</p>
</li>
</ul>
<h2 id="트랜잭션-상태">트랜잭션 상태</h2>
<p>트랜잭션은 실행되는 동안 여러 상태를 거치며 진행</p>
<ul>
<li><p>Active (활성 상태)
: 트랜잭션이 시작되어 실행 중인 상태</p>
</li>
<li><p>Partilly Committed (부분 완료 상태)
: 트랜잭션이 모든 SQL문을 실행했지만 아직 확정(COMMIT)되지 않은 상태</p>
</li>
<li><p>Committed (완료 상태)
: 트랜잭션이 성공적으로 완료되어 데이터가 영구적으로 반영된 상태</p>
</li>
<li><p>Failed (실패 상태)
: 오류로 인해 트랜잭션이 정상적으로 완료되지 못한 상태</p>
</li>
<li><p>Aborted (중단 상태)
: 트랜잭션이 실패하여 ROLLBACK이 수행된 상태</p>
</li>
</ul>
<h2 id="트랜잭션의-복구">트랜잭션의 복구</h2>
<p>장애 발생 시 데이터 일관성을 보장하기 위한 복구 필요
트랜잭션의 원자성 보장을 위한 메커니즘</p>
<h3 id="지연-갱신-방식">지연 갱신 방식</h3>
<p>트랜잭션의 작업 내용을 로그에만 기록하고, 실제 DB 반영은 COMMIT 이후에 수행함
장애 발생 시, COMMIT된 트랜잭션만 반영하면 됨 -&gt; Redo 연산 사용
COMMIT되지 않은 트랜잭션은 무시하면 됨</p>
<blockquote>
<p><strong>즉시 갱신 방식</strong>
트랜잭션이 시작되고 쿼리를 하나하나 실행할 때마다 DB에 바로 반영하며, 변경 전 데이터는 Undo 로그에 함께 저장
장애 발생 시 트랜잭션 일부 작업이 반영되어 있을 수 있음
COMMIT되지 않은 트랜잭션 내의 작업을 되돌리기 위해 Undo 연산 사용
Undo 연산: 트랜잭션 도중 장애 발생 시, 이미 반영된 데이터를 원래 값으로 복구하는 작업</p>
</blockquote>
<h3 id="redo-연산">Redo 연산</h3>
<p>COMMIT된 트랜잭션이 장애로 인해 DB에 반영되지 못했을 경우, 트랜잭션 로그에 따라 다시 반영하는 작업
트랜잭션 로그에 기록된 값(New Value)을 DB에 반영함</p>
<h2 id="트랜잭션-격리-수준">트랜잭션 격리 수준</h2>
<p>하나의 트랜잭션에서 작업 중인 데이터가 다른 트랜잭션에 영향을 받지 않는 정도를 의미
반대로 하나의 트랜잭션에서 작업 중인 데이터를 다른 트랜잭션에서 어느 정도까지 접근 할 수 있는가를 나타냄</p>
<p><strong>트랜잭션 격리 수준의 설정</strong>
격리 수준을 낮게 설정 &gt;&gt; 동시성이 좋아짐
격리 수준을 높게 설정 &gt;&gt; 동시성은 떨어지나 정확성이 좋아짐
따라서 둘 사이의 trade-off를 고려해야 함</p>
<h3 id="1단계-read-uncommitted">1단계: READ UNCOMMITTED</h3>
<p>: 다른 트랜잭션이 CCOMMIT되지 않은 데이터도 읽을 수 있음
가장 낮은 격리 수준,
Dirty Read 발생 가능
(다른 트랜잭션이 COMMIT하지 않은 데이터를 읽을 수 있음) </p>
<h3 id="2단계-read-committed">2단계: READ COMMITTED</h3>
<p>: COMMIT된 데이터만 읽을 수 있음
Dirty Read를 방지하지만 Non-Repeatable Read 발생 가능
(같은 데이터를 다시 조회했을 때 값이 바뀌어 있음)</p>
<h3 id="3단계-repeatable-read">3단계: REPEATABLE READ</h3>
<p>: 같은 트랜잭션 내에서 동일한 데이터를 읽으면 항상 같은 값이 반환됨 (트랜잭션이 시작되기 전에 커밋된 내용에 대해서먄 조회)
Non-Repeatable Read를 방지하지만 Phantom Read 발생 가능
(같은 조건으로 조회했을 때 새로운 데이터가 나타남)
(REPEATABLE READ는 기존 행의 수정/삭제는 막지만, 삽입은 막지 못하기 때문)</p>
<h3 id="4단계-serializable">4단계: SERIALIZABLE</h3>
<p>: 트랜잭션을 직렬화하여 동작 (가장 엄격)
각 트랜잭션이 마치 하나씩 순서대로 실행된 것처럼 보이게 동작</p>
<h4 id="실무에서의-격리-수준-정도">실무에서의 격리 수준 정도</h4>
<p>일반적인 웹 애플리케이션: READ    COMMITTED / REPEATABLE READ
데이터 일관성이 중요한 경우 (EX. 은행 시스템): SERIALIZABLE을 사용하나 성능 저하를 고려해야 함
대량 트랜잭션이 필요한 경우 (EX. 쇼핑몰): READ COMMITTED으로 성능 우선
MySQL 기본값: REPEATABLE READ
PostgreSQL 기본값: READ COMMITTED
Oracle 기본값: READ COMMITTED</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[정규화]]></title>
            <link>https://velog.io/@siha_014/%EC%A0%95%EA%B7%9C%ED%99%94</link>
            <guid>https://velog.io/@siha_014/%EC%A0%95%EA%B7%9C%ED%99%94</guid>
            <pubDate>Mon, 26 Jan 2026 02:34:51 GMT</pubDate>
            <description><![CDATA[<h1 id="이상-현상">이상 현상</h1>
<p>정규화를 하지 않으면, 데이터 수정, 삽입, 삭제 시 문제가 발생할 수 있음</p>
<ol>
<li><p>삽입 이상 (Insertion Anomaly)
데이터를 삽입할 때 불필요한 데이터까지 입력해야 하는 문제</p>
</li>
<li><p>수정 이상 (Update Anomaly)
중복된 데이터가 존재할 경우, 하나만 수정하면 데이터 불일치가 발생하는 문제</p>
</li>
<li><p>삭제 이상 (Deletion Anomaly)
하나의 데이터를 삭제할 때, 연관된 다른 데이터까지 삭제되는 문제</p>
</li>
</ol>
<h1 id="정규화">정규화</h1>
<p>데이터베이스 설계에서 중복을 최소화하고 데이터 무결성을 유지하기 위한 과정
데이터를 여러 테이블로 나누어 <strong>이상 현상 발생을 방지*</strong>
데이터 변경 시 일관성을 유지하고, 불필요한 데이터 수정 비용을 줄임
관계형 데이터베이스에서 가장 중요한 설계 원칙 중 하나
정규화를 적용하면 저장 공간을 절약하고, 데이터 수정 시 오류를 방지할 수 있음</p>
<p>하지만 과도한 정규화는 JOIN 연산이 많아져 성능 저하를 유발할 수 있으므로, 적절한 수준에서 적용하는 것이 중요함</p>
<p><strong>반정규화</strong>
: 정규화를 적용한 데이터베이스에서 성능 최적화를 위해 일부 정규화를 되돌리는 과정
JOIN 연산이 많아져 조회 성능이 저하될 때 해결책으로 사용되며, 
데이터 중복을 일부 허용하여 읽기 속도를 향상시킨다.</p>
<h2 id="제1정규형-1nf">제1정규형 (1NF)</h2>
<p>각 컬럼이 하나의 원자값(하나의 값)만 가져야 한다는 규칙을 의미</p>
<h2 id="제2정규형-2nf">제2정규형 (2NF)</h2>
<p>부분 함수 종속 제거
1NF를 만족하고 ,기본 키의 일부만으로 특정 컬럼이 결정되는 경우 제거
즉, 기본 키의 일부에만 종속된 속성을 분리해야 함</p>
<h2 id="제3정규형-3nf">제3정규형 (3NF)</h2>
<p>제2정규형을 만족하면서, 이행적 종속을 제거하는 과정
기본 키가 아닌 속성이 다른 일반 속성에 종속되는 경우를 제거해야 함
즉, 기본 키가 아닌 컬럼은 오직 기본 키에만 의존해야 함</p>
<h2 id="bcnf-boycecodd-normal-form">BCNF (Boyce/Codd Normal Form)</h2>
<p>보다 일반적인 정규형을 제안
릴레이션 R의 결정자 모두가 후보 키이면 릴레이션 R은 BCNF이다.
강한 제3규형</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 개요]]></title>
            <link>https://velog.io/@siha_014/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EA%B0%9C%EC%9A%94</link>
            <guid>https://velog.io/@siha_014/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EA%B0%9C%EC%9A%94</guid>
            <pubDate>Sat, 24 Jan 2026 12:03:36 GMT</pubDate>
            <description><![CDATA[<h1 id="데이터베이스">데이터베이스</h1>
<p>체계적으로 정리된 데이터의 집합으로, 여러 사용자가 데이터를 효율적으로 저장, 검새, 수정, 삭제할 수 있도록 관리하는 시스템
일반적으로 데이터베이스는 데이터베이스 관리 시스템(DBMS)을 통해 데이터를 운영하며, 데이터의 무결성과 일관성을 유지하도록 설계됨</p>
<p><strong>데이터 VS 정보</strong>
데이터: 가공되지 않은 원시 값
정보: 데이터를 가공하여 의미를 부여한 것</p>
<h2 id="특성">특성</h2>
<ul>
<li>실시간 접근성
언제든지 데이터를 저장하고 조회할 수 있음</li>
<li>동시성 (Concurrency)
여러 사용자가 동시에 같은 데이터를 읽고 쓸 수 있음</li>
<li>일관성 (Consistency)
데이터가 항상 올바른 상태를 유지해야 함</li>
<li>무결성 (Integrity)
데이터가 규칙에 맞게 저장되도록 보장</li>
<li>데이터 중복 최소화
데이터를 효율적으로 저장하고 중복을 방지</li>
<li>보안성
허가된 사용자만 데이터에 접근 가능</li>
<li>독립성
데이터와 프로그램이 분리되어 있음 -&gt; 데이터를 변경해도 프로그램 수정 없이 사용 가능</li>
</ul>
<h2 id="dbms">DBMS</h2>
<p>Database Management System
데이터베이스를 효과적으로 관리하기 위해 필요
데이터를 저장, 수정, 검색, 삭제하는 기능을 제공하는 소프트웨어
MySQL, 오라클 등</p>
<p><strong>기능</strong></p>
<ul>
<li>데이터 저장 및 관리 -&gt; 데이터를 구조화된 형태로 저장</li>
<li>데이터 검색 및 수정 -&gt; SQL을 사용해 데이터를 빠르게 검색하고 변경 가능</li>
<li>동시성 제어 -&gt; 여러 사용자가 같은 데이터를 동시에 사용 가능</li>
<li>보안 관리 -&gt; 사용자 권한을 설정하여 데이터를 접근을 제한</li>
<li>백업 및 복구 -&gt; 장애 발생 시 데이터 손실 없이 복구 가능</li>
</ul>
<p><strong>언어 4종류</strong></p>
<ul>
<li><p>DML (데이터 조작어)
SELECT, INSERT, UPDATE, DELETE
데이터를 검색하고 추가, 수정, 삭제하는 명령어
자주 사용하는 SQL</p>
</li>
<li><p>DDL (데이터 정의어)
CREATE, ALTER, DROP
테이블, 인덱스 등을 생성하거나 수정할 때 사용
초기 DB 설계 시 많이 사용되며, 운영 중에는 가끔 사용됨</p>
</li>
<li><p>DCL (데이터 제어어)
GRANT, REVOKE
DB 관리자(DBA)가 보안 관리를 위해 사용
일반 개발자는 사용할 일이 거의 없음</p>
</li>
<li><p>TCL (트랜재션 제어어)
COMMIT, ROLLBACK, SAVEPOINT
여러 개의 데이터 변경 작업을 하나의 트랜잭션으로 묶을 때 사용
특히 금융, 쇼핑몰 결제 시스템 등에서는 필수</p>
</li>
</ul>
<h3 id="rdbms">RDBMS</h3>
<p>관계형 데이터베이스 관리 시스템(Relational DBMS)
데이터를 테이블 형태로 저장하고, 테이블 간의 관계를 이용하여 데이터를 관리하는 데이터베이스 시스템
SQL 사용 -&gt; 데이터를 조회, 추가, 수정, 삭제
관계 기반 -&gt; 테이블 간 연결을 통해 중복 최소화
MySQL, PostgreSQL, Oracle, SQL Server 등이 대표적</p>
<p>테이블의 관계는 일대일(1:1), 일대다(1:N), 다대일(N:1), 다대다(N:M)이 있다.</p>
<h2 id="데이터베이스-모델링">데이터베이스 모델링</h2>
<p>실제 DB를 만들기 전에 구조를 설계하는 과정으로, 3단곌 진행</p>
<p>1단계 - <strong>개념적 모델링</strong>
: 무엇을 저장할지 정리하는 단계
데이터 구조를 간단한 개념으로 표현 &gt;&gt; 개체(Entity), 관계(Relationship)
ERD(Entity-Relationship diagram)가 많이 사용됨</p>
<p>2단계 - <strong>논리적 모델링</strong>
: 개념적 모델링을 기반으로 릴레이션 스키마 형태로 구체화
테이블, 속성(컬럼), 관계(PK, FK) 설정
데이터 정규화 적용</p>
<p>3단계 - <strong>물리적 모델링</strong>
논리적 모델을 실제 데이터베이스에 적용하는 단계
DBMS에 맞는 데이터 타입, 인덱스, 제약 조건 설정</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[네트워크 보안과 실무]]></title>
            <link>https://velog.io/@siha_014/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%B3%B4%EC%95%88%EA%B3%BC-%EC%8B%A4%EB%AC%B4</link>
            <guid>https://velog.io/@siha_014/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%B3%B4%EC%95%88%EA%B3%BC-%EC%8B%A4%EB%AC%B4</guid>
            <pubDate>Fri, 23 Jan 2026 13:14:35 GMT</pubDate>
            <description><![CDATA[<h1 id="네트워크-보안">네트워크 보안</h1>
<p><strong>네트워크 보안이 필요한 이유</strong></p>
<ul>
<li>개인 정보 보호
사용자 계정, 신용카드 정보 등 중요한 데이터가 보호되지 않으면 유출될 수 있음</li>
<li>데이터 변조 방지
전송 중 데이터가 변조되지 않도록 보호해야 함</li>
<li>시스템 다운 방지
DDos 공격 등으로 네트워크가 마비되지 않도록 보안이 필요</li>
<li>악성 코드 및 해킹 방지
랜섬웨어, 바이러스 등의 공격으로부터 시스템을 보호해야 함</li>
</ul>
<h2 id="네트워크-보안의-3대-요소">네트워크 보안의 3대 요소</h2>
<ol>
<li><p><strong>기밀성</strong> (Confidentiality) - 정보가 보호되어야 함
허가된 사용자만 데이터에 저븐할 수 있도록 보호
암호화를 통해 데이터를 안전하게 유지
예시: 로그인 시스템(비밀번호 보호), HTTPS(암호화된 웹 통신)</p>
</li>
<li><p><strong>무결성</strong> (Integrity) - 데이터가 변조되지 않아야 함
데이터를 전송하는 동안 변경되거나 조작되지 않도록 보호
예시: 디지털 서명, 체크섬을 이용한 데이터 무결성 검증</p>
</li>
<li><p><strong>가용성</strong> (Availability) - 네트워크가 항상 작동해야 함
시스템이 장애 없이 정상적으로 운영될 수 있도록 보호
DDos 공격(분산 서비스 거부 공격) 방어 필요
예시: 방화벽을 이용한 네트워크 보호, 서버 이중화(백업 서버 운영)</p>
</li>
</ol>
<h2 id="대칭키와-비대칭키">대칭키와 비대칭키</h2>
<h3 id="대칭키">대칭키</h3>
<p><strong>대칭키 암호화 (Symmetric Key Encryption)</strong>
<strong>하나의 비밀키를 사용</strong>하여 데이터를 암호화하고 복호화
송신자와 수신자가 같은 키를 공유해야 함
속도가 빠르고 연산이 효율적이지만, 키 분배가 어려움</p>
<p>대칭키 암호화 과정:
A가 데이터를 암호화할 때 비밀키(K)를 사용하여 암호화
암호화된 데이터를 B에게 전송
B가 데이터를 복호화할 때 같은 비밀키(K)를 사용하여 보호화</p>
<p>장점:</p>
<ul>
<li>빠른 속도</li>
<li>연산이 간단하여 대량의 데이터 처리에 적합</li>
</ul>
<p>단점</p>
<ul>
<li>키 분배가 어려움 (중간에 키가 유출되면 보안 취약)</li>
</ul>
<h3 id="비대칭키">비대칭키</h3>
<p><strong>비대칭키 암호화 (Asymmetric Key Encryption)</strong>
<strong>공개키(Public Key)</strong>와 <strong>비밀키(Private Key)</strong> 두 개의 키를 사용
공개키는 누구나 알 수 있고, 비밀키는 본인만 소유
암호화와 복호화에 각각 다른 키를 사용하므로 보안성이 높음</p>
<p>비대칭키 암호화 과정:
A가 B에게 보낼 데이터를 B의 공개키로 암호화
암호화된 데이터를 전송
B는 자신의 비밀키로 데이터를 보호화</p>
<p>용도:</p>
<ul>
<li><p>암호화 용도 (기밀성):
공개키로 암호화 -&gt; 비밀키로 복호화</p>
</li>
<li><p>전자 서명 용도 -&gt; 공개키로 복호화</p>
</li>
</ul>
<p>장점</p>
<ul>
<li>키 분배가 쉽고 보안성이 높음</li>
<li>전자 서명, 인증서 등에 활용 가능</li>
</ul>
<p>단점</p>
<ul>
<li>연산 속도가 느림</li>
<li>대량의 데이터를 처리하기에는 부담이 큼</li>
</ul>
<h2 id="https-ssltls">HTTPS, SSL/TLS</h2>
<p>HTTPS = HTTP + 보안(SSL/TLS)
SSL/TLS는 네트워크에서 데이터를 암호화하는 보안 프로토콜
HTTPS는 HTTP의 보안 강화 버전 (HTTP:80번 포트 / HTTPS: 443번 포트)
데이터를 암호화하여 안전한 웹 통신을 제공하는 프로토콜
웹사이트에서 로그인, 결제 등 민감한 데이터를 보호하기 위해 사용</p>
<p>SSL(Secure Sockets Layer): 초기 보안 프로토콜
TLS(Trasport Layer Security): SSL의 개선 버전, 현재 HTTPS에서 사용됨</p>
<p>SSL/TLS의 주요 기능</p>
<ul>
<li>암호화(Encryption): 데이터가 네트워크에서 노출되지 않도록 암호화</li>
<li>인증(Authentication): 웹사이트가 신뢰할 수 있는 사이트인지 인증</li>
<li>데이터 무결성(Integrity): 데이턱가 전송 중 변경되지 않도록 보호</li>
</ul>
<h2 id="방화벽">방화벽</h2>
<p>네트워크에서 허용된 트래픽만 통과시키고, 불필요하거나 위험한 트래픽을 차단하는 역할
개업, 기관, 개인 네트워킹에서 해킹, 악성 코드, 불법 접근 등을 방지하기 위해 사용</p>
<p>주요 기능</p>
<ul>
<li>IP 주소, 포트 번호, 프로토콜 기반으로 트래픽 필터링</li>
<li>허용된 네트워크 요청만 통과, 비인가된 요청은 차단</li>
<li>DDoS 공격, 바이러스, 악성 코드 차단 기능 제공 가능</li>
</ul>
<p>필터링 과정</p>
<ol>
<li>클라이언트(외부)가 서버에 접속 요청 (예: 웹사이트 접속)</li>
<li>방화벽이 패킷을 검사
출발지 IP, 목적지 IP, 포트 번호, 프로토콜을 확인</li>
<li>방화벼의 보안 규칙에 따라 결정
허용된 트래픽은 내부 네트워크로 전달/ 차단된 트래픽은 즉시 폐기</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터링크 계층]]></title>
            <link>https://velog.io/@siha_014/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A7%81%ED%81%AC-%EA%B3%84%EC%B8%B5</link>
            <guid>https://velog.io/@siha_014/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A7%81%ED%81%AC-%EA%B3%84%EC%B8%B5</guid>
            <pubDate>Wed, 21 Jan 2026 06:17:23 GMT</pubDate>
            <description><![CDATA[<h1 id="데이터링크-계층">데이터링크 계층</h1>
<p>같은 네트워크 내에서 신뢰성 있는 데이터 전송을 담당
즉, 같은 LAN 내에서 장치 간 데이터를 주고 받는 역할을 함</p>
<p><strong>데이터링크 계층의 역할</strong></p>
<ul>
<li>네트워크 계층에서 받은 데이터를 프레임(frame)단위로 변환하여 물리 계층으로 전송</li>
<li>같은 네트워크 (LAN) 내에서 오류 없이 데이터를 전달</li>
<li>MAC 주소를 기반으로 목적지 장치를 식별하여 데이터 전송</li>
<li>충돌을 방지하고 효율적인 데이터를 전달하는 MAC(Media Access Control) 기능 제공</li>
</ul>
<p>데이터링크 계층의 주요 프로토콜: Ethernet(이더넷), Wi-Fi(무선 LAN)</p>
<h2 id="이더넷-ethernet">이더넷 (Ethernet)</h2>
<p>유선 네트워크(LAN)에서 데이터를 전송하는 가장 널리 사용되는 기술
즉, 같은 네트워크 내에서 장치 간 데이터를 MAC 주소를 기반으로 전달</p>
<p>특징</p>
<ul>
<li>MAC 주소를 사용하여 장치 간 데이터 전송</li>
<li>프레임 단위로 데이터를 전송</li>
<li>CSMA/CD(충돌 감지) 방식 사용</li>
<li>스위치를 사용하여 네트워크를 효율적으로 관리</li>
</ul>
<p>CSMA/CD
네트워크에서 충돌을 방지하는 방식
CS: 데이터를 보내기 전에 네트워크가 사용 중인지 확인 (Carrier Sense)
MA: 여러 장치가 네트워크를 공유하며 데이터를 전송 (Multiple Access)
CD: 충돌이 발생하면 잠시 기다린 후 데이터를 재전송 (Collision Detection)
-&gt; 하지만 현재는 스위치를 사용하여 충돌이 거의 발생하지 않음</p>
<h2 id="스위치-switch">스위치 (Switch)</h2>
<p>네트워크에서 장치 간 데이터를 효율적으로 전달하는 장비
MAC 주소를 기반으로 프레임을 전달하며, 허브와 다르게 충돌 없이 통신 가능 (허브 대체)
네트워크 트래픽을 최적화하고 충돌을 방지
현대 이더넷 네트워크에서 필수적인 장비로, 대부분의 네트워크에서 사용됨</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>스위치(Switch)</th>
<th>허브(Hub)</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 전달 방식</td>
<td>MAC 주소 확인 후 해당 포트로만 전달</td>
<td>모든 포트로 브로드캐스트</td>
</tr>
<tr>
<td>충돌 발생 여부</td>
<td>없음</td>
<td>있음</td>
</tr>
<tr>
<td>네트워크 효율성</td>
<td>트래픽을 최적화하여 속도 빠름</td>
<td>불필요한 트래픽 증가로 속도 저하</td>
</tr>
<tr>
<td>사용 여부</td>
<td>현재 대부분의 네트워크에서 사용</td>
<td>거의 사용되지 않음</td>
</tr>
</tbody></table>
<p><strong>전송 과정</strong></p>
<ol>
<li>MAC  주소 학습
장치가 네트워크에 연결되면, 스위치는 해당 포트의 MAC 주소를 기억 (MAC 주소 테이블 생성)</li>
<li>프레임 수신 및 전달
프레임을 받으면 목적지 MAC 주소를 확인
MAC 주소 테이블에 해당 MAC 주소가 있으면 해당 포트로만 데이터 전달
만약 MAC 주소를 모르면 브로드캐스트(네트워크 전체 전송) 후 학습</li>
</ol>
<h2 id="mac-주소">MAC 주소</h2>
<p>네트워크 장치(컴퓨터, 스마트폰, 라우터 등)에 부여된 고유한 식별 주소
네트워크에서 데이터를 올바른 장치로 전달하기 위해 사용됨
하드웨어에 내장된 주소이므로 변경 불가능</p>
<p>구조: 
48비트(6바이트), 16진수 6쌍(12자리)
24비트는 제조회사 번호, 뒤의 24비트는 장치별 고유 식별자</p>
<p><strong>MAC 주소가 사용되는 상황</strong></p>
<ul>
<li>이더넷 통신 -&gt; 네트워크 내에서 데이터 프레임 전송</li>
<li>Wi-Fi 연결 -&gt; 무선 네트워크에서 장치 식별</li>
<li>네트워크 보안 (MAC 주소 필터링) -&gt; 특정 장치만 네트워크에 접근 허용</li>
<li>ARP -&gt; IP 주소를 MAC 주소로 변환</li>
</ul>
<h2 id="arp-address-resolution-protocol">ARP (Address Resolution Protocol)</h2>
<p>IP 주소를 MAC 주소로 변환하는 프로토콜
같은 네트워크(LAN) 내에서 장치 간 통신을 가능하게 함</p>
<p>필요한 이유</p>
<ul>
<li><p>네트워크에서 데이터를 전송할 때 IP 주소만으로는 장치를 직접 찾을 수 없음</p>
</li>
<li><p>이더넷, Wi-Fi같은 데이터링크 게층에서는 MAC 주소를 기반으로 통신</p>
</li>
<li><p>따라서, IP 주소 -&gt; MAC 주소변환 과정 (ARP)가 필요함</p>
</li>
</ul>
<p><strong>동작 과정</strong></p>
<ul>
<li>APR 요청
출발지 장치가 목적지 MAC 주소를 모르므로, 네트워크 전체에 브로드캐스트 전송</li>
<li>ARP 응답
목적지 장치가 MAC 주소 응답</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[네트워크 계층]]></title>
            <link>https://velog.io/@siha_014/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B3%84%EC%B8%B5</link>
            <guid>https://velog.io/@siha_014/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B3%84%EC%B8%B5</guid>
            <pubDate>Tue, 20 Jan 2026 04:00:13 GMT</pubDate>
            <description><![CDATA[<h1 id="네트워크-계층">네트워크 계층</h1>
<p>데이터를 네트워크를 통해 목적지까지 전달하는 역할을 수행
라우팅을 통해 최적으리 경로를 선택하고, IP 주소를 기반으로 패킷을 전달하는 것이 핵심 기능</p>
<p><strong>라우터(Router)</strong>
라우터는 네트워크 간에 데이터 패킷을 전달하고 최적의 경로를 선택하는 역할을 수행
라우터는 IP 주소를 기반으로 패킷을 목적지까지 전달하며, 여러 네트워크를 연결하는 핵심 장치</p>
<p>라우터 주요  기능</p>
<ul>
<li><p>네트워크 간 패킷 전달 (Routing)
서로 다른 네트워크 간에 데이터 패킷을 전달
라우터는 패킷의 목적지 IP 주소를 확인하고, 해당 패킷을 다음 네트워크로 전달</p>
</li>
<li><p>최적 경로 선택
패킷이 목적지까지 도달하기 위해 가장 효율적인 경로를 선택</p>
</li>
<li><p><em>라우팅 테이블*</em>을 이용하여 목적지 네트워크에 대한 정보를 관리하고, 최적 경로를 결정</p>
</li>
</ul>
<h2 id="ip-internet-protocol">IP (Internet Protocol)</h2>
<p>데이터를 목적지까지 전달하는 핵심 프로토콜
패킷을 전송하는 역할을 하지만, 신뢰성을 보장하지 않음 -&gt; TCP, UDP와 함께 사용되어 데이터 전송을 보장</p>
<h4 id="ipv4">IPv4</h4>
<ul>
<li>32비트 주소 체계</li>
<li>점(.)으로 구분된 4개의 10진수</li>
<li>패킷 헤더 크기: 가변적</li>
<li>보안 기능 없음</li>
</ul>
<h4 id="ipv6">IPv6</h4>
<ul>
<li>128비트 주소 체계</li>
<li>콜론(:)으로 구분된 8개의 16진수</li>
<li>패킷 헤더 크기: 고정적 (단순화됨)</li>
<li>보안 강화 (IPSec 기본 지원)</li>
</ul>
<h3 id="ip의-계층화">IP의 계층화</h3>
<p>IP 주소는 계층적인 구조를 가지고 있으며, 네트워크를 효율적으로 관리하고 라우팅을 최적화하기 위해 설계됨
IP 주소 계층화를 통해 대규모 네트워크를 작은 네트워크로 나누고, 라우팅을 간소화하며, 주소 낭비를 최소화할 수 있음</p>
<h4 id="ip-주소의-계층적-구조">IP 주소의 계층적 구조</h4>
<ul>
<li>IP 주소는 네트워크 부분(network)과 호스트 부분(host)으로 나뉨
네트워크 부분: 해당 IP가 속한 네트워크를 식별
호스트 부분 : 네트워크 내 개별 장치를 식별</li>
<li>서브넷 마스크
IP 주소에서 네트워크 부분과 호스트 부분을 구분하는 값
32비트 값으로 표현
네트워크 부분의 개수를 사용하여 /20, /24 등의 형식으로도 표현됨
서브넷: 동일 네트워크 부분에 속한 디바이스 집합
(라우터는 여러 서브넷에 속해서 여러개의 IP를 가질 수 있음)</li>
</ul>
<h2 id="nat-network-address-translation">NAT (Network Address Translation)</h2>
<p>: 네트워크 주소 변환
사설 IP를 공인 IP로 변환하여 인터넷과 통신할 수 있도록 하는 기술</p>
<h4 id="nat의-필요성">NAT의 필요성</h4>
<ul>
<li><p>IPv4 주소 부족 문제 해결
공인 IP 주소는 한정적이므로 모든 기기에 공인 IP를 할당할 수 없음
NAT를 사용하면 하나의 공인 IP를 여러 기기가 공유할 수 있음</p>
</li>
<li><p>보안 강화
내부 네트워크(사설 IP)가 직접 인터넷에 노출되지 않음
외부에서 직접 내부 네트워크 접근이 불가능하여 보안이 강화됨</p>
</li>
</ul>
<h4 id="기본-동작-원리">기본 동작 원리</h4>
<ul>
<li>내부 네트워크(사설 IP) -&gt; 공인 IP로 변환 -&gt; 인터넷 통신 가능</li>
<li>반대로 인터넷에서 받은 응답을 공인 IP에서 내부 네트워크(사설 IP)로 변환하여 전달</li>
</ul>
<h2 id="dhcp-dynamic-host-configuration-protocol">DHCP (Dynamic Host Configuration Protocol)</h2>
<p>네트워크에서 장치에 IP 주소를 자동으로 할당하는 프로토콜</p>
<p>필요한 이유: 
네트워크에 연결된 모든 장치는 IP 주소가 필요함
수동으로 IP를 설정하면 충돌 문제, 관리 어려움, 변경 필요 등의 불편함 발생</p>
<p>역할</p>
<ul>
<li>사용자 개입 없이 장치가 IP를 받을 수 있음</li>
<li>네트워크 설정 자동화
-&gt; 서브넷 마스크, 게이트웨이, DNS 서버 정보도 자동 제공</li>
</ul>
<p>동작 과정</p>
<ol>
<li><p>DHCP DISCOVER (검색)
클라이언트 -&gt; 브로트캐스트로 DHCP 서버를 찾음</p>
</li>
<li><p>DHCP OFFER (제안)
DHCP 서버 -&gt; 클라이언트에게 사용 가능한 IP 주소를 제안</p>
</li>
<li><p>DHCP REQUEST (요청)
클라이언트 DHCP 서버에 특정 IP 주소를 요청</p>
</li>
<li><p>DHCP ACK (승인)
DHCP 서버 -&gt; 클라이언트의 요청을 승인하고 IP를 할당</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[전송 계층]]></title>
            <link>https://velog.io/@siha_014/%EC%A0%84%EC%86%A1-%EA%B3%84%EC%B8%B5</link>
            <guid>https://velog.io/@siha_014/%EC%A0%84%EC%86%A1-%EA%B3%84%EC%B8%B5</guid>
            <pubDate>Mon, 19 Jan 2026 13:30:06 GMT</pubDate>
            <description><![CDATA[<h1 id="전송-계층">전송 계층</h1>
<p>네트워크 통신에서 송신지와 수신자 간의 데이터 전송을 관리하고, <strong>신뢰성</strong>과 정확성을 보장하는 역할 수행
TCP/IP 4계층 모델과 OSI 7계층 모델에서 공통적으로 정의되며, 데이터가 손실 없이 정확히 전달되도록 다양한 메커니즘을 제공함</p>
<h2 id="tcp--udp">TCP &amp; UDP</h2>
<h3 id="tcp-transmission-control-protocol">TCP (Transmission Control Protocol)</h3>
<p>신뢰성 있는 연결, 데이터 순서 보장, 흐름/오류 제어
웹 브라우징(HTTP), 파일 전송 (FTP)</p>
<h3 id="udp-user-datagram-protocol">UDP (User Datagram Protocol)</h3>
<p>비연결형, 신뢰성 낮음, 빠른 데이터 전송
동영상 스트리밍, 온라인 게임</p>
<table>
<thead>
<tr>
<th>특징</th>
<th>TCP</th>
<th>UDP</th>
</tr>
</thead>
<tbody><tr>
<td>연결 방식</td>
<td><strong>연결 지향적</strong>: 3-way handshake</td>
<td><strong>비연결 지향적</strong>: 연결 설정 없이 데이터 전송</td>
</tr>
<tr>
<td>신뢰성</td>
<td>데이터 손실, 중복, 순서 오류 방지</td>
<td>신뢰성 보장하지 않음</td>
</tr>
<tr>
<td>속도</td>
<td>느림(연결 설정, 오류 제어로 인한 오버헤드)</td>
<td>빠름(단순 데이터 전송)</td>
</tr>
<tr>
<td>오류 제어</td>
<td>데이터 전송 중 오류 감지 및 복구</td>
<td>오류 제어 없음</td>
</tr>
<tr>
<td>순서 보장</td>
<td>데이터 순서 보장 (시퀀스 번호 사용)</td>
<td>보장 X</td>
</tr>
<tr>
<td>헤더 크기</td>
<td>20~60바이트 (복잡한 구조)</td>
<td>8바이트 (간단한 구조)</td>
</tr>
<tr>
<td>흐름 제어</td>
<td>수신자의 데이터 처리 속도에 맞게 흐름 제어</td>
<td>흐름 제어 없음</td>
</tr>
<tr>
<td>사용 사례</td>
<td>신뢰성이 중요한 애플리케이션</td>
<td>속도가 중요한 애플리케이션</td>
</tr>
</tbody></table>
<h3 id="tcp-커넥션-생성">TCP 커넥션 생성</h3>
<h4 id="3-way-handshake">3-Way Handshake</h4>
<p>TCP 프로토콜에서 송신자와 수신자 간 신뢰성 있는 연결을 설정하기 위한 과정을 말함
데이터 전송 전에 양측이 통신 준비가 되었는지 확인하고, 초기 연결 상태를 설정함으로써 데이터 전송의 신뢰성을 보장함</p>
<p><strong>3-Way Handshake 과정</strong></p>
<ul>
<li><p>SYN (Synchronize)
송신자(클라이언트)는 연결을 요청하기 위해 수신자(서버)에게 SYN 패킷을 전송
내용: 클라이언트의 초기 시퀀스 번호</p>
</li>
<li><p>SYN-ACK (Synchronize-Acknowledge)
서버는 연결 요청을 받고, 클라이언트의 요청을 승인하는 ACK와 함께 자신의 연결 요청인 SYN을 포함하여 응답
내용: 클라이언트의 시퀀스 번호에 대한 ACK + 서버의 초기 시퀀스 번호</p>
</li>
<li><p>ACK (Acknowledge)
클라이언트는 서버의 응답을 확인한 뒤, 최종적으로 ACK 패킷을 보내 연결이 완료되었음을 알림
내용: 서버의 시퀀스 번호에 대한 ACK</p>
</li>
<li><p>이후 송신자, 수신자 사이에 커넥션 생성</p>
</li>
</ul>
<h3 id="tcp-커넥션-해제">TCP 커넥션 해제</h3>
<h4 id="4-way-handshake">4-Way Handshake</h4>
<p>TCP 프로토콜에서 송신자와 수신자 간의 연결을 종료하기 위한 과정
데이터 전송이 완료된 후, 양측이 연결을 종료하기 위해 FIN(종료) 패킷을 교환하며, 네트워크 자원을 해제하고 연결을 정리함</p>
<p><strong>4-Way Handshake 과정</strong></p>
<ul>
<li><p>FIN (Connection Termination Request)
송신자가 연결 종료를 요청하며 FIN 패킷을 보냄</p>
</li>
<li><p>ACK(Acknowledge for FIN)
수신자가 FIN 패킷을 받고 이를 확인했다는 ACK 패킷을 송신자에게 보냄</p>
</li>
<li><p>FIN (Receiver&#39;s Termination Request)
수신자도 연결 종료를 요청하며 FIN 패킷을 송신자에게 보냄</p>
</li>
<li><p>ACK (Acknowledge for Receiver&#39;s FIN)
송신자가 수신자의 FIN 패킷을 받고, 종료를 확인하는 ACK 패킷을 수신자에게 보냄</p>
</li>
<li><p>이후 연결이 완전히 종료됨</p>
</li>
</ul>
<h2 id="rdt-reliable-data-transfer">RDT (Reliable Data Transfer)</h2>
<p>신뢰성 있는 데이터 전송을 위한 프로토콜을 설명하는 개념적인 모델,
데이터를 송신 측에서 수신 측으로 오류, 손실 없이 순서대로 전달하기 위해 설계</p>
<ul>
<li><p>RDT 1.0
: 전송되는 모든 데이터가 유실 없고 에러도 없는 이상적인 상황
데이터 전송 중 오류가 발생하지 않는 완벽한 환경에서 동작
송신자는 데이터를 전송하고, 수신자는 이를 문제없이 받음
문제점: 오류 검출, 복구, ACK 등이 필요 없음, 이상적인 환경에만 적용 가능</p>
</li>
<li><p>RDT 2.0
: 전송되는 데이터가 유실은 없으나 에러가 생길 수 있음
에러가 발생할 수 있는 환경에서 동작
데이터 손상 여부를 감지하기 위해 체크섬과 같은 오류 검출 메커니즘 사용
수신 측은 데이터를 수신한 후:
ACK: 데이터가 제대로 수신되었음을 확인
NAK: 데이터가 손상되었음을 송신 측에 알림 -&gt; 송신 측은 데이터를 재전송
문제점: ACK/NAK 패킷이 손실되거나 손상되면 문제가 발생할 수 있음</p>
</li>
<li><p>RDT 2.1
: 만약 리시버가 보내는 ACK, NAK에 에러가 생긴다면?
sender가 응답으로 에러 패킷을 받으면 이게 ACK인지 NAK인지 구별 불가능
그래서 sender는 무조건 보낸 데이터를 재전송함
문제는 receiver가 새 데이터인지 중복 데이터인지 구별 불가
구별을 위해 데이터 패킷의 헤더에 시퀀스 번호 추가
이제 receiver는 시퀀스 번호를 통해 중복 데이터를 감지하고 처리 (시퀀스 넘버 확인후 순서 맞으면 받고 안 맞으면 버림)</p>
</li>
<li><p>RDT 2.2
: NAK 없애고 ACK만으로 동작하게
시퀀스 번호가 추가되니 NAK가 불필요
MAK를 제거하고, ACK만을 사용하여 신뢰성 보장
receiver는 에러 데이터의 경우 ACK를 보내되 같은 시퀀스 번호로 전송
sender는 시퀀스 번호를 보고 다음 데이터를 보낼지 동일 데이터를 다시 보낼지 결정</p>
</li>
<li><p><em>즉, 시퀀스 번호만으로 NAK, ACK를 판단*</em></p>
</li>
<li><p>RDT 3.0
: 전송되는 데이터가 에러, 유실 둘 다 발생할 수 있음
데이터 유실이 발생하는 경우를 대비하여 sender는 receiver로부터 일정 시간 응답을 받지 못하면 데이터를 재전송
그래서 sender는 타이머를 사용하게 됨
타이머의 시간을 잘 설정하는 것이 중요
시간이 짧으면 유실이 발생하지 않았는데도 유실로 판단할 수 있고, 길면 유실 발생 시 복구가 느림</p>
</li>
</ul>
<h3 id="gbn-selective-repeat">GBN, Selective Repeat</h3>
<h4 id="gbn-go-back-n">GBN (Go-Back-N)</h4>
<p>RDT 3.0의 구현 방식 중 하나
sender는 N개의 데이터 패킷(슬라이딩 윈도우)을 동시에 전송할 수 있음
하지만, 수신 측에서 오류가 발생한 패킷 이후의 모든 패킷을 무효화하고 해당 패킷부터 다시 전송함
<strong>특징</strong></p>
<ul>
<li>단순함: 손실된 패킷 이후의 모든 데이터를 다시 전송하기 때문에 구현이 간단함</li>
<li>낭비: 손실된 하나의 패킷 때문에 이후의 모든 패킷이 다시 전송되므로 네트워크 대역폭 낭비될 수 있음</li>
</ul>
<p><strong>동작 방식</strong>
sender는 N개의 패킷을 전송하고, 각 패킷에 대해 ACK를 기다림
ACK가 도착하지 않으면, 타임아웃 발생 후 해당 패킷부터 다시 전송</p>
<h4 id="selective-repeat">Selective Repeat</h4>
<p>RDT 3.0의 구현 방식 중 하나 
sender는 N개의 데이터 패킷을 동시에 전송할 수 있음
<strong>손실되거나 오류가 발생한 패킷만 선택적으로 재전송함</strong></p>
<p><strong>특징</strong></p>
<ul>
<li>효율적: 손실된 패킷만 다시 전송하므로 네트워크 대역폭을 절약</li>
<li>복잡함: 각 패킷을 개별적으로 확인하고 저장해야 하므로 구현이 더 복잡</li>
</ul>
<p><strong>동작 방식</strong>
sender는 각 패킷에 대해 개별적으로 ACK를 확인함
손실된 패킷만 재전송하고, receiver는 버퍼를 사용하여 순서가 맞지 않는 패킷을 저장했다가 재조립</p>
<h2 id="tcp의-rdt">TCP의 RDT</h2>
<p>TCP는 RDT 3.0 기반의 신뢰성 있는 데이터 전송 프로토콜</p>
<ul>
<li><p><strong>오류 감지 및 복구</strong></p>
<ul>
<li>체크섬: 데이터가 손상되었는지 감지</li>
<li>ACK: receiver가 데이터를 정상적으로 받았음을 sender에게 알림
이때 시퀀스 넘버는 해당 넘버 이전 데이터까지 잘 받았으니 이 넘버부터 데이터를 보내라는 의미
예: ACK = N: N번 이전까지 잘 받았으니 N번부터 보내라</li>
<li>재전송: 오류가 발생한 패킷을 재전송</li>
</ul>
</li>
<li><p><strong>데이터 손실 및 재전송</strong></p>
<ul>
<li>타이머 기반 재전송
sender는 일정 시간 내에 ACCK를 받지 못하면 데이터가 손실된 것으로 간주하고 재전송</li>
<li>빠른 재전송 (Fast Retransmit)
3번 이상의 중복 ACK를 받으면, 타이머를 기다리지 않고 즉시 재전송</li>
</ul>
</li>
<li><p><strong>순서 보장</strong></p>
<ul>
<li>시퀀스 번호&quot; 패킷이 순서대로 도챡했는지 확인
(순서가 어긋난 패킷은 receiver 측 버퍼에 저장 후 올바른 순서대로 조립)</li>
</ul>
</li>
</ul>
<h2 id="tcp의-흐름제어">TCP의 흐름제어</h2>
<p><strong>흐름 제어(Flow Control)</strong>
송신자가 수신자의 데이터 처리 속도에 맞춰 데이터 전송 속도를 조절하는 메커니즘
수신자가 데이터를 너무 빠르게 받으면 버퍼가 가득 차고 데이터가 손실될 수 있기 때문에, TCP는 흐름 제어를 통해 데이터 전송을 조절</p>
<p><strong>방법</strong>
TCP는 윈도우 크기를 조절하여 흐름 제어를 수행
윈도우 크기는 수신자가 현재 받을 수 있는 데이터 크기를 의미하며, 송신자는 이 크기를 초과하지 않도록 데이터를 보냄</p>
<p><strong>Zero Window</strong> 문제
숫니자의 버퍼가 가득 차면 윈도우 크기를 0으로 설정하여 더 이상 데이터를 받을 수 없음을 송신자에게 알림
송신자는 새로운 윈도우 크기를 받을 때까지 데이터 전송을 멈춤
하지만, 만약 이 정보(윈도우 크기 0)가 네트워크에서 손실되면 송신자는 무한 대기에 빠질 수 있음
이를 방지하기 위해 TCP는 주기적으로 &quot;윈도우 크기 확인 패킷&quot;을 전송하여 수신자의 상태를 확인함</p>
<h2 id="tcp의-혼잡-제어">TCP의 혼잡 제어</h2>
<p><strong>혼잡 제어(Congestion Control)</strong>
네트워크의 트래픽 과부하를 방지하기 위해 송신자가 데이터 전송 속도를 조절하는 기법
한 사용자가 너무 많은 데이터를 보내면 다른 사용자에게 불이익이 발생할 수 있으므로 혼잡 제어를 통해 공정하게 네트워크를 사용할 수 있도록 함</p>
<h3 id="혼잡-제어-메커니즘">혼잡 제어 메커니즘</h3>
<ol>
<li><p>Slow Start
초기 패킷 전송량을 매우 작게 설정하고 2배씩 늘려가면서 네트워크 상태를 점검</p>
</li>
<li><p>Congestion Avoidance
패킷 전송량이 일정 threshold에 도달하면 증가 속도를 선형 증가로 변경</p>
</li>
<li><p>Fast Recovery 
패킷 손실이 발생했을 경우 대처</p>
<ul>
<li>동일 ACK를 3번 받은 경우(일부 패킷 손실)
네트워크 과부하 직전으로 판단 =&gt; Congestion Avoidance부터 재시작
threshold는 현재 전송량의 절반 값으로 설정</li>
<li>타이머에 의한 타임 아웃
네트워크 과부하 상황으로 판단 =&gt; Slow Start부터 재시작
threshold는 현재 전송량의 절반 값으로 설정</li>
</ul>
</li>
</ol>
]]></description>
        </item>
    </channel>
</rss>