<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>zayn_noh.dev</title>
        <link>https://velog.io/</link>
        <description>개발 및 IT기술에 대해 정리하고 기록합니다.</description>
        <lastBuildDate>Thu, 02 Jul 2026 07:34:19 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>zayn_noh.dev</title>
            <url>https://velog.velcdn.com/images/zayn_noh/profile/736934a5-c05c-4fa4-a58b-b7dfc256b494/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. zayn_noh.dev. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/zayn_noh" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[테스트 커버리지는 개발자의 수치다]]></title>
            <link>https://velog.io/@zayn_noh/TestCoverage</link>
            <guid>https://velog.io/@zayn_noh/TestCoverage</guid>
            <pubDate>Thu, 02 Jul 2026 07:34:19 GMT</pubDate>
            <description><![CDATA[<p>이번 포스트에서는 테스트 코드에 대해 배우고 이를 적용하는 과정에서 겪은 경험을 바탕으로</p>
<p><strong>테스트 코드(Test Code)</strong>와 <strong>테스트 커버리지(Test Coverage)</strong>에 대해 다루며</p>
<p>최종적으론 좋은 테스트 코드를 위한 최적의 테스트 커버리지에 대해 개인적인 생각을 정리하여 보았다.</p>
<hr>
<h3 id="1-목표-설정-단계--테스트-코드-작성-전">1. 목표 설정 단계 : 테스트 코드 작성 전</h3>
<p>테스트 코드를 적용하기에 앞서, 이번 과정에서는 단순히 테스트 코드를 작성한다기 보다
테스트 코드가 가지는 본질적인 의미를 함께 고민해보고 싶었다.</p>
<p>사실 아직 테스트 코드에 대해 잘 알고 있다고 말하기 어렵고,
다른 사람들에 비해 많은 시간을 들여 테스트 코드를 작성해본 경험도 부족할 것이다.</p>
<p>그럼에도 지금까지 학습하며 느낀 테스트 코드의 가장 큰 장점은 <strong>TDD</strong>의 흐름 속에서 잘 드러난다고 생각했다.</p>
<p>TDD에서는 <code>Red-Green-Refactoring</code>이라는 사이클을 반복하며 코드를 구현해나간다.
먼저 실패하는 테스트를 작성하고, 그 테스트를 통과하는 최소한의 코드를 구현한 뒤, 다시 코드를 개선한다.</p>
<p>이 과정에서 테스트 코드는 단순히 코드가 정상적으로 동작하는지 확인하는 도구를 넘어,
<strong>구현해야 할 기능을 설명하는 명세처럼 동작</strong>한다.</p>
<p>간단한 예제를 통해서도 이 점은 꽤 쉽게 체감할 수 있었다.</p>
<p>테스트 코드가 왜 중요한지, 왜 살아있는 문서라고 불리는지,
그리고 왜 테스트 코드의 명세가 개발을 이끌어가는 TDD라는 방식이 등장했는지 조금은 이해할 수 있었다.</p>
<p>물론 이번 테스트 코드 적용 과정은 엄밀한 의미의 TDD라고 보기는 어렵다.
이미 프로덕트 코드가 작성되어 있는 상태에서 테스트 코드를 추가하는 작업이었기 때문이다.</p>
<p>그럼에도 테스트 코드가 단순히 기존 코드를 따라가는 방식이 되지 않도록 하고 싶었다.
가능하다면 테스트 코드가 요구사항을 먼저 표현하고,
그 테스트를 통해 프로덕트 코드를 점검하는 흐름을 흉내 내보고자 했다.</p>
<p>이를 위해 스스로 다음과 같은 테스트 코드 작성 규칙을 정했다.</p>
<ul>
<li><strong>테스트 코드 작성 시에는 프로덕트 코드를 별도로 살펴보지 않는다.</strong></li>
<li><strong>테스트 코드 작성 시에는 기획 요구사항과 API 시트를 기반으로 작성한다.</strong></li>
</ul>
<p>즉, 프로덕트 코드의 구현을 기준으로 테스트를 작성하기보다는,
요구사항을 기준으로 테스트할 명세를 먼저 뽑아내고자 했다.</p>
<p>이번 테스트 코드 적용의 개인적인 목표는 바로 이 명세를 테스트 코드로 표현하고,
이를 통해 기존 프로덕트 코드가 요구사항을 제대로 만족하고 있는지 확인하는 것이었다.</p>
<hr>
<h3 id="2-테스트-커버리지-설정--테스트-코드-작성-전">2. 테스트 커버리지 설정 : 테스트 코드 작성 전</h3>
<p>테스트 코드 작성에 앞서 가장 먼저 고민한 것은 <code>테스트 커버리지 목표</code>였다.</p>
<p>처음에는 테스트 커버리지의 목표를 정한다는 말이 잘 와닿지 않았다.
단순히 생각하면 모든 프로덕트 코드를 테스트할 수 있다면 당연히 좋은 것 아닌가 싶었다.</p>
<p>테스트하지 않은 코드가 적을수록 더 안전한 코드처럼 느껴졌기 때문이다.</p>
<p>하지만 커버리지 목표를 정하기 전에 테스트 전략과 관련된 글들을 살펴보며,
단순히 높은 커버리지만을 목표로 삼는 것이 좋은 테스트 전략은 아닐 수 있다는 생각을 하게 되었다.</p>
<p>특히 토스 기술 블로그의 <a href="https://toss.tech/article/test-strategy-server">가치있는 테스트를 위한 전략과 구현</a> 글에서 이 부분에 대한 힌트를 얻을 수 있었다.</p>
<p>해당 글의 핵심은 모든 코드를 무조건 테스트하는 것이 아니라, <strong>비즈니스 가치</strong>와 <strong>유지보수 비용</strong>을 함께 고려하여
<strong>가치 있는 테스트</strong>를 선택적으로 작성해야 한다는 것이었다.</p>
<p>좋은 테스트 전략은 이상적인 원칙이나 숫자만으로 정해지는 것이 아니라,
시스템의 특성과 팀의 상황에 맞게 비용과 가치를 판단하는 데서 출발한다는 내용이었다.</p>
<p>물론 글을 읽었다고 해서 곧바로 체감이 되지는 않았다.</p>
<blockquote>
<p><strong>“과한 테스트 코드는 유지보수 비용을 높일 수 있으니 의미 있는 테스트를 작성해야 한다”</strong></p>
</blockquote>
<p>말은 이해할 수 있었지만 당장의 나는 어떤 테스트가 의미 있는 테스트인지
어떤 테스트가 비즈니스 가치를 가진 테스트인지 명확하게 구분하기 어려웠다.</p>
<p>그래서 조금은 급작스럽지만 직접 부딪혀보기로 했다.
우선 User 도메인의 테스트 커버리지 목표를 <strong><code>100%</code></strong>로 설정하였다.</p>
<p>이는 테스트 커버리지 100%가 항상 정답이라고 생각했기 때문은 아니었다.
오히려 높은 커버리지를 목표로 테스트를 작성해보면, 어떤 테스트는 실제로 의미가 있고
어떤 테스트는 단순히 숫자를 채우기 위한 테스트처럼 느껴지는지 직접 경험할 수 있을 것이라 생각했다.</p>
<p>추가로는 테스트 코드를 적용할 커뮤니티 시스템에서 User 도메인은 회원가입, 로그인,
프로필 수정, 비밀번호 변경, 권한, 탈퇴 등 서비스의 핵심 흐름과 밀접하게 연결되어 있다고 생각했다.</p>
<p>따라서 User 도메인은 그 자체로 테스트할 가치가 큰 대상이라고 판단했고,
이 도메인에 대해서는 가능한 한 빠짐없이 테스트해보는 것이 의미 있다고 생각했다.</p>
<p>결과적으로 테스트 코드 작성 전 내가 세운 목표는 다음과 같았다.</p>
<ul>
<li><strong>User 도메인과 관련된 주요 기능을 테스트 대상으로 삼는다.</strong></li>
<li><strong>가능한 한 테스트 커버리지 <code>100%</code>를 목표로 한다.</strong></li>
<li><strong>단순히 프로덕트 코드를 실행시키는 테스트가 아니라, 요구사항을 검증하는 테스트를 작성한다.</strong></li>
</ul>
<p>이때까지만 해도 테스트 커버리지 <code>100%</code>라는 수치가 꽤 명확한 목표처럼 느껴졌다.</p>
<p>또한 테스트 커버리지의 의미를 확실히 알고 내가 세운 목표에 적용했다 판단하였다.</p>
<blockquote>
<p>하지만 실제로 테스트 코드를 작성하는 과정에서 조금씩 이상함을 느끼기 시작했다.
처음에는 테스트 커버리지 <code>100%</code>를 목표로 잡았지만,
내가 실제로 하려던 일은 프로덕트 코드의 모든 구문을 실행시키는 것이 아니었다.
기획 요구사항과 API 시트를 바탕으로 User 도메인의 명세를 먼저 정리하고,
그 명세가 테스트 코드로 제대로 표현되는지를 확인하는 것이었다.</p>
<p>즉, 내가 목표로 삼은 것은 구문 커버리지 100%에 가까운 테스트라기보다,
User 도메인에 대한 <strong>명세 커버리지 100%</strong>에 가까웠다.</p>
</blockquote>
<p>이 차이를 명확히 구분하지 못한 상태에서 테스트 커버리지라는 수치를 목표로 세우다 보니,
테스트 코드를 작성하는 과정에서 점차 어색함을 느끼게 되었다.</p>
<hr>
<h3 id="3-테스트-코드를-통해-얻은-버그-발견-경험">3. 테스트 코드를 통해 얻은 버그 발견 경험</h3>
<p>테스트 코드를 작성하기에 앞서 기획 문서와 전체 API를 살펴보며,
User 도메인에 어떤 명세가 존재하는지 먼저 추출하는 과정을 거쳤다.</p>
<p>이후 추출한 명세를 바탕으로 각 명세마다 하나씩 테스트 코드를 작성하였다.</p>
<p>이 과정에서 테스트 코드의 가장 기본적인 장점 중 하나인,
요구사항과 실제 구현 사이의 어긋남을 발견하는 경험을 할 수 있었다.</p>
<pre><code class="language-java">@Test
void 회원가입_시_사용자_이미지를_입력하지_않아도_된다() {
    JoinRequestDTO request = joinRequest(
            &quot;test@test.com&quot;,
            &quot;tester&quot;,
            &quot;Password1!&quot;,
            &quot;Password1!&quot;,
            null
    );

    assertThat(validator.validate(request)).noneMatch(v -&gt; v.getPropertyPath().toString().equals(&quot;userImage&quot;));
}</code></pre>
<p>위 테스트는 다음의 명세를 바탕으로 작성한 테스트 코드이다.
<strong>“회원가입을 요청하는 사용자는 이미지를 입력하지 않아도 회원가입할 수 있어야 한다”</strong></p>
<p>하지만 테스트 실행 결과, 다음과 같은 에러가 발생하였다.</p>
<pre><code class="language-bash">Expecting no elements of:
  [ConstraintViolationImpl{interpolatedMessage=&#39;user_image_required&#39;, propertyPath=userImage, rootBeanClass=class com.example.community.dto.JoinRequestDTO, messageTemplate=&#39;user_image_required&#39;}]
to match given predicate but this element did:
  ConstraintViolationImpl{interpolatedMessage=&#39;user_image_required&#39;, propertyPath=userImage, rootBeanClass=class com.example.community.dto.JoinRequestDTO, messageTemplate=&#39;user_image_required&#39;}
java.lang.AssertionError: 

...</code></pre>
<p>에러 내용을 통해 JoinRequestDTO 내부에서 사용자 이미지와 관련된 검증이 잘못 동작하고 있음을 확인할 수 있었다.
실제로 DTO를 확인해보니, 사용자 이미지 필드에 @NotBlank 어노테이션이 붙어 있었다.</p>
<pre><code class="language-java">@JsonProperty(&quot;user_image&quot;)
@NotBlank(message = &quot;user_image_required&quot;)
private String userImage;</code></pre>
<p>하지만 기획 요구사항에 따르면 사용자 이미지는 필수 입력값이 아니었다.
사용자가 이미지를 입력하지 않은 경우에는 기본 이미지가 설정되어야 했다.</p>
<p>따라서 해당 검증은 요구사항과 맞지 않는 검증이었고, @NotBlank 어노테이션을 제거하였다.</p>
<p>여기서 한 가지 의문이 더 생겼다.
<strong>이미지 값이 null로 들어왔을 때, 실제로 기본 이미지 경로가 저장되고 있을까?</strong>
이를 확인하기 위해 다음 테스트 코드를 추가로 작성하였다.</p>
<pre><code class="language-java">@Test
void 사용자_이미지를_입력하지_않고_회원가입하면_기본_이미지가_저장된다() {
    JoinRequestDTO request = joinRequest(&quot;test@test.com&quot;, &quot;tester&quot;, &quot;Password1!&quot;, &quot;Password1!&quot;, null);

    when(passwordEncoder.encode(&quot;Password1!&quot;)).thenReturn(&quot;hashed-password&quot;);
    when(userRepository.save(any(User.class))).thenAnswer(invocation -&gt; invocation.getArgument(0));

    userService.joinProcess(request);

    ArgumentCaptor&lt;User&gt; captor = ArgumentCaptor.forClass(User.class);
    verify(userRepository).save(captor.capture());

    assertThat(captor.getValue().getImage()).isNotBlank();
}</code></pre>
<p>이번 테스트 역시 바로 통과하지 못했다.
사용자 기본 이미지에 대한 프로덕트 코드가 제대로 작성되어 있지 않았던 것이다.</p>
<p>테스트 실패 원인을 확인하기 위해 joinProcess 이후 생성되는 User 엔티티를 살펴보았다.</p>
<p>당시 User 생성자는 다음과 같이 작성되어 있었다.</p>
<pre><code class="language-java">public User(String email, String password, String nickname, String image) {
    this.email = email;
    this.password = password;
    this.nickname = nickname;
    this.image = image;
    this.deletedAt = null;
    this.role = UserRole.USER.getRole();
}</code></pre>
<p>생성자에서는 전달받은 image 값을 그대로 엔티티의 상태로 할당하고 있었다.</p>
<p>따라서 이미지 값이 null로 전달되면 기본 이미지 경로가 설정되지 않고,
그대로 null이 저장되는 문제가 발생했다.</p>
<p>이를 해결하기 위해 생성자 내부에 이미지 값이 없는 경우 기본 이미지 경로를 설정하는 분기를 추가하였다.</p>
<pre><code class="language-java">public User(String email, String password, String nickname, String image) {
    this.email = email;
    this.password = password;
    this.nickname = nickname;
    this.deletedAt = null;
    this.role = UserRole.USER.getRole();

    if (image != null) {
        this.image = image;
        return;
    }
    this.image = &quot;/images/default-profile.png&quot;;
}</code></pre>
<p>수정 후 테스트를 다시 실행하였고,
사용자가 이미지를 입력하지 않아도 기본 이미지 경로가 정상적으로 저장되는 것을 확인할 수 있었다.</p>
<p>이 경험을 통해 테스트 코드는 단순히 코드가 실행되는지 확인하는 도구가 아니라,
<strong>요구사항과 실제 구현이 일치하는지</strong>를 드러내는 장치라는 점을 체감할 수 있었다.</p>
<p>특히 프로덕트 코드를 기준으로 테스트를 작성한 것이 아니라, 요구사항에서 추출한
명세를 기준으로 테스트를 작성했기 때문에 기존 구현에서 놓치고 있던 문제를 발견할 수 있었다.</p>
<hr>
<h3 id="4-테스트-커버리지-올리기">4. 테스트 커버리지 올리기</h3>
<p>전체 테스트 코드를 작성한 뒤, 다시 한 번 테스트 커버리지에 대해 생각해보게 되었다.</p>
<p>앞서 기획 요구사항과 API 시트를 바탕으로 User 도메인의 명세를 추출했고,
각 명세에 대응하는 테스트 코드를 작성했다. 이후 작성한 테스트들도 모두 통과하였다.</p>
<p>그렇다면 이 상태를 두고 테스트 커버리지 <code>100%</code>를 달성했다고 말할 수 있을까?
내가 작성한 테스트는 User 도메인의 명세를 기준으로 했다.</p>
<p>따라서 내가 달성했다고 볼 수 있는 것은,
적어도 내가 추출한 명세에 대한 테스트 커버리지 <code>100%</code>에 가까웠다.</p>
<p>하지만 처음 목표로 설정했던 테스트 커버리지 <code>100%</code>는 일반적으로 구문 커버리지를 의미하는 경우가 많다.</p>
<p>구문 커버리지는 테스트 실행 과정에서 프로덕트 코드의 구문이 얼마나 실행되었는지를 보여주는 지표이다.
즉, 내가 작성한 명세 기반 테스트가 요구사항을 얼마나 검증했는지와는 <strong>다른 관점의 수치</strong>였다.</p>
<p>결국 일반적인 의미의 테스트 커버리지 <code>100%</code>와,
내가 목표로 했던 명세 커버리지 <code>100%</code>는 서로 다른 의미를 가지고 있었다.</p>
<p>따라서 내가 작성한 테스트 코드들이 실제 프로덕트 코드의 어느 부분을 실행하고 있는지 확인해보고 싶었다.
이를 위해 테스트를 실행한 뒤 커버리지를 측정했고,
User 도메인의 핵심 로직과 관련된 <code>UserService</code>와 <code>AuthService</code>를 중심으로 살펴보았다.</p>
<p>처음 측정한 결과는 다음과 같았다.</p>
<ul>
<li><code>AuthService</code>: 메서드 커버리지 50%, 구문 커버리지 48%</li>
<li><code>UserService</code>: 메서드 커버리지 90%, 구문 커버리지 87%</li>
</ul>
<p>명세를 기준으로 테스트를 작성했다고 생각했지만,
실제 구문 커버리지에는 아직 테스트를 거치지 않은 코드가 남아 있었다.</p>
<p>어떤 부분이 테스트되지 않았는지 확인해보니,</p>
<ul>
<li><code>AuthService</code>에서는 사용자 로그아웃 관련 로직이,</li>
<li><code>UserService</code>에서는 사용자 회원 탈퇴 관련 로직이 테스트되지 않고 있었다.</li>
</ul>
<p>이후 해당 기능들에 대한 명세를 다시 정리하고, 각각의 테스트 코드를 추가로 작성하였다.</p>
<p>회원 탈퇴의 경우 존재하지 않는 사용자는 탈퇴할 수 없다, 이미 탈퇴한 사용자는 다시 탈퇴할 수 없다,
탈퇴 성공 시 리프레시 토큰을 삭제하고 사용자를 탈퇴 처리를 진행한다를 테스트하였다.</p>
<p>로그아웃의 경우 Authorization 헤더가 잘못된 경우,
리프레시 토큰이 존재하지 않는 경우, 정상적으로 로그아웃되는 경우를 테스트하였다.</p>
<p>그 결과 <code>UserService</code>는 메서드 커버리지와 구문 커버리지 모두 <code>100%</code>를 달성할 수 있었다.
<code>AuthService</code>는 User 도메인과 관련된 로그아웃 테스트를 추가한 뒤 메서드 커버리지 <code>75%</code>, 구문 커버리지 <code>64%</code>까지 올라갔다.</p>
<hr>
<h3 id="5-의문점">5. 의문점</h3>
<p>앞서 테스트 커버리지를 확인하고, 커버리지를 높이기 위해 누락된 테스트 코드를 추가하는 과정을 거쳤다.</p>
<p>이 과정에서 분명 얻어가는 장점도 있었다.
커버리지를 확인하지 않았다면 놓쳤을 수도 있는 회원 탈퇴나 로그아웃 같은 기능을 다시 발견할 수 있었고,
해당 기능들에 대한 테스트도 추가할 수 있었다.</p>
<p>이러한 점에서 테스트 커버리지는 테스트되지 않은 코드가 어디에 남아 있는지 알려주는 유용한 지표였다.</p>
<p>하지만 동시에 한 가지 의문도 들었다.</p>
<p>프로덕트 코드를 확인한 뒤, 커버리지를 높이기 위해 작성한 테스트 코드는
정말 가치 있는 테스트라고 말할 수 있을까?</p>
<p>본래 처음 목표로 삼았던 방식은 기획 요구사항과 API 시트를 바탕으로 명세를 먼저 추출하고,
그 명세를 테스트 코드로 표현하는 것이었다.</p>
<p>반면 커버리지를 높이기 위해 작성한 테스트는 프로덕트 코드에서 실행되지 않은 부분을 먼저 확인한 뒤,
그 코드를 지나가게 만들기 위해 작성한 테스트에 가까웠다.</p>
<p>물론 이런 테스트가 완전히 의미 없다고 생각하지는 않는다.
실제로 누락된 기능을 발견했고, 특정 분기나 예외 상황이 의도대로 동작하는지도 확인할 수 있었다.</p>
<p>그러나 테스트의 출발점이 요구사항이 아니라 프로덕트 코드가 되는 순간, 테스트가 명세를 검증한다기보다
기존 구현을 따라가는 형태가 될 수 있다는 생각이 들었다.</p>
<p>이 경우 테스트 커버리지 수치는 올라가지만, 그 수치가 곧 테스트의 가치를 보장한다고 말하기는 어렵다.</p>
<p>테스트가 정말 확인해야 하는 것은 코드가 실행되었는지가 아니라,
그 코드가 요구사항과 비즈니스 규칙을 올바르게 만족하고 있는 것인지가 중요하다 배웠기 때문이다.</p>
<hr>
<h3 id="6-결론--테스트-커버리지는-수치다">6. 결론 : 테스트 커버리지는 수치다</h3>
<p>이번 테스트 코드 적용 일련의 과정을 통해
테스트 코드는 명세이자 살아있는 문서처럼 동작할 수 있다는 개념을 어느정도 몸으로 느낄 수 있었다.</p>
<p>조금 갑작스럽지만,
앞선 과정을 통해 테스트 코드와 테스트 커버리지에 대해 느낀 개인 견해를 작성하며 마무리하고자 한다.</p>
<p>요구사항을 테스트 코드로 표현하면 단순히 오류를 찾는 것을 넘어,
이후 리팩토링 과정에서도 기존 기능이 깨지지 않았는지 확인하는 안전망이 될 수 있다.
이렇 듯 <strong>좋은 테스트 코드</strong>를 작성하기 위해 느낀 점 중 하나로는,
<strong>테스트 코드가 프로덕트 코드가 아닌 명세를 바라봐야 한다는 점</strong>이다.</p>
<p>기존 구현을 기준으로 테스트를 작성하면 단순히 현재 코드를 따라가는 테스트가 되기 쉽지만,
요구사항을 기준으로 작성하면 구현이 명세와 어긋나는 부분이 생겨나게 될 것이다.</p>
<br/>

<p><strong>테스트 커버리지</strong>는 결국 이러한 테스트 코드가
실제 프로덕트 코드에 어느 정도 닿아 있는지를 보여주는 <strong>수치</strong>라고 생각한다.</p>
<p>커버리지는 테스트의 가치를 직접 증명하지는 않지만,
테스트되지 않은 코드가 어디에 남아 있는지 알려주는 유용한 지표가 될 수 있다.</p>
<p>결국 중요한 것은 커버리지 숫자 자체가 아니라, 그 숫자를 채우고 있는 테스트 코드의 내용이다.
<strong>좋은 테스트 코드가 작성되어 있을 때 테스트 커버리지는 의미 있는 수치가 되는 것 같다.</strong></p>
<p>따라서 테스트 커버리지는 목표라기보다 결과에 가까워야 한다는 생각이다.
중요한 명세를 테스트로 검증하고,
그 결과로 따라오는 수치일 때 비로소 테스트 커버리지는 의미를 가진다고 느꼈다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security: Filter Chain 구조-동작 이해]]></title>
            <link>https://velog.io/@zayn_noh/Spring-Security1</link>
            <guid>https://velog.io/@zayn_noh/Spring-Security1</guid>
            <pubDate>Mon, 29 Jun 2026 04:01:41 GMT</pubDate>
            <description><![CDATA[<p>웹 개발을 하다보면 언제나 보안과 관련된 일련의 처리 로직을 고려하고 요구사항에 맞는 서비스를 구현한다.</p>
<p>대부분의 웹 서비스에서는 <strong>인증</strong>, <strong>인가</strong> 단계부터 <strong>보안</strong> 관련 기능까지 공통적으로 들어가는 요소가 많고
이를 매번 프로젝트 개발 시마다 고려하여 직접 작성하는 것은 효율적이지 않다.
무엇보다 핵심 비즈니스 로직과 관련이 없는 경우가 많다.</p>
<p>이번 포스트에서는 Spring을 통해 웹 개발을 진행할 때 개발자가 보안 관련 로직 처리에 소요되는 비용을 줄이고
핵심 비즈니스 로직에 집중할 수 있도록 도와주는 <strong>Spring Security</strong>의 기본 개념에 대해 다루어보려한다.</p>
<hr>
<h3 id="1-배경-지식-정리">1. 배경 지식 정리</h3>
<p><strong>Spring Security</strong>가 무엇인지 알아보기 전에 사전에 알아두어야 하는 관련 개념들에 대해서 정리하려한다.</p>
<blockquote>
<p><strong>인증 (Authentication)</strong>
인증(Authentication)이란 간략하게 리소스를 요청하는 대상이 <strong>누구인지</strong>를 판별하는 과정을 말한다.</p>
<p>즉, 신원을 확인하는 과정을 말하며 웹사이트에 들어온 사용자가 로그인하기 위해 
아이디와 비밀번호를 입력하고 요청하는 경우가 이러한 인증 절차에 해당한다.</p>
</blockquote>
<blockquote>
<p><strong>인가 (Authorization)</strong>
인가(Authorization)란 인증된 사용자가 특정 리소스에 접근할 수 있는 <strong>권한</strong>을 의미한다 볼 수 있다.</p>
<p>사용자가 게시글을 삭제한다거나, 수정하고 싶어 요청을 보냈다 가정하면
해당 요청에 대한 처리를 수행할 때  이 사용자가 권한이 있는지 확인하는 것이다.</p>
</blockquote>
<blockquote>
<p><strong>HTTP 무상태성 (Http Stateless)</strong>
웹 통신 프로토콜인 HTTP의 특징 중 하나인 무상태성(Stateless)은
서버가 클라이언트의 이전 요청을 기억하지 못한다는 것이다.</p>
<p>이러한 특징은 기존 요청을 보낸 사용자가 어떤 상태를 가지는지 알 수 없기에
기존 상태를 기반으로 작업을 수행해야하는 로직의 경우 문제가 발생할 수 있는데
이를 세션, 쿠키, JWT 등과 같은 기술들을 활용해 기억하고 해결한다.</p>
</blockquote>
<p>추가적으로 서블릿(Servlet)과 서블릿 컨테이너(Servlet Container)에 대한, 배경 지식이 필요한데
이는 <a href="https://velog.io/@zayn_noh/Servlet-Container">기존 포스트</a>에서 다루었기에 이번 포스트에서는 별도로 다루지 않도록 한다.</p>
<br/>

<p>위 배경지식들이 필요한 이유는 <strong>Spring Security</strong>가 수행하는 대부분의 작업이 다음과 맞닿아 있기 때문이다.</p>
<blockquote>
<p><strong>무상태성</strong>을 가지는 웹 프로토콜에서 기존 인증 사용자를 <strong>어떻게 기억하고</strong>,
다음 요청에서 <strong>어떻게 알아보며</strong>,
<strong>어떤 권한</strong>을 가지고 해당 요청을 보내는지 확인하는 절차</p>
</blockquote>
<p>추가적으로 서블릿 관련 개념의 경우, Spring Security가 Spring에서 어떻게 동작하는지
내부 과정을 이해하기 위해 필요한 배경지식이 된다.</p>
<br/>

<h3 id="2-spring-security란">2. Spring Security란</h3>
<p>앞선 배경지식을 살펴보며 어느정도 예상이 가능하고, 이름에서부터도 유추가 가능하겠지만
<strong>Spring Security</strong> 다음과 같이 정의할 수 있다.</p>
<blockquote>
<p>스프링 기반 어플리케이션의 인증(Authentication), 인가(Authorization)를 포함
보안 기능을 처리하는 하위 프레임워크</p>
</blockquote>
<p>스프링 시큐리티가 수행하는 일은 한 줄로 다음처럼 설명할 수 있다.</p>
<blockquote>
<p>지금 요청을 보낸 사용자가 <strong>누구</strong>이고, 그 사용자가 이 작업을 할 <strong>자격</strong>이 있는가? </p>
<ul>
<li>Authentication</li>
<li>Authorization</li>
</ul>
</blockquote>
<p>인증과 인가 이외에도 스프링 시큐리티는 스프링 프레임워크를 통해 제작한 웹 어플리케이션에서
보안 관련한 것을 모두 관리해주는 역할을 수행한다.</p>
<br/>

<h3 id="3-spring-security-동작-지점">3. Spring Security 동작 지점</h3>
<p><strong>Spring Security</strong>의 경우, Spring MVC의 영역인 컨트롤러나 디스패처 서블릿에서 실행되는 것이 아니라
그보다 앞선 단계인 <strong>서블릿 필터(Servlet Filter)</strong>에서 동작한다.</p>
<p>서블릿 필터는 요청이 서블릿(이때는 디스패처 서블릿)에 도달하기 전에 먼저 요청을 가로채어 필터의 기능을 수행하며 <strong>서블릿 컨테이너</strong>에 의해서 관리된다.</p>
<p>이때 여러 필터가 순서대로 연결되어 요청을 처리하는 구조를 <strong>필터 체인(Filter Chain)</strong>이라 한다.</p>
<p>Spring Security는 이 필터 체인 내부에 자신이 사용하는 보안 필터들을 등록해두고,
클라이언트의 요청이 컨트롤러에 도달하기 전에 인증, 인가와 같은 보안 공통 관심사를 먼저 수행할 수 있다.</p>
<blockquote>
<p><strong>Filter, Interceptor, AOP의 동작 시점 차이</strong></p>
<ul>
<li><strong>Filter</strong>: DispatcherServlet보다 앞단, 컨테이너 레벨에서 동작</li>
<li><strong>Interceptor</strong>: DispatcherServlet과 Controller 사이 (스프링 MVC 수준)</li>
<li><strong>AOP</strong>: 메서드 실행 전/후/주변 (메서드 호출 수준) </li>
</ul>
</blockquote>
<p>스프링 시큐리티의 경우 <strong>어플리케이션 전체의 보안을 책임져야하기에</strong>,
<strong>스프링 MVC에 종속되지 않고</strong> 가장 앞에서 모든 요청을 검사할 수 있는 필터 기반으로 설계한 것이다.</p>
<h3 id="4-웹-요청-흐름">4. 웹 요청 흐름</h3>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/79ad48af-dd73-48ef-a872-cd9981365231/image.png" alt=""></p>
<ol>
<li>클라이언트에서 사용자의 요청이 전송된다.
 요청은 Tomcat 같은 <strong>WAS</strong>에 도착하고, WAS 내부의 서블릿 컨테이너가 요청을 처리하기 시작한다.</li>
</ol>
<blockquote>
<p><strong>WAS(Web Application Server)</strong>란
동적인 웹 애플리케이션 로직을 실행하는 서버를 말한다.
클라이언트의 요청을 받아 애플리케이션 코드를 실행하고, 그 결과를 HTTP 응답으로 반환한다.</p>
<p>예: <code>Tomcat</code>, <code>Jetty</code>, <code>Undertow</code>, <code>JBoss/WildFly</code></p>
<ul>
<li>vs <strong>Web Server</strong><ul>
<li>Web Server는 주로 HTML, CSS, JS, 이미지 같은 정적 리소스를 제공한다.</li>
<li>예: <code>Nginx</code>, <code>Apache HTTP Server</code></li>
</ul>
</li>
</ul>
</blockquote>
<ol start="2">
<li><p>서블릿 컨테이너가 관리하는 서블릿 필터에서 Spring Security가 동작한다.
 Spring Security는 자체 보안 필터들을 필터 체인에 등록해두고,
 요청이 DispatcherServlet에 도달하기 전에 인증과 인가 같은 보안 검사를 수행한다.</p>
</li>
<li><p>필터(필터 체인)를 모두 통과한 요청은 <code>DispatcherServlet</code>으로 이동한다.
 이후 시점부터 요청 처리는 본격적으로 <strong>Spring MVC</strong> 영역에서 진행된다.</p>
</li>
</ol>
<br/>

<h3 id="5-filter-chain-구조-및-흐름">5. Filter Chain 구조 및 흐름</h3>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/7983fa9f-9466-49f7-808b-c6cac84246e9/image.png" alt=""></p>
<p><strong>1. 클라이언트의 요청 수신</strong>
클라이언트의 요청은 <code>Tomcat</code> 같은 <code>WAS</code>에 도착하고, WAS 내부의 <code>Servlet Container</code>가 요청 처리를 시작한다.
단, 실제 운영 환경에서는 Servlet Container 앞에 Nginx, 로드밸런서 같은 장비가 있을 수 있다.</p>
<p><strong>2. Servlet Filter Chain</strong>
Servlet Container가 받은 요청은 Servlet으로 바로 전달되지 않고,
먼저 <code>Servlet Filter Chain</code>을 거친다. Filter Chain은 여러 <code>Servlet Filter</code>가 순서대로 연결된 구조이며,
각 Filter는 요청을 다음 Filter로 넘기거나 중간에 차단할 수 있다.</p>
<p><strong>3. Filter, DelegatingFilterProxy</strong>
Filter Chain 안에는 인코딩, CORS, 로깅 등 공통 처리를 위한 <code>Filter</code>들이 존재할 수 있다.
Spring Security를 사용하는 경우, 이 Filter Chain 안에 <code>DelegatingFilterProxy</code>가 등록된다.</p>
<p><code>DelegatingFilterProxy</code>는 Spring Security의 실제 보안 로직을 직접 수행하기보다는,
<strong>Servlet Container와 Spring Container 사이를 연결</strong>하는 프록시 역할을 한다.</p>
<p><strong>4. FilterChainProxy</strong>
Servlet Container는 Spring Container가 관리하는 Bean을 직접 알지 못한다.
따라서 Servlet Filter Chain에는 <code>DelegatingFilterProxy</code>를 등록해두고, 이 프록시가
Spring Container 안의 <code>springSecurityFilterChain</code> Bean에게 요청 처리를 위임한다.</p>
<p>Spring Security에서 이 Bean은 보통 <code>FilterChainProxy</code> 타입의 단일 객체이다.</p>
<p><code>FilterChainProxy</code>는 내부에 여러 <code>SecurityFilterChain</code>을 가지고 있으며,
현재 요청에 맞는 <code>SecurityFilterChain</code>을 선택한 뒤 그 안의 보안 필터들을 실행한다.</p>
<p><strong>5. SecurityFilterChain</strong>
<code>SecurityFilterChain</code>은 특정 요청에 적용할 Spring Security 보안 필터들의 묶음이다.
개발자가 작성한 <code>SecurityConfig</code>의 설정을 바탕으로 생성되며,
내부에는 인증, 인가, CSRF, 로그아웃 등과 관련된 여러 보안 필터들이 순서대로 들어 있다.</p>
<p>요청은 선택된 <code>SecurityFilterChain</code> 안의 필터들을 순서대로 통과하고,
보안 검사를 통과하면 이후 <code>DispatcherServlet</code>으로 전달된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[기본 키 생성의 네 가지 전략에 따른 성능]]></title>
            <link>https://velog.io/@zayn_noh/%EA%B8%B0%EB%B3%B8-%ED%82%A4-%EC%83%9D%EC%84%B1%EC%9D%98-%EB%84%A4-%EA%B0%80%EC%A7%80-%EC%A0%84%EB%9E%B5%EC%97%90-%EB%94%B0%EB%A5%B8-%EC%84%B1%EB%8A%A5</link>
            <guid>https://velog.io/@zayn_noh/%EA%B8%B0%EB%B3%B8-%ED%82%A4-%EC%83%9D%EC%84%B1%EC%9D%98-%EB%84%A4-%EA%B0%80%EC%A7%80-%EC%A0%84%EB%9E%B5%EC%97%90-%EB%94%B0%EB%A5%B8-%EC%84%B1%EB%8A%A5</guid>
            <pubDate>Mon, 15 Jun 2026 11:14:05 GMT</pubDate>
            <description><![CDATA[<p><code>@Id</code> 어노테이션은 데이터베이스의 PK와 매핑되며, 엔티티의 식별자를 지정해준다.</p>
<p>이떄 <code>@Id</code> 식별자에 대한 값을 지정하지 않고 <code>@GeneratedValue</code>를 사용해 값을 생성할 수 있는데</p>
<pre><code class="language-java">@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;</code></pre>
<p>해당 어노테이션은 엔티티의 식별자가 데이터베이스의 <code>AUTO_INCREMENT</code>처럼, 자동으로 id값을 붙여준다.</p>
<p>이번 포스트에서는 JPA에서 기본 키를 생성하는 네 가지 전략에 대해 알아보고,
각 방식이 어떤 상황에서 사용되는지와 성능상 어떤 차이가 있는지 정리해보려고 한다.</p>
<blockquote>
<p><strong>추상화된 기본 키 생성 전략</strong></p>
<p>JPA는 다양한 DB와 통신을 지원해야한다.
즉 서로 다른 DB에서도 모두 작동하는 방식의 기본 키 생성 전략들이 존재하며,
추상화된 전략들을 통해 각각의 DB에 이를 적용한다.</p>
</blockquote>
<hr>
<h3 id="1-table-전략">1. TABLE 전략</h3>
<p>먼저 <strong>TABLE 전략</strong>은 데이터베이스의 시퀀스 동작을 별도의 테이블로 <strong>흉내 내는 방식</strong>이다.</p>
<blockquote>
<p><strong>시퀀스(Sequence)</strong></p>
<p>데이터베이스에서 연속되며 유일한 숫자 값을 생성해주는 객체이다. 
주로 기본 키를 만들 때 사용되며 순차적으로 증가된다.
MySQL의 경우 이러한 시퀀스 객체가 없고 기본적으로 <code>AUTO_INCREMENT</code> 기능을 쓴다.</p>
</blockquote>
<p>즉, 기본 키 값을 생성하기 위한 전용 테이블을 하나 만들고,
JPA가 엔티티를 저장할 때마다 해당 테이블에서 다음 식별자 값을 조회하고 증가시킨다.</p>
<pre><code class="language-java">@Id
@GeneratedValue(strategy = GenerationType.TABLE)
private Long id;</code></pre>
<p><code>@GeneratedValue(strategy = GenerationType.TABLE)</code> 옵션을 통해 지정 가능하며,
<strong>데이터베이스 안에 키 생성을 위한 전용 테이블을 하나 만든다.</strong></p>
<pre><code class="language-sql">CREATE TABLE sequence_table (
    sequence_name VARCHAR(255) NOT NULL,
    next_val BIGINT NOT NULL,
    PRIMARY KEY (sequence_name)
);</code></pre>
<p>예를 들어 위와 같이 테이블을 만들 수 있는데,</p>
<p>JPA가 엔티티를 저장할 때 이 테이블에서 현재 값을 읽고, 다음 값을 증가시킨 뒤
<strong>그 값을 엔티티의 기본 키로 사용</strong>한다.</p>
<blockquote>
<p><strong>장점</strong>
이 방식은 특정 DB의 AUTO_INCREMENT나 SEQUENCE 기능에 의존하지 않기 때문에
<strong>데이터베이스 종류와 관계없이 사용할 수 있다</strong>는 장점이 있다.</p>
</blockquote>
<blockquote>
<p><strong>단점</strong>
하지만 식별자 값을 <strong>생성할 때마다 별도의 테이블을 조회하고 업데이트</strong>해야 하므로 <strong>성능상 불리</strong>하다.
동시 요청이 많을 경우 키 생성 테이블에 락이 걸리거나 병목이 생길 수 있다.</p>
</blockquote>
<p>따라서 TABLE 전략은 데이터베이스 독립성은 높지만, 성능 문제 때문에 실무에서는 자주 사용되지 않는다.</p>
<hr>
<h3 id="2-sequence-전략">2. SEQUENCE 전략</h3>
<p><strong>SEQUENCE 전략</strong>은 <strong>데이터베이스의 시퀀스 객체를 사용</strong>해 기본 키 값을 생성하는 방식이다.</p>
<p>시퀀스는 데이터베이스에서 연속된 숫자 값을 만들어주는 객체이며, JPA는 엔티티를 저장하기 전에 시퀀스에서 다음 값을 먼저 가져와 엔티티의 식별자로 사용한다.</p>
<pre><code class="language-sql">@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;</code></pre>
<p>추가적으로 시퀀스 이름이나 할당하는 크기를 직접 지정하고 싶다면, <code>SequenceGenerator</code>를 사용한다.</p>
<pre><code class="language-sql">@Entity
@SequenceGenerator(
    name = &quot;user_seq_generator&quot;, // user_seq_generator라는 name에
    sequenceName = &quot;user_seq&quot;, // DB에 있는 user_seq가 매핑된다.
    allocationSize = 50
)
public class User {

    @Id
    @GeneratedValue(
        strategy = GenerationType.SEQUENCE,
        generator = &quot;user_seq_generator&quot; // 해당 시퀀스 생성기 사용
    )
    private Long id;
}</code></pre>
<p>큰 동작 흐름은 다음과 같다.</p>
<ol>
<li>JPA가 <strong>데이터베이스 시퀀스</strong>에서 다음 값을 가져온다.</li>
<li>가져온 값을 <strong>엔티티의 id에 할당</strong>한다.</li>
<li><strong>엔티티를 INSERT</strong>한다.</li>
</ol>
<p>이러한 SEQUENCE 전략은 시퀀스를 지원하는 데이터베이스에서만 사용할 수 있다.
(<code>Oracle</code>, <code>PostgreSQL</code>, <code>H2</code> etc.)</p>
<blockquote>
<p><strong>장점</strong>
<strong>INSERT 전에 식별자 값을 미리 알 수 있다.</strong>
<code>em.persist(user)</code>를 통해 엔티티를 영속성 컨텍스트에 저장하려 한다 가정해보자
JPA 입장에서는 영속성 컨텍스트에 엔티티를 보관하기 위해 식별자(PK)가 필요한데
아직 엔티티로만 존재하고 실제 DB에 저장하기 전이라면, ID값이 존재하지 않게 된다.</p>
<p>이에 해당 <code>SEQUENCE</code> 전략은, DB에서 다음 ID값을 받아오고
엔티티에 ID를 세팅한 다음 DB에 작성되기 전임에도 영속성 컨텍스트에 이를 등록할 수 있는 것이다.</p>
</blockquote>
<blockquote>
<p><strong>쓰기 지연</strong></p>
<p>일반적으로 JPA의 <strong><code>INSERT</code> 쿼리</strong>는 엔티티를 <code>persist()</code>하는 즉시 실행되지 않고,
영속성 컨텍스트의 <strong>쓰기 지연 저장소</strong>에 쌓였다가 <strong><code>flush</code> 시점에 실행</strong>된다.</p>
<p><code>flush</code>는 보통 트랜잭션 커밋 직전이나 <strong>JPQL</strong> 실행 전에 발생하며, 이때 <strong>모아둔 SQL이 DB로 전송</strong>된다.</p>
<p>이러한 구조 덕분에 JPA는 여러 <code>INSERT</code> 쿼리를 모아 <strong>Batch INSERT</strong>로 최적화할 수 있다.</p>
</blockquote>
<blockquote>
<p><strong>네트워크 통신 과정으로 인한 비용, 성능 손해?</strong></p>
<p>해당 방식을 살펴보면, PK를 얻고자 실제 DB와 통신하며 시퀀스에서 PK값을 얻어온다.
실제 DB와 네트워크로 통신하기에 당연히 비용이 발생할 것이고, 성능적 문제가 있다고 생각할 수 있다.</p>
<p>실제로 어느정도는 맞는 말이며, <strong>이를 개선하기 위해 <code>allocationSize</code> 옵션이 존재</strong>한다.
시퀀스 값을 매번 DB에서 하나씩 가져오지 않고,
일정 범위의 ID 값을 미리 가져와 애플리케이션에서 사용하는 방식
ex) <code>allocationSize = 50</code> : 50개의 PK를 미리 확보하여 얻어온다.</p>
</blockquote>
<p>이러한 <code>allocationSize</code> 옵션을 활용한 시퀀스 전략은
DB와의 네트워크 통신 비용을 줄이기도 하고, 쓰기 지연을 잘 활용하는 전략이라 할 수 있다.</p>
<blockquote>
<p><strong>받아온 식별 키 값을 모두 안 쓰면 어떻게 되나?</strong></p>
<p>자바 어플리케이션과, 받아온 값을 다루는 Entity Manager가 계속 살아있다면
이후에 해당 PK는 계속하여 활용하게 된다.
만약 서버가 재시작되거나, 종료된다면 해당 ID 구간은 사라져, 중간에 번호가 빌 수 있다.</p>
</blockquote>
<hr>
<h3 id="3-identity-전략">3. IDENTITY 전략</h3>
<p><code>SEQUENCE</code> 전략이 데이터베이스의 시퀀스 객체를 활용하는 방식이었다면,<code>IDENTITY</code> 전략은
<code>MySQL</code>같은 시퀀스 객체가 없는 데이터베이스의 <code>AUTO_INCREMENT</code>를 활용하는 방식이다.</p>
<pre><code class="language-java">@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;</code></pre>
<p><code>IDENTITY</code> 전략에서도 기본 키 생성을 데이터베이스에 위임한다.</p>
<p>즉, JPA가 직접 식별자 값을 미리 만들어두는 것이 아니라
INSERT 쿼리가 실행될 때 데이터베이스가 자동으로 PK 값을 생성한다.</p>
<blockquote>
<p>데이터베이스에 <strong>INSERT</strong>할 때 PK 컬럼을 비워두고 전달하면,
DB가 알아서 해당 값을 증가시키면서 값을 채워넣어 준다.</p>
</blockquote>
<blockquote>
<p><strong>식별자가 없는데 어떻게 영속성 컨텍스트에 엔티티를 등록하고 관리할까?</strong></p>
<p><code>IDENTITY</code> 전략은 식별자 생성을 데이터베이스에 위임한다.
즉, id 값은 <code>INSERT</code> 쿼리가 실제로 DB에 실행된 이후에야 알 수 있다.</p>
<p><strong>아이덴티티 전략일 때만 예외적으로 동작 방식을 바꾼다</strong>.
때문에 JPA는 <code>IDENTITY</code> 전략을 사용할 때
<code>persist()</code> 시점에 즉시 <code>INSERT</code> 쿼리를 실행한다.</p>
<p>일반적으로 JPA는 INSERT 쿼리를 영속성 컨텍스트의,
쓰기 지연 저장소에 모아두었다가 flush 시점에 실행할 수 있다.
IDENTITY 전략에서는 ID 값을 알아야
엔티티를 영속성 컨텍스트에서 관리할 수 있으므로, persist() 시점에 바로 DB에 INSERT한다.</p>
</blockquote>
<blockquote>
<p><strong>쓰기 지연 및 Batch Insert 최적화 제한</strong></p>
<p>일반적으로 JPA는 <code>INSERT</code> 쿼리를 즉시 실행하지 않고,
영속성 컨텍스트의 쓰기 지연 저장소에 모아두었다가 <code>flush</code> 시점에 실행할 수 있다.</p>
<p>이 과정에서 여러 <code>INSERT</code> 문을 JDBC Batch로 묶어
DB에 전송하는 Batch Insert 최적화도 가능하다.</p>
<p>하지만 <code>IDENTITY</code> 전략은
엔티티의 ID 값을 얻기 위해 <code>persist()</code> 시점에 즉시 <code>INSERT</code>를 실행해야 한다.</p>
<p>따라서 여러 엔티티의 <code>INSERT</code>를 모아두었다가
한 번에 처리하는 쓰기 지연과 <strong>Batch Insert 최적화가 제한</strong>된다.</p>
</blockquote>
<blockquote>
<p><strong>Bulk Insert vs Batch Insert</strong></p>
<ul>
<li>Bulk Insert : 하나의 SQL로 대량 삽입</li>
<li>Batch Insert : 여러 INSERT를 JDBC batch로 묶어 전송하는 최적화</li>
</ul>
</blockquote>
<br/>

<p><strong>MySQL과 JPA를 사용할 때 대량 INSERT 성능을 개선하는 방법</strong></p>
<p>MySQL에서 <code>GenerationType.IDENTITY</code> 전략을 사용하면,
JPA가 엔티티의 ID를 얻기 위해 <code>persist()</code> 시점마다 <code>INSERT</code>를 실행해야 한다.</p>
<p>이로 인해 JPA의 쓰기 지연과 Batch Insert 최적화가 제한될 수 있다.</p>
<p>대량 INSERT 성능이 중요한 경우에는 JPA의 엔티티 저장 방식 대신
<code>JdbcTemplate.batchUpdate()</code>를 사용하거나,
별도의 네이티브 쿼리로 다중 row INSERT를 작성하는 방법을 고려할 수 있다.</p>
<p>이를 통해 <strong>여러 INSERT를 JDBC Batch로 묶어 전송</strong>하거나,
<strong>하나의 SQL로 여러 row를 삽입하여 대량 저장 성능을 개선</strong>할 수 있다.</p>
<hr>
<h3 id="4-auto-전략">4. AUTO 전략</h3>
<p><code>AUTO</code> 전략은 JPA 구현체가 사용하는 데이터베이스에 맞춰 기본 키 생성 전략을 자동으로 선택하는 방식이다.</p>
<pre><code class="language-java">@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;</code></pre>
<p>개발자가 <code>IDENTITY</code>, <code>SEQUENCE</code>, <code>TABLE</code> 중 하나를 직접 지정하지 않고
AUTO를 사용하면, <strong>JPA 구현체가 현재 사용하는 DB의 특성에 맞는 전략을 선택</strong>한다.</p>
<blockquote>
<p>시퀀스를 지원하는 데이터베이스라면 SEQUENCE 전략을 사용할 수 있고</p>
<p>MySQL처럼 AUTO_INCREMENT를 주로 사용하는 데이터베이스라면,
IDENTITY 또는 테이블 기반 전략이 선택될 수 있다.</p>
</blockquote>
<blockquote>
<p><strong>데이터베이스 의존 감소</strong></p>
<p>AUTO 전략은 데이터베이스 종류에 덜 의존적인 코드를 작성할 수 있다는 장점이 있다.
하지만 실제로 어떤 전략이 선택되는지는 <strong>JPA 구현체</strong>와 <strong>DB 방언(Dialect)</strong>에 따라 달라질 수 있다.</p>
</blockquote>
<p>AUTO 전략은 편리하지만, 실제 선택되는 키 생성 전략이
DB와 JPA 구현체에 따라 달라지므로 <strong>동작과 성능을 예측하기 어렵다</strong>는 단점이 있다.</p>
<hr>
<h3 id="5-추가-uuid">5. 추가) UUID</h3>
<p><code>UUID</code>는 IDENTITY, SEQUENCE, TABLE 같은 DB 기반 숫자 PK 생성 전략과는 다르다.
보통 UUID는 애플리케이션에서 직접 식별자 값을 생성해서 넣는 방식에 가깝다.</p>
<p><strong>마이크로서비스 아키텍처(MSA)</strong>처럼,
서비스와 데이터베이스가 여러 개로 나뉘는 분산 환경에서는 기본 키 충돌 문제가 생길 수 있는데</p>
<p>각 DB 안에서는 문제가 없지만, 이후 데이터를 합치거나
여러 서비스의 데이터를 함께 다뤄야 하는 상황에서는 같은 <strong>id 값이 충돌</strong>할 수 있다.</p>
<blockquote>
<p>이러한 문제를 줄이기 위해 UUID를 사용할 수 있다.
UUID는 다음과 같은 형태의 긴 난수 문자열이다.
<code>550e8400-e29b-41d4-a716-446655440000</code></p>
</blockquote>
<p><code>UUID</code>를 기본 키로 사용하면 여러 DB나 여러 애플리케이션 서버에서
동시에 ID를 생성하더라도 <strong>충돌 가능성이 매우 낮다</strong>.</p>
<p>또한 <code>UUID</code>는 데이터베이스에 <strong>INSERT</strong>하기 전에 애플리케이션에서 미리 생성할 수 있다.</p>
<pre><code class="language-java">// 방법 1 : 생성자를 통한 값 할당
@Id
private String id;

public User() {
    this.id = UUID.randomUUID().toString();
}</code></pre>
<pre><code class="language-java">// 방법 2 : Hibernate(JPA) 환경에서의 UUID 생성 전략
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;</code></pre>
<blockquote>
<p><strong>UUID는 INSERT 전에 식별자 값을 알 수 있기에</strong>
IDENTITY 전략처럼 ID를 얻기 위해 persist() 시점에 즉시 INSERT를 실행할 필요가 없다.
따라서 쓰기 지연이나 Batch Insert 최적화 측면에서 IDENTITY보다 유리할 수 있다.</p>
</blockquote>
<blockquote>
<p><strong>다만 다음과 같은 단점들이 존재한다</strong></p>
<p>일반적인 정수형 ID보다 길고 저장 공간을 더 많이 차지 -&gt; 인덱스 비교 비용 증가
데이터가 순차적으로 쌓이는 것이 아니라 중간에 삽입될 수 있어 <strong>페이지 분할</strong>, <strong>인덱스 재정렬 비용</strong> 발생 </p>
<p>이를 보완하기 위한 시간 순서 정렬 가능 식별자들 또한 존재한다.</p>
<ul>
<li><code>UUID v7</code>, <code>ULID</code>, <code>TSID</code> etc.</li>
</ul>
</blockquote>
<hr>
<h3 id="6-개념-한-줄-정리">6. 개념 한 줄 정리</h3>
<ul>
<li>TABLE 전략 : 데이터베이스에 기본 키 생성용 테이블을 만들어 활용하는 전략</li>
<li>SEQUENCE 전략 : 데이터베이스 시퀀스 객체에서 미리 일정 PK를 받아와 활용하는 전략</li>
<li>IDENTITY 전략 : 시퀀스 객체가 없는 DB에서 활용 가능한 기본 키 생성 전략</li>
<li>AUTO 전략 : JPA와 DB에 따라 자동으로 전략을 지정해주는 방식</li>
<li>UUID : MSA와 같이 분산 서버에서 ID 충돌을 막기 위한 난수 문자열 기본 키</li>
<li>Batch Insert : 여러 INSERT문을 JDBC Batch로 묶어 전송하는 방식</li>
<li>Bulk Insert : 하나의 INSERT에 여러 row를 한 번에 삽입해 전송하는 방식</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA의 @Entity, @Id 어노테이션]]></title>
            <link>https://velog.io/@zayn_noh/entity-id</link>
            <guid>https://velog.io/@zayn_noh/entity-id</guid>
            <pubDate>Mon, 15 Jun 2026 09:26:11 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@zayn_noh/orm-jpa">앞선 포스트</a>에서는 ORM의 이점과, 이를 자바 진영에서 구현한 JPA에 대해 알아보았다.</p>
<p>이번 포스트에서는,
JPA에서 테이블과 클래스를 매핑할 때 사용하는 <code>@Entity</code> 어노테이션에 대해 알아보려고 한다.
추가로 엔티티를 식별하는 기준인 <code>@Id</code> 어노테이션에 대해서도 정리하도록 한다.</p>
<hr>
<h3 id="1-entity란">1. Entity란</h3>
<p><strong>엔티티(Entity)</strong>란 데이터베이스의 테이블과 1대1로 매핑되며,
데이터를 객체 형태로 표현하는 자바 클래스를 말한다.</p>
<p>ORM이 존재하기 이전 데이터베이스에서 사용자 목록을 조회하고자할 때는 다음과 같은 쿼리를 DB에 보내야했다.</p>
<pre><code class="language-sql">SELECT * FROM users</code></pre>
<p>이후 해당 쿼리를 받은 DB는 테이블 형태의 결과물을 <code>ResultSet</code>이라는 객체에 담아 반환했다.</p>
<p>자바와 같은 객체지향 언어에서 <code>ResultSet</code>으로 받아온 데이터들을 다루기 위해서는,
추가로 <code>while</code>이나 <code>for</code> 반복문을 돌며 데이터를 하나씩 꺼내며 <code>new User</code>를 진행해야했고
하나씩 데이터를 세팅해주어야 하여 시간적으로도 코드 작성 레벨에서도 아쉬움이 남았다.</p>
<p>이를 JPA에서 해결해주는 방식이 <strong><code>@Entity</code></strong> 어노테이션인 것이다.</p>
<p>해당 어노테이션은 다음과 같이 클래스 선언문 상단에 붙여서 사용 가능하며</p>
<pre><code class="language-java">@Entity
public class User {
    ...</code></pre>
<p>각 필드를 컬럼에 매핑하여 JPA가 해당 객체를 데이터베이스 테이블과 연동할 수 있도록 구성한다.</p>
<hr>
<h3 id="2-entity-어노테이션-동작">2. Entity 어노테이션 동작</h3>
<p><code>@Entity</code> 어노테이션이 붙었다면 다음과 같은 일련의 과정을 거쳐 관리된다.</p>
<ol>
<li>JPA가 클래스를 보며 직접 관리해야할 대상임을 인식하고, DB 테이블 컬럼과 매핑한다.</li>
<li>이후 해당 클래스와 객체들은 JPA의 관리 대상이 되며</li>
<li><strong>영속성 컨텍스트(Persistence Context)</strong>라는 메모리 공간에서 관리하게 된다.</li>
</ol>
<p>영속성 컨텍스트에 대한 자세한 내용은 <a href="https://velog.io/@zayn_noh/persistence-context">해당 포스트</a>에서 확인한다.</p>
<hr>
<h3 id="3-entity-사용-이유">3. Entity 사용 이유</h3>
<p><code>@Entity</code>의 사용 이유는 사실상 ORM의 이점과 크게 다르지 않다.</p>
<p>데이터베이스의 데이터를 하나의 객체로 묶어 구조와 규칙을 함께 관리하여 코드의 오류를 줄일 수 있고
자바 코드를 통해 작성된 백엔드 비즈니스 로직상에서 일관된 설계를 진행할 수 있게 해준다.</p>
<blockquote>
<p><strong>데이터베이스의 문제점</strong></p>
<p>데이터베이스는 좋은 저장소지만, <strong>비즈니스 규칙</strong>을 담을 수 없다는 단점이 존재한다.</p>
<p>엔티티가 존재하지 않는다면, 개발자는 데이터베이스에서 불러온 값에 대하여
<strong>서비스</strong> 클래스 내부에서 <strong>데이터의 상태와 관련된 비즈니스 로직을 처리</strong>해야한다.</p>
<p>이는 일종의 파편화를 유발하는데,
동일 데이터에 대한 상태를 여러 서비스에서 사용할 경우 각각의 서비스에서 모두 구현해야하며
추후 정책이 바뀌게 된다면 전체 서비스를 돌며 해당 부분 코드를 수정해야할 것이다.</p>
<p><code>@Entity</code>를 사용한다면 이러한 엔티티 상태를 변경하는 비즈니스 로직을
<strong>엔티티 클래스 내부에</strong> 넣을 수 있다.
이를 통해 비즈니스 로직의 파편화가 줄어들고, 하나의 클래스가 <strong>명확한 책임</strong>을 가지게 된다.</p>
</blockquote>
<blockquote>
<p><strong>객체 스스로 상태를 책임지고 관리</strong></p>
<p>엔티티 클래스 내부에 상태와 관련된 정책을 두면, 객체가 자신의 상태를 스스로 관리할 수 있다.</p>
<p>즉, 데이터와 그 데이터를 변경하는 행위를 한 곳에 모아둘 수 있다.</p>
<p>이렇게 하면 상태 변경 규칙이 엔티티 내부에 모이기 때문에,
정책이 변경되었을 때 수정 범위를 줄일 수 있고 객체의 일관성도 유지하기 쉬워진다.</p>
</blockquote>
<hr>
<h3 id="4-entity-선언-시-주의할-점">4. Entity 선언 시 주의할 점</h3>
<ol>
<li><strong>기본 생성자가 반드시 존재해야 한다.</strong></li>
</ol>
<p><code>@Entity</code>가 붙은 클래스에는 기본 생성자가 반드시 존재해야 한다.</p>
<p>기본 생성자가 없는 클래스에 <code>@Entity</code> 어노테이션을 붙이면
애플리케이션 실행 시점에 JPA 구현체가 엔티티 객체를 생성하지 못해 에러가 발생할 수 있다.</p>
<p>이는 JPA 구현체가 데이터베이스에서 데이터를 읽어온 뒤,
엔티티 객체를 생성할 때 <strong>리플렉션</strong>을 사용하기 때문이다.</p>
<blockquote>
<p><strong>리플렉션(Reflection)</strong></p>
<p>런타임에 클래스의 메타데이터를 분석하고, 생성자나 메서드, 필드에 동적으로 접근할 수 있게 해주는 기술</p>
</blockquote>
<p>JPA 구현체인 <strong>Hibernate</strong>는 실행 중에 엔티티 객체를 동적으로 생성해야 한다.
이때 리플렉션을 통해 기본 생성자를 호출한 뒤, 필드에 값을 주입하는 방식으로 객체를 만든다.</p>
<p>따라서 파라미터가 있는 생성자만 존재하고 기본 생성자가 없다면,
Hibernate가 어떤 생성자를 어떤 인자로 호출해야 할지 알 수 없기 때문에
엔티티 객체를 생성하지 못하고 에러가 발생할 수 있다.</p>
<p>기본 생성자는 <code>public</code> 또는 <code>protected</code> 접근 제어자를 가져야 하며,
외부에서 무분별하게 생성하지 못하도록 <code>protected</code> 기본 생성자를 자주 사용한다.</p>
<p>Hibernate가 엔티티 객체를 관리하려면, 먼저 JVM 메모리에 올릴 수 있는 객체를 생성할 수 있어야 한다.</p>
<p>이를 위해 엔티티 클래스에는 기본 생성자가 필요하다.
Hibernate는 기본 생성자를 통해 빈 엔티티 객체를 생성한 뒤,
리플렉션을 사용해 데이터베이스에서 조회한 값을 각 필드에 주입하는 방식으로 동작한다.</p>
<br/>

<ol start="2">
<li><strong>클래스나 필드에 final 을 쓰면 안된다.</strong></li>
</ol>
<p>JPA를 공부하다 보면 이후에 지연 로딩과 프록시 개념을 만나게 되는데</p>
<p>JPA는 성능 최적화를 위해 관련 엔티티를 즉시 조회하지 않고,
실제 객체 대신 프록시 객체를 먼저 넣어두는 방식을 사용할 수 있다.</p>
<p>프록시는 실제 엔티티처럼 보이지만,
필요한 시점이 되기 전까지는 실제 데이터를 조회하지 않는 가짜 객체에 가깝다.</p>
<p>Hibernate는 이러한 프록시 객체를 만들 때 원본 엔티티 클래스를 상속하는 방식을 사용한다.</p>
<p><strong>final 클래스는 다른 클래스가 상속할 수 없기 때문에</strong> Hibernate가 프록시 클래스를 만들 수 없다.
그 결과 지연 로딩이 제대로 동작하지 않을 수 있다.</p>
<p>추가로 필드에도 final을 사용하지 않는 것이 좋다.</p>
<p>Hibernate는 기본 생성자를 통해 빈 엔티티 객체를 만든 뒤,
<strong>리플렉션을 사용해 데이터베이스에서 조회한 값을 필드에 주입</strong>하는 것을 앞서 확인했다.</p>
<p>그러나 final 필드는 <strong>생성자 이후에 값을 변경할 수 없기 때문에</strong>,
Hibernate가 필드 값을 채워 넣는 과정과 맞지 않는다.</p>
<hr>
<h3 id="5-id-어노테이션">5. @Id 어노테이션</h3>
<p>데이터베이스를 설계할 때 특정 테이블에 필수로 들어가는 요소 중 하나가 <strong>PK</strong>이다.
이러한 PK는 테이블에서 행을 식별하게 해주는 기준이되며, 자바에서 이를 표현하고 다루는 방식이 <code>@Id</code>이다.</p>
<p>데이터베이스는 DDL을 통해 특정 컬럼이 PK임을 명시하고 알 수 있지만,
자바에서는 이를 <code>@Id</code>를 통해 엔티티를 식별하는 유일한 키값으로 활용한다.</p>
<p>즉 <code>@Id</code> 어노테이션은 엔티티의 식별자를 지정해주는 기본 키 매핑 어노테이션이라 할 수 있다.</p>
<hr>
<h3 id="6-기본-키-할당-방식">6. 기본 키 할당 방식</h3>
<p>기본 키를 할당하는 방식은 크게 <strong>직접 할당</strong>과 <strong>자동 생성</strong>으로 나눌 수 있다.</p>
<p><strong>1. 직접 할당</strong>
직접 할당은 개발자가 자바 코드에서 직접 ID 값을 넣어주는 방식이다.</p>
<pre><code class="language-java">User user = new User();
user.setId(1L);</code></pre>
<p>이 방식은 개발자가 직접 식별자 값을 관리해야 하므로 실무에서는 자주 사용하지 않는다.</p>
<p><strong>2. 자동 생성</strong>
자동 생성은 데이터베이스의<code>AUTO_INCREMENT</code>, <code>시퀀스</code>,
또는 <code>UUID</code> 같은 방식을 사용해 식별자 값을 자동으로 생성하는 방식이다.</p>
<pre><code class="language-java">@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;</code></pre>
<p>일반적인 서비스에서는 직접 ID를 넣기보다 자동 생성 전략을 사용하는 경우가 많다.
생성 전략에는 여러 종류가 있으며, 이는 추후 다른 포스트에서 다루도록 한다.</p>
<blockquote>
<p><strong>자연키(Natural Key)와 대리키(Surrogate Key)</strong></p>
<p>기본 키는 크게 자연키와 대리키로 구분된다.
자연키는 <strong>비즈니스적인 의미를 가지면서 유일성을 만족하는 값</strong>이며,
대리키는 <strong>비즈니스 의미 없이 식별만을 위해 인위적으로 만든 값</strong>을 말한다.</p>
</blockquote>
<p>엔티티의 기본 키로는 자연키보다 <strong>대리키를 사용하는 것이 좋다</strong>.</p>
<p>이메일은 현재는 중복되지 않는 값처럼 보이지만, 나중에 사용자가 이메일을 변경할 수 있다.
만약 이메일을 기본 키로 사용하고 있었다면, 기본 키 자체를 변경해야 한다.</p>
<p>기본 키가 변경되면 해당 키를 외래 키로 참조하는 게시글, 주문, 결제, 장바구니 등
여러 테이블도 함께 영향을 받는다.</p>
<p>이 과정에서 많은 업데이트가 발생하고, <strong>데이터 정합성 문제</strong>나 <strong>락 문제</strong>가 생길 수 있다.</p>
<p>따라서 기본 키는 <strong>비즈니스 정책이 변해도 바뀌지 않는 값</strong>이어야 한다.
이런 이유로 보통 의미 없는 순차 번호나 <strong>UUID</strong> 같은 대리키를 사용한다.</p>
<p>이메일이나 전화번호처럼 유일해야 하는 값은 기본 키로 사용하기보다
<code>UNIQUE</code> 제약 조건을 걸어 중복을 막는 것이 좋다.</p>
<hr>
<h3 id="7-id-사용-이유">7. @Id 사용 이유</h3>
<p>@Id는 단순히 데이터베이스의 PK와 매핑하기 위한 어노테이션만은 아니다.</p>
<p>JPA는 @Id 값을 기준으로 영속성 컨텍스트 안에서 엔티티를 식별하고 관리한다.
즉 @Id는 JPA가 객체를 구분하는 기준이다.</p>
<p>동일 트랜잭션 안에서 같은 ID의 회원을 두 번 조회하면,
JPA는 두 번째 조회에서 DB로 바로 가지 않고 영속성 컨텍스트의 <strong>1차 캐시</strong>를 먼저 확인한다.</p>
<pre><code class="language-java">User user1 = em.find(User.class, 1L);
User user2 = em.find(User.class, 1L);</code></pre>
<p>이때 JPA는 <code>User</code> 타입과 <code>id = 1</code>이라는 값을 기준으로 이미 관리 중인 엔티티가 있는지 확인한다.</p>
<p>같은 엔티티가 1차 캐시에 있다면 DB를 다시 조회하지 않고 기존 객체를 반환한다.</p>
<p>또한 JPA의 <strong>변경 감지(Dirty Check)</strong> 기능도 식별자를 기준으로 동작한다.</p>
<p>조회해온 엔티티의 값을 변경하면, JPA는 트랜잭션이 끝날 때 변경된 내용을 감지하고 <code>UPDATE</code> 쿼리를 실행한다.</p>
<pre><code class="language-java">user.changeNickname(&quot;새이름&quot;);</code></pre>
<p>개발자가 직접 UPDATE SQL을 작성하지 않아도, JPA는 해당 엔티티가
어떤 row에 대응되는지 @Id 값을 통해 알고 있기 때문에 DB에 변경 내용을 반영할 수 있다.</p>
<p><strong>1차 캐시</strong>와 <strong>변경 감지</strong>에 대한 자세한 내용은 영속성 컨텍스트를 다룰 때 더 깊게 살펴보도록 한다.</p>
<hr>
<h3 id="8-사용-예시">8. 사용 예시</h3>
<p>데이터베이스에 다음같은 DDL을 가지는 <code>USERS</code> 테이블이 존재한다 가정하자.</p>
<pre><code class="language-sql">CREATE TABLE USERS (
    id BIGINT NOT NULL,
    email VARCHAR(255) NOT NULL,
    password VARCHAR(255) NOT NULL,
    nickname VARCHAR(50) NOT NULL,
    image VARCHAR(500),
    PRIMARY KEY (id)
);</code></pre>
<p><code>id</code>, <code>email</code>, <code>password</code>, <code>nickname</code>, <code>image</code>의 컬럼을 가지고 있고
<code>id</code>는 <strong>PK(Primary Key)</strong>에 해당한다.</p>
<p>이를 <code>@Entity</code> 어노테이션을 통해 엔티티 클래스와 매핑시킨다면</p>
<pre><code class="language-java">@Entity
@Table(name = &quot;USERS&quot;)
public class User {

    @Id
    private Long id;

    private String email;

    private String password;

    private String nickname;

    private String image;

    protected User() {
    }
}</code></pre>
<p>위 코드처럼 나타내볼 수 있다. 이때 <code>@Table(name = &quot;USERS&quot;)</code>는 어떤 테이블의 이름을 찾을지를 지정하며
해당 어노테이션이 붙어있지 않다면 자동으로 클래스의 이름으로 테이블을 찾는다.</p>
<p>현재 해당 클래스의 이름은 <code>User</code>로 되어있기에, 데이터베이스에 저장되어있는 <code>USERS</code>테이블은 찾지 못한다.</p>
<p>컬럼에 해당하는 값들은 자동으로 필드의 이름들과 동일한 변수에 매핑된다.</p>
<hr>
<h3 id="9-개념-한-줄-정리">9. 개념 한 줄 정리</h3>
<ul>
<li>엔티티 : RDB의 테이블과 자바 객체를 매핑하기 위한 개념으로, 데이터베이스 데이터를 객체 형태로 표현한 것</li>
<li>엔티티 클래스 : <code>@Entity</code> 어노테이션이 붙어 JPA가 관리 대상으로 인식하는 클래스이며, DB의 테이블과 매핑된다.</li>
<li>엔티티 객체 : 엔티티 클래스로부터 생성된 객체이며, DB 테이블의 한 행(row)과 매핑된다.</li>
<li>@Entity : 해당 클래스를 JPA가 관리하는 엔티티 클래스로 만들어주는 어노테이션</li>
<li>@Id : 엔티티의 식별자 필드를 지정하며 DB의 PK와 매핑되는 어노테이션</li>
<li>@Table : 엔티티 클래스가 매핑될 DB 테이블의 이름이나 설정을 지정하는 어노테이션</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ORM과 JPA의 관계 (Hibernate, JDBC)]]></title>
            <link>https://velog.io/@zayn_noh/orm-jpa</link>
            <guid>https://velog.io/@zayn_noh/orm-jpa</guid>
            <pubDate>Sun, 14 Jun 2026 23:59:56 GMT</pubDate>
            <description><![CDATA[<p>자바는 <strong>객체 지향 프로그래밍 언어</strong>이다.</p>
<p>하지만 백엔드 서버에서 다루는 데이터는 결국 데이터베이스에 저장되고,
데이터베이스는 보통 테이블과 row 중심으로 데이터를 관리한다.</p>
<p>즉 애플리케이션에서는 객체를 다루지만,
데이터베이스에서는 테이블을 다루기 때문에 두 세계 사이에는 차이가 존재한다. <strong>(패러다임 불일치)</strong></p>
<p>만약 데이터베이스의 값을 매번 SQL로 직접 조회하고, 조회 결과를 자바 객체에 일일이 옮겨 담고,
다시 저장할 때도 객체의 값을 하나씩 SQL로 작성해야 한다면 개발 과정은 매우 번거로워진다.</p>
<p>이러한 불편함을 줄이기 위해 등장한 개념이 <strong>ORM(Object-Relational Mapping)</strong>이다.
ORM은 객체와 관계형 데이터베이스의 테이블을 매핑하여,
개발자가 SQL 중심이 아니라 객체 중심으로 데이터를 다룰 수 있게 해준다.</p>
<p>그리고 자바에서는 ORM을 위한 표준 인터페이스로 <strong>JPA(Java Persistence API)</strong>가 제공된다.
객체와 테이블 사이의 매핑, 조회, 저장, 수정, 삭제를 일관된 방식으로 처리할 수 있도록 도와준다.</p>
<p>이번 포스트에서는 ORM과 JPA의 기본 개념부터 이러한 기술을 바탕으로,
어떻게 개발자가 비즈니스 로직에 집중할 수 있게 해주는지 알아본다.</p>
<hr>
<h3 id="1-ormobject-relational-mapping이란">1. ORM(Object-Relational Mapping)이란</h3>
<p>ORM(Object-Relational Mapping)은 객체지향과 관계형 데이터베이스 사이의 간극을 줄여,
설계의 일관성과 코드의 생산성을 높여주는 기술이자 개념을 말한다.</p>
<p>주 목적은 앞서 작성한 내용과 동일 내용이지만, <strong>패러다임 불일치</strong>를 해결함에 있다.</p>
<p>ORM 없이 객체 지향 언어와 관계형 DB의 패러다임 불일치가 일어난다면 다음과 같은 문제를 야기한다.</p>
<ol>
<li><p><strong>상속의 차이</strong>
 객체지향에서 상속은 기본 요소인만큼 매우 자주 쓰이고 쉽게 사용하는 개념이다.
 특정 상위 클래스를 상속받는 하위 클래스들을 만들어 낼 수 있는데,
 관계형 데이터베이스에서는 이러한 객체의 상속 개념이 존재하지 않는다.</p>
<p> 하위 클래스에 대해 데이터베이스에 저장하려면 DB 입장에서는
 하위 클래스로 <code>INSERT</code>하고, 상위 클래스로도 <code>INSERT</code>하는 번거로움이 존재한다.</p>
</li>
</ol>
<ol start="2">
<li><p><strong>연관 관계</strong>
 객체와 다른 객체는 참조를 통해 관계를 맺는다.
 컴포지션으로 객체 내부에서 다른 객체를 멤버로 선언하여, <code>member.getTeam()</code>처럼
 다른 객체를 참조할 수도 있다.
 이때 객체 참조의 관계는 <code>member</code> -&gt; <code>team</code>이라는 <strong>단방향</strong>을 띈다.
 즉, <code>member</code>는 <code>team</code>을 알지만, <code>team</code>은 <code>member</code>를 모를 수 있다.</p>
<p>  반면 데이터베이스의 경우 테이블간 관계는 <strong>FK</strong>를 통해 이루어진다.
 회원테이블에서 <code>team_id</code> FK를 두고 이를 통해 <strong>JOIN</strong>하여 값을 확인할 수 있다.
 즉, 외래키만 존재한다면 <code>member</code>에서 <code>team</code>을 조회할 수도,
 <code>team</code>에서 <code>member</code>를 조회할 수도 있다. <strong>양방향</strong></p>
<p> 결국 객체와 테이블을 ORM없이 연결하고자 한다면
 객체 클래스 내부에는 참조를 위한 멤버 클래스를 추가로 작성해야하고,
 데이터베이스에서는 각각에 맞는 FK를 추가 설정해야한다.</p>
<p> 매우 번거롭고 객체지향이과 DB가 완전히 호환이 안되어 불편하다.</p>
</li>
<li><p><strong>데이터 탐색</strong>
 객체지향에서 데이터를 탐색할 때는 참조만 확인하면 된다.
 <code>member.getTeam().getCompany().getName()</code>처럼, 참조와 메서드를 통해
 원하는 데이터를 조회할 수 있다.</p>
<p> 이를 SQL로 데이터베이스에서 조회하려한다면
 SQL로 한번 가져온 데이터의 범위가 고정되어있기에,
 처음 <strong>JOIN</strong>을 통해 잘못된 범위의 데이터를 가져온 경우 <strong>NPE</strong>가 발생할 수 있다.</p>
<pre><code>결국 개발자는 해당 쿼리가 어디까지 데이터를 가져오는지 확인해야하는 불편한 과정을 진행해야한다.</code></pre><p> 이를 <strong>로직이 SQL에 강하게 종속</strong>되었다고 한다.</p>
</li>
<li><p><strong>데이터 식별 방법</strong>
 데이터베이스는 PK를 통해 행을 식별하지만, 자바는 객체의 메모리 주소값을 통해 각 객체를 식별한다.</p>
<pre><code class="language-java"> Member member1 = memberDAO.getMember(100); 
 Member member2 = memberDAO.getMember(100); </code></pre>
<p> <code>member1</code>과 <code>member2</code>는 동일 id값을 가지지만, 메모리 주소가 다르기에 다른 객체로 판단한다.
 결국 데이터베이스에서는 같은 데이터인데, 자바에서는 다른 객체로 취급받는 모순이 생기는 것이다.</p>
</li>
</ol>
<p>이러한 문제를 해결하는 것이 <strong>ORM</strong>으로 자바의 경우 <strong>JPA</strong>라는 표준 ORM 인터페이스를 사용한다.</p>
<hr>
<h3 id="2-jpajava-persistence-api란">2. JPA(Java Persistence API)란</h3>
<p>객체지향 언어와 관계형 데이터베이스 사이의 패러다임 불일치 간극을 줄여주는 ORM 개념이
자바에서 구현된 것이 <strong>JPA(Java Persistence API)</strong>이다.</p>
<p>결국 JPA는 인터페이스이기에, 자바에서 ORM을 만들려면
<strong>이런 이름의 메서드를 만들고, 이런 규칙을 지키라고</strong> 정해둔 자바 진영의 <strong>공식 명세서</strong>이다.</p>
<blockquote>
<p><strong>Hibernate</strong></p>
<p>JPA를 구현하는 대표적인 구현체로 점유율이 가장 높으며,
JPA를 사용한다는 것은 사실상 <strong>Hibernate</strong>를 사용하는 것이라 볼 수 있다.</p>
</blockquote>
<p>JPA 구현체인 <strong>Hibernate</strong>는 <strong>JDBC</strong>를 직접 사용하지 않고도
객체를 데이터베이스에 저장하고 불러올 수 있도록 돕는 중간 계층 역할을 한다.</p>
<blockquote>
<p><strong>JDBC</strong></p>
<p><strong>JDBC(Java Database Connectivity)</strong>란
자바 어플리케이션과 데이터베이스를 연결해주는 표준 API로,
어떻게 DB와 커넥션을 맺고 통신을 처리하는지를 정의한 도구라 할 수 있다.
결국에는 자바에서 데이터베이스와 연결하기 위해 <strong>JDBC</strong>를 거쳐야하며</p>
<p>과거의 경우 JDBC를 직접 다루고 처리하였는데 JPA(Hibernate)를 사용하면
<strong>자동으로 객체의 구조를 분석하여 SQL을 만들고 직접 JDBC를 호출하여 처리</strong>해준다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>DB</th>
<th>JDBC 구현체</th>
</tr>
</thead>
<tbody><tr>
<td>MySQL</td>
<td>MySQL Connector/J</td>
</tr>
<tr>
<td>PostgreSQL</td>
<td>PostgreSQL JDBC Driver</td>
</tr>
<tr>
<td>MariaDB</td>
<td>MariaDB Connector/J</td>
</tr>
<tr>
<td>Oracle</td>
<td>Oracle JDBC Driver</td>
</tr>
<tr>
<td>SQL Server</td>
<td>Microsoft JDBC Driver</td>
</tr>
</tbody></table>
<p>즉 백엔드 어플리케이션에서 DB에 접근하고자 할 때는 아래와 같은 과정을 거치는 것이다.</p>
<p><code>Spring 앱 -&gt; JPA 인터페이스 -&gt; Hibernate 구현체 -&gt; JDBC -&gt; DB</code></p>
<hr>
<h3 id="3-개념-한-줄-정리">3. 개념 한 줄 정리</h3>
<ul>
<li>ORM : 객체지향 언어와 RDB간 패러다임 불일치를 줄이기 위한 개념이자 기술을 의미한다.</li>
<li>JPA : 자바에서 ORM을 구현하는 표준 인터페이스로, ORM 구현을 위한 자바의 명세이자 규약이다.</li>
<li>Hibernate : JPA 인터페이스를 구현하는 대표적인 구현체</li>
<li>JDBC : 자바에서 RDB와 연결 및 SQL의 실제 실행을 담당하는 가장 밑단의 인터페이스이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 확장 방식 전략]]></title>
            <link>https://velog.io/@zayn_noh/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%99%95%EC%9E%A5-%EB%B0%A9%EC%8B%9D-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@zayn_noh/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%99%95%EC%9E%A5-%EB%B0%A9%EC%8B%9D-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Fri, 12 Jun 2026 10:11:14 GMT</pubDate>
            <description><![CDATA[<h3 id="1-데이터베이스의-확장-개념-등장-배경">1. 데이터베이스의 확장 개념 등장 배경</h3>
<p>비즈니스가 성장하면서 서비스의 규모가 커지고 데이터베이스에 저장되는 데이터가 대용량화되면
단일 데이터베이스만으로 구축한 저장소는 다음과 같은 문제점들 마주하게 된다.</p>
<blockquote>
<ul>
<li>저장 용량의 한계</li>
<li>읽기/쓰기 성능 저하</li>
<li>특정 서버에 집중되는 부하</li>
<li>장애 발생 시 서비스에 끼치는 영향 범위 증가</li>
</ul>
</blockquote>
<p>이를 해결하기 위한 가장 단순한 방법은 DB 서버의 하드웨어 성능을 높이는 것이다.</p>
<p>이렇게 CPU, 메모리, 디스크 성능, 네트워크 성능 등을 강화하는 방식을 <strong>스케일업(Scale up)</strong>이라고 한다.
스케일업은 기존 구조를 크게 바꾸지 않고 성능과 저장 공간을 확보할 수 있다는 장점이 있다.</p>
<blockquote>
<p><strong>스케일업(Scale-Up), 스케일아웃(Scale-Out)</strong></p>
<p>스케일업 : 구조 변경이 적고, <strong>한 서버의 성능을 높임</strong>, 비용과 물리적 한계, 수직 확장
스케일아웃 : 대규모 확장 가능, <strong>서버 수를 늘려 분산</strong>, 운영 복잡도 증가, 수평 확장</p>
</blockquote>
<p>하지만 장비 성능에는 물리적 한계가 있고, 고성능 장비일수록 비용이 급격히 증가한다.</p>
<p>또한 하나의 DB 서버에 계속 의존하는 구조라면 장애 대응이나 부하 분산 측면에서도 한계가 존재한다. 
→ 해당 서버가 <strong>단일 장애점(Single Point of Failure, SPOF)</strong>이 될 수 있다.</p>
<p><strong>이러한 한계를 완화하기 위해</strong> 데이터베이스 확장 전략이 사용된다. 대표적으로 파티셔닝, 샤딩, 복제가 있으며</p>
<p>데이터를 나누거나 복제하여 <strong>저장 용량</strong>, <strong>성능</strong>, <strong>가용성 문제</strong>를 해결하는 데 사용된다.</p>
<p>이번 포스트에서는 데이터베이스의 확장 방법인
파티셔닝, 샤딩, 복제에 대해 정리하며 각 방식의 전략을 알아본다.</p>
<hr>
<h3 id="2-파티셔닝partitioning">2. 파티셔닝(Partitioning)</h3>
<h4 id="2-1-파티셔닝이란">2-1) 파티셔닝이란</h4>
<p>단일 DB 내에서 하나의 테이블이나 인덱스가 지나치게 커지면 다음과 같은 문제점을 야기할 수 있다.</p>
<ul>
<li>조회 시 스캔해야 하는 데이터 범위가 증가</li>
<li>인덱스의 크기 증가</li>
<li>백업, 삭제, 아카이빙과 같은 관리 작업의 비용 증가</li>
</ul>
<p>이러한 문제를 완화하기 위해 사용 가능한 기법 중 하나가 <strong>파티셔닝(Partitioning)</strong>이다.</p>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/19c69d4b-a606-4336-9cbc-eb34210ee42b/image.png" alt=""></p>
<p>파티셔닝은 큰 테이블이나 인덱스를 더 작은 단위인 파티션으로 물리적으로 분할하여 관리하는 기법이며,
데이터를 여러 파티션으로 나누면, DB는 필요한 파티션만 접근할 수 있어</p>
<p>불필요한 데이터 접근을 줄일 수 있고, 대용량 데이터를 더 수월하게 관리할 수 있다.</p>
<p>파티셔닝은 물리적으로 데이터를 여러 단위로 나누지만, 어플리케이션(DB를 사용하는) 입장에서는
일반적으로 하나의 테이블에 접근하는 것처럼 사용할 수 있다.</p>
<br/>

<h4 id="2-2-수직-파티셔닝-수평-파티셔닝-예시-코드">2-2) 수직 파티셔닝, 수평 파티셔닝 예시 코드</h4>
<p>파티셔닝은 데이터를 나누는 기준에 따라 크게 수직 파티셔닝과 수평 파티셔닝으로 구분할 수 있다.</p>
<ul>
<li><strong>수직 파티셔닝(Vertical Partitioning)</strong>은 테이블의 <strong>column</strong>을 기준</li>
<li><strong>수평 파티셔닝(Horizontal Partitioning)</strong>은 테이블의 <strong>row</strong>를 기준</li>
</ul>
<p>다음과 같은 구조를 가지는 <code>posts</code> 테이블을 고려하여 보자.</p>
<pre><code class="language-sql">CREATE TABLE posts (
    id BIGINT NOT NULL AUTO_INCREMENT,
    title VARCHAR(255) NOT NULL,
    user_id BIGINT NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    content TEXT NOT NULL,
    PRIMARY KEY (id)
);</code></pre>
<p>이를 각각의 파티셔닝 방법으로 데이터를 나눠보면 다음과 같다.</p>
<blockquote>
<p><strong>수직 파티셔닝(Vertical Partitioning)</strong></p>
<p><em>자주 조회하는 게시글 기본 정보와, 크기가 큰 본문을 따로 분리하는 경우</em></p>
<pre><code class="language-sql">CREATE TABLE posts (
    id BIGINT NOT NULL AUTO_INCREMENT,
    title VARCHAR(255) NOT NULL,
    user_id BIGINT NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
);</code></pre>
<pre><code class="language-sql">CREATE TABLE post_contents (
    post_id BIGINT NOT NULL,
    content TEXT NOT NULL,
    PRIMARY KEY (post_id),
    FOREIGN KEY (post_id) REFERENCES posts(id)
);</code></pre>
<p>게시글 목록의 조회만 원할 경우 기존 <code>posts</code> 테이블만 확인하면 된다.</p>
<pre><code class="language-sql">SELECT id, title, user_id, created_at
FROM posts
ORDER BY created_at DESC;</code></pre>
<p>하나의 게시글의 상세 조회를 원할 경우, 수직 파티셔닝을 통해 분리한 <code>post_contents</code> 를 사용한다.</p>
<pre><code class="language-sql">SELECT p.id, p.title, p.user_id, p.created_at, pc.content
FROM posts p
JOIN post_contents pc ON p.id = pc.post_id
WHERE p.id = 1;</code></pre>
</blockquote>
<blockquote>
<p><strong>수평 파티셔닝(Horizontal Partitioning)</strong></p>
<p><em>작성 연도 기준으로 게시글을 나누는 경우</em></p>
<pre><code class="language-sql">CREATE TABLE posts_2025 (
    id BIGINT NOT NULL,
    title VARCHAR(255) NOT NULL,
    user_id BIGINT NOT NULL,
    created_at DATETIME NOT NULL,
    content TEXT NOT NULL,
    PRIMARY KEY (id)
);</code></pre>
<pre><code class="language-sql">CREATE TABLE posts_2026 (
    id BIGINT NOT NULL,
    title VARCHAR(255) NOT NULL,
    user_id BIGINT NOT NULL,
    created_at DATETIME NOT NULL,
    content TEXT NOT NULL,
    PRIMARY KEY (id)
);</code></pre>
<p>수평 파티셔닝을 통해 분리한 테이블에 데이터를 추가</p>
<pre><code class="language-sql">INSERT INTO posts_2026 (id, title, user_id, created_at, content)
VALUES (1, &#39;게시글 제목&#39;, 10, &#39;2026-06-12 11:00:00&#39;, &#39;본문 내용&#39;);</code></pre>
<p>2026년에 쓰인 게시글만 보고 싶다면 다음과 같은 쿼리로 접근 가능하다.</p>
<pre><code class="language-sql">SELECT *
FROM posts_2026</code></pre>
</blockquote>
<br/>

<h4 id="2-3-수직-파티셔닝-수평-파티셔닝-개념">2-3) 수직 파티셔닝, 수평 파티셔닝 개념</h4>
<p><strong><code>1) 수직 파티셔닝</code></strong></p>
<p>수직 파티셔닝은 테이블의 컬럼을 기준으로 데이터를 나누는 방식이다.</p>
<p>하나의 테이블에 있는 컬럼들을 사용 빈도나 데이터 크기, 접근 패턴에 따라 여러 테이블로 분리한다.</p>
<p>게시글 목록 조회에서는 <code>id</code>, <code>title</code>, <code>user_id</code>, <code>created_at</code> 같은 기본 정보만 필요한 경우가 많다.</p>
<p>반면 <code>content</code>는 게시글 상세 조회에서만 필요하고, 데이터 크기도 상대적으로 크다.</p>
<p><code>content</code> 컬럼을 <code>post_contents</code> 테이블로 분리하면,
목록 조회 시 큰 본문 데이터를 함께 읽지 않아도 된다. </p>
<blockquote>
<p>그 결과 하나의 row 크기가 작아지고, 같은 <strong>데이터 페이지</strong> 안에,
<strong>더 많은 row를 담을 수 있어</strong> I/O 비용을 줄일 수 있다.</p>
</blockquote>
<p><strong>사용하는 경우</strong></p>
<ul>
<li>특정 컬럼의 크기가 매우 큰 경우</li>
<li>자주 조회하는 컬럼과 가끔 조회하는 컬럼이 명확히 구분되는 경우</li>
<li>민감한 정보를 별도 테이블로 분리해 접근 권한을 다르게 관리하고 싶은 경우</li>
</ul>
<br/>

<p><strong><code>2) 수평 파티셔닝</code></strong></p>
<p>수평 파티셔닝은 테이블의 row를 기준으로 데이터를 나누는 방식이다.</p>
<p>하나의 테이블에 있는 row들을 특정 <strong>컬럼</strong>의 값이나 범위에 따라 여러 파티션으로 분리한다.</p>
<blockquote>
<p><strong>왜 컬럼의 값일까</strong></p>
<p>수평 파티셔닝은 테이블의 컬럼 구조는 그대로 유지한 채, 
특정 컬럼의 값을 기준으로 row를 여러 파티션에 나누어 저장하는 방식</p>
<p>⇒ 결국 row를 나누려면 <strong>어떤 기준값</strong>이 필요하고, 그 기준값은 보통 컬럼</p>
<ul>
<li>기준이 되는 것은 컬럼이지만, 실제로 분리되는 대상은 컬럼이 아니라 row이다.</li>
<li>따라서 각 파티션은 동일한 컬럼 구조를 가지며, 조건에 따라 서로 다른 row 집합을 저장한다.</li>
</ul>
</blockquote>
<p>앞선 게시글 테이블 예제처럼 <code>created_at</code> 같은 작성일시를 기준으로 데이터를 나누는 경우가 많다.</p>
<p><code>created_at</code>이 2025년에 해당하는 row는 <code>posts_2025</code>에 저장하고, 
2026년에 해당하는 row는 <code>posts_2026</code>에 저장할 수 있다.</p>
<p>이렇게 row를 나누면 특정 기간의 게시글을 조회할 때,
전체 게시글 데이터를 모두 탐색하지 않고 필요한 파티션만 접근할 수 있다.</p>
<blockquote>
<p><em>그 결과 조회 대상 데이터의 범위가 줄어들고,</em></p>
<p><em>불필요한 파티션 접근을 피할 수 있어 I/O 비용을 줄일 수 있다.</em></p>
<p><em>이를 <strong>파티션 프루닝(Partition Pruning)</strong>이라고 한다.</em></p>
<ul>
<li>파티션 프루닝 : DB가 쿼리를 실행할 때 <strong>필요 없는 파티션을 읽지 않고 제외하는 최적화</strong></li>
</ul>
</blockquote>
<p><strong>사용하는 경우</strong></p>
<ul>
<li>테이블의 row 수가 매우 많은 경우</li>
<li>날짜, 지역, 사용자 ID처럼 데이터를 나눌 기준이 명확한 경우</li>
<li>특정 범위의 데이터만 자주 조회하는 경우</li>
<li>오래된 데이터를 별도로 보관하거나 삭제해야 하는 경우</li>
</ul>
<br/>

<h4 id="2-3-파티션-키-선택-기준">2-3) 파티션 키 선택 기준</h4>
<p>파티션 키는 단순히 데이터를 나눌 수 있는 컬럼이 아니라, 실제 조회 패턴과 운영 정책에 맞는 컬럼이어야 한다. </p>
<p>잘못된 파티션 키를 선택하면 <strong>파티션 프루닝이 제대로 동작하지 않거나, 특정 파티션에 부하가 집중</strong>될 수 있다.</p>
<blockquote>
<p><strong>파티션 키(Partition Key)</strong>란
각 row가 어느 파티션에 저장될지를 결정하는 기준 컬럼이다.</p>
</blockquote>
<p>파티셔닝의 효과는 파티션 키를 어떻게 선택하느냐에 크게 달려 있다.</p>
<ol>
<li><p>적합한 파티션 키는 <strong>자주 사용되는 조회 조건</strong>과 잘 맞아야 한다.
쿼리의 <code>WHERE</code> 조건에 파티션 키가 자주 포함되면,
DB는 필요한 파티션만 접근할 수 있고 결과적으로 <strong>파티션 프루닝 효과</strong>를 얻을 수 있다.</p>
</li>
<li><p>데이터가 특정 파티션에 과도하게 몰리지 않아야 한다.
특정 값이나 특정 범위에 데이터가 집중되면 <strong>일부 파티션만 비대</strong>해지고, 
해당 파티션에 요청이 몰려 <strong>성능 저하</strong>가 발생할 수 있다.</p>
</li>
<li><p>파티션 키는 데이터 보관, 삭제, 아카이빙 정책과도 잘 맞아야 한다.
오래된 데이터를 기간 단위로 삭제하거나 보관해야 한다면,
날짜 컬럼을 파티션 키로 선택하는 것이 유리할 수 있다.</p>
</li>
</ol>
<blockquote>
<p><strong>다음을 고려하자.</strong></p>
<ul>
<li>자주 사용하는 조회 조건에 포함되는 컬럼인가</li>
<li>파티션 프루닝이 잘 일어날 수 있는 컬럼인가</li>
<li>데이터가 여러 파티션에 적절히 분산되는가</li>
<li>특정 파티션에 데이터나 트래픽이 몰리지 않는가</li>
<li>데이터 보관, 삭제, 아카이빙 정책과 잘 맞는가</li>
<li>파티션 수가 과도하게 늘어나지 않도록 관리 가능한 기준인가</li>
</ul>
</blockquote>
<br/>

<h4 id="2-4-파티션-적합-부적합-기준">2-4) 파티션 적합, 부적합 기준</h4>
<blockquote>
<p><strong>적합한 경우</strong></p>
<p><code>수직 파티셔닝</code> : 수직 파티셔닝은 특정 컬럼의 크기가 크거나 접근 패턴이 명확히 나뉘는 경우에 적합</p>
<pre><code>목록 조회에서는 title, created_at만 자주 접근
상세 조회에서만 content에 접근
관리자만 password_hash, phone_number에 접근</code></pre><ul>
<li>특정 컬럼의 데이터 크기가 매우 큰 경우</li>
<li>자주 조회하는 컬럼과 가끔 조회하는 컬럼이 명확히 나뉘는 경우</li>
<li>목록 조회와 상세 조회에서 필요한 컬럼이 다른 경우</li>
<li>민감한 컬럼을 별도 테이블로 분리해 접근 권한을 다르게 관리하고 싶은 경우</li>
<li>하나의 row 크기가 커서 데이터 페이지에 담기는 row 수가 줄어드는 경우</li>
</ul>
<p><code>수평 파티셔닝</code> : 수평 파티셔닝은 row 수가 많고, 데이터를 나눌 기준과 조회 패턴이 명확한 경우에 적합</p>
<pre><code>최근 3개월 주문만 자주 조회
2026년 게시글만 자주 조회
특정 지역의 사용자만 자주 조회
특정 user_id 범위의 데이터를 조회</code></pre><ul>
<li>테이블의 row 수가 매우 많아 조회나 관리 비용이 커진 경우</li>
<li>날짜, ID 범위, 지역 코드처럼 데이터를 나눌 기준이 명확한 경우</li>
<li>쿼리 조건에 파티션 키가 자주 포함되는 경우</li>
<li>특정 기간이나 특정 범위의 데이터만 자주 조회하는 경우</li>
<li>오래된 데이터를 주기적으로 삭제하거나 아카이빙해야 하는 경우</li>
<li>대용량 테이블의 백업, 삭제, 보관 작업을 파티션 단위로 관리하고 싶은 경우</li>
<li>큰 테이블을 작은 단위로 나누어 인덱스와 통계 관리 비용을 줄이고 싶은 경우</li>
</ul>
</blockquote>
<br/>

<blockquote>
<p><strong>부적합한 경우 (잘못된 설계)</strong></p>
<p><code>수직 파티셔닝</code> : 컬럼을 분리했을 때 얻는 이점보다 JOIN 비용이나 관리 복잡도가 더 커지는 경우 부적합</p>
<pre><code>대부분의 조회에서 title, created_at, content를 항상 함께 조회
분리한 테이블을 거의 매번 JOIN해서 사용
컬럼 크기가 작고 row 크기 감소 효과가 거의 없음
분리된 테이블 간 데이터 정합성 관리가 부담</code></pre><ul>
<li>대부분의 쿼리에서 분리된 컬럼들을 항상 함께 조회하는 경우</li>
<li>테이블을 분리해도 row 크기 감소 효과가 거의 없는 경우</li>
<li>분리된 테이블과의 JOIN이 너무 자주 발생하는 경우</li>
<li>컬럼 접근 권한이나 사용 빈도에 큰 차이가 없는 경우</li>
<li>테이블 분리로 인해 INSERT, UPDATE, DELETE 로직이 지나치게 복잡해지는 경우</li>
</ul>
<p><code>수평 파티셔닝</code> : row를 나누는 기준이 조회 조건과 맞지 않거나, 파티션 관리 비용이 더 커지는 경우 부적합</p>
<pre><code>created_at 기준으로 나눴지만 대부분 title이나 status로만 조회
쿼리 조건에 파티션 키가 거의 포함되지 않음
대부분의 조회가 여러 파티션을 모두 확인해야 함
특정 파티션에 데이터나 요청이 지나치게 몰림
테이블 크기가 작아 파티셔닝 이점이 거의 없음</code></pre><ul>
<li>테이블 크기가 작아 파티셔닝으로 얻는 이점이 거의 없는 경우</li>
<li>쿼리 조건에 파티션 키가 거의 포함되지 않는 경우</li>
<li>대부분의 조회가 여러 파티션을 모두 확인해야 하는 경우</li>
<li>파티션 키를 기준으로 데이터가 고르게 나뉘지 않고 특정 파티션에 몰리는 경우</li>
<li>파티션 수가 과도하게 늘어나 관리 비용이 커지는 경우</li>
<li>JOIN, 유니크 제약, 외래키 관리가 복잡해지는 것을 감당하기 어려운 경우</li>
<li>파티션 추가, 삭제, 보관 정책을 운영하기 어려운 경우</li>
</ul>
</blockquote>
<br/>

<hr>
<h3 id="3-샤딩sharding">3. 샤딩(Sharding)</h3>
<h4 id="3-1-샤딩이란">3-1) 샤딩이란</h4>
<p>샤딩(Sharding)은 데이터를 <strong>여러 DB 서버에 나누어</strong> 저장하는 수평 분할 방식이다.</p>
<p>하나의 DB 서버가 모든 데이터를 저장하고 모든 요청을 처리하기 어려워질 때,
데이터를 여러 조각으로 나누어 각각 다른 DB 서버에 분산 저장한다.</p>
<p>이때 나누어진 각각의 DB 또는 저장 단위를 <strong>샤드(Shard)</strong>라고 한다.</p>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/fe6d4bef-9c20-45bb-b2ee-d112c5369cb2/image.png" alt=""></p>
<ul>
<li>저장 용량과 읽기/쓰기 부하를 여러 서버로 분산 가능하며</li>
<li>단일 DB 서버의 용량과 성능 한계를 완화할 수 있다</li>
</ul>
<p>다만 데이터가 여러 서버에 흩어지기 때문에,
<strong>샤드 라우팅</strong>, <strong>분산 트랜잭션</strong>, <strong>조인</strong>, <strong>데이터 재분배</strong> 같은 문제가 발생할 수 있다.</p>
<blockquote>
<p><strong>샤딩</strong>은 수평 파티셔닝과 비슷하게 row를 나누는 방식이지만,
차이는 데이터가 여러 서버에 분산된다는 점이다. </p>
<ul>
<li><strong>수평 파티셔닝</strong> : 하나의 DBMS 내부에서 row를 나누는 방식</li>
<li><strong>샤딩</strong> : 여러 DB 서버에 row를 나누어 저장하는 방식</li>
</ul>
</blockquote>
<br/>

<h4 id="3-2-샤딩-키sharding-key-선택-기준">3-2) 샤딩 키(Sharding Key) 선택 기준</h4>
<p>샤딩 키는 <strong>각 row가 어느 샤드에 저장될지를 결정하는 기준 값</strong>이다.
샤딩 키를 어떻게 선택하느냐에 따라 데이터 분산, 쿼리 성능, 운영 복잡도가 크게 달라진다.</p>
<ul>
<li><p><strong>데이터가 여러 샤드에 고르게 분산되는가</strong>
특정 값에 데이터가 몰리면 일부 샤드만 비대해지고, 해당 샤드에 요청이 집중될 가능성이 있다.
따라서 샤딩 키는 가능한 데이터와 트래픽을 여러 샤드에 고르게 나눌 수 있어야 한다.</p>
</li>
<li><p><strong>자주 사용하는 조회 조건에 포함되는가</strong>
쿼리 조건에 샤딩 키가 포함되어 있으면 어느 샤드에 접근해야 하는지 바로 결정할 수 있다.
반대로 샤딩 키가 조건에 없으면 여러 샤드를 모두 조회해야 할 수 있다.</p>
</li>
</ul>
<ul>
<li><strong>값이 자주 변경되지 않는가</strong>
샤딩 키 값이 변경되면 해당 row가 다른 샤드로 이동 가능
이는 데이터 재배치 비용과 정합성 문제를 발생시킬 수 있기에, <strong>샤딩 키는 가능하면 변경되지 않는 값</strong>이 좋다.</li>
</ul>
<ul>
<li><strong>특정 샤드에 트래픽이 몰리지 않는가</strong>
데이터가 고르게 분산되어도 요청이 특정 값에 집중되면 특정 샤드에 부하가 몰릴 수 있다.
따라서 데이터 분포뿐 아니라 트래픽 분포도 함께 고려해야 한다.</li>
</ul>
<ul>
<li><strong>데이터의 소유 관계나 접근 패턴과 잘 맞는가</strong>
함께 조회되는 데이터가 같은 샤드에 위치하면 <strong>cross-shard query</strong>를 줄일 수 있다.
사용자 중심 서비스라면 <code>user_id</code>를 기준으로 관련 데이터를 동일 샤드에 모으는 것이 유리</li>
<li><em>키를 잘못 잡으면 데이터가 고르게 나뉘지도 않고 조회할때 여러 샤드를 훑는 비효율적인 문제 발생 가능*</em></li>
</ul>
<br/>

<h4 id="3-3-샤딩의-방식에-따른-분류">3-3) 샤딩의 방식에 따른 분류</h4>
<ul>
<li><p><code>Range-based Sharding</code> : 샤딩 키의 값 범위를 기준으로 데이터를 나누는 방식</p>
</li>
<li><p><code>Hash-based Sharding</code> : 샤딩 키를 해시 함수에 넣고, 그 결과값을 기준으로 저장할 샤드를 결정하는 방식</p>
</li>
<li><p><code>Composite Sharding</code> : 두 가지 이상의 샤딩 방식을 조합하여 사용하는 방식</p>
</li>
</ul>
<blockquote>
<h4 id="range-based-sharding-범위-기반-샤딩">Range-based Sharding (범위 기반 샤딩)</h4>
<p>Range-based Sharding은 샤딩 키의 값 범위를 기준으로 데이터를 나누는 방식</p>
<pre><code>user_id가 0~999인 사용자는 1번 샤드, 1000~1999인 사용자는 2번 샤드에 저장하는</code></pre><p>구조가 직관적이고, 샤딩 키의 범위와 조회 조건이 잘 맞을 때 효율적이다.</p>
<h4 id="주의-사항-문제점">주의 사항 (문제점)</h4>
<ul>
<li>샤딩 키가 순차적으로 증가하는 값이라면 문제가 생길 수 있음</li>
</ul>
<p><code>user_id</code>가 가입 순서대로 1, 2, 3처럼 증가한다면,<br>새로 가입하는 사용자의 데이터는 항상 마지막 범위의 샤드에 기록</p>
<p>→ <strong>쓰기 요청이 특정 샤드에 집중</strong>되어 부하가 고르게 분산되지 않는다.</p>
</blockquote>
<blockquote>
<h4 id="hash-based-sharding-해시-기반-샤딩">Hash-based Sharding (해시 기반 샤딩)</h4>
<p>Hash-based Sharding은 샤딩 키를 해시 함수에 넣고,</p>
<p>그 결과값을 기준으로 저장할 샤드를 결정하는 방식</p>
<pre><code>hash(user_id) % shard_count의 결과에 따라 0번, 1번, 2번 샤드 중 하나에 데이터를 저장</code></pre><p>해시 함수는 입력값을 비교적 고르게 분포시켜 데이터와 쓰기 부하를 여러 샤드에 분산시키는 데 유리 </p>
<p>Range-based Sharding에서 발생할 수 있는 특정 샤드 집중 문제를 완화할 수 있다.</p>
<h4 id="주의-사항-문제점-1">주의 사항 (문제점)</h4>
<ul>
<li>범위 조회에 불리<br><code>user_id</code> 1000번과 1001번은 해시 결과에 따라 서로 다른 샤드에 저장될 수 있다.<br><code>user_id</code> 1000번부터 2000번까지 조회하는 쿼리를 가정하면<br/><br>특정 샤드 하나만 보면 되는 것이 아니라, 모든 샤드를 확인해야 할 수 있다. (풀스캔 가능성)</li>
</ul>
</blockquote>
<blockquote>
<h4 id="composite-sharding-복합-샤딩">Composite Sharding (복합 샤딩)</h4>
<p>Composite Sharding은 두 가지 이상의 샤딩 방식을 조합하여 사용하는 방식</p>
<p>대표적으로</p>
<ul>
<li>먼저 Range-based Sharding으로 큰 범위를 나누고</li>
<li>그 범위 안에서 다시 Hash-based Sharding을 적용</li>
</ul>
<p>주문 데이터를 다룰 때, 주문 생성 월을 기준으로 먼저 데이터를 나눈 뒤, 
각 월 안에서 <code>user_id</code>나 <code>order_id</code>를 해싱하여 여러 샤드에 분산할 수 있다.</p>
<p>먼저 범위를 좁힐 수 있고, 
동일 범위 안에서 발생하는 쓰기 부하는 해시를 통해 여러 샤드에 분산</p>
<p>즉 Composite Sharding은 범위 조회의 장점과 해시 분산의 장점을 함께 활용 가능한 방식</p>
</blockquote>
<br/>

<h4 id="3-4-샤드-라우팅-방식">3-4) 샤드 라우팅 방식</h4>
<p><strong>샤드 라우팅(Shard Routing)</strong>은 요청이 들어왔을 때
해당 데이터가 어느 샤드에 있는지 판단하고, 올바른 샤드로 요청을 보내는 과정이다.</p>
<p>샤딩 환경에서는 데이터가 여러 DB 서버에 나누어 저장되기 때문에, 애플리케이션이나 중간 계층은
어떤 샤드에 접근해야 하는지 알아야 하며 이때 사용되는 방식이 샤드 라우팅이다.</p>
<blockquote>
<h4 id="1-애플리케이션-레벨-라우팅">1. 애플리케이션 레벨 라우팅</h4>
<p>애플리케이션 코드에서 직접 샤딩 규칙을 알고, 요청을 보낼 샤드를 결정하는 방식</p>
<ul>
<li>구조가 단순하고 빠르지만, 애플리케이션 코드가 샤딩 규칙에 강하게 의존</li>
</ul>
<ul>
<li>샤드 수가 변경되거나 샤딩 정책이 바뀌면 애플리케이션 코드와 설정도 함께 수정</li>
</ul>
</blockquote>
<blockquote>
<h4 id="2-라우팅-테이블-기반-라우팅">2. 라우팅 테이블 기반 라우팅</h4>
<p>별도의 라우팅 테이블을 두고, 특정 데이터가 어느 샤드에 저장되어 있는지 매핑 정보를 관리하는 방식</p>
<ul>
<li>요청이 들어오면 먼저 라우팅 테이블에서 샤드 정보를 찾고, 해당 샤드로 쿼리를 보냄</li>
</ul>
<ul>
<li>특정 데이터를 다른 샤드로 옮기더라도 라우팅 테이블만 갱신하면 되기에,
  샤드 재배치나 데이터 이동에 비교적 유연 </li>
</ul>
<ul>
<li>애플리케이션 코드에서 샤딩 로직을 줄일 수 있다는 장점</li>
</ul>
<ul>
<li>라우팅 테이블 자체를 조회하고 관리해야 하므로 추가 비용과 복잡도
 ⇒ 라우팅 테이블이 장애 지점이 되지 않도록 관리하는 것도 중요</li>
</ul>
</blockquote>
<blockquote>
<h4 id="3-샤딩-프록시-기반-라우팅">3. 샤딩 프록시 기반 라우팅</h4>
<p>애플리케이션과 DB 사이에 샤딩 프록시 또는 미들웨어를 두는 방식</p>
<ul>
<li>애플리케이션은 프록시에 쿼리를 보내고,
  프록시가 샤딩 키와 샤딩 규칙을 본 후 적절한 샤드로 요청을 전달</li>
</ul>
<ul>
<li>애플리케이션 코드에서 샤딩 로직을 줄일 수 있다는 장점</li>
</ul>
<ul>
<li>프록시 계층의 운영 복잡도가 추가되고, 프록시가 병목이나 장애 지점이 되지 않도록 구성</li>
</ul>
</blockquote>
<table>
<thead>
<tr>
<th>방식</th>
<th>설명</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>애플리케이션 레벨 라우팅</td>
<td>애플리케이션이 직접 샤드 결정</td>
<td>단순하고 빠름</td>
<td>샤딩 규칙 변경 시 코드 영향</td>
</tr>
<tr>
<td>라우팅 테이블 기반 라우팅</td>
<td>매핑 테이블로 샤드 위치 관리</td>
<td>데이터 이동에 유연</td>
<td>라우팅 테이블 관리 필요</td>
</tr>
<tr>
<td>샤딩 프록시 기반 라우팅</td>
<td>중간 프록시가 샤드 결정</td>
<td>애플리케이션 부담 감소</td>
<td>프록시 운영 복잡도 증가</td>
</tr>
</tbody></table>
<br/>

<h4 id="3-5-샤딩-적합-부적합-기준">3-5) 샤딩 적합, 부적합 기준</h4>
<p><strong><code>적합한 경우</code></strong>
샤딩은 단일 DB 서버가 저장 용량이나 읽기/쓰기 부하를 감당하기 어렵고,
데이터를 나눌 샤딩 키와 접근 패턴이 명확한 경우에 적합</p>
<pre><code>user_id 기준으로 사용자별 데이터를 자주 조회
tenant_id 기준으로 특정 고객사의 데이터만 자주 조회
order_id 또는 user_id 기준으로 주문 데이터를 조회
채팅방 ID 기준으로 메시지 데이터를 조회</code></pre><ul>
<li>단일 DB 서버의 저장 용량 한계에 가까워진 경우</li>
<li>단일 DB 서버의 읽기/쓰기 처리량이 한계에 도달한 경우</li>
<li>데이터와 트래픽을 여러 DB 서버에 분산해야 하는 경우</li>
<li>샤딩 키를 기준으로 데이터가 고르게 분산될 수 있는 경우</li>
<li>주요 쿼리 조건에 샤딩 키가 자주 포함되는 경우</li>
<li>함께 조회되거나 함께 변경되는 데이터를 같은 샤드에 배치할 수 있는 경우</li>
<li>특정 사용자, 테넌트, 채팅방, 주문처럼 데이터의 소유 단위가 명확한 경우</li>
<li>샤드 단위로 장애 영향 범위나 운영 범위를 줄이고 싶은 경우</li>
</ul>
<br/>

<p><strong><code>부적합한 경우</code></strong></p>
<p>샤딩 키와 조회 패턴이 맞지 않거나, 여러 샤드에 걸친 조회와 트랜잭션이 자주 발생하는 경우에 부적합</p>
<pre><code>user_id 기준으로 샤딩했지만 대부분 status나 created_at으로 조회
하나의 쿼리가 여러 샤드를 항상 조회해야 함
여러 샤드에 있는 데이터를 자주 JOIN해야 함
하나의 트랜잭션이 여러 샤드의 데이터를 동시에 변경해야 함
샤딩 키 값이 자주 변경됨</code></pre><ul>
<li>테이블 크기와 트래픽이 아직 단일 DB로 충분히 감당 가능한 경우</li>
<li>적절한 샤딩 키를 정하기 어려운 경우</li>
<li>샤딩 키 기준으로 데이터나 트래픽이 특정 샤드에 몰리는 경우</li>
<li>대부분의 쿼리에 샤딩 키가 포함되지 않는 경우</li>
<li>Scatter-Gather Query가 자주 발생하는 경우</li>
<li>Cross-shard Join이 자주 필요한 경우</li>
<li>Cross-shard Transaction이 자주 필요한 경우</li>
<li>샤딩 키가 자주 변경되어 데이터 이동 비용이 큰 경우</li>
<li>샤드 추가, 리샤딩, 백업, 장애 대응을 운영하기 어려운 경우</li>
<li>데이터 정합성보다 단순한 JOIN과 트랜잭션 처리가 더 중요한 경우</li>
</ul>
<br/>

<hr>
<h3 id="4-복제replication">4. 복제(Replication)</h3>
<h4 id="4-1-복제란">4-1) 복제란</h4>
<p>복제(Replication)는 하나의 DB 서버에 있는 데이터를
다른 DB 서버에 복사하여 동일한 데이터를 여러 서버에 유지하는 방식이다.</p>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/e25c84eb-ad26-4484-93c8-d06558b851a5/image.png" alt=""></p>
<ul>
<li>원본 데이터를 처리하는 서버를 <code>Primary</code>, <code>Master</code></li>
<li>원본의 데이터를 복사해 따라가는 서버를 <code>Replica</code>, <code>Slave</code></li>
</ul>
<p><code>Primary</code>에서 데이터 변경이 발생하면, 그 변경 내역이 <code>Replica</code>에 전달되고,
<code>Replica</code>는 이를 반영하여 <code>Primary</code>와 같은 상태를 유지한다.</p>
<blockquote>
<p>복제의 주 목적은 <strong>고가용성 확보</strong>와 <strong>읽기 부하 분산</strong>이다.</p>
</blockquote>
<p><code>Primary</code>에 장애가 발생했을 경우,
<code>Replica</code>를 새로운 <code>Primary</code>로 승격하면 서비스 중단 시간을 줄일 수 있다.</p>
<p>쓰기 작업은 <code>Primary</code>에서 처리하고, 읽기 작업은 <code>Replica</code>로 분산하여 <code>Primary</code>의 부하를 줄일 수 있다.</p>
<p>복제는 데이터를 여러 DB 서버에 복사해 두어 장애 대응 능력과 읽기 처리 성능을 높이는 방법이다.</p>
<blockquote>
<p><strong>가용성</strong> : 서버나 네트워크, DB등의 인프라나 시스템이 오랜 기간동안 지속적으로 정상 운영 가능한 성질</p>
</blockquote>
<br/>

<h4 id="4-2-복제-동작-방식">4-2) 복제 동작 방식</h4>
<p>복제는 Primary에서 발생한 데이터 변경 사항을 Replica가 따라가며 같은 상태로 맞추는 방식으로 동작한다.</p>
<p>먼저 Primary에서 <code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code> 같은 변경 작업이 발생하면</p>
<p>Primary는 해당 변경 내역을 로그에 기록한다. MySQL에서는 이를 <strong>binary log</strong>라고 부른다.</p>
<p>Replica는 Primary의 변경 로그를 가져오고, 그 로그에 기록된 작업을 자신의 DB에 순서대로 반영한다.</p>
<p>이 과정을 통해 Replica는 Primary의 데이터를 복사해 같은 상태로 유지한다.</p>
<pre><code>Primary :
    데이터 변경 발생
    변경 로그 기록 (바이너리 로그)

Replica
    변경 로그 확인 (바이너리 로그)
    변경 내용 반영</code></pre><p>이러한 과정을 통해서 replica는 primary의 변경을 따라가면서 같은 상태로 수렴하게 된다.</p>
<blockquote>
<h4 id="복제의-문제와-주의사항-복제-지연">복제의 문제와 주의사항, 복제 지연</h4>
<p>Replica가 Primary의 변경 사항을 따라가는 과정에서는 지연이 발생할 수 있다.
이를 <strong>복제 지연(Replication Lag)</strong>이라고 한다.</p>
<p>네트워크 통신과 로그 반영 과정을 거치기 때문에 짧더라도 시간 차이가 생길 수 있다.</p>
<p>또한 다음과 같은 상황에서는 복제 지연이 더 커질 수 있다.</p>
<ul>
<li>Primary와 Replica 사이의 네트워크 지연이 있는 경우</li>
<li>Replica에 읽기 부하가 많이 걸린 경우</li>
<li>큰 트랜잭션을 처리해야 하는 경우</li>
<li>Primary에서 짧은 시간에 많은 변경 작업이 발생한 경우</li>
</ul>
<p>복제 지연이 발생하면 Primary에는 이미 반영된 데이터가
Replica에는 아직 반영되지 않았을 수 있다.</p>
<p>즉, 사용자가 방금 변경한 데이터를 바로 조회했을 때 이전 데이터를 보게 만드는 문제를 일으킬 수 있다.</p>
<p>이를 <strong>read-after-write consistency(쓰기 직후 읽기 일관성)</strong> 문제라고 볼 수 있다.</p>
</blockquote>
<br/>

<h4 id="4-3-복제-지연-문제를-해결하기-위한-전략">4-3) 복제 지연 문제를 해결하기 위한 전략</h4>
<p>복제 지연은 완전히 없애기 어렵기 때문에, 
서비스의 정합성 요구사항에 따라 읽기 경로와 복제 방식을 조정해야 한다.</p>
<ul>
<li><strong>중요한 쓰기 직후의 읽기는 Primary로</strong>
사용자가 데이터를 변경한 직후, 바로 같은 데이터를 조회해야 하는 경우
해당 읽기 요청을 Replica가 아닌 Primary로 보내기</li>
</ul>
<ul>
<li><strong>복제 지연을 모니터링</strong>
  Replica가 Primary를 얼마나 늦게 따라가고 있는지 지속적으로 확인
  복제 지연이 일정 기준 이상 커지면 해당 Replica를 읽기 대상에서 제외</li>
</ul>
<ul>
<li><strong>동기 복제 또는 준동기 복제를 고려</strong>
  더 강한 정합성이 필요한 경우, Primary의 변경 사항이 Replica에 반영되었는지 
  확인한 뒤 쓰기 작업을 완료하는 복제 방식을 고려할 수 있다.</li>
</ul>
<br/>

<h4 id="4-4-동기-복제-준동기-복제">4-4) 동기 복제, 준동기 복제</h4>
<p>동기 복제와 준동기 복제는 복제 지연과 데이터 손실 가능성을 줄이기 위한 복제 방식이다.</p>
<blockquote>
<h4 id="비동기-복제">비동기 복제</h4>
<p>비동기 복제는 Primary가 자기 DB에 변경을 반영하면 바로 클라이언트에게 성공 응답</p>
<pre><code></code></pre></blockquote>
<ol>
<li>클라이언트가 Primary에 쓰기 요청</li>
<li>Primary가 변경 반영</li>
<li>Primary가 클라이언트에게 성공 응답</li>
<li>이후 Replica가 변경 로그를 가져가 반영<blockquote>
<pre><code>⇒ 앞서 이해한 기본 복제 방식에 해당한다.

빠르지만 쓰기 직후 읽기 일관성 문제가 있으며,
**Primary가 장애 나기 직전에 Replica가 아직 변경 내용을 못 가져갔다면 데이터 손실이 발생**한다.
</code></pre><p>Primary에는 반영됨
Replica에는 아직 반영 안 됨
그 순간 Primary 장애
→ Replica 승격 시 방금 쓴 데이터가 사라질 수 있음</p>
<pre><code></code></pre></blockquote>
</li>
</ol>
<blockquote>
<h4 id="동기-복제">동기 복제</h4>
<p>동기 복제는 Primary의 변경 사항이 Replica에도 반영되어야 쓰기 성공으로 처리하는 방식이다.</p>
<pre><code></code></pre></blockquote>
<ol>
<li>클라이언트가 Primary에 쓰기 요청</li>
<li>Primary가 변경 반영</li>
<li>Replica에도 변경 반영</li>
<li>Replica 반영 확인</li>
<li>Primary가 클라이언트에게 성공 응답<blockquote>
<pre><code>
- 데이터 정합성이 강하다
- Primary에 장애가 나도 Replica가 같은 데이터를 가지고 있을 가능성이 높다

**느리다 ( 복제 지연에서 발생 가능한 문제를 해결하기 위해 복제 지연을 늘리는)**

- Replica 응답까지 기다려야 하여 쓰기 지연 시간이 늘어난다
- Replica나 네트워크가 느려지면 Primary의 쓰기 성능도 같이 영향을 받는다</code></pre></blockquote>
</li>
</ol>
<blockquote>
<h4 id="준동기-복제">준동기 복제</h4>
<p>준동기 복제는 비동기와 동기의 중간 방식,
“Replica가 최소한 변경 내용을 받았는가?”를 확인하는 방식</p>
<pre><code></code></pre></blockquote>
<ol>
<li>클라이언트가 Primary에 쓰기 요청</li>
<li>Primary가 변경 반영</li>
<li>Replica가 변경 로그 수신</li>
<li>Replica가 로그 받았다고 응답</li>
<li>Primary가 클라이언트에게 성공 응답</li>
<li>Replica가 나중에 자기 DB에 반영<blockquote>
<pre><code>
- Primary가 변경을 수행한 뒤, Replica가 **변경 로그를 받았다는 것까지는 확인**하고
클라이언트에게 성공 응답을 준다.

- DB에 완전히 적용 완료했는지까지 기다리지는 않는 경우가 많다.</code></pre></blockquote>
</li>
</ol>
<table>
<thead>
<tr>
<th>방식</th>
<th>Primary 성공 응답 시점</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>비동기 복제</td>
<td>Primary만 반영 후</td>
<td>빠름</td>
<td>데이터 손실 가능성 큼</td>
</tr>
<tr>
<td>준동기 복제</td>
<td>Replica가 로그 수신 확인 후</td>
<td>손실 가능성 감소</td>
<td>쓰기 지연 증가</td>
</tr>
<tr>
<td>동기 복제</td>
<td>Replica 반영까지 확인 후</td>
<td>정합성 강함</td>
<td>가장 느림</td>
</tr>
</tbody></table>
<br/>

<hr>
<h3 id="5-전략-비교-정리">5. 전략 비교 정리</h3>
<p>파티셔닝, 샤딩, 복제는 모두 데이터베이스의 확장성과 안정성을 높이기 위한 전략이지만,
해결하려는 문제와 적용 방식이 다르다.</p>
<ol>
<li><p><strong>파티셔닝</strong>은 주로 단일 DB 내에서 하나의 큰 테이블이나 인덱스를 더 작은 단위로 나누어 관리하는 방식</p>
<p> 주로 대용량 테이블의 조회 성능과 관리 효율을 높이는 데 사용</p>
</li>
</ol>
<br/>

<ol start="2">
<li><p><strong>샤딩</strong>은 데이터를 수직 파티셔닝처럼 데이터를 나누어 여러 DB 서버에 저장하는 방식</p>
<p> 단일 DB 서버의 저장 용량과 읽기/쓰기 처리량 한계를 넘어서기 위한 수평 확장 전략</p>
</li>
</ol>
<br/>

<ol start="3">
<li><p><strong>복제</strong>는 같은 데이터를 여러 DB 서버에 복사해 두는 방식</p>
<p> 주로 읽기 부하를 분산하고, 장애 발생 시 서비스를 지속하기 위한 고가용성 전략</p>
</li>
</ol>
<table>
<thead>
<tr>
<th>구분</th>
<th>파티셔닝</th>
<th>샤딩</th>
<th>복제</th>
</tr>
</thead>
<tbody><tr>
<td>핵심 개념</td>
<td>데이터를 작은 파티션으로 나눔</td>
<td>데이터를 샤드로 여러 DB 서버에 나눔</td>
<td>데이터를 여러 DB 서버에 복사</td>
</tr>
<tr>
<td>데이터 저장 방식</td>
<td>하나의 DBMS 내부에서 분할</td>
<td>여러 샤드에 분산 저장</td>
<td>Primary 데이터를 Replica에 복사</td>
</tr>
<tr>
<td>주요 목적</td>
<td>대용량 테이블 관리, 조회 범위 축소</td>
<td>저장 용량 확장, 읽기/쓰기 부하 분산</td>
<td>읽기 부하 분산, 고가용성 확보</td>
</tr>
<tr>
<td>데이터 중복</td>
<td>보통 없음</td>
<td>보통 없음</td>
<td>있음</td>
</tr>
<tr>
<td>읽기 확장</td>
<td>제한적</td>
<td>가능</td>
<td>강함</td>
</tr>
<tr>
<td>쓰기 확장</td>
<td>제한적</td>
<td>가능</td>
<td>기본적으로 Primary 중심</td>
</tr>
<tr>
<td>장애 대응</td>
<td>장애 대응 목적이 아님</td>
<td>샤드 단위 장애 격리 가능</td>
<td>Replica 승격으로 장애 대응</td>
</tr>
<tr>
<td>주요 문제</td>
<td>파티션 키 선택, 파티션 관리</td>
<td>샤드 라우팅, Cross-shard Join/Transaction</td>
<td>복제 지연, 데이터 정합성</td>
</tr>
<tr>
<td>애플리케이션 영향</td>
<td>보통 적음</td>
<td>큼</td>
<td>읽기/쓰기 라우팅 필요</td>
</tr>
<tr>
<td>적합한 상황</td>
<td>큰 테이블을 효율적으로 관리하고 싶을 때</td>
<td>단일 DB 서버 한계를 넘어서야 할 때</td>
<td>읽기 요청이 많거나 장애 대응이 중요할 때</td>
</tr>
</tbody></table>
<hr>
<blockquote>
<p><strong>Q.</strong> 수직 파티셔닝과 수평 파티셔닝의 차이?</p>
<p><strong>A.</strong> 수직 파티셔닝은 테이블의 컬럼을 기준으로 데이터를 나누는 방식이고,
수평 파티셔닝은 테이블의 row를 기준으로 데이터를 나누는 방식이다.
수직 파티셔닝은 컬럼 구조가 나뉘고, 수평 파티셔닝은 컬럼 구조는 유지한 채 row 집합이 나뉜다.</p>
</blockquote>
<blockquote>
<p><strong>Q.</strong> 샤딩이 수평 파티셔닝과 어떻게 연결되는지?</p>
<p><strong>A.</strong> 샤딩은 수평 파티셔닝처럼 row를 나누는 방식이다.
다만 수평 파티셔닝이 보통 하나의 DBMS 내부에서 row를 나누는 것이라면,
샤딩은 row를 여러 DB 서버에 분산 저장하는 방식이다.</p>
</blockquote>
<blockquote>
<p><strong>Q.</strong> 복제가 샤딩과 달리 읽기 부하 분산과 고가용성에 초점을 두는 이유?</p>
<p><strong>A.</strong> 복제는 샤딩처럼 데이터를 나누는 것이 아니라,
같은 데이터를 여러 DB 서버에 복사해 두는 방식이기에
Replica로 읽기 요청을 분산할 수 있고, 
Primary 장애 시 Replica를 승격해 서비스 중단 시간을 줄이는 것에 초점을 둔다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[트랜잭션(Transaction)의 격리 수준과 동시성]]></title>
            <link>https://velog.io/@zayn_noh/Transaction-Isolation-Level</link>
            <guid>https://velog.io/@zayn_noh/Transaction-Isolation-Level</guid>
            <pubDate>Wed, 10 Jun 2026 03:47:46 GMT</pubDate>
            <description><![CDATA[<p>데이터베이스에서 <strong>트랜잭션(Transaction)</strong>은 데이터 정합성을 지키기 위한 핵심 개념이다.</p>
<p>서비스 로직은 보통 여러 개의 SQL 작업으로 이루어지지만,
비즈니스 관점에서는 이 작업들이 하나의 단위로 함께 성공하거나 함께 실패해야 하는 경우가 많다.</p>
<p>이때 트랜잭션은 여러 작업을 하나의 논리적 단위로 묶어 데이터가 잘못된 상태로 남지 않도록 보장한다.
그리고 이러한 트랜잭션의 안정성을 설명하는 대표적인 특성이 <strong>ACID</strong>이다.</p>
<p>이번 포스트에서는 트랜잭션의 기본 개념과 상태, ACID 속성을 먼저 정리하고</p>
<p>그중 동시성 문제와 가장 밀접한 Isolation을 중심으로,
<strong>Isolation Level</strong>과 <strong>트랜잭션 이상 현상</strong>이 어떤 관계를 가지는지 살펴본다.</p>
<hr>
<h3 id="1-트랜잭션transaction과-acid-속성">1. 트랜잭션(Transaction)과 ACID 속성</h3>
<h4 id="1-1-트랜잭션의-정의">1-1) 트랜잭션의 정의</h4>
<p><strong>트랜잭션(Transaction)이란</strong> 비즈니스 로직, 정책 특성상 나누어지면 안되는 하나의 작업 단위를 말한다.</p>
<p>이를 데이터베이스에서의 관점에서 바라보면, DB와의 상호작용 단위이자, 일련의 트랜잭션 과정 속에서
특정 데이터를 다룰 때 절대 나뉘어져서는 안되는 논리적인 작업의 최소 단위라 할 수 있다.</p>
<p>예시를 통해 이를 이해하면,
어떤 사용자가 상품을 구매하는 일련의 과정을 하나의 트랜잭션으로 다룰 수 있다.</p>
<ul>
<li>상품 재고 확인</li>
<li>사용자 통장에서 잔액 차감 <code>통장 잔액 - 상품 가격</code></li>
<li>상품 재고 감소 <code>-1</code></li>
<li>판매자 통장에 잔액 추가 <code>통장 잔액 + 상품 가격</code></li>
</ul>
<p>만약 위 과정에서 사용자 통장의 잔액이 빠져나갔지만, 이후 과정에서 에러가 발생한다면
사용자는 주문을 했음에도 상품을 받지 못하고 통장에서 잔액만 빠져나갈 수 있다.</p>
<p>이러한 일련의 과정은 모두 동시에 성공하거나,
하나라도 실패하면 전체 과정을 없던 일로 해야 올바른 서비스 로직이 될 수 있다.</p>
<p>이에 <strong>트랜잭션(Transaction)</strong>이라는 하나의 흐름으로 작업의 단위를 정의하고 다룰 수 있다.</p>
<blockquote>
<p>트랜잭션은 여러 SQL 쿼리를 하나의 논리적 묶음으로 만들어 <strong>전부 성공</strong>하거나 <strong>전부 실패</strong>를 보장한다.</p>
</blockquote>
<br/>

<h4 id="1-2-트랜잭션의-상태">1-2) 트랜잭션의 상태</h4>
<p>트랜잭션은 로직 처리 과정에서 각각 다음과 같은 상태를 가질 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/e620ec44-807f-4284-8414-516baddccf15/image.png" alt=""></p>
<ul>
<li><strong>Active</strong>
  트랜잭션이 시작되어 SQL 작업을 수행 중인 Active 상태(활성 상태)
  이는 트랜잭션의 시작점이자 <strong>지금부터 시작하는 SQL 쿼리들은 동일 단위 트랜잭션에 포함됨을</strong> 의미한다.</li>
</ul>
<ul>
<li><strong>Partially Committed</strong>
  단어의 뜻은 부분적 완료 상태를 의미하며, <strong>SQL 모든 작업이 성공적으로 마지막까지 실행된 상태</strong>이다.
  모든 작업이 끝났기에 마지막 확정(<code>Committed</code>)만을 기다린다.</li>
</ul>
<ul>
<li><p><strong>Committed</strong>
  <code>Commit</code> 명령이 실행되면 트랜잭션은 Committed, 완료 상태가 된다.
  이후 수정되거나 추가, 삭제된 데이터가 DB에 영구적으로 저장되고 지속성을 갖는 데이터가 된다.</p>
</li>
<li><p><strong>Failed</strong>
  트랜잭션 수행 중 오류나 장애가 발생해 더 이상 정상적으로 진행할 수 없는 상태이다.
  이후 변경사항을 되돌리기 위해 rollback 과정으로 넘어간다.
  <strong>오류의 종류</strong>나 <strong>DBMS(<code>PostgreSQL</code> <code>MySQL</code> etc.)</strong>에 따라 트랜잭션 전체가 실패 처리되는 지,
  해당 SQL만 실패하고 트랜잭션은 계속 가능한지 처리가 다를 수 있다.</p>
</li>
</ul>
<ul>
<li><p><strong>Aborted</strong>
  <code>Failed</code> 상태에서 <strong>ROLLBACK</strong>이 수행되어 트랜잭션 시작 이전 상태로 데이터가 되돌아간 상태이다.
즉, <strong>실패한 트랜잭션의 변경사항이 모두 철회된 상태</strong>를 의미한다.</p>
</li>
<li><p><strong>Terminated</strong>
  <strong>트랜잭션 생명주기의 최종 종료 상태</strong>
   트랜잭션이 <strong>commit</strong> 또는 <strong>rollback</strong> 이후 완전히 종료된 상태를 말한다.</p>
</li>
</ul>
<blockquote>
<p><strong>왜 Partial Committed와 Committed 상태가 나뉘어 있나?</strong></p>
<p>트랜잭션 내부 모든 SQL 작업이 성공적으로 실행되었다고 하여
그 결과가 즉시 영구적으로 확정되는 것은 아니다.
모든 작업이 끝난 뒤에도 commit 로그 기록, 디스크 반영, 장애 복구를 위한 정보 저장 같은
<strong>커밋 처리</strong>가 성공해야 비로소 트랜잭션은 Committed 상태가 된다.</p>
<p>이 <strong>커밋 처리 과정에서 오류가 발생하면 트랜잭션은 Failed 상태로 전이</strong>될 수 있다.
따라서 <code>Partially Committed</code> 상태는 
“작업은 모두 끝났지만 <strong>아직 최종 확정은 되지 않은 상태</strong>”를 구분하기 위해 존재한다.</p>
</blockquote>
<p>트랜잭션은 단순히 여러 SQL 구문을 하나로 묶는 것이라 보면 안된다.
비즈니스적 관점에서 &quot;해당 작업은 모두 동시에 성공하거나, 실패하여야 한다&quot;라고 보는 작업의 단위를
데이터베이스 속 데이터의 관점에서 지키는 장치 역할을 하는 것이라 생각해야 한다.</p>
<br/>

<h4 id="1-3-트랜잭션의-범위-설정">1-3) 트랜잭션의 범위 설정</h4>
<p>트랜잭션은 보통 개발자가 비즈니스 규칙, 논리에 따라서 어디까지 묶고 처리할지를 결정한다.</p>
<p>이를 <strong>트랜잭션의 범위</strong>를 결정한다고 하며
이러한 범위는 언제나, <strong>가능한 최소 범위로 잡고</strong> SQL 작업을 처리해야 한다.</p>
<blockquote>
<p><strong>데이터베이스의 커넥션 풀(Connection Pool)</strong></p>
<p>백엔드 서버 어플리케이션은 DB와 통신을 위해 커넥션(Connection)을 사용한다.</p>
<p>하지만 모든 스레드에서 DB 데이터 관련 요청이 들어올 때마다, DB 커넥션을 새로 생성하고 종료한다면
비용이 크기 때문에 보통 미리 일정 개수의 커넥션을 만들어두고 재사용한다.
이를 <strong>커넥션 풀(Connetion Pool)</strong>이라 한다.</p>
<p>트랜잭션이 시작되면 일반적으로 <strong>하나의 DB 커넥션을 점유한 상태</strong>로 작업이 진행된다.
특히 내부에서 외부 API 호출, 파일 처리, 긴 연산, 사용자 응답 대기 같은 작업을 수행하면
실질적으로 <strong>DB 작업을 하지 않는 동안에도 커넥션을 점유</strong>하게 된다.
이는 <strong>커넥션 풀 고갈, 요청 지연, 전체 처리량 감소</strong>와 같은 성능 문제로 이어질 수 있다.</p>
<p>따라서 트랜잭션은 <strong>데이터 정합성을 보장해야 하는 DB 작업만 묶고</strong>,
가능한 짧은 범위로 설정하는 것이 좋다.</p>
</blockquote>
<br/>

<h4 id="1-4-트랜잭션이-가지는-특성--acid">1-4) 트랜잭션이 가지는 특성 : ACID</h4>
<p>트랜잭션은 데이터의 <code>무결성</code>과 <code>일관성</code>을 보장하고,
<strong>여러 작업을 하나의 논리적인 작업 단위로 묶어서 처리</strong>한다. <strong>ACID</strong>는 이러한 트랜잭션의 네 가지 특성을 말한다.</p>
<ul>
<li><p><strong>Atomicity(원자성)</strong>
  <code>All or Nothing</code>이라 할 수 있는 트랜잭션의 대표적인 특성을 말한다.
  트랜잭션에 포함된 작업은 전부 성공하거나, 단 하나도 실행되지 않은 것처럼 모두 취소되어야 하며
  부분적으로 성공하는 상태는 절대 허용하지 않는다.</p>
</li>
<li><p><strong>Consistency(일관성)</strong>
  트랜잭션이 성공적으로 끝난 이후 데이터베이스는 일관된 상태를 유지해야한다는 특성이다.
  이때 일관성이란 특정 데이터 몇 개만이 아니라,
  DB에 정의된 제약조건과 비즈니스 규칙을 포함하여 유효한 상태를 말한다.</p>
<pre><code class="language-sql">예를 들어 사용자의 이름에는 숫자가 들어갈 수 없다는 규칙이 있다고 가정하자.
이 규칙이 DB의 CHECK 제약조건이나 애플리케이션 로직으로 보장되고 있다면,
트랜잭션 처리 결과로 특정 사용자의 이름에 숫자가 들어가는 것은 일관성을 깨는 결과가 된다.
따라서 이러한 변경은 제약조건 위반으로 실패하거나,
애플리케이션 검증 단계에서 차단되어 트랜잭션이 롤백되어야 한다.</code></pre>
</li>
<li><p><strong>Isolation(격리성)</strong>
  동시에 여러 트랜잭션이 실행될 때,
  <strong>각 트랜잭션은 다른 트랜잭션의 작업으로 인해 영향을 받지 않아야 한다</strong>는 특성이다.
  즉, 여러 트랜잭션이 동시에 실행되더라도
  <strong>각각의 트랜잭션은 독립적으로 실행되는 것처럼 보장</strong>되어야 한다.</p>
<p>  이러한 격리성 문제는 동시성 문제와도 밀접하게 관련되어 있는데, 격리성이 충분히 보장되지 않으면</p>
<ul>
<li>하나의 트랜잭션이 아직 커밋되지 않은 데이터를 읽거나</li>
<li>트랜잭션 내에서 동일 데이터를 다시 조회했을 때 값이 달라지거나</li>
<li>기존에 없던 row가 새로 나타나는 등 <strong>이상 현상</strong>이 발생할 수 있다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>데이터베이스는 <strong>이러한 문제(이상 현상)를</strong> 제어하기 위해
트랜잭션의 <strong>격리 수준(Isolation Level)</strong>을 옵션으로 제공한다.</p>
</blockquote>
<ul>
<li><p><strong>Durability(지속성)</strong>
  트랜잭션이 성공적으로 커밋되었다면, 이후 시스템 장애가 발생하더라도
  <strong>해당 변경사항은 데이터베이스에 영구적으로 보존되어야 한다는 특성</strong>이다.</p>
<p>  즉, <strong>커밋된 데이터는 서버가 종료되거나 장애가 발생하더라도 사라지지 않아야 한다</strong>.</p>
<p>  데이터베이스는 이를 보장하기 위해 변경 내용을 디스크에 바로 반영하기 전,
  먼저 로그 파일에 기록하는 방식으로 복구 정보를 남긴다.
  장애가 발생하더라도 DBMS는 이 로그를 기반으로 커밋된 트랜잭션의 변경사항을 복구할 수 있다.</p>
</li>
</ul>
<br/>

<hr>
<h3 id="2-isolation-level과-트랜잭션-이상-현상">2. Isolation Level과 트랜잭션 이상 현상</h3>
<p>앞서 살펴보았던 ACID 중 I, Isolation(격리성)을 보장하기 위해,
데이터베이스에서 제공하는 네 가지 <strong>격리 수준(Isolation)</strong> 옵션들에 대해서 알아보도록 하겠다.</p>
<p>이때 격리 수준이란 여러 트랜잭션이 동시에 실행될 때,
<strong>서로 다른 작업 내용을 어디까지 보지 못하게 할 것인지를 정한 기준</strong>이라 생각하면 된다.
<em>“트랜잭션 간 데이터 간섭을 얼마나 허용할 것인가&quot;</em></p>
<blockquote>
<p><strong>무조건 격리 수준이 높은게 좋은 것 아닐까</strong></p>
<p>트랜잭션의 격리 수준이 높으면 여러 트랜잭션이 동시에 실행될 때
발생할 수 있는 이상 현상을 더 강하게 막을 수 있다. 하지만</p>
<p>격리 수준이 높다면 동시성과 연관되는 문제로 이어져 성능적 문제로 이어질 수 있다.</p>
<p><strong>격리 수준이 높은 경우</strong>
<code>동시성 하락</code>, <code>락 유지 시간 증가</code>, <code>다른 트랜잭션의 대기 시간 증가</code>, <code>처리 성능 감소</code></p>
</blockquote>
<blockquote>
<p><strong>트랜잭션 이상 현상</strong></p>
<p><code>Dirty Read</code>
두 개의 트랜잭션이 있을 때, <strong>다른 트랜잭션이 수정한 아직 커밋하지 않은 데이터를 읽는 현상</strong></p>
<p><em>다른 트랜잭션이 수정한 데이터를 읽었는데,</em>
<em>해당 트랜잭션이 롤백되어 잘못된 데이터를 사용하는 경우 문제 발생</em></p>
<p><code>Non-Repeatable Read</code>
하나의 트랜잭션 안에서 같은 데이터를 <strong>여러 번 조회했는데, 각 조회 결과가 다르게 나오는 현상</strong></p>
<p><code>Phantom Read</code>
하나의 트랜잭션 안에서 <strong>특정 범위의 데이터를 여러 번 조회</strong>했는데
<strong>첫 조회에서 없었던 유령같은 데이터가 나타나는 현상</strong></p>
</blockquote>
<p><strong>1. Read Uncommitted</strong></p>
<p><strong>사실상 격리성을 보장해주지 않는 옵션</strong>에 해당한다.
트랜잭션 중 한 트랜잭션이 아직 커밋하지 않은 값을, 다른 트랜잭션이 그대로 조회하는 것을 허용한다.</p>
<p><strong>데이터의 정합성이 쉽게 깨질 수 있어</strong> 거의 사용되지 않는다.</p>
<p>발생 가능 이상 현상 : <code>Dirty Read</code>, <code>Non-Repeatable Read</code>, <code>Phantom Read</code></p>
<p><strong>2. Read Committed</strong></p>
<p>대부분의 RDBMS에서 사용하는 기본 격리 수준이다.
두 트랜잭션 중 한 트랜잭션이 값을 변경하고 커밋하면, 다른 한 트랜잭션이 다시 조회했을 때 값이 바뀔 수 있다.</p>
<p>즉, 다른 트랜잭션 내부에서, 커밋되기 전에 변경된 데이터는 읽지 않는다.</p>
<p>발생 가능 이상 현상 : <code>Non-Repeatable Read</code>, <code>Phantom Read</code></p>
<p><strong>3. Repeatable Read</strong></p>
<p>트랜잭션 중 한 트랜잭션이 값을 변경하고 커밋하더라도, 다른 트랜잭션이 다시 조회하면 값이 바뀌지 않는다.</p>
<p>즉 개별 <strong>트랜잭션마다 안에서 데이터를 조회 할 때 일관된 스냅샷을 보여주도록 보장</strong>한다.</p>
<p>하나의 트랜잭션 안에서 데이터의 일관성이 매우 중요하거나,
여러 단계에 걸쳐 데이터를 조회하고 계산해야하는 경우에는 해당 격리 수준이 사용된다.</p>
<p>발생 가능 이상 현상 : <code>Phantom Read</code></p>
<p><strong>4. Serializable</strong></p>
<p><code>Serializable</code>은 가장 높은 격리 수준을 가지며,
<strong>트랜잭션이 순차적으로 실행되는 것처럼 처리</strong>되어, 다른 트랜잭션이 <strong>중간에 끼어들 수 없다</strong>.</p>
<p>여러 트랜잭션이 동시에 실행되더라도 결과적으로는 순차 실행된 것과 같은 결과를 보장하는 격리 수준이다.</p>
<p>이를 위해 DBMS는 충돌 가능성이 있는 읽기/쓰기 작업에 락을 걸거나,
충돌을 감지해 트랜잭션을 실패시킬 수 있다.</p>
<hr>
<h3 id="3-격리-수준과-동시성">3. 격리 수준과 동시성</h3>
<blockquote>
<p>결국 트랜잭션의 격리 수준은 <strong>데이터 정합성</strong>과 <strong>동시성</strong> 사이의 균형을 조절하는 기준이다.</p>
</blockquote>
<p><strong>격리 수준이 낮을수록</strong> 여러 트랜잭션이 동시에 처리될 수 있는 범위가 넓어지기 때문에
<strong>동시성은 높아지고 성능상 이점</strong>이 생길 수 있다.</p>
<p>하지만 그만큼 Dirty Read, Non-Repeatable Read,
Phantom Read 같은 <strong>이상 현상이 발생할 가능성도 커진다</strong>.</p>
<br/>

<p><strong>반대로 격리 수준이 높을수록</strong> 트랜잭션 간 간섭은 줄어들고 <strong>데이터 정합성은 더 강하게 보장</strong>된다.
하지만 이를 위해 락을 더 오래 유지하거나, 충돌 가능성이 있는 작업을 대기시키거나,
경우에 따라 트랜잭션을 실패시켜 재시도해야 할 수 있다.
그 결과 <strong>동시성은 낮아지고 전체 처리량이 감소</strong>할 수 있다.</p>
<p>따라서 격리 수준은 무조건 높게 설정하는 것이 아니라,
서비스에서 요구하는 정합성 수준과 성능 요구사항을 함께 고려해 선택해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인덱스(Index)의 개념 및 동작 원리와 설계 시 고려 조건]]></title>
            <link>https://velog.io/@zayn_noh/index1</link>
            <guid>https://velog.io/@zayn_noh/index1</guid>
            <pubDate>Tue, 09 Jun 2026 06:15:08 GMT</pubDate>
            <description><![CDATA[<p>데이터베이스 성능을 이야기할 때 가장 자주 등장하는 개념 중 하나가 <strong>인덱스(Index)</strong> 이다.</p>
<p>인덱스를 사용하면 조회 성능을 크게 개선할 수 있지만,
단순히 “자주 조회하는 컬럼에 인덱스를 걸면 된다” 정도로 이해하고 사용하면
오히려 <strong>기대한 효과를 얻지 못하거나 불필요한 오버헤드</strong>만 늘어날 수 있다.</p>
<p>인덱스는 결국 데이터를 빠르게 찾기 위한 자료구조이기에</p>
<ul>
<li>어떤 구조로 동작하며,</li>
<li>어떤 컬럼에 만들어야 효과적인지,</li>
<li>여러 컬럼을 묶을 때는 어떤 순서로 설계해야 하는지를 함께 이해해야 한다.</li>
</ul>
<p>이번 포스트에서는 인덱스를 단순히 “조회 성능을 높이는 기능”으로만 보지 않고,
인덱스를 왜 사용하는지와 어떤 기준으로 설계해야 하는지를 목표로 정리해보려 한다.</p>
<hr>
<h3 id="1-인덱스index의-자료구조">1. 인덱스(Index)의 자료구조</h3>
<h4 id="1-1-이진-탐색-트리binary-search-tree">1-1) 이진 탐색 트리(Binary Search Tree)</h4>
<p>인덱스는 <strong>트리 구조</strong>를 가진다. 인덱스에서 실제로 사용하는 트리 구조를 알아보기 전에
먼저 <strong>이진 탐색 트리(Binary Search Tree)</strong>에 대해 알아보면</p>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/8a5fff52-191f-43b4-b1ac-ff5bd3ac3568/image.png" alt=""></p>
<p>이진 탐색 트리는 각 노드가 최대 두 개의 노드를 가지며, 값의 크기 관계에 따라 데이터를 배치한다.</p>
<p>트리 내 어떤 노드냐에 상관없이 모두 다음과 같은 규칙 하에 구조를 이룬다.</p>
<blockquote>
<p><strong>왼쪽 서브트리의 모든 값 &lt; 현재 노드의 값 &lt; 오른쪽 서브 트리의 모든 값</strong></p>
</blockquote>
<p>이진 탐색 트리에서 값을 찾을 때는 다음과 같은 과정을 거친다. (상단 이미지 기준)</p>
<ol>
<li>자료구조에서 <strong>7</strong>을 찾고자 한다 가정</li>
<li>루트노드의 값(8)과 찾고자 하는 값(7)을 비교한다.</li>
<li>찾고자 하는 값(8)이 루트 노드의 값(7)보다 작기에 좌측 노드로 이동한다.</li>
<li>이동한 노드의 값(3)과 찾고자 하는 값(7)을 비교한다.</li>
<li>찾고자 하는 값(7)이 위치한 노드의 값(3)보다 크기에 우측 노드로 이동한다.</li>
<li>찾고자 하는 값(7)과 해당 노드의 값(6)을 비교한다.</li>
<li>찾고자 하는 값(7)이 해당 노드의 값(6)보다 크기에 우측 노드로 이동한다.</li>
<li>찾고자 하는 값(7)과 해당 노드의 값(7)을 비교한다.</li>
<li>일치하기에 해당 값을 반환한다.</li>
</ol>
<p>예시로 든 그래프가 값이 삽입될 당시 다음과 같은 순서로 삽입되었다 가정해보면</p>
<pre><code class="language-java">8 -&gt; 3 -&gt; 10 -&gt; 1 -&gt; 6 -&gt; 14 -&gt; 4 -&gt; 7 -&gt; 13</code></pre>
<p>배열로 동일 삽입 내용을 관리한다 하였을 때, 원하는 값 7을 탐색하기 위해
8, 3, 10, 1, 6, 14, 4, 7을 모두 확인하며 비교해야 했을 것이다.</p>
<p>이진 탐색 트리 구조는 배열과 비교하였을 때 월등한 탐색적 성능 효과를 가지는 것을 알 수 있다.</p>
<p>그러나 이러한 이진 탐색이 언제나 시간복잡도적으로 이점을 가지는 것은 아니다.
만약 이진 탐색 트리에 다음과 같은 순서로 값을 삽입한다고 가정해보자</p>
<pre><code class="language-java">1 -&gt; 3 -&gt; 4 -&gt; 6 -&gt; 7 -&gt; 8 -&gt; 10 -&gt; 13 -&gt; 14</code></pre>
<p>결과적으로 트리 내 모든 노드는 오른쪽에 위치하여, 구조상 편향된 트리가 만들어진다.
여기서 14라는 값을 탐색해야 한다면,
사실상 모든 노드를 조회(Full Scan)해야하기에 <strong>O(N)</strong>의 시간복잡도가 나온다.</p>
<p>사실상 배열을 전체 순회하는 것과 다를 바 없는 탐색 시 성능적 문제를 야기한다.</p>
<blockquote>
<p>이러한 문제를 해결할 수 있는
<code>AVL Tree</code>, <code>Red-Black Tree</code>같은 자가 균형 이진 탐색 트리가 존재한다.
다만 <strong>데이터베이스의 인덱스에서는 일반적으로 B-Tree/B+Tree 계열 구조를 사용한다.</strong></p>
<p>데이터베이의 대용량 데이터는 메모리(RAM)가 아닌 HDD, SSD와 같은 하드 디스크에 저장되며,
해당 환경에서는 <strong>비교 횟수보다 디스크 I/O 횟수를 줄이는 것이 더 중요</strong>하기 때문이다.</p>
</blockquote>
<h4 id="1-2-b-tree">1-2) B-Tree</h4>
<p>앞서 살펴본 이진 탐색 트리와 이를 개선한 <code>AVL Tree</code>, <code>Red-Black Tree</code>는
각 노드가 최대 두 개의 자식 노드 포인터와 <strong>하나의 키 값</strong>만을 가지는 자료구조이다.</p>
<p>그렇기에 데이터가 많아지면 결국 트리의 height가 높아질 수 밖에 없고
이는 값을 탐색할 때 거쳐야하는 노드가 많아짐을 의미하는데, 디스크에 저장되는 데이터베이스에서는
이러한 탐색 시 <strong>값을 비교하는 연산</strong>보다 <strong>디스크 I/O</strong> 과정이 훨씬 큰 비용을 차지한다.</p>
<p>이에 데이터베이스와 파일 시스템에서
대량의 데이터를 효율적으로 검색하기 위해 설계된 트리 자료구조인 <strong>B-Tree</strong>가 등장한다.</p>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/26895312-93bc-42ba-a0a4-fabf34c01391/image.png" alt=""></p>
<ul>
<li><strong>하나의 노드에 데이터(키)를</strong> 하나만 담지 않고 <strong>여러 개 저장</strong>한다.</li>
<li>여러 키가 담기기에 해당 키들을 기준으로 <strong>더욱 많은 분기점이 발생</strong>하여 <strong>다수의 자식 노드</strong>를 가진다.</li>
</ul>
<p>결과적으로 개별 노드당 담고 있는 키도 많아지고, 자식 노드의 수도 늘어나기에
루트에서 리프노드까지 거쳐야하는 노드 수가 줄어들게 된다. 즉, <strong>디스크 I/O가 줄어드는 것</strong>이다.</p>
<blockquote>
<p><strong>B-Tree는 DB 인덱스와 파일 시스템에 최적화되어 있을까?</strong></p>
<p>결론을 먼저 말하자면, B-Tree는 DB 인덱스와 파일 시스템에서 사용하기에
다음과 같은 아쉬움이 남아있다.</p>
<p><strong>1. 중간 노드에 실제 데이터 저장</strong>
B-Tree는 중간 노드와 리프 노드 모두에 실제 데이터가 포함되어 있는데,
이는 한 노드에서 나타낼 수 있는 분기점이 줄어들게 되는 것이며
트리의 height를 더 낮출 수 있는 기회를 잃게 되는 것이다.</p>
<p><strong>2. 범위 검색에 취약</strong>
DB에서 데이터를 검색할 때는, 특정 하나의 데이터뿐만 아니라 범위를 검색하는 경우가 많다.</p>
<pre><code class="language-sql">WHERE age BETWEEN 20 AND 30
ORDER BY created_at</code></pre>
<p>앞서 말했지만 B-Tree는 중간 노드에도 실제 데이터가 들어있을 수 있기에
리프노드뿐만 아니라 내부노드까지 오가며 범위에 포함되는 값을 탐색해 디스크 I/O가 추가로 발생한다.</p>
</blockquote>
<h4 id="1-3-btree">1-3) B+Tree</h4>
<p>B+Tree는 MySQL의 InnoDB 스토리지 엔진에서 <strong>인덱스를 관리하기 위해 사용하는 대표적인 자료구조</strong>이다.</p>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/1b4d025a-443b-4f6f-8659-29f8002abf47/image.png" alt=""></p>
<p>앞선 <strong>B-Tree</strong>를 DB 인덱스에 사용할 때 문제점들은 <strong>B+Tree</strong>의 특징으로 보완할 수 있다.</p>
<blockquote>
<p><strong>B+Tree 특징</strong></p>
<p><strong>1. 모든 데이터가 리프 노드에만 저장된다.</strong>
B-Tree는 중간 노드에도 실제 값(데이터)이 저장될 수 있었는데,
B+Tree는 중간 노드에 오직 자식 노드를 찾아가기 위한 경로 정보(키값)만 들어가고
실제 다루는 데이터는 리프 노드에만 저장된다.
<strong>&quot; 트리 높이 감소 -&gt; 디스크 I/O 감소</strong> &quot;</p>
<p><strong>2. 리프 노드들이 Linked List 구조를 가진다.</strong>
B-Tree의 경우 <strong>범위 검색(Range Scan)</strong>에 취약함을 앞선 내용에서 짚고 넘어갔다.
B+Tree는 모든 실제 데이터가 최하단 리프 노드에 저장되기에
한 리프노드 내부 데이터의 범위 탐색이 끝났다면, 다음 리프노드의 포인터를 따라가면 된다.
<strong>&quot; 범위 검색 시 노드 이동 수 감소 -&gt; 디스크 I/O 감소 &quot;</strong></p>
</blockquote>
<hr>
<h3 id="2-clustered--non-clustered">2. Clustered / Non Clustered</h3>
<h4 id="2-1-특징을-통한-개념-이해">2-1) 특징을 통한 개념 이해</h4>
<p>인덱스(Index)의 종류는 크게 <strong>Clustered Index</strong>와 <strong>Non Clustered Index</strong>로 나눌 수 있다.</p>
<p>각각의 정의를 살펴보기보단 특징을 기반으로 각 개념을 이해하여 보겠다.</p>
<blockquote>
<p><strong>클러스터형 인덱스(Clustered Index)의 특징</strong></p>
<ol>
<li><strong>단일성</strong>
한 테이블에는 오직 하나의 클러스터형 인덱스만 존재한다.
클러스터형 인덱스는 테이블의 물리적 구조를 결정하기 때문이다.</li>
</ol>
<ol start="2">
<li><strong>물리적 정렬</strong>
클러스터형 인덱스는 테이블의 레코드를 인덱스가 정의한 순서대로 재정렬한다.</li>
</ol>
<ol start="3">
<li><strong>자동 생성</strong>
일반적으로 테이블의 <strong>기본 키(Primary Key)</strong>를 설정하면 해당 키에 대해서
클러스터형 인덱스가 자동으로 생성된다.
PK가 없다면 직접 클러스터형 인덱스를 지정할 수 있다.</li>
</ol>
<ol start="4">
<li><strong>인덱스 구조</strong>
클러스터형 인덱스는 별도의 인덱스 테이블을 만들지 않고,
실제 데이터 테이블 자체를 인덱스로 사용한다. 
이는 데이터 조회 시 인덱스 테이블과 데이터 테이블 사이의 별도 이동 없이 바로 접근이 가능하다.</li>
</ol>
</blockquote>
<p>쉽게 생각해 클러스터형 인덱스는 우리가 사용하는 데이터베이스 내 테이블의 Primary Key라 할 수 있다.</p>
<blockquote>
<p><strong>비클러스터형 인덱스(Non Clustered Index)의 특징</strong></p>
<ol>
<li><strong>다중성</strong>
하나의 테이블에 여러 개의 비클러스터형 인덱스가 존재 가능하다.
서로 다른 검색 조건, 쿼리에 최적화된 다양한 인덱스를 생성할 수 있다.</li>
</ol>
<ol start="2">
<li><strong>논리적 정렬</strong>
비클러스터형 인덱스는 데이터를 <strong>물리적으로 재정렬하지 않는다</strong>.
인덱스 키 값이 정렬되어 있는 별도의 인덱스 페이지를 생성하며, 원본 데이터를 가리키게 한다.</li>
</ol>
<ol start="3">
<li><strong>공간 요구</strong>
비클러스터형 인덱스는 인덱스 페이지를 생성하기에, 별도의 저장공간을 차지한다.
때문에 인덱스를 많이 만들면 만들 수록, 더 많은 디스크 공간을 차지하게 된다.</li>
</ol>
</blockquote>
<p>역시 쉽게 생각하면 비클러스터형은 PK가 아닌 개발자가 직접 쿼리를 통해 설정하는 인덱스라 할 수 있다.</p>
<p>결국 클러스터형 인덱스는 PK가 실제 테이블의 데이터 탐색 기준이되는 것이기에,
우리가 관심 깊게 봐야하는 것은 <strong>비클러스터형 인덱스(Non Clustered Index)</strong>일 것이다.</p>
<h4 id="2-2-non-clustered-index의-장단점">2-2) Non Clustered Index의 장단점</h4>
<p><strong><code>장점) 데이터 입력, 수정, 삭제</code></strong></p>
<p>클러스터형 인덱스의 경우 데이터를 입력하거나 수정, 삭제한다면
<strong>물리적 정렬되어</strong> 있는 특성상, 데이터 자체를 재정렬해야할 수 있는 어쩔 수 없는 손실을 지닌다.<br/>
하지만 비클러스터형 인덱스에서는 별도의 인덱스 페이지만 갱신하면 되기에,
상대적으로 입력, 수정, 삭제 작업에서 이점이 있다. (<strong>논리적 정렬되어 있기에</strong>)</p>
<br/>

<p><strong><code>단점) 인덱스 페이지의 존재</code></strong></p>
<p>비클러스터형 인덱스는 인덱스 페이지에서 실제 데이터를 가리키는 포인터로 작동하기에
바로 실제 테이블에서 PK를 통해 값에 접근하는 것이 아니라,
<strong>인덱스를 탐색하고 데이터를 탐색하는 두 단계를 거치게 된다</strong>.</p>
<p>또한 결국 인덱스 페이지가 디스크의 저장 공간을 차지하기에 공간적 손해를 보게 된다.</p>
<blockquote>
<p><strong>커버링 인덱스(Covering Index)</strong></p>
<p>커버링 인덱스는 쿼리를 처리하는 데 필요한 컬럼들이 <strong>모두 인덱스에 포함되어 있어</strong>,
테이블의 실제 데이터 row까지 접근하지 않고 <strong>인덱스만으로 결과를 반환하는 것</strong>을 말한다.
= <strong>key lookup 과정의 생략</strong></p>
<p>이러한 커버링 인덱스는 비클러스터형 인덱스에서의 단점을 보완할 수 있다.
+) 자주 사용하는 쿼리인 경우 분석하여 커버링 인덱스를 의도적으로 설계하는 경우도 있다.</p>
</blockquote>
<hr>
<h3 id="3-인덱스index의-필요성">3. 인덱스(Index)의 필요성</h3>
<p>인덱스의 간단한 필요 이유는 데이터베이스에서 원하는 데이터를 빠르게 찾기 위해 사용된다 할 수 있다.</p>
<p>즉, 적절한 인덱스가 있다면 데이터베이스는 전체 데이터를 모두 확인(<strong>풀스캔(Full Scan)</strong>)하지 않고
인덱스를 통해 조건에 맞는 데이터의 위치를 빠르게 찾을 수 있는 것이다.</p>
<pre><code class="language-sql">// 예시
// 1억건의 주문 데이터를 담은 order 테이블이 존재
// 특정 사용자 주문 내역을 조회하는 쿼리가 있다 가정

SELECT * FROM orders WHERE user_id = 123;

// 이때 만약에 user_id 컬럼에 인덱스가 없다면,
// 데이터베이스는 1억건의 주문 데이터를 모두 다 확인하면서 user_id가 123인 주문을 찾아야한다.

// user_id가 비클러스터형 인덱스로 지정되어 있다면,
// 데이터베이스는 자료구조(인덱스 페이지)를 통해 user_id가 123인 데이터가
// 어디에 저장되어 있는지, 디스크IO 몇 번으로 찾을 수 있는 것이다.

ex) orders
id | user_id | product | price
---|---------|---------|------
1  | 123     | A       | 10000
2  | 456     | B       | 20000
3  | 123     | C       | 15000
4  | 789     | D       | 30000
5  | 123     | E       | 12000

ex) idx_orders_user_id (인덱스 테이블)
user_id | PK(id)
--------|-------
123     | 1
123     | 3
123     | 5
456     | 2
789     | 4
// 인덱스 테이블은 user_id(인덱스) 기준으로 정렬되어 있기에, 풀스캔을 피할 수 있는 것</code></pre>
<hr>
<h3 id="4-인덱스index는-항상-이점인가">4. 인덱스(Index)는 항상 이점인가</h3>
<p>인덱스는 조회 성능을 개선하는 데 효과적이지만, 모든 상황에서 항상 이점을 주는 것은 아니다.
먼저 인덱스는 테이블과 별도로 관리되는 자료구조이기 때문에 <strong>추가적인 저장공간이 필요</strong>하며</p>
<p><strong>데이터가 변경될 때마다 인덱스도 함께 갱신되어야 한다</strong>는 점에서 오버헤드가 발생한다.</p>
<pre><code class="language-sql">INSERT, UPDATE, DELETE 작업이 발생하면,
테이블 데이터뿐만 아니라 관련 인덱스 페이지들도 수정해야 한다.
따라서 인덱스가 많을 수록 쓰기 작업의 비용은 증가된다.

-&gt; 조회가 많이 없고, 쓰기 작업이 자주 일어난다면 인덱스를 최소화하거나 쓰지 않는게 나을 수 있다.</code></pre>
<p>또한 조회 조건의 선택도가 낮거나, 테이블 대부분의 데이터를 읽어야한다면,
인덱스 타면서 테이블 옮겨다니는 오버헤드보다 풀스캔이 나은 경우도 있다.</p>
<blockquote>
<p>실제로 DB의 <strong>옵티마이저(Optimizer)</strong>가 이를 자동으로 판단해,
풀 테이블 스캔이 더 효율적이라고 판단하면 해당 인덱스를 사용하지 않을 수 있다.</p>
</blockquote>
<p>따라서 인덱스를 사용할 때는 조회 성능 개선 효과와 함께,
<code>저장 공간</code>, <code>쓰기 성능 저하</code>, <code>인덱스 갱신 비용</code> 같은 오버헤드를 함께 고려해야 한다.</p>
<p>인덱스로 인한 이점이 이러한 비용보다 큰 경우에만 적절한 인덱스 설계라고 할 수 있다.</p>
<hr>
<h3 id="5-인덱스index-설계">5. 인덱스(Index) 설계</h3>
<p>컬럼의 <strong>카디널리티(Cardinality)</strong>와 <strong>셀렉티비티(Selectivity)</strong>는, 인덱스 설계의 기준이라 할 수 있다.</p>
<ul>
<li><strong>카디널리티(Cardinality)</strong> : 컬럼에 저장된 값의 고유성, <em>해당 컬럼 내 중복값이 얼마나 적은가</em></li>
</ul>
<p><code>주민등록번호, e-mail, 주문번호 etc.</code></p>
<p>카디널리티는 값이 거의 중복되지 않은 컬럼의 경우 카디널리티가 높다고 표현할 수 있으며,
값의 종류가 적고 중복이 많은 컬럼의 경우에는 카디널리티가 낮다 표현한다.</p>
<br/>

<ul>
<li><p><strong>셀렉티비티(Selectivity)</strong> : <em>특정 조건으로 값을 얼마나 걸러낼 수 있는지</em></p>
<p>  성별, 회원 상태, 게시글 공개 여부 etc.</p>
</li>
</ul>
<p>Selectivity의 경우 다음 수식을 통해 파악할 수 있다.</p>
<p><code>Selectivity = Cardinality / 전체 row 수</code> Selectivity의 값이 <code>1</code>에 가까울 수록, &quot;선택도가 좋다&quot;</p>
<p><strong>인덱스는 결국 조회 대상 데이터를 줄여주는 역할을 해야 의미를 가진다.</strong>
따라서 인덱스를 설계할 때는 해당 컬럼의 카디널리티와 셀렉티비티를 함께 고려해야 한다.
중복 값이 적고 조건에 맞는 데이터를 많이 걸러낼 수 있는 컬럼일수록,
인덱스를 사용했을 때 조회 범위를 효과적으로 줄일 수 있다.</p>
<blockquote>
<p><strong>카디널리티가 낮으면 항상 인덱스의 의미가 없는가?</strong></p>
<p>카디널리티가 낮다고 해서 항상 인덱스의 의미가 없는 것은 아니다.
다른 컬럼과 함께 <strong>복합 인덱스</strong>로 구성되거나,
<strong>특정 값의 분포가 매우 치우쳐 있는 경우</strong>에는 낮은 카디널리티의 컬럼도 인덱스 설계에 포함될 수 있다.</p>
</blockquote>
<hr>
<h3 id="6-복합-인덱스composite-index">6. 복합 인덱스(Composite Index)</h3>
<p><strong>복합 인덱스(Composite Index)</strong>는 두 개 이상의 컬럼을 묶어서 만든 인덱스이다.</p>
<pre><code class="language-sql">CREATE INDEX idx_location_first ON users(Location, Name, ID);
CREATE INDEX idx_id_first ON users(ID, Name, Location);</code></pre>
<p>위와 같이 복합 인덱스를 만들 수 있으며, 복합 인덱스를 사용할 때는</p>
<p><strong>컬럼의 순서가 인덱스의 성능을 결정한다</strong>는 점을 주의해서 설계해야 한다.</p>
<p>B+Tree 구조를 가지는 인덱스의 특성상 위 예시에서는, <code>Location -&gt; Name -&gt; ID</code>순으로 정렬이 일어난다.</p>
<p>따라서 <code>WHERE Location = &#39;서울&#39; AND Name = &#39;Lamine&#39;</code> 같은 경우는 문제 없지만</p>
<p><code>WHERE Name = &#39;Kylian&#39;</code>같은 경우, 첫 정렬 조건인 <code>Location</code>을 명시하지 않아 풀스캔의 가능성이 크다.</p>
<blockquote>
<p><strong>+ 복합 인덱스의 설계 방식</strong></p>
<p>복합 인덱스는 보통 <strong>동등 조건</strong>(<code>=</code>)에 사용되는 컬럼을 앞에 두고,</p>
<p>그다음 <strong>범위 조건</strong>(<code>&gt;</code>, <code>&lt;</code>, <code>BETWEEN</code>, <code>LIKE &#39;prefix%&#39;</code>)이나,
<strong>정렬 조건</strong>(<code>ORDER BY</code>)에 사용되는 컬럼을 배치하는 방식으로 설계한다.</p>
</blockquote>
<pre><code class="language-sql">WHERE user_id = 10 AND created ≥ ‘2026-06-01’
    ORDER BY created_at DESC, id DESC LIMIT 20</code></pre>
<blockquote>
<p>위와 같은 특정 유저의 최근 주문 목록을 가져오는 쿼리가 있다 하였을 때</p>
<p><strong>(user_id, created_at, id)</strong> 순서로 복합 인덱스를 설정했었다면
<code>user_id = 10</code>을 통해 특정 유저의 주문만으로 데이터 탐색 범위를 제한하고,
이후 범위 조건을 타며 정렬과 리미트까지 효율적으로 이어질 가능성이 있다.</p>
<p>반면 <strong>(created_at, user_id, id)</strong> 순서로 복합 인덱스를 설정했다면
<code>created_at</code> 범위 기준으로 첫 정렬된 데이터들 중 몇 개를 뽑아내고,
기준에 맞는 <code>user_id</code>를 흩뿌려진 데이터들 사이에서 찾아내야 한다.
<strong>결과적으로 인덱스를 사용함으로 얻는 효과적인 탐색을 누리지 못하는 것이다.</strong></p>
<p><strong><code>&quot;복합 인덱스는 자주 쓰는 컬럼을 다 넣는게 아니라, 실제 쿼리 순서를 보고 결정할 것&quot;</code></strong></p>
</blockquote>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring에서의 Servlet Container 기능과 역할]]></title>
            <link>https://velog.io/@zayn_noh/Servlet-Container</link>
            <guid>https://velog.io/@zayn_noh/Servlet-Container</guid>
            <pubDate>Thu, 04 Jun 2026 01:59:32 GMT</pubDate>
            <description><![CDATA[<p>Spring에서 <strong>DispatcherServlet</strong>은 Spring MVC의 단일진입점으로 동작하며,
프론트 컨트롤러의 역할을 맡는 것을 저번 포스트에서 정리하였다.</p>
<p>모든 클라이언트의 HTTP 요청을 가장 먼저 받고,
다양한 컴포넌트들 사이의 중앙 관리자 역할을 수행하며 요청에 대한 처리를 수행하는데</p>
<p>단일진입점으로 동작하는 프론트 컨트롤러의 특성상 사용자 요청이 많아지면
병목현상이 발생하지 않을까 고민하였으나,</p>
<p>DispatcherServlet 자체에서 직접적으로 처리하는 로직은 많이 없고
빠르게 요청에 따른 처리들을 각 컴포넌트에 위임해주어서 부하로 인한 문제는 드물다.</p>
<blockquote>
<p>그럼에도 모든 요청을 DispatcherServlet이 먼저 받는다면,
요청을 <strong>HttpServletRequest</strong>로 감싸고 <strong>HttpServletResponse</strong>를 함께 생성하는 로직을 수행할텐데 병목현상이 발생할 수 있지 않을까?</p>
</blockquote>
<p>위와 같은 의구심이 들어 찾아보다,
클라이언트에서 사용자가 보낸 HTTP요청을 가장 먼저 받는 건, <strong>Servlet Container</strong>임을 알게 되었다.</p>
<p>이번 포스팅에서는 Servlet Container의 개념과 기능에 대해 알아보며,
HTTP요청과 HTTP응답 과정 사이 속, 어느 계층에서 동작하는지 알아보도록 한다.</p>
<hr>
<h3 id="1-servlet">1. Servlet</h3>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/39f8ded7-79ef-4d85-a3dc-5db46888667f/image.png" alt=""></p>
<p><strong>Servlet Cotainer</strong>에 대해 알아보기 전에,
<code>DispatcherServlet</code>, <code>Servlet Container</code>, <code>HttpServletRequest</code>, <code>HttpServletResponse</code> 등
다양한 개념 속 이름에서 등장하는 <strong>Servlet</strong>이라는게 뭔지 정리해보고 가려 한다.</p>
<blockquote>
<p><strong>서블릿(Servlet)</strong>이란,
동적 기반 웹 페이지를 만들 때 사용되는 자바 기반의 웹 어플리케이션 프로그래밍 기술을 말한다.</p>
</blockquote>
<p>브라우저가 서버에 요청을 보내면 서블릿은</p>
<ul>
<li>요청을 받고 : <code>클라이언트의 HTTP 요청 받기</code></li>
<li>처리하며 : <code>요청 파라미터 읽기</code>, <code>비즈니스 로직 실행</code>, <code>DB 조회 및 저장</code>  etc.</li>
<li>응답을 내려준다 : <code>응답 HTML</code>, <code>JSON</code>, <code>리다이렉트 생성</code></li>
</ul>
<blockquote>
<p><strong>Spring MVC의 역할 아닌가?</strong></p>
<p>전통적인 서블릿 방식의 경우 개발자가 서블릿 클래스 내부에서
요청 파라미터 처리, 비즈니스 로직  호출, DB 접근, 응답 생성 등을 직접 구현했다.</p>
<p><em>(<strong>서블릿은 자바 웹 어플리케이션에서 HTTP 요청과 응답을 처리하기 위한 기반 기술</strong>)</em></p>
<p>Spring이 나타나면서 <strong>Spring MVC는 기존 서블릿 기반 위에서 동작</strong>하며,
<strong>DispatcherServlet</strong>을 통해 서블릿 기능을 프레임워크 구조 안으로 추상화한 것이다.</p>
<p><em>(<strong>기존 Servlet에서 직접 다루던 웹 요청 기반 처리 작업들을 여러 구성요소로 분리
개발자가 더 구조적으로 작성할 수 있게 만든 서블릿 기반 MVC 프레임워크</strong>)</em></p>
</blockquote>
<p>아래 표를 통해 전통적인 Servlet과 Servlet Container가 수행하던 일들을,
지금의 Spring에서 역할과 책임을 나누어 담당하게 바뀐 전반적인 내용을 담아보았다.</p>
<table>
<thead>
<tr>
<th align="center">처리 과정</th>
<th align="center">담당 구성요소 1</th>
<th align="center">담당 구성요소 2</th>
<th align="center">담당 구성요소 3</th>
</tr>
</thead>
<tbody><tr>
<td align="center">HTTP 요청 최초 수신</td>
<td align="center">Servlet Container</td>
<td align="center">Tomcat / Jetty / Undertow</td>
<td align="center"></td>
</tr>
<tr>
<td align="center">요청/응답 객체 생성</td>
<td align="center">Servlet Container</td>
<td align="center">HttpServletRequest</td>
<td align="center">HttpServletResponse</td>
</tr>
<tr>
<td align="center">Spring MVC 진입</td>
<td align="center">DispatcherServlet</td>
<td align="center">Front Controller</td>
<td align="center"></td>
</tr>
<tr>
<td align="center">요청 URL에 맞는 코드 찾기</td>
<td align="center">HandlerMapping</td>
<td align="center">HandlerExecutionChain</td>
<td align="center"></td>
</tr>
<tr>
<td align="center">Controller 실행</td>
<td align="center">HandlerAdapter</td>
<td align="center">RequestMappingHandlerAdapter</td>
<td align="center">Controller Method</td>
</tr>
<tr>
<td align="center">요청 파라미터 읽기</td>
<td align="center">ArgumentResolver</td>
<td align="center">@RequestParam</td>
<td align="center">@PathVariable</td>
</tr>
<tr>
<td align="center">요청 Body 읽기</td>
<td align="center">HttpMessageConverter</td>
<td align="center">@RequestBody</td>
<td align="center">JSON → Java 객체</td>
</tr>
<tr>
<td align="center">객체 바인딩/검증</td>
<td align="center">DataBinder</td>
<td align="center">Validator</td>
<td align="center">BindingResult</td>
</tr>
<tr>
<td align="center">비즈니스 로직 실행</td>
<td align="center">Service</td>
<td align="center">Domain</td>
<td align="center"></td>
</tr>
<tr>
<td align="center">DB 조회 저장</td>
<td align="center">Repository</td>
<td align="center">Mapper</td>
<td align="center">DAO</td>
</tr>
<tr>
<td align="center">반환값 처리</td>
<td align="center">ReturnValueHandler</td>
<td align="center">HandlerMethodReturnValueHandler</td>
<td align="center"></td>
</tr>
<tr>
<td align="center">HTML 렌더링</td>
<td align="center">ViewResolver</td>
<td align="center">View</td>
<td align="center">JSP / Thymeleaf</td>
</tr>
<tr>
<td align="center">JSON 응답 생성</td>
<td align="center">HttpMessageConverter</td>
<td align="center">@ResponseBody</td>
<td align="center">Java 객체 → JSON</td>
</tr>
<tr>
<td align="center">예외 처리</td>
<td align="center">HandlerExceptionResolver</td>
<td align="center">@ExceptionHandler</td>
<td align="center">@ControllerAdvice</td>
</tr>
<tr>
<td align="center">응답 정리</td>
<td align="center">DispatcherServlet</td>
<td align="center">HttpServletResponse</td>
<td align="center"></td>
</tr>
<tr>
<td align="center">HTTP 응답 전송</td>
<td align="center">Servlet Container</td>
<td align="center">HTTP Response Message</td>
<td align="center"></td>
</tr>
</tbody></table>
<hr>
<h3 id="2-servlet-container">2. Servlet Container</h3>
<p><strong>서블릿 컨테이너(Servlet Container)</strong>는 서블릿을 실행하고 관리하는 서버 환경이다.
Spring Boot 애플리케이션의 경우 보통 내장 <strong>Tomcat</strong>이 <strong>Servlet Container</strong>로 동작하며, Spring MVC의 핵심 서블릿인 <strong>DispatcherServlet을 실행하고 관리</strong>한다.</p>
<p>서블릿 컨테이너는 가장 먼저 클라이언트가 보낸 HTTP 요청을 수신한다.
이때 요청을 확인하고 어떤 서블릿에게 이를 위임할지 결정하는데,</p>
<pre><code class="language-java">@WebServlet(&quot;/login&quot;)
public class LoginServlet extends HttpServlet {
    // 로그인 요청 처리
}

@WebServlet(&quot;/users&quot;)
public class UserServlet extends HttpServlet {
    // 사용자 요청 처리
}</code></pre>
<p><strong>이는 전통적인 Servlet 구조</strong>에 해당하며,</p>
<pre><code class="language-java">public class LoginServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) {
        String id = request.getParameter(&quot;id&quot;);
        String password = request.getParameter(&quot;password&quot;);

        // 로그인 처리
        // 응답 생성
    }
}</code></pre>
<p>한 서블릿 안에 요청 처리, 파라미터 읽기, 비즈니스 로직 호출, 응답 생성 등 여러 로직이 모두 섞여있다.</p>
<p><strong>현대 Spring MVC 구조에서는</strong> URL을 여러 서블릿에 직접 연결하지 않고,
대부분의 요청을 하나의 서블릿인 <strong>DispatcherServlet</strong>으로 보낸다.
DispatcherServlet은 Spring MVC의 단일 진입점이자 프론트 컨트롤러로 동작하며,
요청 URL과 HTTP 메서드에 맞는 Controller를 찾아 요청을 위임한다.</p>
<pre><code>/login     → DispatcherServlet → LoginController
/users     → DispatcherServlet → UserController
/products  → DispatcherServlet → ProductController
/orders    → DispatcherServlet → OrderController</code></pre><hr>
<h3 id="3-spring-mvc에서-servlet-container의-기능">3. Spring MVC에서 Servlet Container의 기능</h3>
<p>서블릿 컨테이너는 <strong>서블릿을 실행하고 관리하는 역할</strong>을 한다.
서블릿 컨테이너가 실제로 어떤 동작과 기능을 수행하는지 조금 더 자세히 알아보았다.</p>
<p>아래 내용은 Spring Boot 기반 Spring MVC 어플리케이션에서
Servlet Container와 Spring MVC가 HTTP 요청을 처리하며 상호작용하는 과정을 기준으로 설명한다.</p>
<h4 id="0-http-요청-받기">0. HTTP 요청 받기</h4>
<pre><code>GET /users/1 HTTP/1.1
Host: example.com
Cookie: JSESSIONID=...</code></pre><p>Spring MVC 바깥 쪽에서, 클라이언트가 보낸 HTTP 요청을 처음 받아온다.</p>
<br/>

<h4 id="1-요청응답-객체-생성">1. 요청/응답 객체 생성</h4>
<pre><code class="language-java">HttpServletRequest request
HttpServletResponse response</code></pre>
<p>원본 HTTP 요청을 자바 객체로 감싸서 만든다.
추가로 클라이언트에게 보낼 데이터를 모아놓은 응답 객체도 생성한다.</p>
<ul>
<li><p><code>HttpServletRequest</code>
  요청 URL, HTTP method, header, cookie, query parameter, body 등 정보를 모아둔 객체  생성</p>
</li>
<li><p><code>HttpServletResponse</code>
  상태 코드, 응답 헤더, 쿠키, 응답 body등을 담기 위한 객체 생성</p>
</li>
</ul>
<br/>

<h4 id="2-httpservletrequest--httpservletresponse--전달">2. HttpServletRequest , HttpServletResponse  전달</h4>
<p>DispatcherServlet에게 생성한 <code>HttpServletRequest</code>, <code>HttpServletResponse</code>를 전달하고,
DispatcherServlet을 호출한다.</p>
<br/>

<h4 id="3-filter-실행">3. Filter 실행</h4>
<p><strong>서블릿(DispatcherServlet)</strong>을 호출하기 전후로 <strong>Filter</strong>가 실행될 수 있다.</p>
<p>이러한 Filter는 Spring MVC보다 앞단에서 동작하며 주로 다음과 같은 기능들을 수행한다.</p>
<ul>
<li>문자 인코딩 설정</li>
<li>인증/인가 검사</li>
<li>CORS 처리</li>
<li>요청/응답 로깅</li>
<li>XSS 방어 처리</li>
<li>압축 처리</li>
<li>요청/응답 래핑</li>
<li>공통 헤더 설정</li>
<li>Spring Security 필터 체인 실행</li>
<li>특정 URL 접근 제한 등</li>
</ul>
<p>Spring MVC보다 앞단에서 동작하기에 모든 요청에 대해
UTF-8 인코딩을 설정하거나, 로그인하지 않은 사용자가 특정 경로에 접근하지 못하도록 검사할 수 있다.</p>
<pre><code class="language-java">...
import org.springframework.web.filter.OncePerRequestFilter;
...

// JWT 토큰 검사 필터 예시 코드
@RequiredArgsConstructor
public class JWTAuthFilter extends OncePerRequestFilter {
    private final JWTProvider jwtProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain)
            throws IOException, ServletException {
            ...</code></pre>
<br/>

<h4 id="4-스레드-관리">4. 스레드 관리</h4>
<p>서블릿 컨테이너는 클라이언트의 요청마다 스레드를 할당하여 여러 요청을 동시에 처리한다.</p>
<pre><code>사용자 A 요청 → Thread-1 → DispatcherServlet → Controller
사용자 B 요청 → Thread-2 → DispatcherServlet → Controller
사용자 C 요청 → Thread-3 → DispatcherServlet → Controller</code></pre><p>이때 서블릿 컨테이너는 보통 <strong>스레드 풀(Thread Pool) 방식</strong>을 사용한다.
요청이 들어올 때마다 매번 새 스레드를 생성하는 것이 아니라,
미리 만들어 둔 스레드를 재사용하여 요청을 처리한다.</p>
<p>Spring MVC에서 DispatcherServlet, Controller, Service는 대부분 <strong>싱글톤 객체</strong>로 관리된다.
따라서 여러 요청이 동시에 들어오면, 서로 다른 스레드가 같은 객체의 메서드를 동시에 실행할 수 있다.</p>
<blockquote>
<p><strong>Spring MVC 싱글톤 빈의 상태 관리와 동시성 문제</strong></p>
<p>싱글톤 빈에 요청마다 달라지는 값을 인스턴스 변수에 저장하면,
여러 스레드가 같은 변수를 동시에 읽고 쓰게 된다.</p>
<pre><code class="language-java">@Controller
public class UserController {

  private Long currentUserId; // 여러 요청이 공유하는 상태

  @GetMapping(&quot;/users/{id}&quot;)
  public String getUser(@PathVariable Long id) {
      this.currentUserId = id;
      return &quot;user&quot;;
  }
}</code></pre>
<p>그 결과 <strong>한 사용자의 요청 데이터가 다른 사용자의 요청 데이터로 덮어써지거나</strong>
<strong>의도하지 않은 값이 응답에 사용되는 동시성 문제가 발생</strong>할 수 있다.</p>
<p>따라서 Controller와 Service같은 싱글톤 빈에는
사용자별 데이터, 요청별 데이터처럼 변하는 상태를 인스턴스 변수로 저장하지 않는 것이 좋다.
요청마다 필요한 값은 메서드 파라미터, 지역 변수, 세션, DB 같은 적절한 위치에 저장해야 한다.</p>
</blockquote>
<br/>

<h4 id="5-세션-관리">5. 세션 관리</h4>
<p>서블릿 컨테이너는 <strong><code>HttpSession</code></strong> 또한 관리한다.</p>
<p>HTTP는 기본적으로 상태를 저장하지 않는 stateless 프로토콜이다.<br>따라서 서버는 여러 요청이 같은 사용자로부터 온 것인지 구분하기 위해 세션을 사용할 수 있다.</p>
<p>사용자가 처음 서버에 접근하면 서블릿 컨테이너는 필요에 따라 <code>HttpSession</code>을 생성하고,
해당 세션을 식별할 수 있는 <code>JSESSIONID</code>를 클라이언트에게 쿠키로 전달한다.</p>
<pre><code>서버 응답
Set-Cookie: JSESSIONID=abc123</code></pre><p>이후 브라우저는 같은 서버로 요청을 보낼 때 JSESSIONID 쿠키를 함께 전송한다.
서블릿 컨테이너는 이 JSESSIONID 값을 보고 서버에 저장된 HttpSession을 찾아낸다.</p>
<pre><code>Servlet Container
→ JSESSIONID 확인
→ 해당 사용자의 HttpSession 조회
→ Controller에서 사용할 수 있도록 제공</code></pre><blockquote>
<p><strong>컨트롤러에서의 사용 예시</strong></p>
<pre><code class="language-java">@GetMapping(&quot;/mypage&quot;)
public String myPage(HttpSession session) {
    Object user = session.getAttribute(&quot;loginUser&quot;);
    return &quot;mypage&quot;;
}</code></pre>
<p>Controller에서 HttpSession을 사용하는 것처럼 보이지만,
실제로 <strong>세션 객체의 생성, 조회, 만료 같은 기본 관리는 서블릿 컨테이너가 담당</strong>한다.</p>
</blockquote>
<br/>

<h4 id="6-http응답-반환">6. HTTP응답 반환</h4>
<p>Controller에서 처리가 끝나면,
최종적으로 <code>DispatcherServlet</code>이 <code>HttpServletResponse</code> 에 응답 관련 데이터를 담는다.</p>
<pre><code class="language-java">Status: 200 OK
Content-Type: application/json
Body: {&quot;id&quot;:1,&quot;name&quot;:&quot;Kim&quot;}</code></pre>
<p>이후 응답은 Filter 후처리를 거쳐 다시 Servlet Container로 돌아가고
해당 응답 객체 내부 데이터를 다시 실제 HTTP 응답 메시지로 감싸서 클라이언트에게 전송한다.</p>
<br/>

<h4 id="7-서블릿dispatcherservlet-생명-주기-관리">7. 서블릿(DispatcherServlet) 생명 주기 관리</h4>
<p>서블릿 컨테이너는 자신이 관리하는 서블릿들의 생명 주기를 관리한다.</p>
<p>전통적인 서블릿 방식에서는 여러 개의 서블릿이 URL별로 등록될 수 있고,<br>서블릿 컨테이너는 각각의 서블릿 객체를 생성, 초기화, 실행, 종료한다.</p>
<p><strong>Spring MVC에서</strong>는 대부분의 웹 요청을 하나의 프론트 컨트롤러인 <code>DispatcherServlet</code>이 담당한다.
따라서 Spring MVC 구조에서는 서블릿 컨테이너가
주로<code>DispatcherServlet</code>의 생명 주기를 관리한다고 볼 수 있다.</p>
<p>서블릿 생명 주기는 대략 다음과 같다.</p>
<pre><code>애플리케이션 시작
→ Servlet Container가 DispatcherServlet 생성
→ init() 호출
→ Spring MVC 초기화

요청 발생
→ service() 호출
→ DispatcherServlet이 Spring MVC 요청 처리 수행

애플리케이션 종료
→ destroy() 호출
→ DispatcherServlet 종료</code></pre><hr>
<p><code>Servlet Container</code>가 <code>DispatcherServlet</code>을 호출한 이후,
Spring MVC 내부의 과정은 <a href="https://velog.io/@zayn_noh/%EC%8A%A4%ED%94%84%EB%A7%81-MVC">기존 포스트</a>에 작성하였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Bean, 스프링이 객체를 관리하는 방법]]></title>
            <link>https://velog.io/@zayn_noh/Spring-Bean-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%B4-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EA%B4%80%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@zayn_noh/Spring-Bean-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%B4-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EA%B4%80%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 02 Jun 2026 07:29:43 GMT</pubDate>
            <description><![CDATA[<p><strong>Bean</strong>은 <strong>Spring IoC Container</strong>가 생성하고 관리하는 재사용 객체로,
단순하게 힙 메모리 영역에 생성되어 있는 객체가 아니라 스프링 컨테이너의 관리 대상이 되는 객체를 말한다.</p>
<blockquote>
<p><strong>Spring IoC Container</strong>
스프링에서 객체를 직접 생성하고, 보관하고, 필요한 곳에 주입해주는 핵심 관리자이다.</p>
<pre><code class="language-java">ApplicationContext context = SpringApplication.run(App.class, args);</code></pre>
<p><code>SpringApplication.run()</code>을 실행하면, 대표적인 <strong>Spring IoC Container</strong> 중 하나인
<code>ApplicationContext</code>를 반환한다.</p>
</blockquote>
<p>이번 포스팅에서는 스프링에서의 재사용 객체 <strong>Bean</strong>에 대해 알아보며,
최종적으로 <strong>IoC</strong>, <strong>DI</strong>, <strong>Bean</strong>이 맞물려 동작하는 Spring을 이해할 수 있도록 한다.</p>
<hr>
<h3 id="1-bean-vs-instance">1. Bean vs Instance</h3>
<p>Bean에 대한 개념을 이해하기 위해 기존에 알고 있는 일반적인 Instance와 비교를 해보려 한다.</p>
<p>일반 객체와 스트링에서의 빈의 가장 큰 차이점은 누가 만들고, 관리하냐에 따라 나눌 수 있을 것 같다.</p>
<p><strong>일반적인 객체의 경우</strong> 개발자가 직접 코드를 통해 생성한다.</p>
<pre><code class="language-java">UserService userService = new UserService();</code></pre>
<p>개발자가 필요 시 직접 생성하여 힙 메모리에 생성되고, 참조가 끊어질 수도 있고 GC에 의해 수거되기도 한다.</p>
<p><strong>Bean 객체의 경우</strong>도 힙 메모리에 생성되며, 참조가 끊길 경우 GC의 대상이 될 수 있으나</p>
<p>객체의 생성부터, 생명주기 동안의 관리를 개발자가 아닌 <strong>스프링 IoC 컨테이너</strong>가 역할을 담당한다.</p>
<blockquote>
<p><strong>IoC(Inversion of Control)</strong></p>
<p>제어의 역전이라는 철학적 개념으로,
스프링의 경우 객체(빈)의 생성과 의존성 주입(DI) 등의 관리 역할(권한)을 개발자가 아닌 IoC 컨테이너에게 위임하여 이러한 IoC를 구현한다.
이는 결과적으로 개발자에게 <strong>비즈니스 로직에 집중</strong>할 수 있는 시간적 비용의 이점을 준다.</p>
</blockquote>
<p>정리하자면 스프링의 IoC 컨테이너는 스프링 어플리케이션 시작 지점에 객체(Bean)를 생성하고,
객체가 필요한 곳에 의존성 주입을 해주며 어플리케이션이 셧다운될 때 안전하게 남은 자원을 반납하는 것이다.</p>
<blockquote>
<p><strong>그렇다면 모든 객체를 Bean으로 관리하는게 좋은 것 아닐까?</strong></p>
<p>처럼 생각할 수 있지만, 프론트엔드와 통신하기 위해 만드는(<strong>DTO</strong>)나
데이터베이스의 한 <code>row</code>를 표현하기 위한 객체들은 개별 API 요청이 들어올 때마다
수십, 수백개가 동적으로 만들어졌다가 데이터베이스를 거치고 응답을 주면 사라지는 객체들이다.</p>
<p>이러한 일회성 소모품과도 같은 객체들까지 스프링 컨테이너가 관리하게 된다면,
<strong>컨테이너가 관리해야 할 대상이 불필요하게 늘어나고 객체의 역할도 모호</strong>해진다.</p>
<p>이 객체들은 특정 요청의 데이터를 잠시 담기 위한 용도이므로,
스프링 컨테이너가 생명주기를 관리하는 Bean이 아니라 일반 객체로 사용하는 것이 자연스럽다.
불필요하게 Bean으로 관리하면 컨테이너 관리 대상이 늘어나고 설계도 복잡해질 수 있다.</p>
</blockquote>
<p><strong>그렇다면 어떤 객체들을 Bean으로 등록하여야 할까?</strong></p>
<p>Bean으로 등록하기 적합한 객체는 어플리케이션의 핵심 처리 흐름을 담당하는 객체들이다.</p>
<ul>
<li><p><strong>Controller</strong>, <strong>Service</strong>, <strong>Repository</strong>처럼 요청 데이터를 받아 비즈니스 로직을 수행하거나, 데이터베이스 접근을 담당하는 클래스들이 여기에 해당한다.</p>
</li>
<li><p>이런 객체들은 요청이 들어올 때마다 새로 생성할 필요가 없다. 대부분 사용자별 데이터를 직접 보관하지 않고, 전달받은 요청 값이나 DTO를 기반으로 로직을 수행하기 때문이다.</p>
</li>
</ul>
<p>하나의 클래스를 만들어두고,
모든 사용자의 요청이 해당 객체를 공유해서 사용하는게 메모리나 성능 측면에서도 효율적일 것이다.</p>
<hr>
<h3 id="2-bean으로-인한-장점">2. Bean으로 인한 장점</h3>
<p>Bean을 왜 사용해야 하는 지에 대하여,
Bean을 통해 얻을 수 있는 다양한 측면의 이점을 통해 알아보도록 하겠다.</p>
<ul>
<li><strong>의존성 관리 자동화</strong>
  시스템이 커질 수록 애플리케이션의 객체들은 서로 복잡하게 얽혀 의존하게 된다.
  Controller는 Service에 의존하고, Service는 Repository나 외부 API Client, 메시지 Producer 등에 의존할 수 있다.
  이러한 객체들을 Bean으로 등록해두면 스프링 컨테이너가 애플리케이션 시작 시점에
  의존성 그래프를 분석하고 필요한 객체들을 자동으로 연결해준다.
  객체에 대한 권한을 위임한 <strong>IoC</strong> 측면에서 얻을 수 있는 이점에 해당한다.</li>
</ul>
<br/>

<ul>
<li><strong>일관된 생명 주기</strong>
  일부 객체는 생성 직후 초기화 작업이 필요하거나, 애플리케이션 종료 시 자원을 정리해야 한다.
  예를 들어 외부 서버와의 연결을 준비하거나, 자주 사용하는 데이터를 미리 캐싱하는 작업이 여기에 해당한다.
  이런 객체를 개발자가 직접 생성하면 초기화 시점과 종료 시점을 직접 관리해야 한다.
  반면 Bean으로 등록하면 스프링 컨테이너가 객체의 생성, 초기화, 사용, 소멸 과정을 일관되게 관리해준다.</li>
</ul>
<br/>

<ul>
<li><strong>구성 변경 및 유연한 확장</strong>
  어플리케이션은 환경에 따라 다른 구현체가 필요할 수 있다.
  개발 환경에서는 실제 결제가 일어나지 않는 가짜 결제 객체가 필요하고,
  운영 환경에서는 실제 결제 모듈이 필요하다.
  빈 기반 구조에서는 <code>UserService</code> 같은 핵심 로직을 수정하지 않고,
  스프링 설정이나 프로필에 따라 주입되는 Bean만 바꿀 수 있다.
  즉 애플리케이션의 뼈대는 유지한 채, 상황에 맞는 구현체를 갈아 끼우며 유연하게 확장할 수 있다.</li>
</ul>
<hr>
<h3 id="3-bean-등록-방법">3. Bean 등록 방법</h3>
<p>Bean을 등록하는 방법은 크게 <strong>컴포넌트 스캔 기반 등록</strong>과 <strong>설정 클래스 기반 등록</strong>으로 나눌 수 있다.</p>
<p><strong>1. 컴포넌트 스캔 기반 등록</strong>
컴포넌트 기반 등록은 클래스에 <code>@Component</code> 계열 어노테이션을 붙여
스프링 IoC 컨테이너가 자동으로 Bean으로 등록하게 하는 방식을 말한다.</p>
<p>대표적으로 다음과 같은 어노테이션들이 있으며
<code>@Component</code> <code>@Controller</code> <code>@RestController</code> <code>@Service</code> <code>Repository</code>
각각의 어노테이션들은 모두 내부에 <code>@Component</code>를 포함하고 있다.
따라서 스프링은 <code>@Component</code> 어노테이션이 붙은 클래스들을 컴포넌트 스캔의 대상, 즉 Bean 등록 대상으로 인지하는 것이다.</p>
<pre><code class="language-java">@Service
public class UserService {
    ...
}</code></pre>
<blockquote>
<p><strong>왜 모두 @Component로 하지 않고 다른 이름을 사용할까?</strong></p>
<p>각 어노테이션이 붙은 클래스의 역할을 명시적으로 표현하기 위해서라 할 수 있다.
코드를 처음 읽는 개발자라도 어노테이션을 통해, 해당 클래스가 어떤 역할을 하는지 알 수 있고, 어떤 방식으로 동작할지 예측도 가능하다.</p>
<p>다만, <code>@RestController</code>, <code>@Repository</code>의 경우
반환값을 HTTP 응답 Body로 직접 내려주거나, 예외 변환 기능과 같은 추가 기능을 제공한다.</p>
<p><strong>추가)</strong> <code>@RestController</code>의 경우 <code>@Controller</code> + <code>@ResponseBody</code>로 나뉘고,
<code>@Controller</code>가 내부적으로 <code>@Component</code>를 포함하기에 컴포넌트 스캔의 대상이 된다.</p>
</blockquote>
<p><strong>2. 설정 클래스 기반 등록</strong>
컴포넌트 기반 빈 등록 방식의 경우, 직접 작성한 클래스에 어노테이션을 붙여 이를 보고 스캔한다.</p>
<p>하지만 직접 작성하지 않은 코드(외부 라이브러리에서 제공하는 객체)들을, 빈으로 사용하는 경우
이러한 <code>@Component</code>계열 어노테이션을 붙일 수 없기에 위 방식만으로는 빈 등록의 한계가 있다.</p>
<ul>
<li>보안 처리를 위해 <code>BCryptPasswordEncoder</code>를 사용</li>
<li>결제 연동을 위해 외부 라이브러리의 <code>PaymentClient</code>를 사용</li>
</ul>
<p>그렇다고 스프링의 의존성 주입을 포기하고 매번 <strong>new</strong>로 생성해서 사용하면,
객체 관리와 의존관계 연결을 개발자가 직접하며 시간적 비용이 들어가 비즈니스 로직에 집중할 수 없다.</p>
<p>이럴 때 사용하는 방식이 <strong>설정 클래스 기반 등록</strong>이다.
<strong><code>@Configuration</code></strong> 클래스 안에 <strong><code>@Bean</code></strong> 메서드를 작성하고,
<strong>그 메서드에서 생성해 반환한 객체</strong>를 스프링 컨테이너가 Bean으로 관리하게 한다.</p>
<pre><code class="language-java">@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}</code></pre>
<p>즉 설정 클래스 기반 등록은 <strong>외부 라이브러리 객체처럼 클래스에 직접 어노테이션을 붙일 수 없거나</strong>,
<strong>객체 생성 과정에 별도의 설정값과 초기화 로직이 필요한 경우</strong>에 사용한다.</p>
<hr>
<h3 id="4-bean-scope">4. Bean Scope</h3>
<p><strong>빈 스코프(Bean Scope)</strong>란, 스프링 IoC 컨테이너가</p>
<ul>
<li>Bean을 몇 개 만들고</li>
<li>어디까지 공유하며</li>
<li>언제까지 유지할지를 정하는 범위를 말한다.</li>
</ul>
<p>이러한 빈 스코프를 통해 같은 타입의 빈이라도 Scope의 설정에 따라 하나의 객체만 생성할 수도,
요청이나 상황에 따라 새로운 객체가 생성될 수도 있다.</p>
<p>빈 스코프는 빈 등록과 마찬가지로 어노테이션을 통해 설정할 수 있다.</p>
<pre><code class="language-java">

@Configuration
public class AppConfig {

    @Scope(&quot;prototype&quot;) // 프로토타입 스코프 설정
    @Bean // 설정 클래스 기반 빈 등록
    public ReportGenerator reportGenerator() {
        return new ReportGenerator();
    }
}</code></pre>
<pre><code class="language-java">@Scope(&quot;request&quot;) // 리퀘스트 스코프 설정
@Component // 컴포넌트 스캔 기반 빈 등록
public class RequestInfo {

    private String userId;

    public RequestInfo() {
        System.out.println(&quot;RequestInfo 생성&quot;);
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUserId() {
        return userId;
    }
}</code></pre>
<p>대표적인 빈 스코프들과 각각의 기능은 다음과 같다.</p>
<ul>
<li><strong>singleton</strong> : 스프링 컨테이너 안에 Bean 인스턴스를 하나만 생성 (default)</li>
<li><strong>prototype</strong> : 요청할 때마다 새로운 Bean 인스턴스 생성</li>
<li><strong>request</strong> : HTTP 요청 하나 동안 유지</li>
<li><strong>session</strong> : HTTP 세션 하나 동안 유지</li>
<li><strong>application</strong> : 웹 애플리케이션 하나 동안 유지</li>
<li><strong>websocket</strong> : WebSocket 세션 동안 유지</li>
</ul>
<p>스프링에서는 어떠한 추가 설정도 없이 빈을 등록하면 기본적으로 <strong>singleton</strong>이 되며,
기본값으로 싱글톤이 설정되어 있는 이유는 싱글톤 빈의 특징인 단일 객체를 통해
어플리케이션 전반에서 동일한 인스턴스를 재사용할 수 있기 때문이다.</p>
<p>다만 <strong>하나의 인스턴스(싱글톤 빈)</strong>를 <strong>여러 요청 스레드가 동시에 사용</strong>할 수 있으므로,
싱글톤 빈 내부에는 <strong>상태값을 필드로 저장하지 않고 무상태로 설계</strong>해야 한다.</p>
<blockquote>
<p><strong>빈 안에서 상태를 가지면 안되는 이유</strong></p>
<p>싱글톤 빈은 메모리에 딱 1개만 존재한다. 따라서 해당 빈에 접근하는 모든 요청은
해당 빈이 보유하고 있는 필드에 동시 접근이 가능할 수 있는데,
이 필드 역시 모든 스레드(요청)가 공유하기에 <strong>동시성 문제</strong>, <strong>race-condition</strong>이 발생할 수 있다.</p>
<pre><code class="language-java">@Service
public class UserService {
    private String currentLoginUserId; // 동시성 문제 발생 가능
    ...
    public void createOrder(String userId, String itemName) {
        this.currentLoginUserId = userId;
    ...</code></pre>
<p><strong>그렇다면 동시성 처리를 하면 되지 않을까?</strong></p>
<p>이론적으로는 가능하다. <code>synchronized</code>, <code>Lock</code>, <code>Atomic</code> 등
자바의 동기화 도구들을 사용하면 공유 필드에 대한 동시 접근을 제어할 수 있다.
하지만 <strong>싱글톤 Bean 안에 요청별 상태를 저장한 뒤,
동시성 제어로 해결하려는 방식은 좋은 설계가 아니다.</strong>
(굳이 그렇게 할 필요도 없다, 동기화로 인한 오버헤드와 코드 복잡도 상승 등)</p>
<p>요청마다 달라지는 값은 필드에 저장하지 않고, <strong>메서드 파라미터나 지역 변수로</strong> 다룰 수 있기 때문이다.
이러한 방식은 <strong>스택 영역에 값이 저장</strong>되어 스레드간 충돌 위험이 없다.</p>
<pre><code class="language-java">@Service
public class UserService {
    // 생성자를 통해 주입받은 의존성 객체일 뿐,
    // orderRepository 역시 레포지토리 빈으로 무상태
    private final OrderRepository orderRepository;
    ...
    public void createOrder(String userId, String itemName) {
        String currentLoginUserId = userId;
    ...</code></pre>
</blockquote>
<hr>
<h3 id="5-정리">5. 정리</h3>
<blockquote>
<ol>
<li>Bean은 일반 자바 객체와 달리 스프링 IoC 컨테이너(ApplicationContext)에 의해
생성, 의존성 주입, 생명주기 관리의 대상이 되는 객체를 말한다.</li>
</ol>
<ol start="2">
<li>IoC는 객체 생성과 관리의 제어권을 개발자가 아니라 컨테이너에게 넘기는 개념이고,
DI는 컨테이너가 Bean 사이의 의존관계를 외부에서 주입해주는 방식이다.</li>
</ol>
<ol start="3">
<li>DTO, Entity처럼 요청마다 생성되고 사라지는 데이터 객체는 Bean으로 등록하지 않는다.
Bean으로 등록하기 적합한 객체는 Controller, Service, Repository처럼
애플리케이션의 핵심 처리 흐름을 담당하고 재사용 가능한 객체들이다.</li>
</ol>
<ol start="4">
<li>Bean 등록 방식에는 <code>@Component</code>, <code>@Service</code>, <code>@Repository</code>, <code>@Controller</code> 등을 사용하는
컴포넌트 스캔 기반 등록과, <code>@Configuration</code>, <code>@Bean</code>을 사용하는 설정 클래스 기반 등록이 있다.</li>
</ol>
<ol start="5">
<li>Spring Bean의 기본 스코프는 singleton이다.
즉 별도의 설정이 없다면 하나의 Bean 인스턴스만 생성되고,
여러 요청 스레드가 이 인스턴스를 공유해서 사용한다.</li>
</ol>
<ol start="6">
<li>싱글톤 Bean은 여러 요청이 동시에 접근할 수 있으므로,
요청마다 달라지는 상태값을 필드에 저장하면 동시성 문제가 발생할 수 있다.
따라서 요청별 데이터는 메서드 파라미터, 지역 변수처럼 요청 단위로 분리된 형태로 다뤄야 한다.</li>
</ol>
<ol start="7">
<li>결국 Spring은 IoC 컨테이너가 Bean을 관리하고,
DI를 통해 Bean 사이의 관계를 연결함으로써 객체 생성과 조립의 부담을 줄여준다.
덕분에 개발자는 객체 관리보다 비즈니스 로직에 더 집중할 수 있다.</li>
</ol>
</blockquote>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring MVC의 요청 처리 흐름]]></title>
            <link>https://velog.io/@zayn_noh/%EC%8A%A4%ED%94%84%EB%A7%81-MVC</link>
            <guid>https://velog.io/@zayn_noh/%EC%8A%A4%ED%94%84%EB%A7%81-MVC</guid>
            <pubDate>Mon, 01 Jun 2026 01:00:38 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅에서는 실제로 사용자(클라이언트)의 요청이 <strong>Spring</strong> 어플리케이션에 들어왔을 때,</p>
<p><strong>Spring MVC(Model-View-Controller)</strong> 내부에서 어떤 흐름으로 요청이 처리되는지 살펴보려한다.</p>
<p><code>DispatcherServlet</code>을 시작으로,
<code>HandlerMapping</code>, <code>HandlerAdapter</code>, <code>ArgumentResolver</code>, <code>MessageConverter</code>, <code>ViewResolver</code>까지</p>
<p>요청 처리 과정에 등장하는 주요 구성 요소들이 각각 어떤 역할을 하는지 흐름을 통해 이해하여 보고자 한다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/c1a2590e-2612-4e0d-ae05-81fb78b699d9/image.png" alt=""></p>
<h3 id="1-dispatcherservlet">1. DispatcherServlet</h3>
<p>가장 먼저 클라이언트의 <strong>HTTP Request</strong>가 들어오면,
Spring MVC의 DispatcherServlet이 해당 요청을 수신한다.</p>
<p>이때 DispatcherServlet은 Spring MVC의 <strong>Front Controller</strong>로서,
어플리케이션으로 들어온 HTTP 요청을 가장 먼저 받아 적절한 처리 흐름으로 전달한다.</p>
<p>DispatcherServlet은 단지 요청을 분석하고, 적절한 컨트롤러를 찾는 등
<strong>HttpRequest</strong>가 최종적으로 <strong>HttpResponse</strong>가 되기까지 실행 흐름을 처리하는 중앙 관문 역할을 한다.</p>
<hr>
<h3 id="2-handlermapping">2. HandlerMapping</h3>
<p><strong>HandlerMapping</strong>은 어플리케이션이 시작하는 시점에,</p>
<p><code>@RequestMapping</code>, <code>@GetMapping</code>, <code>@PostMapping</code></p>
<p>같은 정보를 스캔하여 매핑 테이블을 만들어둔다.</p>
<pre><code class="language-java">@GetMapping(&quot;/products/{id}&quot;)
public ProductResponse getProduct(@PathVariable Long id) {
    ...
}</code></pre>
<p>DispatcherServlet은 HandlerMapping에게
들어온 HTTP 요청을 처리하는 컨트롤러가 누구인지 확인하는 과정을 거친다.
( URL, HTTP Request Method와 같은 요청 정보를 통해 매핑되는 컨트롤러를 찾는다 )</p>
<p>HandlerMapping은 해당 과정에서 <strong>정확하게 일치하는 메서드의 컨트롤러</strong>를 찾아
DispatcherServlet에게 알려준다. 이때 요청에 일치하는게 없다면 <code>404</code>를 던진다.</p>
<hr>
<h3 id="3-handleradapter">3. HandlerAdapter</h3>
<p>앞선 과정에서 어떤 Handler가 해당 요청을 처리할지 찾았다고 해서,
DispatcherServlet이 바로 Controller 메서드를 호출하는 것은 아니다.</p>
<p>Spring MVC에서는 요청을 처리하는 대상이 하나의 형태로 고정되어 있지 않다.
우리가 흔히 사용하는 <code>@Controller</code>, <code>@RestController</code> 기반의 어노테이션 컨트롤러도 있고</p>
<p>과거 방식의 <code>Controller</code> 인터페이스 구현체나 <code>HttpRequestHandler</code>,
정적 리소스를 처리하는 Handler도 존재할 수 있다.</p>
<p>이처럼 Handler마다 실행 방식이 다르기 때문에,
DispatcherServlet이 모든 Handler의 호출 방식을 직접 알고 실행한다면
코드 복잡도가 증가하고 DispatcherServlet의 책임이 지나치게 커진다.</p>
<p>이를 분리하기 위해 Spring MVC는 <strong>HandlerAdapter</strong>라는 어댑터 계층을 둔다.</p>
<p>DispatcherServlet은 HandlerMapping이 찾아준 Handler를 직접 실행하지 않고,
<strong>해당 Handler를 실행할 수 있는 HandlerAdapter에게 실행 책임을 위임</strong>한다.</p>
<p>즉 HandlerAdapter는 다양한 형태의 Handler를 DispatcherServlet이
일관된 방식으로 실행할 수 있도록 중간에서 호출 방식을 맞춰주는 역할을 하는 것이다.</p>
<blockquote>
<p><strong>Handler와 Controller의 차이</strong></p>
<p>상단 내용에서 Hanler와 Controller를 섞어 사용하였는데 둘의 차이는 무엇일까?</p>
<p><strong>Controller</strong>는 MVC 패턴에서 사용자의 요청을 받아 비즈니스 로직을 호출하고,
그 결과를 응답으로 반환하는 웹 계층 객체를 의미한다.</p>
<p>반면 <strong>Handler</strong>는 Spring MVC 내부에서 사용하는 더 넓은 개념이다.
Handler는 요청을 실제로 처리할 수 있는 모든 대상을 의미한다.</p>
<p>일반적인 Spring MVC 애플리케이션에서는 Handler가 대부분 Controller 메서드이기에
“컨트롤러를 찾는다”라고 표현해도 크게 틀리지는 않다.
하지만 <strong>정확히 말하면 HandlerMapping은</strong>
요청을 처리할 Controller 자체가 아니라 <strong>요청을 처리할 Handler를 찾는다</strong>.</p>
<p><strong>즉 Controller는 Handler의 한 종류이며,</strong>
Handler는 Controller 메서드뿐 아니라 <code>HttpRequestHandler</code>,
정적 리소스 Handler 등 <strong>다양한 요청 처리 대상을 포함하는 더 넓은 표현</strong>이다.</p>
</blockquote>
<p><strong>Spring MVC</strong>에서는 다양한 형태의 컨트롤러가 존재할 수 있기에,
<strong>HandlerAdapter</strong>가 DispatcherServlet과 Controller 중간에서 실행 방식을 맞춰준다.</p>
<p>DispatcherServlet이 다양한 형태의 컨트롤러를 각각 다른 방식으로 호출해야한다면,
코드 복잡도가 올라가고 DispatcherServlet의 책임이 지나치게 무거워진다.</p>
<p>따라서 어댑터 계층을 두어 실행 가능한 형태의 컨트롤러를 호출하게 책임을 위임하는 것이다.</p>
<hr>
<h3 id="4argumentresolver">4.ArgumentResolver</h3>
<pre><code class="language-java">@GetMapping(&quot;/products/{id}&quot;)
public ProductResponse getProduct(@PathVariable Long id) {
    ...
}</code></pre>
<p>컨트롤러가 요청에 맞는 메서드를 실행하려면 파라미터가 필요한 경우가 있다.</p>
<p>위 예시에서 <strong>@PathVariable Long id</strong>를 통해,
개발자는 URL 경로에 포함된 상품 id 값을 메서드 파라미터로 편하게 받을 수 있다.</p>
<p>하지만 실제로 클라이언트가 보내는 값은 <code>Long</code>타입이 아니며, 자바의 타입도 모른다.</p>
<pre><code class="language-java">GET /products/1</code></pre>
<p>여기서 1은 HTTP 요청 경로에 포함된 <strong>문자열 값일 뿐</strong>이다.
그럼에도 컨트롤러 메서드에서는 이 값을 Long id로 받을 수 있다.</p>
<p>이처럼 HTTP 요청에 담긴 문자열, JSON Body, Query Parameter 등의 값을 읽어
컨트롤러 메서드가 요구하는 자바 객체나 값으로 만들어주는 역할을 하는 것이
<strong>ArgumentResolver</strong>다.</p>
<p>이 과정에서 Long 타입으로 변환해야 하는 자리에 문자가 들어오거나,
JSON Body 파싱에 실패하면 컨트롤러 메서드가 실행되기 전에 <code>400</code> Bad Request가 발생할 수 있다.</p>
<hr>
<h3 id="5-controller">5. Controller</h3>
<p>이전까지의 내용은 <strong>Spring Framework가 자동으로</strong> 처리해주는 영역이었다.</p>
<p><strong>ArgumentResolver</strong>를 통해 메서드에 전달될 파라미터까지 준비되면,
<strong>HandlerAdapter</strong>가 실제 <strong>Controller</strong> 객체의 메서드를 호출한다.</p>
<p>이 시점부터 우리가 작성한 애플리케이션 코드가 본격적으로 실행된다.</p>
<ul>
<li><p>Controller는 요청에 필요한 값을 전달받고,</p>
</li>
<li><p>Service 계층을 호출해 비즈니스 로직을 수행</p>
</li>
<li><p>Repository 계층을 통해 DB와 데이터 통신 등</p>
</li>
</ul>
<p>일련의 처리 과정이 끝나면, Controller는 해당 요청에 대한 처리 결과를 반환한다.</p>
<hr>
<h3 id="6-messageconverter-returnvaluehandler">6. MessageConverter, ReturnValueHandler</h3>
<p>Controller 메서드의 실행이 끝나면 처리 결과가 반환된다.
예를 들어 REST API에서 다음과 같은 <code>UserResponse</code> 객체를 반환한다고 가정해보면</p>
<pre><code class="language-java">return new UserResponse(id, &quot;username&quot;);</code></pre>
<p>Controller가 반환한 UserResponse는 자바 객체이기에,
클라이언트, 예를 들어 React 어플리케이션은 자바 객체를 그대로 이해할 수 없다.</p>
<p>따라서 이 객체를 HTTP 응답으로 전달할 수 있는 형태로 변환해야 한다.
REST API에서는 보통 JSON 문자열로 변환된다.
이런 과정에 관여하는 것이 <strong>ReturnValueHandler</strong>와 <strong>HttpMessageConverter</strong>다.</p>
<ul>
<li><p><strong>ReturnValueHandler</strong>
  ReturnValueHandler는 Controller 메서드의 반환값을 어떻게 처리할지 결정한다.
@ResponseBody나 @RestController가 붙어 있다면,
반환값을 View 이름으로 해석하지 않고 HTTP Response Body에 직접 쓰는 방식으로 처리한다.</p>
</li>
<li><p><strong>HttpMessageConverter</strong>
  HttpMessageConverter는 자바 객체를 JSON 같은 HTTP 메시지 본문 형태로 변환한다.
Spring Boot에서는 보통 Jackson 라이브러리가 사용되어 객체를 JSON으로 직렬화한다.</p>
</li>
</ul>
<pre><code class="language-json">{
  &quot;id&quot;: 1,
  &quot;name&quot;: &quot;username&quot;
}</code></pre>
<hr>
<h3 id="7-viewresolver">7. ViewResolver</h3>
<p>이 단계는 REST API 방식이 아니라, Thymeleaf나 JSP 같은 전통적인 템플릿 엔진을 사용할 때 거치는 과정이다.
( 전통적인 템플릿 엔진을 쓸 때만 거치는 과정, React가 이러한 View 역할을 맡는다 볼 수 있다 )</p>
<p>REST API에서는 Controller가 반환한 객체를 JSON으로 변환해
HTTP Response Body에 담기 때문에, 보통 ViewResolver를 거치지 않는다.</p>
<p>반면 템플릿 기반 MVC에서는 Controller가 문자열을 반환하는 경우가 많다.</p>
<pre><code class="language-java">@GetMapping(&quot;/products/{id}&quot;)
public String productDetail(Model model) {
    model.addAttribute(&quot;product&quot;, productService.findProduct(id));
    return &quot;product/detail&quot;; // view 이름
}</code></pre>
<p><strong>DispatcherServlet</strong>은 이 View 이름을 <strong>ViewResolver</strong>에게 전달하고,
ViewResolver는 실제 렌더링할 View 파일을 찾아준다.</p>
<pre><code class="language-java">View 이름: product/detail
파일: src/main/resources/templates/product/detail.html</code></pre>
<p>즉 <strong>ViewResolver</strong>는 Controller가 반환한 View 이름을,
실제 View 파일 위치로 변환해주는 역할을 한다.</p>
<p>이렇게 만들어진 HTML 응답이 브라우저로 전달되고, 브라우저는 그 HTML을 화면에 렌더링한다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTPS에서의 TLS Handshake 진행 과정]]></title>
            <link>https://velog.io/@zayn_noh/HTTPS%EC%97%90%EC%84%9C%EC%9D%98-TLS-Handshake-%EC%A7%84%ED%96%89-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@zayn_noh/HTTPS%EC%97%90%EC%84%9C%EC%9D%98-TLS-Handshake-%EC%A7%84%ED%96%89-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Thu, 28 May 2026 08:26:39 GMT</pubDate>
            <description><![CDATA[<h3 id="1-https의-등장-배경">1. HTTPS의 등장 배경</h3>
<p>기존 <strong>HTTP(Hypertext Transfer Protocol)</strong>의 경우,
클라이언트(웹 브라우저)와 서버가 통신하며 데이터를 주고 받을 때 어떠한 보호장치도 없이
평문(plain text)을 그대로 전송하였다.</p>
<p>이러한 방식은 네트워크 중간에서 데이터가 탈취된다면 어떠한 암호화도 되어있지 않기에
요청과 응답 내용에 해당하는 정보가 바로 노출될 위험성이 존재하였다.</p>
<p>이러한 문제점을 해결하고자 기존 HTTP 통신에 <strong>TLS</strong> 암호화 프로토콜을 적용한
<strong>HTTPS(Hypertext Transfer Protocol Secure)</strong>가 등장하였다.</p>
<p>HTTPS는 클라이언트와 서버 사이의 <strong>데이터를 암호화</strong>하여,
네트워크 중간에서 탈취되더라도 내용을 쉽게 알 수 없도록 보호한다.</p>
<p>또한 전송되는 데이터의 변조를 방지하고, 인증서를 통해 서버의 신뢰성을 확인할 수 있게 한다.</p>
<blockquote>
<p><strong>TLS(Transport Layer Security)</strong>
TLS는 뜻을 살펴보면 <strong>전송 계층 보안</strong>이다.</p>
<p>이러한 이름이 붙은 이유는 전송 계층 위에서 오가는 데이터를 안전하게 보호하기 위한 프로토콜이기 때문이다.</p>
<pre><code>HTTP : HTTP 메시지 생성
TLS : HTTP 메시지를 암호화
TCP : 암호화된 데이터를 전달
IP : 목적지(destination)까지 라우팅</code></pre><p>정확히 말하자면,
TLS는 전송 계층의 연결을 사용하는 어플리케이션 데이터를 보호하는 계층이라 할 수 있다.</p>
</blockquote>
<hr>
<h3 id="2-tls-handshake의-목적">2. TLS Handshake의 목적</h3>
<p><strong>TLS Handshake</strong>는 HTTPS 통신을 시작하기 전, 클라이언트와 서버가 안전한 통신을 준비하는 과정이다.</p>
<p>주요 목적은</p>
<ul>
<li><p>서버의 신원을 확인하고</p>
</li>
<li><p>사용할 암호화 방식을 협상하며</p>
</li>
<li><p>데이터 암호화에 사용할 세션키(<strong>대칭키(Symmetric key)</strong>)를 생성한다.</p>
</li>
</ul>
<blockquote>
<p>이때 세션키의 세션은 로그인 세션 과정 등에서 사용하는 세션이 아닌,
일반적인 의미의 세션을 뜻한다.
즉 TLS를 통한 암호화 통신을 진행하는 기간, 상태를 의미한다.</p>
</blockquote>
<hr>
<h3 id="3-tls-handshake-진행">3. TLS Handshake 진행</h3>
<h4 id="0-웹-서버의-인증서가-만들어지는-과정">0. 웹 서버의 인증서가 만들어지는 과정</h4>
<p>서버가 HTTPS를 사용하려면 먼저 인증서를 발급받아야 한다. 이때 다음과 같은 과정을 거친다.</p>
<ol>
<li><p>서버 관리자는 공개키(<code>Ks+</code>), 비밀키(<code>Ks-</code>)를 생성한다.</p>
</li>
<li><p>이후 공개키와 도메인 정보를 담은 <strong>CSR</strong>을 만든다.
이때 해당 CSR은 서버의 <code>Ks-</code>로 서명되어있다.
<strong>CSR = Ks-(message)</strong> // message = Ks+, 서버 공개키, 도메인 정보 등이 적혀있다.</p>
</li>
<li><p>CA에 CSR(<code>Ks-(message)</code>)을 제출한다.</p>
</li>
<li><p>CA는 해당 CSR의 신청자가 실제 도메인 소유자인지 검증한다.
(특정 파일을 업로드해봐라, 어떤 부분 수정해봐라 etc.)</p>
</li>
</ol>
<blockquote>
<p>이때 CSR은 Ks-로 서명되어 있기에, CA는 <strong>Ks+</strong>로 내용을 볼 수 있다.</p>
</blockquote>
<ol start="5">
<li><p>검증이 끝났다면 CA가 디지털 인증서를 발급한다.
<strong>Kca-(인증서 내용)</strong> : CA의 비밀키(<code>Kca-</code>)로 서명되어있다.</p>
</li>
<li><p>서버 관리자는 해당 인증서 <strong>Kca-(인증서 내용)</strong>를 웹 서버에 설치한다.</p>
</li>
</ol>
<br/>

<h4 id="1-서버-신원-확인-절차">1. 서버 신원 확인 절차</h4>
<p>사용자가 웹 브라우저를 통해 <code>velog.io</code>에 접속한다 가정하면
해당 접속 서버가 진짜 서버인지, 벨로그를 사칭한 가짜 서버인지 확인해야한다.</p>
<p>이를 위해 서버는 <strong>CA(Certificate authority)</strong>, 인증기관이 발급한 인증서를 브라우저에게 보여준다.
해당 인증서에는 다음과 같은 내용들을 포함한다.</p>
<p><code>Kca-(인증서 내용)</code></p>
<ul>
<li>도메인 이름</li>
<li><strong>서버 공개키</strong></li>
<li>인증서 유효 기간</li>
<li>인증서 발급 CA 정보</li>
<li>CA의 디지털 서명</li>
</ul>
<p>클라이언트는 받은 인증서를 CA의 공개키(<code>Kca+</code>)로 내부 내용을 확인한다.
<strong>Kca+(<code>Kca-(인증서 내용)</code>) = 인증서 내용</strong></p>
<ul>
<li>신뢰할 수 있는 CA가 발급했는가</li>
<li>인증서가 위조되지는 않았는가</li>
<li>유효 기간이 남아있는가</li>
<li>접속한 도메인과 인증서의 도메인이 일치하는가</li>
</ul>
<p>해당 과정이 끝나면 브라우저는 &quot;이 서버는 신뢰할 수 있다&quot;고 판단한다.</p>
<br/>

<h4 id="2-대칭키-생성-절차">2. 대칭키 생성 절차</h4>
<p>서버가 신뢰할 수 있음을 확인하였기에,
클라이언트와 서버는 이후 통신과정에서 송신할 데이터를 암호화하는 세션키(공개키)를 만들어야한다.</p>
<ol>
<li><p>대칭키 생성 : <code>Kc</code>
 클라이언트는 서버와 빠르고 안전하게 통신하기 위해 대칭키를 만든다.</p>
</li>
<li><p>서버의 공개키 <code>Ks+</code>로 대칭키를 암호화하여 전송한다. : Ks+(<code>Kc</code>)</p>
</li>
</ol>
<blockquote>
<p>해당 절차는 과거 초기 방식으로, 현대 TLS에서는 다음과 같은 과정을 거친다.</p>
<pre><code>클라이언트와 서버가 키 교환 정보를 주고받는다.
네트워크에는 공개되어도 되는 값만 오간다.
각자 자신의 비밀값과 상대의 공개값을 이용해 같은 공유 비밀값을 계산한다.
그 공유 비밀값으로 세션키를 만든다.</code></pre><p>자세히는 다루지 않겠지만 정리하자면
본인들의 공개키, 비밀키를 사용하여 각각 계산을 해서 동일한 비밀키를 만드는 것이다.</p>
</blockquote>
<ol start="3">
<li>서버는 암호화된 대칭키를 받아 본인의 비밀키로 대칭키를 확인한다.
 Ks-(<code>Ks+(Kc)</code>) = <code>Kc</code></li>
</ol>
<p>이후 안전하게 공유된 세션키(대칭키)를 통해 네트워크에서 데이터를 송수신한다. Kc(<code>data</code>)</p>
<blockquote>
<p><strong>왜 클라이언트와 서버간 통신에서 대칭키를 활용할까?</strong></p>
<p>비대칭키 암호화는 안전하지만 계산 비용이 크다.
반면 대칭키 암호화는 비대칭키에 비해 덜 안전하지만, 빠른 속도를 가진다.</p>
<p>결과적으로 <strong>비대칭키(공개키, 비밀키)를 사용하여 안전하게 대칭키를 공유</strong>하고
공유된 <strong>대칭키로 빠른 통신</strong>을 가능하게 하는 것이다.</p>
</blockquote>
<hr>
<p><a href="https://jynoh00.github.io/security/2024/10/28/security1.html">대칭키-공개키 암호화 관련, 과거 정리 링크</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[3-way Handshake를 이용한 TCP의 양방향 통신 논리적 연결]]></title>
            <link>https://velog.io/@zayn_noh/3-way-Handshake%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-TCP%EC%9D%98-%EC%96%91%EB%B0%A9%ED%96%A5-%ED%86%B5%EC%8B%A0-%EB%85%BC%EB%A6%AC%EC%A0%81-%EC%97%B0%EA%B2%B0</link>
            <guid>https://velog.io/@zayn_noh/3-way-Handshake%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-TCP%EC%9D%98-%EC%96%91%EB%B0%A9%ED%96%A5-%ED%86%B5%EC%8B%A0-%EB%85%BC%EB%A6%AC%EC%A0%81-%EC%97%B0%EA%B2%B0</guid>
            <pubDate>Wed, 27 May 2026 02:24:03 GMT</pubDate>
            <description><![CDATA[<p>OSI 7계층 중 <strong>4계층(Transport Layer)</strong>에서의 데이터 전송 규약 중 하나인</p>
<p><strong>TCP(Transmission Control Protocol)</strong>는 신뢰성 있는 데이터 전송을 지향하며 안정성을 보장해준다.</p>
<p>이번 포스트에서는 TCP에서 실제 전달하고자하는 데이터를 전송하기 전</p>
<p>호스트간 논리적 접속을 성립(<strong>Establish</strong>)하기 위한 방법인
<code>3-Way handshake</code>에 대해 자세히 알아보도록 한다.</p>
<hr>
<h3 id="1-3-way-handshake-란">1. <code>3-Way Handshake</code> 란</h3>
<p>TCP의 <code>3-Way handshake</code>는 실제 데이터를 전송하기 전, 송신자와 수신자 간 연결을 설정해주는 매커니즘이다.</p>
<p>클라이언트와 서버 모두 데이터를 전송하거나 받을 준비가 되었는지 확인하는 과정으로</p>
<p>TCP의 기능 중 하나인 <strong>신뢰성 있는 데이터 전송(RDT)</strong>을 증명하는 동작 원리 중 하나로 볼 수 있다.</p>
<blockquote>
<p><strong>RDT란?</strong>
<strong>RDT(Reliable Data Trasnfer)</strong>란 의미 그대로 신뢰성 있는 데이터 전송으로,
전송된 데이터가 소실되거나 손상되지 않도록 데이터를 전송하는 프로토콜 설계 원리를 말한다.<br/>
실제로 데이터를 전송되는 네트워크는 본질적으론 <strong>unreliable</strong>하다.
패킷이 손실되거나, 비트가 바뀌거나, 패킷의 전달 순서가 뒤바뀔 수 있다.
RDT는 이러한 불안정한 채널위에서 상위 계층에게 믿을 수 있는 데이터를 제공하기 위한 추상화인 것이다.<br/>
RDT는 현실적인 채널 모델을 가정하며 단계적으로 발전해왔고,
현재 <strong>TCP</strong>는 <strong>RDT3.0을 기반으로하여 파이프라이닝 기술을 얹은</strong> 형태를 사용한다.</p>
</blockquote>
<pre><code>
---

### 2. TCP Header

TCP에서는 상위 계층에서 내려온 데이터스트림(Datastream)을 받아들여

이를 일정 단위로 분할한 뒤 TCP Header를 덧붙여 세그먼트(Segment)를 생성한다.

이후 생성된 TCP Segment는 데이터그램(datagram)으로 하위 계층에 내려간다.

이때의 TCP Header는 하단 이미지와 같은 구조를 가진다.

![](https://velog.velcdn.com/images/zayn_noh/post/7e401914-e54a-4786-b505-f5d19028fe53/image.png)


크기는 대략 `20~60Byte` 이며 3-way Handshake 과정에서 주의 깊게 살펴볼 것은

플래그 비트 중 **ACK, SYN과** **Sequence number, Acknowledgement number**이다.

추가로 4-Way handshake로 연결을 끊을 때 **FIN** 플래그 비트도 사용한다.

**플래그 비트** 

- `URG` : 긴급 데이터 포함을 알림 → 수신자는 버퍼 순서에 상관없이 해당 데이터를 먼저 처리
- `ACK` : Acknowledgement number 필드가 유효함을 알림 → 거의 대부분 패킷에서 1
- `PSH` : 수신 버퍼에 쌓아두지 말고 즉시 어플리케이션 계층에 전달하라 → ex) 키보드 입력
- `RST` : 연결을 강제로 끊음 → 정상 종료(FIN)가 아닌 비정상 종료 상황
- `SYN` : 연결 수립을 요청하며 ISN을 동기화(synchronize) → 연결 수립 이후에는 보통 0
- `FIN` : 연결 종료 요청 비트 → 4-Way Handshake에 사용

&gt; **ISN(Initial Sequence Number)**
&gt; 
&gt; 
&gt; 
&gt; ISN(Initial Sequence Number)이란, 패킷 순서 추적에 사용하는 랜덤 초기값이다.
&gt; **랜덤값을 사용하는 이유** 중 하나로는
&gt; 네트워크 공격자가 시퀀스 번호를 알아내어 패킷을 위조하는 것을 막기 위함이 있다.
&gt;

---

### 3. 동작 방식 (SYN, SYN-ACK, ACK)

![](https://velog.velcdn.com/images/zayn_noh/post/1a646238-8963-417c-8707-5fcf2dfa8190/image.png)


`3-Way Handshake`는 세 가지 방식으로 진행된다.

- &lt;span style = &quot;color: red; font-weight: bold;&quot;&gt;SYN&lt;/span&gt; (클라이언트 → 서버)
- &lt;span style = &quot;color: red; font-weight: bold;&quot;&gt;SYN-ACK&lt;/span&gt; (서버 → 클라이언트)
- &lt;span style = &quot;color: red; font-weight: bold;&quot;&gt;ACK&lt;/span&gt; (클라이언트 → 서버)

첫 연결을 요청하는 호스트를 클라이언트, 해당 요청을 받는 호스트를 서버라 하였을 때

세부적으로 아래와 같은 과정이 진행된다.

**0. 3-Way Handshake 이전 포트 상태**

```java
CLIENT : CLOSED

SERVER : LISTEN</code></pre><p>*<em>1. SYN : 연결 요청 <code>SYN(seq=ISN_c)</code> *</em></p>
<p>클라이언트는 서버와 연결하기 위해 SYN을 보낸다.</p>
<p>이때 <strong>Sequence Number</strong>는 <strong>ISN(Initial Sequence Number)</strong>으로 랜덤 숫자가 지정되며,</p>
<p><strong>TCP Header</strong>의 <strong>SYN 플래그 비트</strong>를 <strong>1</strong>로 설정한 세그먼트를 전송한다.</p>
<pre><code class="language-java">// SYN 전송(C-&gt;S) 후

CLIENT : SYN_SENT

SERVER : LISTEN</code></pre>
<p>*<em>2. SYN-ACK : 요청 수락, 서버 연결 요청 <code>{SYN(seq=ISN_s)+ACK(ack=ISN_c+1)}</code> *</em></p>
<p>서버는 클라이언트로부터 <code>SYN(seq=ISN_c)</code>을 받고 자신의 연결 가능 여부를 파악한 후</p>
<p><strong>Acknowledgement Number</strong>에 <code>ISN_c + 1</code>값을 지정하고</p>
<p><strong>SYN</strong>, <strong>ACK</strong> 플래그를 각각 <strong>1</strong>로 설정한 후 세그먼트를 전송한다.</p>
<pre><code class="language-java">// SYN 수신 (C-&gt;S) 후

CLIENT : SYN_SENT

SERVER : SYN_RCVD</code></pre>
<p>*<em>3. ACK : 서버 SYN 확인 <code>ACK(ack=ISN_s+1)</code> *</em></p>
<p>클라이언트는 서버로부터 <code>{SYN(seq=ISN_s)+ACK(ack=ISN_c+1)}</code> 정보가 담긴 패킷을 받고</p>
<p><code>ACK(seq=ISN_s+1)</code> 정보를 담은 세그먼트를 서버에 전송한다.</p>
<pre><code class="language-java">// ACK 전송 (C-&gt;S) 후
CLIENT : ESTABLISHED

SERVER : SYN_RCVD

// ACK 수신 (C-&gt;) S후
CLIENT : ESTABLISHED

SERVER : ESTABLISHED</code></pre>
<h3 id="4-4-way-handshake">4. 4-Way Handshake</h3>
<p><code>3-Way Handshake</code> 과정을 거쳐 논리적 연결이 이루어졌다면(<code>ESTABLISHED</code>)</p>
<p>클라이언트와 서버는 데이터를 송수신하며 통신을 진행한다.</p>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/f1a5c007-5e4c-441d-a4df-5d4143d6923e/image.png" alt=""></p>
<p>모든 데이터의 송수신이 끝나고 처리해야 할 통신이 완료되었다면</p>
<p>두 호스트는 연결을 종료하기 위해 <code>4-Way Handshake</code> 과정을 거친다.</p>
<ul>
<li><span style="color: red; font-weight: bold;">FIN</span> (클라이언트 → 서버)</li>
<li><span style="color: red; font-weight: bold;">ACK</span> (서버 → 클라이언트)</li>
<li><span style="color: red; font-weight: bold;">FIN</span> (서버 → 클라이언트)</li>
<li><span style="color: red; font-weight: bold;">ACK</span> (클라이언트 → 서버)</li>
</ul>
<blockquote>
<p><strong>왜 TCP 연결의 시작처럼 3-Way가 아닌 4-Way일까?</strong></p>
<p>종료 시점에 연결을 끊기 위한 메커니즘이 4-Way로 나뉘는 것은</p>
<p>TCP는 양방향 연결이기에 클라이언트와 서버 두 호스트가 <strong>각각 독립적으로 종료</strong>되어야 하기 때문이다.</p>
</blockquote>
<p>먼저 연결 종료를 요청하는 호스트를 클라이언트,
해당 요청을 받는 호스트를 서버라 하였을 때 아래와 같은 과정이 진행된다.</p>
<blockquote>
<p><code>3-Way Handshake</code>와 다르게
<code>4-Way Handshake</code>는 꼭 클라이언트가 먼저 <strong>FIN</strong>을 보내지 않는다.</p>
<p>연결 종료는 통신이 끝났다 판단하는 쪽이 먼저 FIN을 보낼 수 있으며,
클라이언트, 서버의 구분 없이 <strong>Active Close</strong>가 된다.</p>
<ul>
<li><strong>Active Close</strong> : TCP 통신 중 먼저 <strong>FIN을 보내는</strong> 호스트</li>
<li><strong>Passive Close</strong> : TCP 통신 중 <strong>FIN을 받은</strong> 호스트</li>
</ul>
</blockquote>
<p><strong>0. 4-Way Handshake 이전 포트 상태</strong></p>
<pre><code class="language-java">CLIENT : ESTABLISHED

SERVER : ESTABLISHED</code></pre>
<p><strong>1. FIN : 연결 종료 요청 <code>FIN(seq=x)</code></strong></p>
<p>클라이언트와 서버 중 통신이 끝났다 판단하는 쪽에서 연결을 종료하기 위해 <strong>FIN</strong>을 보낸다.</p>
<p>보낼 세그먼트 내 <strong>TCP Header</strong>의 <strong>FIN 플래그 비트</strong>의 값을 <strong>1</strong>로 한다.</p>
<p>이때 <code>3-Way Handshake</code>의 경우 첫 <strong>SYN</strong>을 <strong>ISN</strong>으로 랜덤한 값을 부여했지만,
<strong>FIN</strong>의 경우 연결 수립 후 주고 받아온 시퀀스에 따라 증가해온 <code>seq#</code>을 사용하여 다음 번호를 쓴다.</p>
<pre><code class="language-java">// 클라이언트가 Active Close인 경우를 가정
// FIN 전송 (C -&gt; S)
CLIENT : FIN_WAIT_1

SERVER : ESTABLISHED</code></pre>
<p><strong>2. ACK : 종료 요청 확인 <code>ACK(ack=x+1)</code></strong></p>
<p>Passive Close에 해당하는 호스트가 FIN을 받은 후 응답으로,
<strong>Acknowledgement Number</strong>에 <strong>x+1</strong>값을 저장한 후 세그먼트를 전송한다.</p>
<p>이때 Passive Close는 아직 전송하지 못한 데이터가 남아 있을 수 있기에 바로 FIN을 보내지 않는다.</p>
<pre><code class="language-java">// 클라이언트가 Active Close인 경우를 가정

// FIN (C -&gt; S) 수신 후
CLIENT : FIN_WAIT_1

SERVER : CLOSE_WAIT

// ACK (S -&gt; C) 수신 후
CLIENT : FIN_WAIT_2

SERVER : CLOSE_WAIT</code></pre>
<p><strong>3. FIN : 연결 종료 요청 <code>FIN(seq=y)</code></strong></p>
<p><strong>Passive Close</strong>는 남은 데이터를 모두 전송한 후, 클라이언트에 <strong>FIN</strong>을 보낸다.</p>
<p>이때 <code>seq#</code> 또한 이전까지 <strong>Active Close</strong>와 통신할 때 사용한 <code>seq#</code>을 사용한다.</p>
<blockquote>
<p>** <code>seq#</code>와 <code>ack#</code>**</p>
<p><code>seq#</code> : 내가 보내는 데이터의 시작 바이트 번호
<code>ack#</code> : 내가 다음으로 받기를 기대하는 바이트 번호 (<strong>= 상대방 seq# + 데이터 크기</strong>)</p>
<p><strong>ex)</strong></p>
<pre><code>CLIENT → SERVER : seq=100, data=100bytes

SERVER → CLIENT : ack=200  // 100~199 다 받았고, 다음엔 200번 달라

CLIENT → SERVER : seq=200, data=100bytes 
// 받은 ack#를 그대로 자신의 seq#로 사용</code></pre></blockquote>
<pre><code class="language-java">// 클라이언트가 Active Close인 경우를 가정

// FIN (S -&gt; C) 전송 후
CLIENT : FIN_WAIT_2

SERVER : LAST_ACK

// FIN (S -&gt; C) 수신 후
CLIENT : TIME_WAIT

SERVER : LAST_ACK</code></pre>
<p><strong>4. ACK : FIN 확인 <code>ACK(ack=y+1)</code></strong></p>
<p><strong>Active Close</strong>는 <strong>Passive Close</strong>로부터 온 <strong>FIN</strong>을 받고 확인 응답 <strong>ACK</strong>를 보낸다.</p>
<p><strong>Acknowledgement Number</strong>에 <code>y+1</code>값을 저장하고,
<strong>ACK 플래그 비트</strong>를 <strong>1</strong>로 설정한 후 세그먼트를 전송한다.</p>
<p>이때 <strong>ACK</strong>를 보낸 Active Close 호스트는 바로 <strong>CLOSED</strong>가 되지 않고
<strong>TIME_WAIT</strong> 상태에서 일정 시간 대기 후 <strong>CLOSED</strong>된다.</p>
<blockquote>
<p><strong>TIME_WAIT가 필요한 이유?</strong>
Active Close 호스트가 보낸 마지막 ACK가 네트워크에서 손실될 수 있다.
이 경우 Passive Close 호스트는 LAST_ACK 상태에서 FIN을 재전송하는데,
클라이언트가 이미 CLOSED가 되어버리면 서버는 영원히 CLOSED가 되지 못한다.</p>
</blockquote>
<pre><code class="language-java">// 클라이언트가 Active Close인 경우를 가정

// ACK 전송 전 (클라이언트가 FIN 수신 후)
CLIENT : TIME_WAIT
SERVER : LAST_ACK

// ACK 전송 후 (C-&gt;S)
CLIENT : TIME_WAIT → CLOSED // 일정 시간 후 CLOSED로
SERVER : LAST_ACK

// ACK 수신 후
CLIENT : CLOSED
SERVER : CLOSED</code></pre>
<hr>
<h3 id="5-syn-flooding-공격">5. SYN Flooding 공격</h3>
<p>SYN Flooding 공격이란, 앞서 설명한 TCP의 <code>3-Way Handshake</code> 메커니즘의 약점을 노린 공격이다.</p>
<p>공격자가 노리고자하는 서버에 대량의 <strong>SYN</strong>을 보내고,
이후 서버로부터 온 <strong>SYN+ACK</strong>에 <strong>ACK</strong>를 보내지 않는다면</p>
<p>서버는 <strong>SYN_RCVD</strong> 상태의 반쪽 연결(half-open connection)을 대량으로 유지하다가
자원이 고갈되어 정상 사용자에게 서비스를 제공할 수 없는 상태가 될 수 있다.</p>
<p>이를 방어하기 위해 <strong>SYN Cookie</strong>와 같은 기법이 사용된다.</p>
<blockquote>
<p><strong>SYN Cookie</strong>
서버가 half-open connection을 저장하지 않고
쿠키로 검증하여 SYN Flooding을 방어하는 기법</p>
</blockquote>
<hr>
<h3 id="6-주요-개념-한-줄-정리">6. 주요 개념 한 줄 정리</h3>
<ul>
<li><p>TCP : 전송 계층에서의 신뢰성 있는 양방향 데이터 전송 프로토콜</p>
</li>
<li><p>RDT : 불안정한 채널에서 상위 계층에게 신뢰성 있는 정보를 전달하기 위한 추상화</p>
</li>
<li><p>3-Way Handshake : TCP에서 신뢰성 있는 데이터 전송을 위한 양방향 연결 수립 절차</p>
</li>
<li><p>4-Way Handshake : TCP의 양방향 논리적 연결을 종료하기 위한 메커니즘</p>
</li>
<li><p>TCP Header : TCP 세그먼트의 정보를 담은 20~60Byte 크기의 헤더</p>
</li>
<li><p>Segment : 상위 계층에서 받은 datastream을 일정 단위 분할 후 TCP헤더를 붙인 전송 단위</p>
</li>
<li><p>seq# : 자신이 보내는 데이터의 시작 바이트 번호</p>
</li>
<li><p>ack# : 다음으로 받길 기대하는 바이트 번호 (상대 seq# + 상대가 전송한 데이터 크기)</p>
</li>
<li><p>ISN : 3-Way Handshake에서 사용하는 랜덤 초기 시퀀스 번호</p>
</li>
<li><p>SYN : 연결 수립을 요청하며 ISN을 동기화하는 플래그</p>
</li>
<li><p>ACK : 수신 확인용 응답 플래그</p>
</li>
<li><p>FIN : 연결 종료 요청 플래그</p>
</li>
<li><p>Active Close : 먼저 FIN을 보낸 종료 요청 호스트</p>
</li>
<li><p>Passive Close : FIN을 받아 연결 종료에 응답하는 호스트</p>
</li>
<li><p>TIME_WAIT : Active Close가 마지막 ACK 손실 대비 대기하는 상태</p>
</li>
<li><p>SYN Flooding : 대량의 SYN을 서버에 보내 반쪽 연결을 유도, 자원을 고갈시키는 공격 기법</p>
</li>
<li><p>SYN Cookie : 서버가 반쪽 연결을 저장하지 않고, cookie로 검증하여 SYN Flooding을 방어하는 기법</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[나는 왜 개발자여야 하는가]]></title>
            <link>https://velog.io/@zayn_noh/%EB%82%98%EB%8A%94-%EC%99%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%97%AC%EC%95%BC-%ED%95%98%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@zayn_noh/%EB%82%98%EB%8A%94-%EC%99%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%97%AC%EC%95%BC-%ED%95%98%EB%8A%94%EA%B0%80</guid>
            <pubDate>Wed, 27 May 2026 01:00:13 GMT</pubDate>
            <description><![CDATA[<p><span style = "font-size: 1.5rem"><strong>왜 개발자를 지망하냐</strong></span>는 질문에 나는 늘 고민하게 되는 것 같았다.</p>
<ul>
<li><p>돈을 많이 벌고 싶어서</p>
</li>
<li><p>문제 해결 과정이 재밌어서</p>
</li>
<li><p>단순 코드를 작성하는게 즐거워서</p>
</li>
</ul>
<p>모두 어느정도는 맞는 말이라고 생각한다. 실제로 개발하며 코드를 작성하는 순간엔</p>
<p>어떤 생각도 안들고 온전히 코딩에 집중하기도 하였고 그러한 과정 속에서</p>
<p>막혔던 부분이 풀렸을 때 오는 성취감은 행복이란 감정으로 나에게 다가온다.</p>
<p>그리고 돈은 당연히 모두가 많이 벌고 싶을 것이다.</p>
<p>하지만 정말로 그런 것들 때문에 나는 개발자를 꿈꾸고 목표로 하는 것일까?</p>
<hr>
<p>어린 시절 겪었던 경험에 대해 이야기해보려 한다.</p>
<p>초등학교 5학년쯤, 친구가 널리 퍼져있던 해킹툴을 사용해 내 컴퓨터를 해킹한 적이 있다. </p>
<p>단순히 친구가 만든 프로그램을 내가 실행하면 내 컴퓨터가 서버가 되고,
친구는 클라이언트로 내 컴퓨터에 접속해 여러 기능을 실행하는 방식이었다.</p>
<p>지금 생각해보면 대단한 기술은 아니었을지도 모른다. </p>
<p>하지만 당시의 나는 정말 어마어마한 충격을 받았다.
그런데 그 충격은 해킹을 당했다는 두려움에서 비롯된 것이 아니었다.</p>
<p>오히려 “이런 게 가능하다고?”라는 신기함에 가까웠다.</p>
<p>내 컴퓨터가 다른 사람의 명령에 따라 움직인다는 사실, 화면 밖 어딘가에 있는 누군가가 
프로그램을 통해 내 컴퓨터와 연결될 수 있다는 사실이 너무 놀라웠다. </p>
<p>그때 처음으로 컴퓨터가 단순히 게임을 하거나 인터넷을 보는 도구가 아니라, 내가 이해하고 다룰 수 있다면
훨씬 많은 일을 할 수 있는 세계라는 생각을 하게 되었다.</p>
<p>이후 나는 어떻게 이런 일이 가능한지 찾아보기 시작했다. </p>
<p>그러다 자연스럽게 코딩이라는 것을 알게 되었다. 사실 당시에는 그것을 코딩이라고 부르는지도 잘 몰랐다. 
그냥 무언가를 작성하면 프로그램이 동작하고,
내가 쓴 내용에 따라 기능이 실행된다는 정도로만 이해했던 것 같다.</p>
<p>그러면서 Visual Basic으로 간단한 프로그램을 만들어보고,
게임메이커 스튜디오 8.0으로 직접 게임도 만들어보았다.</p>
<p>버튼을 누르면 창이 뜨고, 캐릭터가 움직이고, 점수가 올라가고, 내가 정한 규칙대로 프로그램이 반응했다.
지금 보면 조잡하고 단순한 결과물이었겠지만, 그때의 나에게는 분명히 내가 만든 작은 세계였다.</p>
<p>무엇보다 그 모든 과정은 누가 시켜서 한 일이 아니었다. 스펙을 쌓기 위해서도 아니었고, 좋은 평가를 받기 위해서도 아니었다. 그냥 궁금했고, 재미있었고, 만들고 싶었다. 코드를 쓰고, 실행해보고, 안 되면 다시 고치고, 결국 내가 생각한 대로 무언가가 움직이는 순간이 좋았다.</p>
<hr>
<p>사람은 누구나 어린 시절 행복했던 기억을 하나쯤은 가지고 있다고 생각한다.</p>
<p>시간이 지나도 이상하게 선명하게 남아 있는 순간들,
별것 아닌 장면인데도 다시 떠올리면 마음 한쪽이 따뜻해지는 기억들</p>
<p>누구나 한 번쯤은 그때로 돌아가고 싶다는 생각을 한다. 
하지만 당연하게도 타임머신은 없고, 우리는 과거로 돌아갈 수 없다. 
아무리 그리워해도 그 시절의 방, 그때의 컴퓨터, 그때의 나 자신을 완전히 다시 만날 수는 없다.</p>
<p>그럼에도 내 어린 시절의 행복 중 하나는 분명 컴퓨터 앞에 앉아 있던 시간이었다.</p>
<p>윈도우 XP 화면을 켜두고 Visual Basic으로 코드를 작성하던 시간,
게임메이커 스튜디오로 직접 게임을 만들며 혼자 신나 하던 시간,
그 순간의 기억들이 아직도 머릿 속에 남아 있다.</p>
<p>어쩌면 내가 아직도 개발자를 꿈꾸는 이유는,</p>
<p>그저 이런 과거의 향수일 뿐인 추억의 결과가 허상으로 끝나지 않길 바라기 때문일지도 모른다.</p>
<p><span style = "font-size: 2rem"><strong>그렇기에 나는 개발자여야 한다.</strong></span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OSI 모델의 계층별 특징과 독립적 역할]]></title>
            <link>https://velog.io/@zayn_noh/OSI-%EB%AA%A8%EB%8D%B8%EC%9D%98-%EA%B3%84%EC%B8%B5%EB%B3%84-%ED%8A%B9%EC%A7%95%EA%B3%BC-%EB%8F%85%EB%A6%BD%EC%A0%81-%EC%97%AD%ED%95%A0</link>
            <guid>https://velog.io/@zayn_noh/OSI-%EB%AA%A8%EB%8D%B8%EC%9D%98-%EA%B3%84%EC%B8%B5%EB%B3%84-%ED%8A%B9%EC%A7%95%EA%B3%BC-%EB%8F%85%EB%A6%BD%EC%A0%81-%EC%97%AD%ED%95%A0</guid>
            <pubDate>Tue, 26 May 2026 04:14:44 GMT</pubDate>
            <description><![CDATA[<p><strong>OSI(Open Systems Interconnection) 모델</strong>은 서로 다른 시스템 간의 통신을 표준화하기 위해
<strong>ISO((International Organization for Standardization)</strong>에서 제정한 참조 모델이다.</p>
<p>OSI 모델에서는 네트워크의 통신 과정을 <code>7개의 계층</code>으로 나누어,
<strong>각 계층이 독립적으로 기능하게</strong> 설계하였다.</p>
<p>이번 포스팅에서는 각 계층의 특징과 역할을 살펴보며,
해당 계층에서 어떤 기술과 장비가 있는 지 정리하여 보도록 하겠다.</p>
<hr>
<h3 id="1-osi-계층-분리-이유">1. OSI 계층 분리 이유</h3>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/8815b6e2-5fa5-474c-baf0-6f3e17115897/image.png" alt=""></p>
<p>OSI에서 각 계층을 분리한 이유는 계층별로 역할을 다르게하여 <strong>독립적으로 동작</strong>하게 했기에
특정한 곳에 문제가 생겨도 다른 계층에 있는 장비는 건드리지 않고,
해당 계층의 장비만 고치게 된다면 빠르게 네트워크 통신에서 문제를 해결할 수 있기 때문이다.</p>
<p>즉 <strong>네트워크 통신 과정을 역할별로 분리하여 표준화</strong>하고,
이를 통해 개발, 유지보수, 호환성을 쉽게하기 위하여 계층을 나눈 것이다.</p>
<p>뿐만 아니라 해당 OSI 모델로 표준화되기 이전에는
회사마다 네트워크 방식이 달랐고 상호간에 호환도 잘 되지 않아 문제점이 많았다.</p>
<p>이러한 불편함을 하나의 단일 모델로 통일하여,
지금처럼 여러 시스템이 동일 네트워크에서 통신할 수 있는 것이다.</p>
<hr>
<h3 id="2-application-layer-layer-7-l7">2. Application Layer [Layer 7, L7]</h3>
<p><strong>응용 계층(Application Layer)</strong>은 사용자와 가장 맞닿아 있는 7계층으로,
우리가 컴퓨터를 할 때 사용하는 응용 프로그램, 프로세스가 이 곳에서 동작한다.</p>
<p>사용자와 네트워크 간 <strong>인터페이스 기능을 제공</strong>하며 <strong>사용자와 직접 상호작용</strong>할 수 있다.</p>
<p>이메일, 웹 브라우저, 메신저, 파일 전송 등 다양한 네트워크 서비스를 사용자는 이용할 수 있다.</p>
<p>해당 계층에서 사용하는 대표적인 프로토콜로는 다음과 같은 예시가 있으며,
해당 포스트에서는 각 프로토콜에 대해 간단하게 정리만 하고 지나가겠다.</p>
<ul>
<li>HTTP / HTTPS : 웹 브라우저와 웹 서버가 데이터를 주고 받기 위한 프로토콜
  <code>HyperText Transfer Protocol</code></li>
</ul>
<ul>
<li><p>FTP : 파일을 업로드하거나 다운로드할 때 사용하는 프로토콜
  <code>File Transfer Protocol</code></p>
</li>
<li><p>SMTP : 이메일을 전송할 때 사용하는 프로토콜
  <code>Simple Mail Transfer Protocol</code></p>
</li>
<li><p>DNS : 사람이 읽기 쉬운 형태의 문자열 도메인 주소를 IP주소로 변환해주는 시스템
  <code>Domain Name System</code></p>
</li>
<li><p>SSH : 원격 서버에 안전하게 접속하기 위한 암호화 기반의 프로토콜
  <code>Secure Shell</code></p>
</li>
</ul>
<blockquote>
<p>전송 데이터 단위 : <strong>Data or Message</strong></p>
</blockquote>
<hr>
<h3 id="3-presentation-layer-layer-6-l6">3. Presentation Layer [Layer 6, L6]</h3>
<p><strong>표현 계층(Presentation Layer)</strong>은 6계층으로 7계층에서 내려온 <strong>Data의</strong> 형식을 변환하며
암호화, 복호화 및 압축을 하는 역할을 수행한다.</p>
<p>즉 서로 다른 시스템이 이해할 수 있도록 <strong>Data</strong>를 번역하는 계층인 것이다.
(서로 다른 시스템이 같은 방식으로 데이터를 읽게 만든다)</p>
<ul>
<li><p>문자 인코딩 변환</p>
</li>
<li><p>압축</p>
</li>
<li><p>암호화 / 복호화</p>
</li>
</ul>
<p>다음과 같은 예시를 살펴보면 해당 계층이 수행하는 역할을 이해할 수 있다.</p>
<pre><code>가정) A 시스템 : UTF-8, B 시스템 : EUC-KR

서로다른 문자 인코딩 방식을 사용하는 시스템에서 각 방식으로 인코딩된 글은 읽을 수 없다.

이를 Presentation Layer, 6계층에서 문자 인코딩 변환을 해주어
해당 시스템에 맞는 형식으로 바꿔주는 것이다.</code></pre><blockquote>
<p>전송 데이터 단위 : <strong>Data</strong></p>
</blockquote>
<hr>
<h3 id="4-session-layer-layer-5-l5">4. Session Layer [Layer 5, L5]</h3>
<p><strong>세션 계층(Session Layer)</strong>에서는 두 시스템 간의 연결(세션)을 수립하고,
유지, 종료하는 역할을 담당한다.</p>
<p>이때 주의해서 확인해야할 점은 <strong>Session Layer</strong>는 &quot;데이터 전달&quot; 자체를 담당하는 것이 아니다.</p>
<p>실제 데이터 전송의 경우 4계층에서 TCP, UDP와 같은 프로토콜이 처리한다.</p>
<p><strong>Session Layer</strong>는 그 위에서 대화의 상태를 관리한다.</p>
<ul>
<li><p>어떤 대화가 진행 중인지</p>
</li>
<li><p>어느 시점까지 처리되었는지</p>
</li>
<li><p>누가 송신 권한을 가지는지</p>
</li>
</ul>
<p>즉, 통신을 위한 세션을 관리하고 연결 설정 및 해제하며 동기화까지 역할을 맡는다.</p>
<blockquote>
<p>사실 현재의 인터넷에서 컴퓨터들이 서로 데이터를 주고 받을 때는
<strong>TCP/IP 4계층 모델</strong>을 사용하며 OSI 7계층에서의
L5, L6, L7은 하나의 Application Layer로 다룬다.</p>
<p>이러한 TCP/IP 4계층 모델은 OSI 7계층을 기반으로 만들어졌다.</p>
</blockquote>
<blockquote>
<p>전송 데이터 단위 : <strong>Data</strong></p>
</blockquote>
<hr>
<h3 id="5-transport-layer-layer-4-l4">5. Transport Layer [Layer 4, L4]</h3>
<p><strong>전송 계층(Transport Layer)</strong>은 <strong>종단 간(End-to-End)</strong>의 신뢰성 있는 <strong>데이터 전송</strong>을 담당한다.
기본적으로 포트를 관리하며 수신된 데이터가 어느 응용프로그램에 전송될지 판독도 한다.</p>
<p>대표적인 프로토콜로는 <strong>TCP</strong>, <strong>UDP</strong>가 있으며
해당 계층에서는 다음을 포함한 데이터 전송 관련 기능들을 제공한다.</p>
<ul>
<li><p>흐름 제어(flow control)</p>
</li>
<li><p>혼잡 제어(congestion control)</p>
</li>
<li><p>오류 제어(error control)</p>
</li>
<li><p>멀티플렉싱(multiplexing)</p>
</li>
</ul>
<p>특히 TCP와 UDP의 경우 데이터에 대한 각기 다른 요구사항을
충족시키기 위해 서로 다른 동작 방식을 가지는데</p>
<p><strong>TCP</strong>의 경우 속도보다 안전하고 신뢰성있는 데이터 전송을 보장하는 프로토콜이고,
<strong>UDP</strong>의 경우 데이터의 신뢰성보다 빠른 전송 속도를 중시하는 프로토콜이다.</p>
<blockquote>
<p>전송 데이터 단위 : <strong>Segment</strong></p>
</blockquote>
<hr>
<h3 id="6-network-layer-layer-3-l3">6. Network Layer [Layer 3, L3]</h3>
<p><strong>네트워크 계층(Network Layer)</strong>은 IP 주소를 기반으로 하여,
최적 경로를 찾아 패킷을 전달하는 라우팅을 담당한다.</p>
<p>물리적인 네트워크 사이의 라우팅과, 네트워크 호스트의 논리적인 주소(IP)를 관리하고
패킷을 분할하여 프로토콜을 식별하기도하며, <strong>오류 탐지(error detection)</strong> 등을 수행한다.</p>
<blockquote>
<p><strong>라우팅(Routing)</strong>
여러 개의 네트워크가 연결되어 있을 때,
데이터 패킷을 목적지까지 가장 효율적으로 전달하기 위해 
어떤 경로를 거쳐야 하는지, 최적의 경로를 결정하는 과정을 말한다.</p>
</blockquote>
<blockquote>
<p><strong>IP는 비연결형 프로토콜이다.</strong>
IP(Internet Protocol)는 비연결형 프로토콜로,
패킷을 전송하기 전에 미리 연결을 설정하지 않고 IP 주소를 향해 전송한다.
이러한 특성은 신뢰성과 안정성 측면에서 한계가 나타나기에,
L4와 같은 상위 계층에서 TCP와 같은 신뢰성 있는 데이터 전송 프로토콜을 제공한다.</p>
</blockquote>
<blockquote>
<p>전송 데이터 단위 : <strong>Packet</strong></p>
</blockquote>
<hr>
<h3 id="7-data-link-layer-layer-2-l2">7. Data Link Layer [Layer 2, L2]</h3>
<p><strong>데이터 링크 계층(Data Link Layer)</strong>은 동일 네트워크 내에서 노드간 데이터 전송을 담당한다.</p>
<p><strong>MAC</strong>주소로 장치를 식별하며, <strong>frame</strong> 단위로 데이터를 처리하고 오류를 검출한다.</p>
<p>이때 해당 계층에서 진행하는 오류 검출 과정은,
상위 계층인 <strong>L1</strong>, 물리 계층에서 발생하는 오류를 검출하고 수정하는 것이며</p>
<p>물리적인 장비를 식별하는데 사용되는 주소 지정 체계와 데이터가
변조되지 않았음을 확증하기 위해 오류를 확인하는 기능을 수행한다.</p>
<p>해당 계층에서는 프레임을 데이터의 단위로 다루는데,
이러한 프레임은 물리적 매체를 통해 전송되는 데이터의 논리적 그룹이라 볼 수 있다.
프레임은 일반적으로 다음과 같은 필드를 포함하여 구성된다.</p>
<ul>
<li>시작 프레임 마커</li>
<li>목적지 주소</li>
<li>출발지 주소</li>
<li>프로토콜 식별자</li>
<li>데이터</li>
<li>오류 검출, 제어를 위한 프레임 마커</li>
</ul>
<blockquote>
<p><strong>프레이밍(Framing)</strong>
프레이밍이란, 데이터 링크 계층에서 수행되며
프레임에 대한 시작과 끝을 알리는 프레임 마커를 추가하여 프레임을 구분하는 작업이다.</p>
</blockquote>
<blockquote>
<p>전송 데이터 단위 : <strong>Frame</strong></p>
</blockquote>
<hr>
<h3 id="7-physical-layer-layer-1-l1">7. Physical Layer [Layer 1, L1]</h3>
<p><strong>물리 계층(Physical Layer)</strong>은 실제 물리 매체를 통해 비트를 전송하는 계층으로,
데이터의 의미를 전혀 해석하지 않으며 단순히 <code>0, 1</code>을 전기, 광, 무선 신호 형태로 변환해 전달한다.</p>
<ul>
<li>전압 레벨</li>
<li>전송 속도</li>
<li>케이블 규격</li>
<li>주파수/변조 방식 정의</li>
<li>비트 타이밍 처리</li>
</ul>
<p>등을 하드웨어에 구현된 물리 신호 규칙을 이용하여 설정할 수 있다.</p>
<blockquote>
<p>전송 데이터 단위 : <strong>Bit</strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바에서의 오류와 예외 처리 정리]]></title>
            <link>https://velog.io/@zayn_noh/%EC%9E%90%EB%B0%94%EC%97%90%EC%84%9C%EC%9D%98-%EC%98%A4%EB%A5%98%EC%99%80-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@zayn_noh/%EC%9E%90%EB%B0%94%EC%97%90%EC%84%9C%EC%9D%98-%EC%98%A4%EB%A5%98%EC%99%80-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 22 May 2026 06:50:10 GMT</pubDate>
            <description><![CDATA[<h3 id="1-오류란">1. 오류란,</h3>
<p>자바에서는 발생할 수 있는 <strong>오류(예외, 에러)</strong>에 대해서
기본적으로 다음 두 개의 클래스를 통해 정의하였다.</p>
<pre><code class="language-java">public class Exception extends Throwable { ... }

public class Error extends Throwable { ... }</code></pre>
<p><strong>에러(Error)</strong> : 프로그램 코드로 수습될 수 없는 심각한 오류
<code>OutOfMemoryError</code> <code>StackOverFlowError</code> <code>NoClassDefFoundError</code>  </p>
<p><strong>예외(Exception)</strong> : 프로그램 코드로 수습될 수 있는 미약한 오류
<code>ArithmeticException</code> <code>NullPointerException</code> <code>FileNotFoundException</code></p>
<p>이때 두 오류 클래스의 선언부를 보면
Throwable이라는 클래스를 상속 받는 것이 확인 가능한데</p>
<pre><code class="language-java">public class Throwable implements Serializable { ... }</code></pre>
<p>이를 통해 <code>Exception</code>뿐만 아니라 <code>Error</code> 역시
<strong>throw</strong> 키워드를 통해 개발자가 에러 객체를 생성할 수 있음을 알 수 있다.</p>
<blockquote>
<p><strong><code>Error</code>객체를 throw 하는 경우?</strong>
자바에서의 <strong>Error</strong>는 JVM 수준의 심각한 문제이다.
따라서 일반적으로 개발자가 <strong>Error</strong>를 직접 <strong>throw</strong>하는 것은 권장되지 않는다.<br/>
굳이 사용한다면 다음과같은 사항에 넣을 수 있다.</p>
</blockquote>
<pre><code class="language-java">// 절대 발생해서는 안 되는 코드 경로임을 명시
throw new AssertionError(&quot;이 코드는 절대 실행되면 안 됩니다.&quot;);</code></pre>
<pre><code class="language-java">// switch의 모든 case를 처리했음에도 도달했을 때
switch (direction) {
    case NORTH: ...
    case SOUTH: ...
    default:
        throw new AssertionError(&quot;알 수 없는 방향: &quot; + direction);
}</code></pre>
<pre><code class="language-java">// 테스트코드에서의 상황
throw new Error(&quot;테스트 환경에서 강제 실패&quot;);</code></pre>
<p><strong>Throwable</strong> 클래스는 왜 모든 오류(예외 + 에러) 객체의 조상 클래스일까?</p>
<p>이는 <code>throw</code> 키워드와 <code>catch</code> 블록 내에서
<code>Throwable</code>클래스 타입만 다룰 수 있도록 <strong>JVM</strong>이 설계되어 있기에</p>
<p><code>Throwable</code>을 상속받는 클래스만 예외나 에러를,
<strong>던지고(throw)</strong> <strong>받을(catch)</strong> 수 있도록 하였다.</p>
<p>또한 프레임워크나 테스트 도구에서 모든 종류의 오류를 한 번에
처리해야하는 코드의 경우 다음처럼</p>
<pre><code class="language-java">catch(Throwable t) { ... }</code></pre>
<p>단일 표현이 가능하여 코드의 중복을 막아 복잡도를 개선할 수 있고
<strong>상속을 통한 자바의 다형성</strong> 측면이 드러남을 알 수 있다.</p>
<blockquote>
<p><strong><code>+</code> 프로그램의 오류는 크게 다음과 같은 분류로 나눌 수 있다.</strong></p>
</blockquote>
<ul>
<li><strong>컴파일 에러</strong> : 컴파일 시 발생하는 오류
  <code>문법 오류</code>  <code>타입 오류</code> <code>checked exception 미처리</code></li>
<li><strong>런타임 에러</strong> : 런타임 시 발생하는 오류
  <code>NullPointerException</code> <code>OutOfMemoryError</code></li>
<li><strong>논리적 에러</strong> : 실행은 되지만, 의도와 다른 동작</li>
</ul>
<hr>
<h3 id="2-클래스-계층-구조">2. 클래스 계층 구조</h3>
<pre><code class="language-java">Throwable
        &lt;- Error
        &lt;- Exception
                &lt;- RuntimeException (checked exception)

                (checked exception)
                &lt;- IOException
                &lt;- ReflectiveOperationException
                &lt;- ClassNotFoundException
                &lt;- ...</code></pre>
<p>이 중 개발자가 제어가 불가능한 <strong>Error</strong> 클래스를 제외하고
<strong>Exception</strong> 클래스의 상속 클래스들을 살펴보면 크게 다음 두 가지로 나눌 수 있다.</p>
<ul>
<li><strong>RuntimeException</strong></li>
<li><strong>~RuntimeException</strong></li>
</ul>
<blockquote>
<p><strong>RuntimeException</strong>의 경우,
<code>unchecked exception</code>으로 컴파일 시점에
<code>try-catch</code> 와 같은 <strong>예외 처리를 강제하지 않지만</strong>,
올바르게 예외처리가 되어있지 않다면 발생 시 예상치 못한 문제를 야기할 수 있다.</p>
</blockquote>
<pre><code class="language-java">// 사용자 id 길이 check해야하는데 런타임에 null값이 할당된 경우
String username = null;
if (username.length() &lt; 5) System.out.println(&quot;id 길이 5자 이상&quot;);</code></pre>
<pre><code class="language-bash">// NullPointerException 발생
Exception in thread &quot;main&quot; java.lang.NullPointerException:
    Cannot invoke &quot;String.length()&quot; because &quot;username&quot; is null</code></pre>
<blockquote>
<p><strong>RuntimeException 외 Exception 상속 클래스</strong>의 경우
<code>checked exception</code>이며
소스코드에서 <code>try-catch</code> 와 같은 <strong>예외처리를 강제</strong>한다.
예외처리가 되지 않은 <strong>checked exception</strong>은,
컴파일을 진행할 경우 컴파일 에러가 발생한다.</p>
</blockquote>
<pre><code class="language-java">public static void main(String[] args) {
    Thread t = new Thread(() -&gt; System.out.println(&quot;test Thread1&quot;));
    t.start();
    t.join();
}</code></pre>
<pre><code class="language-bash">// InterruptedException
java: unreported exception java.lang.InterruptedException;
    must be caught or declared to be thrown</code></pre>
<p>정리하자면 컴파일러가 강제하는 <code>checked exception</code>의 예외처리와,
런타임 상황에서 발생할 수 있는 <code>uncheked exception</code>으로 인한 문제를 예방하기 위해 </p>
<p><strong>예외 처리가 필요하다.</strong></p>
<hr>
<h3 id="3-예외-처리">3. 예외 처리</h3>
<p>예외처리의 근본적인 목적은 예외의 발생으로 인한
실행 중인 프로그램의 비정상 종료를 막으며 <strong>정상 실행상태를 유지</strong>하는 것이다.</p>
<blockquote>
<p><strong>Error</strong>와 <strong>Exception</strong>은 모두 실행 시 (<strong>runtime</strong>) 발생할 수 있는 오류이다.
→ 따라서 실행 중 발생하는 에러는 다음 문제이고,
<strong>=&gt; 예외는 코드레벨에서 미리 처리해야</strong> 한다.</p>
</blockquote>
<pre><code class="language-java">try {
    // 예외가 발생할 가능성이 있는 문장들
} catch (Exception1 e1) {
    // Exception1이 발생할 경우, 이를 처리하기 위한 문장
} catch (Exception2 e2) {
    // Exception2가 발생할 경우, 이를 처리하기 위한 문장
} catch (ExceptionN eN){
    // ExceptionN이 발생할 경우, 이를 처리하기 위한 문장
}</code></pre>
<p>위 코드는 기본적인 <code>try-catch</code>의 문법 구조이다.
<strong>try</strong> 코드 블럭 내에서 <strong>예외가 발생</strong>하면, 발생한 <strong>예외에 해당하는 인스턴스</strong>가 만들어진다.</p>
<p>이 인스턴스는 발생한 예외에 대한 정보가 담겨있으며,
 <code>catch</code> 문의 괄호 <code>()</code> 를 통해 참조변수로 정보를 얻을 수 있다.</p>
<blockquote>
<p><strong>예외 발생 시 <code>try-catch</code> 동작 과정</strong>
<code>try</code> 블록 내에서 예외가 발생했다면, 코드 순서대로 <strong>catch</strong>의 괄호 내 선언된
참조변수의 타입과 발생한 예외를,
<strong>instanceof</strong> 연산자를 통해 검사하는 과정을 거친다.</p>
</blockquote>
<pre><code class="language-java">try {
    ...
} catch (Exception e) {
    ...
} catch (IllegalArgumentException e) {
    ...
}
// Exception의 경우 모든 예외의 조상이기에
// 하단 IllegalArgumentException에 해당하더라도
// catch (Exception e) catch 블럭 내부만 실행된다.</code></pre>
<hr>
<h3 id="4-고의적-예외-발생">4. 고의적 예외 발생</h3>
<p>개발자는 <strong>throw</strong> 키워드를 통해 고의적으로 예외(<strong>Exception</strong>)를 발생시킬 수 있다.</p>
<pre><code class="language-java">class ExceptionEx {
    public static void main(String[] args) {
        try {
            Exception e = new Exception(&quot;고의 발생 예외&quot;);
            throw e; // 예외를 발생시킨다.
            // Exception객체를 만들 때 String을 인자로 넣어주면
            // 해당 문자열이 Exception 객체의 메시지로 저장된다.

        } catch (Exception e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        }

        System.out.println(&quot;정상 종료&quot;);
    }
}</code></pre>
<pre><code class="language-bash">고의 발생 예외 // by e.getMessage()
정상 종료 // catch 이후 로직 진행으로 인해
java.lang.Exception: 고의 발생 예외
    at ExceptionEx.main(Main.java:4)</code></pre>
<p>이러한 고의적 예외 발생 방식을 통해
<code>checked exception</code>과 <code>unchecked exception</code>의 차이에 대해 알 수 있는데</p>
<pre><code class="language-java">class ExceptionEx {
    public static void main(String[] args) {
//        throw new ArrayIndexOutOfBoundsException();
//        throw new IllegalArgumentException();
//        throw new IOException();
//        throw new ClassNotFoundException();
    }
}</code></pre>
<p><strong>throw</strong>를 통해 고의적으로 발생 시킨 각 예외 객체를,
실제로 주석을 풀어 main 메서드에서 실행시켜보면 각각 아래와 같은 결과를 얻는다.</p>
<ul>
<li><strong><code>ArrayIndexOutOfBoundsException</code> <code>IllegalArgumentException</code></strong></li>
</ul>
<pre><code class="language-bash">Exception in thread &quot;main&quot; java.lang.ArrayIndexOutOfBoundsException
    at ExceptionEx.main(Main.java:5)

Exception in thread &quot;main&quot; java.lang.IllegalArgumentException
    at ExceptionEx.main(Main.java:6)</code></pre>
<ul>
<li><strong><code>IOException</code> <code>ClassNotFoundException</code></strong></li>
</ul>
<pre><code class="language-java">java: unreported exception java.io.IOException; 
    must be caught or declared to be thrown

java: unreported exception java.lang.ClassNotFoundException;
    must be caught or declared to be thrown</code></pre>
<p><strong>checked exception</strong>인 <code>ArrayIndexOutOfBoundsException</code> 와 <code>IllegalArgumentException</code>의 경우 
컴파일이 정상 진행되나 이후 런타임에서 예외가 발생하여 프로그램이 종료된다.</p>
<p>반대로 <strong>checked exception</strong>인 <code>IOException</code>과 <code>ClassNotFoundException</code>은
예외처리를 하지 않아 컴파일 조차 진행되지 않음을 알 수 있다.</p>
<blockquote>
<p><strong>그렇다면 <code>throw</code>를 통한 예외 객체는 왜 쓸까?</strong>
<code>throw</code> 로 예외 객체를 만드는 이유 중 하나는
단순 <code>return</code> , <code>if</code> 와 같은 분기를 통해 표현하기 어려운
<strong>비정상 상황을 명시적으로 하여</strong> 알리기 위해서이다.</p>
</blockquote>
<ul>
<li>예외 발생 시 <code>return</code> 사용 분기
  호출자가 반환된 값을 제대로 처리하지 못하면, 예외가 묻힘</li>
<li>예외 발생 시 if  사용 분기
  메서드 호출 스택이 깊게 쌓여 있다면, 오류 전달하기 번거러움</li>
</ul>
<p><code>throw</code>의 경우 발생한 예외를 강제로 위로 전달하고 무시할 수 없게 한다.</p>
<p><code>throw</code>를 통해 발생한 예외 객체는 <code>try-catch</code>와 같은
예외 처리를 담당하는 부분까지 <strong>Call Stack</strong>을 타고 올라가며</p>
<p>예외처리 담당 부분을 만나면 <code>catch</code>의 괄호 내에서
<strong>instanceof</strong> 연산을 통해 확인하고 이를 처리하는 것이다.</p>
<hr>
<h3 id="5-메서드-예외-선언">5. 메서드 예외 선언</h3>
<p>메서드의 선언부에 붙은 <code>throws</code> 키워드는 실제로 예외를 처리한다가 아닌,
<strong>예외 처리의 책임</strong>을 해당 메서드 호출부에 넘긴다는 의미이다.</p>
<pre><code class="language-java">void method() throws Exception1, Exception2, ... ExceptionN {
    ...
}</code></pre>
<p>한 <code>throws</code> 로 여러 예외 클래스 타입을 선언부에 쓸 수 있는데
이를 통해 해당 메서드를 사용할 때 어떠한 예외가 발생할 수 있는 지 알 수 있다.</p>
<ul>
<li><strong><code>checked exception</code>의 경우</strong>
반드시 예외처리를 해야하기에, 호출부에서 예외처리가 이루어져야 한다.
호출부에서는 해당 메서드를 <code>try-catch</code>로 감싸거나,
<code>throws</code>를 통해 다시 상위 호출 메서드로 책임을 넘길 수 있다.</li>
</ul>
<ul>
<li><strong><code>unchecked exception</code>의 경우</strong>
예외처리가 필수는 아니나,
호출부에서 이를 사용할 때 이러한 런타임예외가 발생할 수 있음을 알 수 있다.</li>
</ul>
<blockquote>
<p><strong>main 메서드에 throws 가 붙으면 어떻게 될까?</strong></p>
</blockquote>
<pre><code class="language-java">class ExceptionEx {
    public static void main(String[] args) throws ClassNotFoundException {
        throw new ClassNotFoundException();
    }
}</code></pre>
<p>위 예제를 보면 <code>ClassNotFoundException</code>은
<strong>checked exception</strong>이기에 원래는 예외처리가 없으면 컴파일되지 않는 코드임에도
main 메서드 선언부에 붙은 <strong>throws</strong>를 통해 컴파일이 진행되어 프로그램이 실행된다.<br/>
이후 main을 호출한 <strong>JVM</strong>에서 <strong>해당 예외를 받아 예외에 대해 출력하며</strong>,
프로그램이 비정상 종료된다.</p>
<pre><code class="language-bash">Exception in thread &quot;main&quot; java.lang.ClassNotFoundException
    at ExceptionEx.main(Main.java:5)</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[문제 상황에 따른 동기화 도구 선택 전략]]></title>
            <link>https://velog.io/@zayn_noh/%EB%AC%B8%EC%A0%9C-%EC%83%81%ED%99%A9%EC%97%90-%EB%94%B0%EB%A5%B8-%EB%8F%99%EA%B8%B0%ED%99%94-%EB%8F%84%EA%B5%AC-%EC%84%A0%ED%83%9D-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@zayn_noh/%EB%AC%B8%EC%A0%9C-%EC%83%81%ED%99%A9%EC%97%90-%EB%94%B0%EB%A5%B8-%EB%8F%99%EA%B8%B0%ED%99%94-%EB%8F%84%EA%B5%AC-%EC%84%A0%ED%83%9D-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Wed, 20 May 2026 07:50:29 GMT</pubDate>
            <description><![CDATA[<p>자바의 프로그램은 JVM이라는 하나의 프로세스 위에서 동작하며,
다양한 스레드를 통해 <strong>동시성(Concurrency)을 구현</strong>한다.</p>
<p>멀티스레드를 통한 비동기 처리로 구현된 동시성 개념은 <strong>대규모 작업을 나누어 처리</strong>하거나
서로 다른 내용의 작업들을 <strong>동시에 처리하여 성능 개선</strong>이 가능하고</p>
<p>사용자 요청에 대한 작업의 진행 사항 같은 내용을 비동기로 처리하여,
<strong>UX(User Experience)</strong>와 관련된 응답성 또한 향상 가능하다.</p>
<p>그럼에도 이러한 비동기 처리 방식들은 언제나
공유 자원에 대한 <strong>경쟁 상태(race condition)</strong>를 유발할 수 있다.</p>
<p>이번 포스팅에서는 이러한 경쟁 상태를 막기 위해 고안된
<strong>자바의 동기화 도구들</strong>에 대하여 <code>synchronized</code> <code>volatile</code> <code>Lock</code> <code>Atomic Type</code></p>
<p>주어진 문제 상황에 따른 <strong>적합한 동기화 방식을 선택하는 전략</strong>에 대해 다루어 보았다.</p>
<hr>
<h3 id="1-동기화-도구-설명">1. 동기화 도구 설명</h3>
<p>문제 상황을 생각하기에 앞서, 이번 포스트에서 다룰
각각의 동기화 도구들의 개념과 사용 예시를 살펴보도록 하겠다.</p>
<h4 id="synchronized">synchronized</h4>
<blockquote>
<p><strong>synchronized</strong>는 간단한 <strong>키워드</strong>를 메서드나 코드 블럭 앞에 붙여,
해당 구역에 대한 접근을 <strong>하나로 제한</strong>하는 키워드이다.</p>
</blockquote>
<pre><code class="language-java">public class SynchroEx {
    ...
    public synchronized void methodA() { // 메서드에 붙는 synchronized
        ...
    }

    public void methodB() {
        ...
        synchronized(this) { // synchronized 코드 블럭 지정
            ...
        }
        ...
    }
}</code></pre>
<p>이때 synchronized 키워드가 붙은 메서드 혹은 코드 블럭은
<strong>임계 구역(critical section)</strong>으로 설정되며</p>
<p>여러 스레드의 <strong>동시 진입을 막고</strong> <strong>하나의 스레드만 접근</strong>할 수 있도록 제한된다.</p>
<p>구체적으로는 임계구역에 진입할 스레드는 <strong>락(lock)</strong>을 얻어야 진입할 수 있으며,
해당 임계 구역을 벗어날 경우 얻은 락을 반납하는 형식으로 진행된다.</p>
<pre><code class="language-java">public synchronized void methodA() {
        ...
}</code></pre>
<p>클래스 내부 메서드에 <strong>synchronized</strong> 키워드가 붙은 경우
해당 메서드 전체가 임계구역이 되며, <strong>락의 기준</strong>은 해당 메서드를 보유한 <strong>this</strong> 객체가 된다.</p>
<p>다음으로 <code>synchronized(lock){}</code>와 같이 코드 블럭을 통해 처리하는 경우
<code>()</code> 내부에 들어간 객체에 따라 <strong>락의 기준</strong>이 정해진다.</p>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        Object lock = new Object();
        synchronized(lock) {
            ../
        }
    }
    ...
}</code></pre>
<p>위와 같이 synchronized 코드 블럭을 통해 임계 구역을 설정할 때는
<code>synchronized(lock)</code>에서 <code>lock</code>에 특정 객체를 인자처럼 설정 가능한데
이러한 방식을 통해 <strong>명시적으로 지정된 객체</strong>가 <strong>락의 기준</strong>이 되는 것이다.</p>
<p>추가로 <code>synchronized(this)</code> 의 경우에는 this 객체가 명시적으로 <strong>락의 기준</strong>이 된다.</p>
<blockquote>
<p><strong>락의 기준이 된다는 것은 정확히 무슨 의미일까?</strong>
    락의 기준이란 임계 구역에 진입할 수 있는 티켓으로 볼 수 있다.</p>
</blockquote>
<pre><code class="language-java">public void methodA() {
        synchronized (this) {
            System.out.println(&quot;method-A start: &quot; + Thread.currentThread().getName());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                System.out.println(&quot;interrupt occur!&quot;);
            }
            System.out.println(&quot;method-A end: &quot; + Thread.currentThread().getName());
        }
    }</code></pre>
<pre><code class="language-java">    public void methodB() {
        synchronized (new Object()) {
            System.out.println(&quot;method-B start: &quot; + Thread.currentThread().getName());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                System.out.println(&quot;interrupt occur!&quot;);
            }
            System.out.println(&quot;method-B end: &quot; + Thread.currentThread().getName());
        }
    }    </code></pre>
<p><code>methodA()</code>와, <code>methodB()</code> 모두
<code>Example</code>이란 동일 클래스의 메서드라 가정해보자.<br/></p>
<pre><code class="language-java">Example ex = new Example();
Thread t1 = new Thread(ex::methodA, &quot;T1&quot;);
Thread t2 = new Thread(ex::methodA, &quot;T2&quot;);
Thread t3 = new Thread(ex::methodA, &quot;T3&quot;);</code></pre>
<p>위와 같이 main 스레드에서 3개의 개별 스레드를 만들고,
<code>methodA</code>를 실행해보면 아래와 같은 결과를 얻을 수 있다.</p>
<pre><code>method-A start: T3
method-A end: T3
method-A start: T1
method-A end: T1
method-A start: T2
method-A end: T2</code></pre><p>프로그램을 다수 실행해보아도, 각 스레드가 실행하는 작업(메서드)이
종료되어야 다음 스레드가 해당 작업을 실행할 수 있다.
(스레드간 순서는 보장 x)<br/>
즉, <code>methodA</code>라는 임계구역에 모두 동일한 <code>ex</code>객체를 티켓으로
입장하는 것이며 <code>ex</code> 객체 티켓이 사용 중이라면<br/>
<strong>다른 스레드에서 해당 작업(<code>methodA</code>)을 수행할 수 없다.</strong> (<strong>무한 대기</strong>)<br/>
이어서 <code>methodB()</code>의 경우를 확인하여 보겠다.</p>
<pre><code class="language-java">Example ex = new Example();
Thread t1 = new Thread(ex::methodB, &quot;T1&quot;);
Thread t2 = new Thread(ex::methodB, &quot;T2&quot;);
Thread t3 = new Thread(ex::methodB, &quot;T3&quot;);</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code class="language-java">method-B start: T2
method-B start: T3
method-B start: T1
method-B end: T3
method-B end: T2
method-B end: T1</code></pre>
<p>동일 작업을 수행하지만, 각자 서로 다른 객체를
락의 기준으로하였기에 모든 스레드가 동시에 실행되는 것이다.<br/>
이러한 락의 기준 객체를 다르게 두는 이유는
어찌보면 <strong>동기화를 못하는 것 아닌가?</strong> 하는 생각이 들 수 있지만
각 <strong>락의 기준 객체마다 동기화</strong>를 진행한다 볼 수 있다.</p>
<pre><code class="language-java">// 사용자 A의 객체
User userA = new User(); User userB = new User();
Thread t1 = new Thread(userA::moneyIncrease());
Thread t2 = new Thread(userA::moneyDecrease());
Thread t3 = new Thread(userB::moneyIncrease());
Thread t4 = new Thread(userB::moneyDecrease());
t1.start(); t2.start();</code></pre>
<p>위 예시처럼 <code>userA</code>와 <code>userB</code>
서로간의 메서드(<code>userA의 메서드</code>와 <code>userB의 메서드</code>)는
동기화를 할 필요가 없다. 각 스레드에서 동시에 진행되어도 되는 것이다.<br/>
그러나 <code>userA</code>의 <code>moneyIncrease()</code>와 <code>moneyDecrease()</code>는
동기화가 필요한 내용이기에 동시에 발생해서는 안된다.<br/>
따라서 락의 기준을 다르게 하여 <strong>synchronized</strong>를 통해
동기화를 진행하는 것이다.<br/>
락의 기준이 된다는 것은,
해당 객체를 기준으로 보호된 임계 구역에 대한 접근 권한을
작업을 실행하는 스레드가 가진다는 것이며 이를
스레드가 객체의 <strong>모니터락(Monitor Lock)</strong>을 얻는다라고 할 수 있다.</p>
<p>추가로 클래스의 <code>static</code> 메서드에 붙은 <code>synchronized</code>는</p>
<pre><code class="language-java">static synchronized void method() {
    ...
}</code></pre>
<p>락의 기준이 클래스 자체(<strong>Class 객체</strong>)가 된다.
클래스 객체는 JVM 내에 하나 뿐이기에, 모든 스레드가 동일 락을 공유한다.
즉, 해당 락을 기준으로 보호된 임계구역에 다른 스레드는 접근할 수 없다.</p>
<hr>
<h4 id="lock-인터페이스">Lock 인터페이스</h4>
<pre><code class="language-java">public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}</code></pre>
<p><strong>synchronized</strong>의 경우 특정 코드 블럭이나, 메서드를 기준으로
락을 자동으로 적용하고 해제하지만,</p>
<p><strong>Lock</strong> 인터페이스를 사용하면
락의 <strong>적용 시점</strong>과 <strong>해제 시점</strong>, 락의 <strong>대상 객체</strong>까지도 직접 제어할 수 있다.</p>
<ul>
<li><p>동기화 범위를 더욱 세밀하게 조절</p>
</li>
<li><p>락을 기다릴지 말지 결정, <strong>Interruptible</strong></p>
</li>
<li><p>일정 시간 대기 후 포기하는 방식</p>
</li>
</ul>
<p>또한 <strong>Lock</strong>을 통해 단순한 상호 배제뿐만 아니라</p>
<p><strong>공정성 설정</strong> + <strong>조건 변수 활용</strong> + <strong>비차단 락 시도</strong> 등
복잡한 멀티스레드 환경에서 시나리오를 지원할 수 있는 구조적 유연성을 제공한다.</p>
<blockquote>
<p><strong>비차단 락(non-blocking lock)</strong>이란
스레드가 작업을 위한 락을 즉시 휙득하지 못하면,
<strong>기다리지 않고 바로 다음 로직으로</strong> 넘어가는 방식의 락 처리 기법을 말한다.<br/>
<strong>synchronized</strong>에서 발생 가능한 문제점인 <strong>무한 대기를 예방</strong>할 수 있다.</p>
</blockquote>
<blockquote>
<p><strong>synchronized의 한계</strong>
synchronized 키워드를 사용하면 임계 구역 진입 시 <strong>자동으로 락을 걸고</strong>,
블록을 빠져나갈 때 <strong>자동으로 락을 반납</strong>하는 구조이기에<br/>
<strong>예외 상항, 대기 조건, 우선 순위, 타임 아웃, 흐름 분기 등</strong>
다양한 운영 정책에 맞춘 시나리오를 제대로 반영할 수 없다.</p>
</blockquote>
<p>보통 <strong>Lock</strong> 인터페이스를 구현한 <strong>ReentrantLock</strong> 클래스를 사용한다.</p>
<pre><code class="language-java">public class ReentrantLock implements Lock, java.io.Serializable {
    ...</code></pre>
<p>사용 방법에 대해 간단하게 살펴보면</p>
<pre><code class="language-java">import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

...

Lock lock = new ReentrantLock();

lock.lock(); // 락 휙득
try {
    // 임계 영역
} finally {
    lock.unlock(); // 락 해제
}</code></pre>
<p><code>ReentrantLock</code> 객체를 생성하여 <code>Lock</code> 인터페이스 타입 변수에 할당한다.</p>
<p>이후 <code>lock()</code> 메서드를 호출해 락을 휙득하고,
<code>unlock()</code>을 통해 앞서 휙득한 락을 반납해주어야 한다.</p>
<blockquote>
<p><strong>unlock()을 하지 않으면?</strong>
<code>unlock()</code>을 호출하지 않을 경우, 해당 락이 영구적으로 스레드에 점유되며
다른 스레드가 접근할 수 없는 무한대기에 빠지게 된다.</p>
</blockquote>
<p>기본적인 <code>ReentrantLock</code>의 사용법은 다음 예시를 통해 확인 가능하다.</p>
<pre><code class="language-java">private final Lock lock = new ReentrantLock();

public void reFuelByLock(String carName) {
    try {
        lock.lock();
        Thread.sleep(10);
        fuelAmount -= 10;
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        lock.unlock();
    }
}</code></pre>
<p><strong>비차단 락(non-blocking lock)</strong> 방식을 통해
일정 시간만 락 휙득을 시도하고 실패 시 다른 로직을 진행하고자 할 때는
아래 예시처럼 <code>tryLock()</code>메서드를 활용할 수 있다.</p>
<pre><code class="language-java">public void reFuelByTryLock(String carName) {
    try {
        if (lock.tryLock(100, TimeUnit.MILLISECONDS)) { // 락 휙득 시도, 0.1s 타임아웃
            try {
                Thread.sleep(200); // 0.2s + @ 동안 점유
                fuelAmount -= 10;

                return;
            } finally {
                lock.unlock();
            }
        }
        System.out.println(&quot;next logic&quot;);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}
// 스레드 3개에서 위 메서드를 실행시킨다면, tryLock의 타임아웃으로 인해
// 첫 락 휙득 시도 스레드만 임계 구역 내 코드를 실행하고
// 지연되는 0.2s + @(코드실행시간)로 인하여 다른 두 스레드는
// 0.1s의 타임 아웃이 지나 next logic이 실행될 것을 알 수 있다.
// 스레드의 sleep()시간이 100이어도, +@(코드실행시간)로 인해,
// 첫 스레드만 락을 얻을 수 있다.
</code></pre>
<p>이때 <code>tryLock()</code> 메서드에 인자로 넘어가는 값은 다음과 같다.</p>
<pre><code class="language-java">boolean tryLock(long time, TimeUnit unit) throws InterruptedException;</code></pre>
<p>또한 <strong>TreentrantLock</strong>의 생성자에 <strong>boolean</strong>값을 전달하여 <strong>공정성</strong>을 설정할 수 있다.</p>
<pre><code class="language-java">private final Lock lock = new TreentrantLock(true);</code></pre>
<p><strong>ReentrantLock</strong> 클래스 내부 구현 코드를 보면</p>
<pre><code class="language-java">private final Sync sync;
...
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}</code></pre>
<p>위와 같이 전달된 boolean값이 <code>true</code>일 경우 <code>FairSync()</code>
<code>false</code>일 경우 <code>NonfairSync()</code>를 객체 내부 멤버 변수 <code>sync</code>에 설정한다.</p>
<p><strong>FairSync</strong>로 내부 <strong>sync</strong>가 설정되면 <strong>FIFO</strong> 대기 순서대로 락을 얻으며
<strong>NonfairSync</strong>로 내부 <strong>sync</strong>가 설정되면 순서무관하게 락을 얻는다.</p>
<p>이러한 <strong>NonfairSync</strong>의 순서 무관함은 <strong>기아 상태(starvation)</strong>를 유발할 수 있다.</p>
<blockquote>
<p><strong>FairSync가 좋아보이는데, 왜 NonfairSync가 기본값일까?</strong>
FairSync의 경우 매번 대기열을 확인하는 비용이 발생한다.
반면 NonfairSync의 경우 락이 풀리는 순간 바로 휙득을 시도하여 비교적 비용이 적다.<br/>
그럼에도 캐시와 스케줄링에 따라 유리한 스레드가 락을 계속해서 차지할 수 있다.
<strong>=&gt; 기아 상태 유발 가능성</strong></p>
</blockquote>
<p>다음으론 시간이 아닌 특정 조건을 통한 논블로킹 락 방식이다.</p>
<pre><code class="language-java">private final Condition hasSlot = lock.newCondition();</code></pre>
<p>이미 생성된 <strong>lock</strong>으로부터 <code>newCondition()</code>메서드를 통해 <code>Condition</code>을 얻는다.</p>
<pre><code class="language-java">lock.lock();
try {
    while (availableSlots == 0) {
        System.out.println(carName + &quot; → 충전 대기 중...&quot;);
        hasSlot.await(); // 스레드 대기 + 락 반납
    }

    availableSlots--;
    System.out.println(carName + &quot; → 충전 시작&quot;);
    Thread.sleep(200);
    System.out.println(carName + &quot; → 충전 완료&quot;);

    availableSlots++;
    hasSlot.signal(); // 조건대기열 내 스레드를 락대기열로 이동 (FIFO)
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} finally {
    lock.unlock();
}</code></pre>
<p><code>Condition</code>인터페이스를 구현한 객체<code>hasSlot</code>을 통해
<code>.await()</code>메서드를 실행하면 해당 스레드는 <strong>조건 대기열</strong>에 들어간다.</p>
<p>이후 락을 얻고 임계구역 내에서 코드를 진행 중이던 타 스레드가 <code>.signal()</code>을 호출하면
조건대기열에 있던 스레드는 <strong>락 대기열</strong>로 이동하고, 다시 락을 얻기 위한 과정을 거친다.</p>
<blockquote>
<p><strong>if()가 아닌 while()을 사용하는 이유</strong>
위 내용을 통해 어느정도 추측이 가능하다.
<strong>조건 대기열</strong>에 들어간 스레드는 락을 얻어 임계구역을 진행 중이던 다른 스레드가 실행한 <code>.signal()</code>로부터 <strong>락 대기열</strong>로 이동한다.<br/>
이때 락 대기열에는 본인 스레드만 있는 것이 아닌, 다른 스레드들이 존재할 수 있다.
즉 타 스레드가 락을 얻어 다시 임계구역에 들어가면 <strong>조건이 변할 수 있기에</strong>
<code>await()</code> 이후부터 실행되는 코드 흐름에서 <strong>while</strong>문을 통해
또 다시 조건을 검사하는 안전장치의 역할을 한다고 볼 수 있다.</p>
</blockquote>
<hr>
<h4 id="volatile">volatile</h4>
<p><strong>volatile</strong>는 변수의 타입 앞에 붙이는 키워드로,
모든 스레드가 해당 키워드가 붙은 변수를 읽고자 할 때 <strong>항상 메모리에서 직접 읽어오도록</strong> 보장하는 키워드이다.</p>
<p>일반적으로 JVM 위에서 동작하는 자바 프로그램은 성능 향상을 위해
각 스레드마다 CPU 캐시에 변수값을 메모리로부터 복사해두고 사용한다.</p>
<p>이러한 방식은 스레드마다 서로 다른 복사본을 가질 수 있어 문제가 발생한다.
어떤 스레드가 변수의 값을 바꾸어도 다른 스레드는 이전 복사 값을 바라보는 것이다.</p>
<p>이를 <strong>가시성 문제(visibility ploblem)</strong>라 부른다.</p>
<pre><code class="language-java">private volatile boolean running;</code></pre>
<p><strong>volatile</strong>는 항상 메모리에서 직접 값을 읽어오기에,
스레드 간 값 불일치와 같은 가시성 문제를 원천적으로 차단해준다.</p>
<p>특히 <strong>volatile</strong>는 플래그 변수처럼
변경된 값을 다른 스레드가 빠르게 알아야 하는 경우에 적합하다.</p>
<pre><code class="language-java">private static boolean loopFlag = true;

public static void main(String[] args) throws InterruptedException {
    new Thread(() -&gt; {
        while (loopFlag) {
            // 루프 발생
        }
        System.out.print(&quot;루프 종료&quot;);
    }).start();

    Thread.sleep(1000);
    loopFlag = false; // 플래그 변수 변경
}</code></pre>
<p>위 코드를 실행해보면 main 스레드에서의 <code>engineOn</code> 플래그 변수의 값 변경이
생성 스레드에 반영이 되지 않아 스레드가 끝나지 않는다.</p>
<p>이를 <code>engineOn</code> 플래그 변수에 <strong>volatile</strong> 키워드를 붙임으로써 해결할 수 있다.</p>
<pre><code class="language-java">private static volatile boolean loopFlag = true;
// 루프 종료
// 종료 코드 0(으)로 완료된 프로세스</code></pre>
<p>다만 <strong>volatile</strong>은 가시성을 보장하지 <strong>원자성을 보장하지는 않는다</strong>.</p>
<blockquote>
<p><strong>원자성을 보장하지 않는다는 무슨 뜻일까?</strong>
복잡한 연산의 경우에는 CPU 내부 ALU의 명령어 처리 과정이 나누어져 있어
문제가 발생할 수 있다. <code>num++</code>도 복잡한 연산에 해당한다.
(<code>num</code>값 읽어와 레지스터에 저장 -&gt;레지스터 +1 연산 진행
    -&gt; <code>num</code>에 연산 결과 저장)
이러한 원자적 단위에서 값을 읽어올 때는 <strong>volatile</strong>로 인해
메인 메모리의 최신값을 가져오지만, 연산 진행 도중
다른 스레드에서 +1한 값을 저장한다면, 실제 +1은 2번 일어났지만
1번 일어난 것처럼 될 수 있는 것이다. (아래 코드를 통해 확인 가능하다)</p>
</blockquote>
<pre><code class="language-java">private static volatile int num = 0;
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i &lt; 10000; i++) {
        List&lt;Thread&gt; tList = new ArrayList&lt;Thread&gt;();
        for (int j = 0; j &lt; 4; j++)
            tList.add(new Thread(() -&gt; num++));
        for (int j = 0; j &lt; 4; j++)
            tList.get(j).start();
        for (int j = 0; j &lt; 4; j++)
            tList.get(j).join();
        if (num != 4) System.out.println(&quot;error!: &quot; + &quot;num = &quot; + num);
        num = 0;
    }
}</code></pre>
<p>결과</p>
<pre><code>error!: num = 3</code></pre><hr>
<h4 id="atomic-type">Atomic Type</h4>
<p><strong>volatile</strong>이 값을 읽어올 때만 최신값을 보장하기에 원자성 문제가 발생하였다.
<strong>Atomic Type</strong>은 이러한 원자성 문제를 해결하는 방법으로
값을 읽고, 수정하여 저장하는 과정 전체를 원자적으로 진행해 <strong>원자성 문제를 해결한다.</strong></p>
<pre><code class="language-java">// AtomicInteger
public class AtomicInteger extends Number implements java.io.Serializable {
    ...

// AtomicBoolean
public class AtomicBoolean implements java.io.Serializable {
    ...

// AtomicLong
public class AtomicLong extends Number implements java.io.Serializable {
    ...</code></pre>
<blockquote>
<p><strong>아토믹 타입은 어떻게 이러한 원자적 진행이 가능할까?</strong>
아토믹은 내부적으로 <strong>CAS(Compare-And-Swap)</strong>연산을 사용한다.
<code>현재 메모리 값 : V</code> , <code>예상 값 : E</code> , <code>새로 바꿀 값 : N</code>으로 하여</p>
</blockquote>
<pre><code class="language-java">while(true) {
  if (V == E) {
      V = N;
      break;
  }
}</code></pre>
<p>현재 메인 메모리의 값이 예상하는 값과 일치하면 값을 바꾸며
일치하지 않다면 계속 비교를 진행하는 방식이다.<br/>
이러한 연산은 CPU 내 하드웨어 레벨에서 다루기에,
동기화 없이도 정확하게 동시성 문제를 해결할 수 있는 것이다.</p>
<p>다만 이러한 <strong>CAS</strong>연산은 <strong>ABA</strong>문제를 야기할 수 있기에
값의 변화 과정이 중요하지 않은 상황에만 적용할 수 있도록 한다.</p>
<pre><code>originalValue = 3

Thread A : originalValue++
Thread B : originalValue--
Thread C : originalValue++

A 값 불러오기
-&gt; (B 값 불러오기 -&gt; 연산 -&gt; 비교 -&gt; 저장)
-&gt; (C 값 불러오기 -&gt; 연산 -&gt; 비교 -&gt; 저장)
-&gt; A 값 연산 -&gt; 비교(V(3) == E(3)) -&gt; 저장 : N = 4</code></pre><hr>
<h3 id="2-동기화-도구-요약-정리">2. 동기화 도구 요약 정리</h3>
<p>앞서 설명했던 내용을 축약하여,
각 동기화 도구들의 특성과 주의점을 기반으로 간단하게 정리하여 보았다.</p>
<p><strong>1. synchronized</strong> : <code>모니터락</code> <code>상호배제</code> <code>자동 락 해제</code> <code>재진입 허용</code>
단순한 키워드 기반 동기화이다.
임계구역을 자동으로 보호하고 JVM이 락 휙득/반납을 관리한다.</p>
<ul>
<li><p><span style = "color: green">적합 상황</span></p>
<ul>
<li>단순한 임계 구역 보호</li>
<li>복잡하지 않은 코드</li>
<li>락 휙득을 위해 무한 대기하여도 무방할 때</li>
<li>조건 분기가 필요없는 임계 구역</li>
</ul>
</li>
<li><p><span style = "color: red">주의</span></p>
<ul>
<li>타임아웃 설정 불가</li>
<li>데드락/무한대기 위험</li>
<li>공정성 제어 불가</li>
<li>조건에 따른 세분화 힘듦</li>
</ul>
</li>
</ul>
<p><strong>2. Lock(ReentrantLock)</strong> : <code>tryLock</code> <code>타임아웃</code> <code>공정성</code> <code>Condition</code> <code>비차단 락</code> <code>재진입 허용</code>
락의 휙득과 반납 시점, 대상, 조건을 직접 제어할 수 있어
복잡한 멀티스레드 시나리오에 적합하다.</p>
<ul>
<li><p><span style = "color: green">적합 상황</span></p>
<ul>
<li>락 휙득 실패 시 다른 로직으로 분기 필요</li>
<li>무한 대기를 피해야할 때</li>
<li>기아 상태 방지를 위한 공정성이 필요할 때</li>
<li>여러 조건 변수(Condition)로 대기 그룹을 나눌 때</li>
</ul>
</li>
<li><p><span style = "color: red">주의</span></p>
<ul>
<li><code>unLock()</code> 반드시 호출 - <strong>finally</strong></li>
<li><code>FairSync</code>의 경우 성능 비용이 발생</li>
<li>코드 복잡도 증가</li>
<li><code>NonfairSync</code>가 기본값 -&gt; 기아 상태 발생 가능</li>
</ul>
</li>
</ul>
<p><strong>3. volatile</strong> : <code>가시성 보장</code> <code>CPU 캐시 우회</code> <code>원자성 미보장</code> <code>플래그 변수</code></p>
<ul>
<li><p><span style = "color: green">적합 상황</span></p>
<ul>
<li>값의 변경을 다른 스레드가 즉시 알아야 할 때</li>
<li>플래그 변수(<strong>boolean</strong>)</li>
<li>단순 읽기/쓰기만 발생하는 변수</li>
<li><code>++</code>, <code>+=</code>과 같은 복합 연산 없는 경우</li>
</ul>
</li>
<li><p><span style = "color: red">주의</span></p>
<ul>
<li>복합 연산에는 사용 불가</li>
<li>원자성이 필요하면 <strong>Atomic Type</strong> 사용</li>
<li>상호 배제가 필요하면 <strong>synchronized</strong> 사용</li>
<li>오직 가시성만 해결</li>
</ul>
</li>
</ul>
<p><strong>Atomic Type</strong> : <code>CAS</code> <code>원자성 보장</code> <code>락 프리</code> <code>AtomicInteger</code> <code>AtomicBoolean</code></p>
<ul>
<li><p><span style = "color: green">적합 상황</span></p>
<ul>
<li>카운터, 누적합같은 단일변수에 대한 빈번한 수정</li>
<li>synchronized 없이 원자적 연산 필요</li>
<li>값의 변화 과정이 중요하지 않을 때</li>
<li>락 경합 줄이고 성능을 높이고자 할 때</li>
</ul>
</li>
<li><p><span style = "color: red">주의</span></p>
<ul>
<li>단일 변수에만 사용 가능</li>
<li>ABA 문제 발생 가능</li>
<li>여러 변수를 동시 보호하려면 <strong>Lock</strong> 사용</li>
<li>가시성만 필요하다면 <strong>volatile</strong></li>
</ul>
</li>
</ul>
<hr>
<h3 id="3-문제-상황별-도구-선택과-이유">3. 문제 상황별 도구 선택과 이유</h3>
<p><strong>1. 여러 스레드가 공유 객체의 메서드를 동시에 실행하면 안 될 때</strong></p>
<blockquote>
<p>가장 기본적인 임계 구역 보호 상황이다.
메서드에 <strong>synchronized</strong>를 붙이면 해당 객체의 모니터락을 기준으로
한 번에 하나의 스레드만 진입할 수 있어 메서드의 동시성 문제를 해결할 수 있다.<br/>
락 휙득/반납 과정을 JVM이 자동으로 처리하기에 실수의 여지가 적다.<br/>
구조가 단순하다면 <strong>synchronized</strong>를 사용하여도 좋으나,
이후 조건을 추가하거나 확장이 필요하다면 <strong>ReentrantLock</strong>을 사용한다.</p>
</blockquote>
<br/>

<p><strong>2. 락 획득 실패 시 무한 대기 대신 다른 작업으로 넘어가야 할 때</strong></p>
<blockquote>
<p><strong>synchronized</strong>는 락 휙득에 실패하였을 때 무조건 대기한다.
이는 타 스레드가 락을 계속하여 점유하거나, 계속해서 락 휙득에 실패한다면
무한대기로 이어질 수 있어 사용하는 것이 좋지 않다. <br/>
따라서 <strong>ReentrantLock</strong>의 타임아웃을 활용한다면
<code>tryLock(1000, TimeUnit.MILLISECONDS)</code>과 같은 메서드를 통해
다음 로직으로 넘어가거나, 추후 재시도하는 흐름을 만들 수 있다.</p>
</blockquote>
<br/>

<p><strong>3. 특정 조건이 충족될 때까지 스레드를 대기시키고 신호로 깨워야 할 때</strong></p>
<blockquote>
<p>단순히 들어가지 못하게 막는게 아니라 특정 조건이 붙은 임계 구역의 경우
<strong>ReentrantLock</strong>의 <code>.newCondition()</code> 메서드를 통해 얻은
<strong>Condition</strong> 객체에서 <code>.await()</code>, <code>.signal()</code>을 활용하여
락대기열만 사용하지않고 조건대기열을 추가로 사용하여 해결할 수 있다.</p>
</blockquote>
<p><strong>4. 한 스레드의 플래그 변경을 다른 스레드가 즉시 알아야 할 때</strong></p>
<blockquote>
<p>성능을 위해 JVM은 각 스레드마다 캐시에 메모리에서 값을 복사해두기에
플래그 변수가 바뀐 경우 기존 복사값을 통해 잘못된 로직이 진행될 수 있다.<br/>
<strong>volatile</strong>을 사용하여 메인 메모리에 직접 변수의 값을 읽도록 해
이러한 가시성 문제를 해결할 수 있다.</p>
</blockquote>
<p><strong>5. 카운터를 여러 스레드에서 동시에 증감해야 할 때</strong></p>
<blockquote>
<p>카운터의 경우 <code>++</code>, <code>--</code>와 같은 연산을 진행하는데
이러한 연산은 단순해 보이지만 CPU 내부 원자적으로 보면<br/>
값 읽기 - 연산 - 저장과 같은 단계로 구분이 된다.<br/>
이 지점에서 멀티스레드 환경의 경우 동시성 문제가 발생하기에
이를 보호하기 위해 <strong>Atomic Type</strong> 객체를 활용한다.</p>
</blockquote>
<p><strong>6. 공정한 락 순서가 필요하고 기아 상태를 방지해야 할 때</strong></p>
<blockquote>
<p><strong>synchronized</strong>를 활용해 객체의 모니터락을 사용하면
락 휙득 과정에서 특정 스레드가 경쟁에서 항상 우위에 있을 수 있다.<br/>
때문에 이를 막기 위해서 <strong>ReentrantLock</strong>의 생성자 인자로 <code>true</code>를 넘기면
ReentrantLock 객체 내부에 있는 <strong>sync</strong> 멤버가 <strong>FairSync</strong>가 되어
<strong>FIFO</strong> 순서대로 스레드가 락을 휙득할 수 있다.</p>
</blockquote>
<p><strong>7. 단순 boolean 상태를 원자적으로 변경해야 할 때</strong></p>
<blockquote>
<p><strong>volatile boolean</strong>의 경우 가시성만 보장하기에
현재 값을 보고 비교하여 상태를 변경하는 <strong>CAS</strong>방식의 <strong>Atomic Type</strong>을 사용해야한다.<br/>
별도의 락 없이도 원자적 전환이 가능하기에 성능상 이점을 가진다.</p>
</blockquote>
<p><strong>8. 여러 변수를 묶어 하나의 트랜잭션처럼 보호해야 할 때</strong></p>
<blockquote>
<p><strong>Atomic Type</strong>이나 <strong>volatile</strong>의 경우 변수 하나에만 적용된다.
두 변수의 값 수정이 항상 함께 일어나며 이러한 트랜잭션이 중요하다면
사이에 다른 스레드가 껴서는 안된다.<br/>
때문에 임계구역을 정하고 락을 통해 보호하는 것이 필요하며
이는 <strong>synchronized</strong>나 <strong>ReentrantLock</strong>을 통해 구현 가능하다.<br/>
코드의 복잡도가 낮고 특별한 조건 분기가 필요없다면 <strong>synchronized</strong>를
내부에서 함수를 추가 호출하거나, 특별 조건이 있다면 <strong>ReentrantLock</strong>을 사용한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[ExecutorService를 통한 동시성 문제 해결]]></title>
            <link>https://velog.io/@zayn_noh/ExecutorService%EB%A5%BC-%ED%86%B5%ED%95%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@zayn_noh/ExecutorService%EB%A5%BC-%ED%86%B5%ED%95%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Tue, 19 May 2026 10:07:14 GMT</pubDate>
            <description><![CDATA[<p>JVM 위에서 동작하는 자바 어플리케이션은
내부에서 작업의 흐름 단위로 스레드를 사용하여 병렬 처리를 구현한다.</p>
<p>이때 모든 개별 작업마다 스레드를 직접 생성하고 관리하는 방식은 다음과 같은 문제를 가진다.</p>
<ul>
<li><p><strong>컨텍스트 스위칭 비용 증가</strong></p>
</li>
<li><p><strong>무분별한 자원 소비</strong></p>
</li>
<li><p><strong>예외처리 누락과 같은 코드 레벨의 문제</strong></p>
</li>
</ul>
<p>이번 포스팅에서는 이러한 스레드(Thread) 단위의 <span style = "color: skyblue">작업 흐름을 자동으로 관리</span>하고
대규모 작업을 안전하게 병렬처리해주며</p>
<p><span style = "color: skyblue">서버에서의 시스템 자원을 예측 가능하게 통제</span>할 수 있는,
<strong>ExecutorService</strong> 인터페이스에 대해 다루어보도록 한다.</p>
<hr>
<h3 id="thread-pool">Thread Pool</h3>
<p><strong>스레드 풀(Thread Pool)</strong>이란 단어의 의미 그대로 스레드들을 모아둔 것이라 생각할 수 있다.
사용 목적과 함께 풀어서 개념에 대해 다루어보면 </p>
<p>스레드풀은 작업을 처리하기 위한 <strong>스레드를 매번 새로 생성하지 않고</strong>,
<strong>미리 만들어 둔 스레드 집합을 재사용</strong>하여 작업을 처리하는 구조라 볼 수 있다.</p>
<blockquote>
<p><strong><em>만약 서버에 100,000개의 사용자 요청이 들어온다 했을 때
이를 개별 스레드로 다루면 어떻게 될까?</em></strong></p>
</blockquote>
<pre><code class="language-java">for (int i = 0; i &lt; 99_999; i++) {
    Thread t = new Thread();
    t.start();
}</code></pre>
<p>스레드 하나당 약 <code>1MB</code>의 스택 메모리를 소비하고 (100000MB ~= <code>100GB</code>)
스레드의 <strong>생성 및 소멸 시 발생하는 오버헤드</strong> 또한 100,000개가 누적되며<br/>
너무 많은 개별 스레드에 따라 <strong>컨텍스트 스위칭</strong>에 들어가는 비용이 커진다.
실제 작업의 처리 시간보다 다음 스레드를 결정하는 시간이 더 큰 문제가 발생한다.<br/>
결과적으로 너무 많은 멀티스레드 때문에 <strong>OOM</strong>이 발생하거나
컨텍스트 스위칭 문제로 CPU 사용률이 100%에 가깝게 고착된다.
이로 인해 처리량이 급감하고 응답 지연이 폭증하게 되어 서비스 불능 상태가 될 것이다.</p>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/11c8d129-24e2-4323-b398-bf8236e0601c/image.png" alt=""></p>
<p>위 그림처럼 스레드 풀은 고정된 수의 스레드가 작업을 꺼내 처리하는 구조로 동작한다.</p>
<p>작업이 완료되면 결과가 어플리케이션으로 반환되거나 종료되며
작업을 완료한 스레드는 다시 유휴 상태가 되어, 스레드 풀에 유지되는 방식이다.</p>
<p>이러한 스레드의 재사용을 통해 생성과 소멸 시 발생하는 오버헤드를 줄이고
각 스레드가 차지하는 자원의 낭비를 예방하게 스레드를 사용하는 구조를
<strong>스레드 풀(Thread Pool)</strong>이라 한다.</p>
<p>그렇다면, <code>Application</code>에서 각 수행해야 할 작업들이, <code>Thread Pool</code>로 넘어가 스레드에서 실행되는 과정에서 어떤 방식으로 작업을 선택하고 처리할까?</p>
<hr>
<h3 id="blocking-queue">Blocking Queue</h3>
<p>어플리케이션에 전달된 각 작업들은 <strong>블로킹 큐(Blocking Queue)</strong>에 저장되며
실행을 대기하는 작업들이 <strong>FIFO</strong>로 큐에 쌓이게 된다.</p>
<p>이러한 작업 큐는 스레드 수보다 많은 작업이 들어올 때
자동으로 작업을 대기시키는 <strong>버퍼(buffer)</strong> 역할을 동시에 수행한다.</p>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/75f6ca50-4b35-4945-9156-cf0e54661539/image.png" alt=""></p>
<hr>
<h3 id="executorservice-인터페이스">ExecutorService 인터페이스</h3>
<p><strong>ExecutorService</strong>는 <strong>Thread Pool</strong> 기반의,
작업 실행을 관리해주는 고수준 스레드 실행 서비스 <strong>인터페이스</strong>이다.</p>
<pre><code class="language-java">public interface ExecutorService extends Executor, AutoCloseable {
    ...</code></pre>
<p><code>Thread</code>를 직접 생성해 실행 로직을 관리하는 기존 과정을 추상화하여
작업 실행, 스레드 생성, 재사용, 종료까지를 <strong>자동으로 관리하는</strong> 실행 환경을 제공한다.</p>
<p>해당 인터페이스는 내부적으로</p>
<ul>
<li><strong>작업 대기열</strong> : <code>Blocking Queue</code></li>
<li><strong>스레드 풀</strong> : <code>Thread Pool</code></li>
</ul>
<p>을 함께 관리하며, 외부에서 전달된 작업(<code>Runnable</code>, <code>Callable</code>)을 받아
스레드 풀 내에서 효율적으로 처리하는 구조로 동작한다.</p>
<p><img src="https://velog.velcdn.com/images/zayn_noh/post/f33c609b-47cf-4942-a1be-d4c729ea16d7/image.png" alt=""></p>
<p>결과적으로 <code>ExecutorServcie</code>는 실행할 작업과 스레드 실행 관리를 <strong>분리</strong>해주는 실행 서비스이자 <strong>일종의 스레드 관리자</strong>라 할 수 있다.</p>
<p>ExecutorService를 사용하는 일련의 과정을 통해 아래 코드를 이해하여 보겠다.</p>
<pre><code class="language-java">public class ExecutorServiceEx {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        executorService.execute(() -&gt; threadMethod(&quot;method A&quot;));
        executorService.execute(() -&gt; threadMethod(&quot;method B&quot;));

        executorService.shutdown();
    }

    private static void threadMethod(String s) {
        System.out.println(s + &quot;: &quot; + Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println(s + &quot;] finished&quot;);
    }
}</code></pre>
<blockquote>
<p>실행 결과는 다음과 같다. <strong>(실행마다 다른 결과가 나올 수 있다)</strong><br/></p>
</blockquote>
<pre><code>method A: pool-1-thread-1
method B: pool-1-thread-2
method A finished
method B finished</code></pre><p><strong>1. ExecutorService 생성</strong></p>
<p>Executors 유틸리티 클래스를 통해 ExecutorService의 구현 객체를 생성한다.</p>
<pre><code class="language-java">ExecutorService executorService = Executors.newFixedThreadPool(2);</code></pre>
<p>이때 <code>newFixedThreadPool()</code> 메서드의 실제 코드를 살펴보면 다음과 같은데</p>
<pre><code class="language-java">public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue&lt;Runnable&gt;());
}</code></pre>
<p>매개변수 <code>nThreads</code>를 통해 넘겨준 <code>int</code>값을 활용하여
<code>ThreadPoolExecutor</code> 객체를 생성하여 반환함을 알 수 있다.</p>
<pre><code class="language-java">public class ThreadPoolExecutor extends AbstractExecutorService {
    ...

  public ThreadPoolExecutor(int corePoolSize,
          int maximumPoolSize,
          long keepAliveTime,
          TimeUnit unit,
          BlockingQueue&lt;Runnable&gt; workQueue) {
      this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
               Executors.defaultThreadFactory(), defaultHandler);
  }

  ...</code></pre>
<p><code>ThreadPoolExecutor</code> 클래스는 <code>AbstractExecutorService</code> 추상클래스의 자손이며</p>
<pre><code class="language-java">public abstract class AbstractExecutorService implements ExecutorService {
    ...</code></pre>
<p><code>AbstractExecutorService</code> 추상클래스는 <code>ExecutorService</code>의 구현체기에
결과적으로
<code>ExecutorService executorService = Executors.newFixedThreadPool(2);</code>
처럼 <code>ExecutorService</code>를 참조변수로 하는 <strong>executorService</strong>에
스레드 풀의 크기가 2인 <code>ExecutorService</code>의 구현체가 할당된 것이다.</p>
<p>이와 함께 <strong>Executors</strong> 유틸리티 클래스는 다음과 같은 주요 메서드들을 통해
<code>ExecutorService</code> 인터페이스의 구현체를 만들 수 있다.</p>
<ul>
<li><p><code>newFixedThreadPool(int nThreads)</code>
  <strong>지정한 개수(n)의 스레드를 고정</strong>으로 유지하는 풀을 생성</p>
</li>
<li><p><code>newCachedThreadPool()</code>
  <strong>필요한 만큼 스레드를 무한정 생성</strong>하고, <strong>일정 시간 유휴 상태면 스레드를 제거</strong></p>
<pre><code class="language-java">public static ExecutorService newCachedThreadPool() {
  return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
          60L, TimeUnit.SECONDS,
          new SynchronousQueue&lt;Runnable&gt;());
}</code></pre>
</li>
<li><p><code>newSingleThreadExecutor()</code>
  <strong>하나의 스레드</strong>로 구성된 풀을 생성하며, <strong>모든 작업을 순차적으로</strong> 실행</p>
</li>
</ul>
<pre><code class="language-java">public static ExecutorService newSingleThreadExecutor() {
    return newSingleThreadExecutor(defaultThreadFactory());
}</code></pre>
<ul>
<li><code>newScheduledThreadPool(int corePoolSize)</code>
  일정 시간 <strong>지연 후 실행</strong>하거나, 주기적으로 <strong>반복 실행되는 작업</strong>을 위한 풀을 생성</li>
</ul>
<pre><code class="language-java">public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
//overloading
public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}</code></pre>
<br/>

<p><strong>2. 작업 제출</strong></p>
<pre><code class="language-java">executorService.execute(() -&gt; threadMethod(&quot;method A&quot;));
executorService.execute(() -&gt; threadMethod(&quot;method B&quot;));</code></pre>
<p><code>ExecutorService</code>는 <strong>Rnnable</strong>, <strong>Callable</strong>을 작업 단위로 받아 실행한다.
이때 작업을 <strong>제출하는 방식</strong>에 따라서 반환값 처리, 예외처리 방식이 달라진다.</p>
<ul>
<li><p><code>execute(Runnable)</code>
  : 오직 작업을 실행만하며 실행 결과, 예외 정보는 호출자에게 전달하지 않는다.
  때문에 실행 중 예외가 발생해도 잡아내기 어려우며, 작업의 정상 종료 여부도 알 수 없다.</p>
<p>  주로 결과가 중요하지않은 <strong>단순 비동기 작업</strong>(로그 기록, 알림 전송)에 사용한다.</p>
</li>
<li><p><code>submit(Runnable or Callable)</code>
  : 작업 실행이 끝나면 <strong><code>Future</code></strong> 객체를 반환한다.
  Future객체를 통해 결과를 비동기로 조회하거나, 예외 여부를 확인할 수 있다.</p>
<p>  데이터 처리, 외부 API 응답 대기, 비즈니스 로직 실행 결과와 같은
  작업 결과를 확인해야 하거나 예외를 추적해야하는 경우 사용한다.</p>
</li>
</ul>
<blockquote>
<p>제출된 작업은 스레드 풀에 존재하는 유휴 스레드가 실행하는데
설령 스레드가 1개라도 실행에는 문제가 없다.<br/></p>
</blockquote>
<pre><code class="language-java">ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.execute(() -&gt; threadMethod(&quot;method A&quot;));
executorService.execute(() -&gt; threadMethod(&quot;method B&quot;));</code></pre>
<p>단지 스레드 사용의 주 목적 중 하나인 동시성을 구현할 수 없을 뿐
작업 수행이 종료된 후 유휴 스레드가 되어 새로운 작업(<code>method B</code>)을 실행한다.</p>
<p>예시 코드에서 제출이 완료된 이후 다음 메서드를 두 개의 스레드에서 실행하게 되며</p>
<pre><code class="language-java">private static void threadMethod(String s) {
        System.out.println(s + &quot;: &quot; + Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println(s + &quot;] finished&quot;);
    }
}</code></pre>
<p>각 스레드는 스레드의 컨텍스트 스위칭이 일어나
<strong>Concurrency로 메서드가 실행</strong>된다.</p>
<br/>

<p><strong>3. 실행 종료 처리</strong></p>
<pre><code class="language-java">executorService.shutdown();</code></pre>
<p>위 코드를 <code>main</code>함수에서 제외하고 실행하여 보면 프로그램이 계속 실행되며 멈추지 않는다.</p>
<p><code>ExecutorService</code>를 명시적으로 종료하지 않으면 JVM이 종료되지 않기에
<strong>모든 작업이 끝난 뒤에는 반드시 자원을 해제</strong>해야 한다.</p>
<ul>
<li><p><code>shutdown()</code>
  : Executor의 종료를 요청하고,
현재 큐에 있는 작업은 모두 완료한 후 종료합니다.</p>
<p>즉, <strong>BlockingQueue</strong>에 대기 중인 작업은 유지되며, 추가 작업은 거부한다.
일반적인 <code>ExecutorService</code>의 종료처리 방식이다.</p>
</li>
<li><p><code>shutdownNow()</code>
  : 현재 대기 중인 작업은 취소하고,
  실행 중인 스레드에는 인터럽트를 요청하여 가능한 한 빠르게 종료</p>
<p>  <strong>BlockingQueue</strong>에 있는 요소를 모두 비우며, 실행 중인 스레드에 <strong>interrupt</strong>를 보낸다.
  예외 상황이나 강제 종료 필요 시에 사용한다.</p>
</li>
</ul>
<hr>
<h3 id="사용자-정의-거부-정책">사용자 정의 거부 정책</h3>
<p><strong>OOM</strong>(<code>OutOfMemoryError</code>)은 자바 프로그램이 사용할 수 있는 메모리를 모두 소진하여,
더 이상 객체를 생성할 수 없을 때 발생하는 오류이다.</p>
<p><strong>거부 정책</strong>은 이러한 메모리 관리 정책과 관련된 것으로
<code>ExecutorService</code>에서 <code>BlockingQueue</code>가 가득 찼을 때
<strong>새로운 작업(Runnable, Callable 구현한 작업)을 어떻게 처리할 지</strong> 정하는 전략이다.</p>
<p>그렇다면 <strong>개발자가 직접 거부 정책을 설정할 수 있을까?</strong></p>
<p><strong>가능하다.</strong>
<code>ExecutorService</code> 구현체인 <code>ThreadPoolExecutor</code>에 <code>RejectedExecutionHandler</code>를 직접 구현해서 넘길 수 있다.</p>
<pre><code class="language-java">RejectedExecutionHandler customPolicy = (task, executor) -&gt; {
    System.out.println(&quot;사용자 정의 거부 정책: &quot; + task.toString());
};

ExecutorService executor = new ThreadPoolExecutor(
    2,                          // corePoolSize
    4,                          // maximumPoolSize
    60L, TimeUnit.SECONDS,      // keepAliveTime
    new ArrayBlockingQueue&lt;&gt;(10), // BlockingQueue
    customPolicy                // 커스텀 거부 정책
);</code></pre>
<p>사용자 정의를 통한 거부 정책 설정 이외에도
기본적으로 다음과 같은 거부 정책을 설정할 수 있다.</p>
<ul>
<li><p><strong>AbortPolicy</strong>: 기본값, <code>RejectedExecutionException</code> 예외를 던진다</p>
</li>
<li><p><strong>CallerRunsPolicy</strong>: 작업을 제출한 스레드가 직접 실행한다</p>
</li>
<li><p><strong>DiscardPolicy</strong>: 조용히 그냥 버린다</p>
</li>
<li><p><strong>DiscardOldestPolicy</strong>: 큐에서 가장 오래된 작업은 버리고, 새 작업을 추가한다</p>
</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[제네릭(Generic)의 제약 사항, 타입 소거]]></title>
            <link>https://velog.io/@zayn_noh/%EC%A0%9C%EB%84%A4%EB%A6%ADGeneric%EC%9D%98-%EC%A0%9C%EC%95%BD-%EC%82%AC%ED%95%AD-%ED%83%80%EC%9E%85-%EC%86%8C%EA%B1%B0</link>
            <guid>https://velog.io/@zayn_noh/%EC%A0%9C%EB%84%A4%EB%A6%ADGeneric%EC%9D%98-%EC%A0%9C%EC%95%BD-%EC%82%AC%ED%95%AD-%ED%83%80%EC%9E%85-%EC%86%8C%EA%B1%B0</guid>
            <pubDate>Mon, 18 May 2026 12:39:05 GMT</pubDate>
            <description><![CDATA[<p>자바의 <strong>제네릭(Generic)</strong>에 대한 이해를 기본으로 하는 글입니다.</p>
<p><a href="https://velog.io/@zayn_noh/Generic-Wildcard-%ED%83%80%EC%9E%85%EC%95%88%EC%A0%95%EC%84%B1-%EB%B0%8F-%EB%8B%A4%ED%98%95%EC%84%B1">Generic과 Wildcard의 이해 : 타입 안정성 및 다형성</a>을 통해
제네릭에 대한 개념 및 사용법에 대해 확인할 수 있습니다.</p>
<hr>
<p><strong>제네릭(Generic)</strong>을 사용하다 보면, 많은 컴파일 에러와 마주치게 된다.</p>
<pre><code class="language-java">------------------------------------------------------------------
List&lt;String&gt; list = new ArrayList&lt;Integer&gt;(); // error

java: incompatible types: java.util.ArrayList&lt;java.lang.Integer&gt; cannot be converted to java.util.List&lt;java.lang.String&gt;
------------------------------------------------------------------</code></pre>
<pre><code class="language-java">------------------------------------------------------------------
static T item; // error
...
static int compare(T t1, T t2){ ... } // error

java: non-static type variable T cannot be referenced from a static context
------------------------------------------------------------------</code></pre>
<pre><code class="language-java">------------------------------------------------------------------
T[] tmpArr = new T[itemArr.length]; // error

java: generic array creation
------------------------------------------------------------------</code></pre>
<p>이번 포스팅에서는 제네릭을 사용할 때의 주의점과
<strong>컴파일 단계에서 일어나는 에러의 원인</strong>을 알아보고자 한다.</p>
<hr>
<h3 id="1-제네릭-클래스-내-static멤버에-타입-변수-사용-불가">1. 제네릭 클래스 내 static멤버에 타입 변수 사용 불가</h3>
<p>다음과 같은 <code>Box</code> 제네릭 클래스를 만든다 가정하여 보자</p>
<pre><code class="language-java">class Box&lt;T&gt; {
    static T item; // error
    static int compare(T t1, T t2) { ... } // error
}</code></pre>
<p>주석을 통해 표시한 것처럼, 위 코드는 컴파일 단계에서 에러가 발생한다.</p>
<p><strong>static 멤버</strong>는 모든 객체에 대해 동일하게 동작해야하기에 타입 변수 <strong>T</strong>는 사용이 불가하다.</p>
<p>제네릭 클래스의 타입 변수의 경우 인스턴스를 생성하는 시점에
<code>Box&lt;String&gt;</code>인지 <code>Box&lt;Integer&gt;</code>인지 실제 타입이 결정되기 때문이다.</p>
<p>추가로 설명하자면 <strong>static 멤버</strong>는 타입 변수에 지정된 타입(=대입된 타입)의
종류에 관계없이 동일한 것이어야 하기 때문이다.</p>
<p><code>Box&lt;Apple&gt;().item</code>과 <code>Box&lt;Grape&gt;.item</code>이 서로 다른 것이어서는 안된다는 뜻이다.</p>
<p><strong>static</strong> 메서드의 경우에도 마찬가지로, 고정된 인자가 아닌
인스턴스 생성 이후에 결정되는 타입변수를 인자의 참조변수로 사용하기에 불가하다.</p>
<hr>
<h3 id="2-제네릭-타입-배열-생성-불가">2. 제네릭 타입 배열 생성 불가</h3>
<pre><code class="language-java">class Box&lt;T&gt; {
    T[] itemArr; // ok
    ...
    T[] toArray() {
        T[] tmpArr = new T[itemArr.length]; // error
        ...
        return tmpArr;
    }
}</code></pre>
<p>클래스의 멤버 변수로 제네릭 타입 배열을 만드는 것과,
메서드 실행 내에서 제네릭 타입 배열을 생성하는 것은 차이가 있다.</p>
<p>클래스 내 멤버 변수로 제네릭 타입 배열을 사용하는 경우
컴파일 시점에 제네릭 타입 <strong>T</strong>가 결정되며, 이를 기반으로
클래스 내부 멤버 변수인 <code>itemArr</code>를 대입된 타입에 따라 바이트 코드로 컴파일한다.</p>
<p>그러나 <strong>new</strong> 키워드를 통해 생성하는 배열의 경우
<strong>컴파일 시점이 아닌</strong>, <strong>런타임에</strong> 실제 타입 정보가 필요한데</p>
<p>컴파일 시점에 사용된 제네릭 타입 변수 T의 정보는 런타임 시점에 존재하지 않는다.
이는 자바의 <strong>제네릭 타입 소거(Type Erasure)</strong> 개념을 통해 설명 가능하다.</p>
<hr>
<h3 id="제네릭-타입-소거">제네릭 타입 소거</h3>
<p>컴파일러는 제네릭 타입을 이용해 소스파일 확인하여 필요한 곳에 형변환을 넣어준다.
이후 컴파일된 바이트코드(<code>.class</code>)에는 제네릭 타입에 대한 정보가 존재하지 않는다.</p>
<p>이를 <strong>제네릭 타입 소거(Type Erasure)</strong>라 한다.</p>
<blockquote>
<p><strong>왜 이렇게 제네릭 타입 소거 과정을 거칠까? 정보를 남겨두면 안될까?</strong>
이는 제네릭 도입 이전의 소스코드와의 호환성을 유지하기 위해서 도입되었다.
즉, 이전 원시타입을 사용하여 작성된 코드들을 위한 것인데<br/></p>
</blockquote>
<pre><code class="language-java">List li = new ArrayList(); // 원시타입 사용</code></pre>
<p>언젠가 새로운 기능을 위해 하위 호환성을 포기하게 될 때가 올 수 있다.
뿐만 아니라 다음과 같은 이유로 원시타입을 사용하는 것은 문제를 야기한다.<br/>
    - 컴파일 타임에 타입 체크가 되지 않아 런타임 에러 위험
    - 불필요한 형변환 코드가 늘어남
    - IDE와 컴파일러의 unchecked warning 발생<br/>
때문에 코<strong>드를 작성할 때 가능하면 원시 타입을 사용하지 않도록</strong> 하자</p>
<p>제네릭 타입 제거의 기본적인 과정은 다음과 같다.</p>
<p><strong>1. 제네릭 타입의 경계(<code>bound</code>) 제거</strong>
제네릭 타입이 <code>&lt;T extends Fruit&gt;</code>로 되어있으면
타입변수 <code>T</code>는 <code>Fruit</code>로 치환된다.</p>
<p>또한 <code>&lt;T&gt;</code>인 경우는 <code>Object</code>로 치환된다. 이를 코드로 살펴보면</p>
<pre><code class="language-java">class Box&lt;T extends Fruit&gt; {
    void add(T t) {
        ...
    }
}</code></pre>
<p>위 제네릭 클래스는 <strong>제네릭 타입 제거</strong>를 통해 아래처럼 치환된다.</p>
<pre><code class="language-java">class Box {
    void add(Fruit t) {
        ...
    }
}</code></pre>
<p><strong>2. 제네릭 타입 제거 이후, 타입 불일치 시 추가 형변환 진행</strong>
실제 자바 표준 라이브러리 내 <code>List</code> 인터페이스의 <code>get()</code> 메서드를 보면</p>
<pre><code class="language-java">public interface List&lt;E&gt; extends SequencedCollection&lt;E&gt; {
    ...
    E get(int index);
    ...</code></pre>
<p>위와 같이 작성되어 있는 것을 확인할 수 있다. 
<strong>E</strong> 타입변수는 (1) 경계 제거 과정에서 <code>Object</code> 클래스로 변환된다.</p>
<p>따라서 컴파일 후 바이트코드에서 <code>get()</code>메서드의 반환타입은 <code>Object</code>가 되는데</p>
<pre><code class="language-java">// 소스 코드
List&lt;String&gt; list = new ArrayList&lt;&gt;();
String s = list.get(0);

// 컴파일 후 (컴파일러가 형변환 자동 삽입)
List list = new ArrayList();
String s = (String) list.get(0);</code></pre>
<p>실제 생성된 <code>List</code> 내부에 들어간 객체의 타입과 불일치가 발생한다 : <code>Obejct</code>, <code>String</code></p>
<p>따라서 <code>String s = (String) list.get(0);</code>처럼 적절한 형변환이 추가되는 것이다.
이러한 형변환 내용은 컴파일 타임에 추가하여, 이후 런타임 과정에서 실제 형변환이 일어난다.</p>
<p>추가로, <strong>와일드 카드(Wildcard)</strong>가 있는 경우에는
다음처럼 적적한 타입으로 컴파일 과정에서 <strong>형변환이 추가</strong>된다.</p>
<pre><code class="language-java">static Juice makeJuice(FruitBox&lt;? extends Fruit&gt; box) {
    String tmp = &quot;&quot;;
    for (Fruit f : box.getList()) tmp += f + &quot; &quot;;
    return new Juice(tmp);
}</code></pre>
<p>( 위 소스코드를 컴파일하여 아래와 같은 소스코드처럼 동작하는 바이트코드로 )</p>
<pre><code class="language-java">static Juice makeJuice(FruitBox box) {
    String tmp = &quot;&quot;;
    Iterator it = box.getList().iterator();
    while(it.hasNext()) {
        tmp += (Fruit) it.next() + &quot; &quot;;
    }
    return new Juice(tmp);
}</code></pre>
<ul>
<li><p><code>&lt;? extends Fruit&gt;</code> 제거</p>
</li>
<li><p><code>it.next()</code>는 <code>Object</code>를 반환하기에 <code>(Fruit)</code> 형변환 코드 추가</p>
</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Generic과 Wildcard의 이해 : 타입 안정성 및 다형성]]></title>
            <link>https://velog.io/@zayn_noh/Generic-Wildcard-%ED%83%80%EC%9E%85%EC%95%88%EC%A0%95%EC%84%B1-%EB%B0%8F-%EB%8B%A4%ED%98%95%EC%84%B1</link>
            <guid>https://velog.io/@zayn_noh/Generic-Wildcard-%ED%83%80%EC%9E%85%EC%95%88%EC%A0%95%EC%84%B1-%EB%B0%8F-%EB%8B%A4%ED%98%95%EC%84%B1</guid>
            <pubDate>Mon, 18 May 2026 08:17:46 GMT</pubDate>
            <description><![CDATA[<p>Java에서 표준 라이브러리 내 클래스, 인터페이스들의 구현 코드를 살펴보면
다음과 같은 문자를 볼 수 있다.</p>
<pre><code class="language-java">// HashMap 클래스
public class HashMap&lt;K,V&gt; extends AbstractMap&lt;K,V&gt;
    implements Map&lt;K,V&gt;, Cloneable, Serializable {
    ...

// List 인터페이스
public interface List&lt;E&gt; extends SequencedCollection&lt;E&gt; {
    ...

// Comparable 인터페이스
public interface Comparable&lt;T&gt; {
    ...</code></pre>
<p>이번 포스팅에서는 JDK5에서 처음 도입되었으며
이제는 모른다면 Java API문서조차 제대로 읽기 힘든,</p>
<p><strong>제네릭(Generic)</strong>에 대하여 학습하며,
제네릭의 다형성을 위해 고안된 와일드카드(Wildcard)를 배우고</p>
<pre><code class="language-java">List&lt;T&gt;
List&lt;?&gt;
List&lt;Object&gt;</code></pre>
<p>최종적으로 <code>Object</code>클래스와 함께 위 코드를 비교-이해함을 목표로 한다.</p>
<hr>
<h3 id="제네릭generic--t">제네릭(Generic) : T</h3>
<p>Generic은 간단하게 데이터 타입을 클래스나 메서드가 <strong>사용할 때까지 지정하지않고</strong>
나중에 지정해줄 수 있도록 해주는 타입 매개변수이다.</p>
<blockquote>
<p><strong>사용할 때</strong>라 함은 런타임 시에 데이터 타입이 결정된다는 것일까?
    그렇다면 런타임 다형성이라 볼 수 있지 않을까?<br/>
    <strong>아니다.</strong> 제네릭의 경우 <strong>컴파일 시점</strong>에 타입이 고정되며,
    이를 통해 <strong>잘못된 타입이 들어오는 것을 사전에</strong> 막을 수 있다.</p>
</blockquote>
<p>즉, <span style = "color: skyblue;">다양한 타입</span>의 객체들을 다루는 메서드나 컬렉션 클래스에
<strong>컴파일 시의 타입 체크(compile-time type check)</strong>를 해주는 기능이라 할 수 있다.</p>
<br/>

<h4 id="제네릭generic의-필요성">제네릭(Generic)의 필요성</h4>
<p>다음 코드는 인자로 전달받은 두 객체의 <code>total</code>값을 비교하는 클래스의 예시이다.</p>
<pre><code class="language-java">class TotalComparator implements Comparator {
    public int compare(Object o1, Object o2) {
        if (o1 instanceof Student &amp;&amp; o2 instanceof Student) {
            Student s1 = (Student) o1;
            Student s2 = (Student) o2;

            return Integer.compare(s1.getTotal(), s2.getTotal());
        }

        return -1;
    }
}</code></pre>
<p>위 코드에서 비교 메서드의 매개변수로 <code>Object</code> 타입을 지정한 이유는
모든 타입을 비교할 수 있게 하기 위해서이다.</p>
<p>이를 통해 매개변수의 타입이 다른 무수히 많은 버전의 compare() 오버로딩을 막는다.</p>
<p>이처럼 매개변수로 <code>Object</code>클래스의 타입을 지정하면
메서드 오버로딩으로 인한 코드의 중복을 제거할 수 있지만</p>
<p>실제 타입 정보를 알 수 없기에
<strong>컴파일러의 주요 기능인 타입 체크</strong>가 동작되지 않는다는 단점이 있다.</p>
<p>때문에 위 코드에선 <code>instanceof</code>로 실제 타입을 체크하고,
관련 로직을 수행하는 코드를 추가로 작성한 것이다.</p>
<p>물론 위 코드에서 최상위 클래스 <code>Object</code>가 아닌 실제 클래스 타입을 매개변수로 지정하면
다음처럼 깔끔하게 코드를 수정할 수 있다.</p>
<pre><code class="language-java">public int compare(Student s1, Student s2) {
    return Integer.compare(s1.getTotal(), s2.getTotal());
}</code></pre>
<p>다만 이러한 방식을 통해 구현한 <code>compare()</code> 메서드는
<strong>오직</strong> <code>Student</code> 객체를 매개변수로 넘겨주었을 때만 컴파일 시 문제없이 진행될 것이다.</p>
<p>이에 어떤 타입의 객체도 올 수 있으나,
컴파일 시점에 잘못된 타입의 객체가 들어오는 것을 사전에 막아주기까지 하는
<strong>제네릭(Generic)</strong>의 필요성을 느낄 수 있는 것이다.</p>
<p>어떻게 제네릭을 사용하도록 <code>TotalComparator</code> 클래스를 수정할 수 있을까?</p>
<br/>

<h4 id="제네릭generic의-사용법">제네릭(Generic)의 사용법</h4>
<p>제네릭타입은 <strong>클래스</strong>와 <strong>메서드</strong>에 선언할 수 있다.</p>
<p><strong>1) 클래스에 선언하는 경우</strong></p>
<pre><code class="language-java">// 제네릭 사용 전
class Box {
    Object item;

    void setItem(Object item) { this. item = item; }
    Object getItem() { return this.item; }
}

// 제네릭 사용
class Box&lt;T&gt; {
    T item;

    void setItem(T item) { this.item = item; }
    T getItem() { return this.item; }
}</code></pre>
<p>이때 <code>Box&lt;T&gt;</code>의 <code>T</code>를 타입변수라 하며, 타입변수 T는 T가 아닌 다른 것을 사용해도 된다.</p>
<blockquote>
<p>관례적으로 다음과 같은 타입변수들이 주로 쓰인다.
    - <strong>T</strong> : Type
    - <strong>E</strong> : Element
    - <strong>K</strong> : Key
    - <strong>V</strong> : Value
    - <strong>N</strong> : Number</p>
</blockquote>
<blockquote>
<ol>
<li>지네릭 클래스에서 <strong><code>static</code>멤버</strong>에는 타입변수 <code>T</code>를 <strong>사용할 수 없다.</strong></li>
</ol>
</blockquote>
<pre><code>class Box&lt;T&gt; {
    static T item; // error
    static int compare(T t1, T t2) {...} // error
}</code></pre><ol start="2">
<li><strong>제네릭 타입의 배열</strong>을 <strong>생성</strong>하는 것도 <strong>허용되지 않는다.</strong><br/>
제네릭 배열 타입의 <strong>참조변수를 선언하는 것은</strong> 가능하지만<pre><code class="language-java">T[] itemArr;</code></pre>
다음의 예시처럼 배열을 생성하는 것은 안 된다.<pre><code class="language-java">T[] toArray() {
 T[] tmpArr = new T[itemArr.length]; // error
}</code></pre>
이에 대한 자세한 설명은 <a href="https://velog.io/@zayn_noh/%EC%A0%9C%EB%84%A4%EB%A6%ADGeneric%EC%9D%98-%EC%A0%9C%EC%95%BD-%EC%82%AC%ED%95%AD-%ED%83%80%EC%9E%85-%EC%86%8C%EA%B1%B0">제네릭의 제약사항과 타입소거</a>에서 설명하도록 한다.</li>
</ol>
<p>이제 다음 <code>Box&lt;T&gt;</code> 제네릭 클래스의 실제 객체를 만드는 경우를 살펴보자</p>
<pre><code class="language-java">class Box&lt;T&gt; {
    ArrayList&lt;T&gt; list = new ArrayList&lt;T&gt;();

    void add(T item) { list.add(item); }
    T get(int i) { return list.get(i); }
    ArrayList&lt;T&gt; getList() { return list; }
    int size() { return list.size(); }
    public String toString() { return list.toString(); }
}</code></pre>
<p><strong>실제 객체 생성 및 사용</strong></p>
<pre><code class="language-java">Box&lt;Apple&gt; appleBox = new Box&lt;Apple&gt;(); // ok
Box&lt;Apple&gt; appleBox = new Box&lt;&gt;(); // ok
Box&lt;Apple&gt; appleBox = new Box&lt;Grape&gt;(); // error</code></pre>
<p>제네릭 클래스의 객체를 생성할 때는,
참조변수와 생성자에 대입된 타입(<strong>매개변수화된 타입</strong>)이 일치해야한다.
일치하지 않다면 에러가 발생한다.</p>
<blockquote>
<ol>
<li><strong>생성 시에 상속관계에 있다 하여도</strong> 에러가 발생한다.</li>
</ol>
</blockquote>
<pre><code class="language-java">Box&lt;Fruit&gt; appleBox = new Box&lt;Apple&gt;(); // error</code></pre>
<ol start="2">
<li>다음과 같은 메서드의 매개변수로 상속관계에 있는 객체는 사용 가능하다.<pre><code class="language-java">Box&lt;Fruit&gt; fruitBox = new Box&lt;Fruit&gt;();
fruitBox.add(new Fruit()); // ok
fruitBox.add(new Apple()); // ok, 다형성</code></pre>
</li>
<li><strong>제네릭 클래스끼리 상속</strong>되어있고, <strong>대입된 타입이 같은</strong> 것도 가능하다.<pre><code class="language-java">Box&lt;Apple&gt; appleBox = new FruitBox&lt;Apple&gt;(); // ok, 다형성</code></pre>
이는 상속을 통한 다형성 구현이라는 객체지향의 개념과도 일치한다.</li>
</ol>
<hr>
<h4 id="-제네릭-제한-사용">* 제네릭 제한 사용</h4>
<blockquote>
<p>타입 변수 <code>T</code>에 지정할 수 있는 타입을 다음처럼 추가 제한할 수 있다.<br/>
<code>T extends Fruit</code></p>
</blockquote>
<ul>
<li><strong>Fruit</strong>클래스와 이를 <strong>상속받은 클래스</strong><br/>
<code>T extends Eatable</code></li>
<li><strong>인터페이스</strong>의 구현체 역시 <strong>extends</strong>를 사용한다.<br/>
동시에 제한을 두고 싶다면 <code>&amp;</code>기호를 통해 연결하여 준다.<br/><pre><code class="language-java">class FruitBox&lt;T extends Fruit &amp; Eatable&gt; { ... }</code></pre>
</li>
</ul>
<hr>
<p><strong>2) 메서드에 선언하는 경우</strong></p>
<pre><code class="language-java">static &lt;T&gt; void sort(List&lt;T&gt; list, Comparator&lt;? super T&gt; c)</code></pre>
<p>위 코드는 <code>Collections.sort()</code> 메서드의 선언부이다.
이를 <strong>제네릭 메서드(Generic Method)</strong>라 하며,
제네릭 타입의 선언은 <strong>반환 타입 바로 앞에</strong> 위치한다.
( 제네릭 메서드는 제네릭 클래스가 아니어도 선언 가능하다 )</p>
<pre><code class="language-java">class FruitBox&lt;T&gt; {
    ...
    static &lt;T&gt; void sort(List&lt;T&gt; list, Comparator&lt;? super T&gt; c) {
        ...
    }
}</code></pre>
<p>위 코드를 보았을 때, 얼핏보면 제네릭 클래스의 선언부 <code>FruitBox&lt;T&gt;</code>의 <code>T</code>를
<code>sort()</code>메서드의 인자의 매개변수화된 타입변수 <code>T</code>로 쓰는 것 같지만</p>
<p><strong>두 <code>T</code>는 문자만 같을 뿐 서로 다른 것이다.</strong></p>
<p>또한 <code>sort()</code> 함수를 보면 <strong>static</strong> 제어자가 붙은 것을 알 수 있는데
static멤버에는 앞서 살펴본 것처럼 타입변수를 사용할 수 없지만
이처럼 제네릭 메서드의 경우에는 선언하고 사용하는 것이 가능하다.</p>
<hr>
<h3 id="wildcard--">Wildcard : ?</h3>
<p><code>&lt;? extends T&gt;</code> <code>&lt;? super T&gt;</code> <code>&lt;?&gt;</code>
<strong>와일드카드(Wildcard)</strong>는 제네릭의 다형성을 위한 개념이다.</p>
<p>와일드카드를 사용하면 하나의 참조변수로 대입된 <strong>타입이 다른 여러 객체</strong>를 다룰 수 있다.
단, 참조변수에만 와일드카드의 사용이 가능하다.
-&gt; 객체를 생성하고자 할 때는, 명확한 하나의 타입이 지정되어야 하기 때문이다.</p>
<pre><code class="language-java">FruitBox&lt;? extends Fruit&gt; box = new FruitBox&lt;Fruit&gt;(); // ok
FruitBox&lt;? extends Fruit&gt; box = new FruitBox&lt;Apple&gt;(); // ok
FruitBox&lt;? extends Fruit&gt; box = new FruitBox&lt;Grape&gt;(); // ok</code></pre>
<ul>
<li><strong>Fruit</strong> 자손 클래스들을 타입변수로 <strong>FruitBox</strong> 제네릭 클래스를 생성 가능하다.</li>
</ul>
<pre><code class="language-java">FruitBox&lt;? super Fruit&gt; box = new FruitBox&lt;Fruit&gt;(); // ok
FruitBox&lt;? super Fruit&gt; box = new FruitBox&lt;Object&gt;(); // ok</code></pre>
<ul>
<li><strong>Fruit</strong>클래스와 <strong>Fruit</strong> 클래스의 조상인 <strong>Object</strong>클래스 역시 타입변수로 대입이 가능하다.</li>
</ul>
<hr>
<h3 id="object와의-비교">Object와의 비교</h3>
<p>다시 처음으로 돌아가 아래 코드가 의미하는 것이 무엇인지 확인하여 보자</p>
<pre><code class="language-java">List&lt;T&gt;
List&lt;?&gt;
List&lt;Object&gt;</code></pre>
<br/>

<p><strong>- <code>List&lt;T&gt;</code>의 경우,</strong>
메서드가 아닌 <strong>List</strong>클래스의 타입변수로 <code>T</code>가 작성되어있는 것을 보아</p>
<pre><code class="language-java">class A&lt;T&gt; {
    List&lt;T&gt; list;
    ...
    void setList(List&lt;T&gt; list) { this.list = list; }
    List&lt;T&gt; getList() { return this.list; }
}</code></pre>
<p>위 예시처럼 제네릭 클래스에서 static이 아닌 멤버변수, 
static이 아닌 메서드의 반환값 혹은 매개변수 코드의 일부라 생각할 수 있다.</p>
<br/>

<p><strong><code>List&lt;?&gt;</code>의 경우</strong></p>
<p>앞선 <code>List&lt;T&gt;</code>와 기능적으로 동일하나, 와일드카드로 작성된 것임을 알 수 있다.
<code>&lt;?&gt;</code>의 경우 <code>&lt;? extends Object&gt;</code>와 동일하며</p>
<p><code>List&lt;? extends T&gt; list;</code>처럼 멤버 변수에 작성할 경우</p>
<p><strong>클래스의 타입 변수 T에 따른</strong> 특정 클래스 혹은
이를 상속한 클래스의 객체가 list의 요소로 들어갈 수 있는 것이다.</p>
<br/>

<p><strong><code>List&lt;Object&gt;</code>의 경우</strong></p>
<pre><code class="language-java">List&lt;Object&gt; list = new ArrayList&lt;Object&gt;();</code></pre>
<p>위와 같이 생성된 <code>list</code>에는 <code>Object</code> 클래스를 상속받은 모든 클래스가
list 객체에 들어갈 수 있으며</p>
<p><code>Object</code> 클래스는 모든 클래스의 조상이기에
대부분의 클래스가 자동 업캐스팅되어 list의 요소로 들어갈 것이다.</p>
<p>즉, <code>List&lt;T&gt;</code>에 타입 변수를 <code>Object</code>클래스 타입으로 한 것과 동일하다.</p>
]]></description>
        </item>
    </channel>
</rss>