<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>MemoMemo.log</title>
        <link>https://velog.io/</link>
        <description>개발자</description>
        <lastBuildDate>Thu, 29 May 2025 13:11:04 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>MemoMemo.log</title>
            <url>https://velog.velcdn.com/images/dlaudrb09-/profile/0f3dae16-3d6d-4fe0-8487-6779fc45c7b3/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. MemoMemo.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dlaudrb09-" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[엑셀 주문 업로드 속도 개선]]></title>
            <link>https://velog.io/@dlaudrb09-/%EC%97%91%EC%85%80-%EC%A3%BC%EB%AC%B8-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%EA%B8%B0-%EB%B3%91%EB%A0%AC-%EC%B2%98%EB%A6%AC%EC%99%80-%EC%A3%BC%EC%86%8C-%EC%BA%90%EC%8B%9C</link>
            <guid>https://velog.io/@dlaudrb09-/%EC%97%91%EC%85%80-%EC%A3%BC%EB%AC%B8-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%EA%B8%B0-%EB%B3%91%EB%A0%AC-%EC%B2%98%EB%A6%AC%EC%99%80-%EC%A3%BC%EC%86%8C-%EC%BA%90%EC%8B%9C</guid>
            <pubDate>Thu, 29 May 2025 13:11:04 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-인식">문제 인식</h2>
<p>저희 시스템에는 엑셀 파일을 통한 주문 업로드에 대한 기능이 존재합니다. 
이를 대량 배차 시뮬레이션을 위해 가끔씩 많은 데이터를 한 번에 엑셀 파일로 주문 업로드를 하는 건들이 존재했습니다 </p>
<p>하지만 약 5,000 건의 주문을 업로드하는 데 <strong>20분 이상</strong> 이 소요되고 있었고,
이는 사용자 경험과 시스템 신뢰성 모두에 영향을 줄 수 있는 문제였습니다.
엑셀 업로드 시 한 행마다 검증 및 저장을 처리하는 비효율적인 구조가 성능 저하의 주요 원인이었고,
이를 해결하기 위해 문제의 우선순위를 정해 단계적으로 접근했습니다.</p>
<p><strong>START 기법</strong></p>
<ol>
<li>문제 인식 및 원인 분석 (<code>Situation</code>)</li>
<li>목표 설정 (<code>Task</code>)</li>
<li>행동 (<code>Action</code>)</li>
<li>결과 (<code>Result</code>)</li>
</ol>
<p>이 방향으로 문제를 해결하기 위해 먼저 원인 분석을 시작했습니다</p>
</br>

<h2 id="원인-분석">원인 분석</h2>
<p>기존 업로드 로직은 다음과 같은 특징이 있습니다</p>
<ul>
<li>모든 로직이 완전 동기 처리 </li>
<li>각 Row 마다 주소 검증, 주문 검증을 수행 </li>
<li><strong>병목은 대부분 외부 주소 정제 API에서 발생</strong></li>
</ul>
<p>더불어 사전에 이에 대한 성능 테스트를 하지않은 것도 원인 중에 하나였습니다</p>
</br>

<h2 id="목표-설정">목표 설정</h2>
<p>그리하여 아래 목표로 설정하여 개선 작업을 시작했습니다 </p>
<ul>
<li>업로드 시간을 <strong>20분</strong> → 최소 5분 이내로 줄이자</li>
<li>안정성과 정확성을 해치지 않도록 구현하자 </li>
</ul>
</br>

<h2 id="행동-action">행동 (Action)</h2>
<p><strong>응답 대기 시간 최적화를 위한 I/O 분산 처리 구조 도입</strong></p>
<ul>
<li>기존 구조는 각 주문을 순차적으로 처리하면서, 주소 검증 API 응답을 기다리는 시간이 전체 업로드 시간의 대부분을 차지했습니다 </li>
<li>이를 개선하기 위해 각 요청을 비동기로 처리하고, 응답은 모아서 기다리는 방식으로 구조를 변경하였습니다 </li>
</ul>
<p>→ 이렇게 하면 <strong>요청-응답 대기 시간</strong>을 겹치게 하여, 실제로 처리되는 시간은 요청수에 비례하지 않고 짧은 시간에 여러 요청을 처리할 수 있습니다. </p>
<p>예시 코드</p>
<pre><code class="language-kotlin">coroutineScope {
    orders.map { order -&gt;
        launch(Dispatchers.IO) {
            process(order)
        }
    }
}</code></pre>
<p><strong>I/O Bound 작업</strong> 이 대부분이므로 외부 API 응답 대기 소모시간을 줄이고 빠른 처리가 가능하게 되었습니다. </p>
<p>결과적으로 업로드 시간은 <strong>기존 20분 → 약 2분 ~ 3분 수준으로 단축</strong>되었습니다.</p>
<blockquote>
<p><a href="https://velog.io/@dlaudrb09-/%EC%97%91%EC%85%80-%EC%A3%BC%EB%AC%B8-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%EA%B8%B0-%EB%B3%91%EB%A0%AC-%EC%B2%98%EB%A6%AC%EC%99%80-%EC%A3%BC%EC%86%8C-%EC%BA%90%EC%8B%9C#%EC%95%9E%EC%9C%BC%EB%A1%9C-%EA%B0%9C%EC%84%A0-%EB%B0%A9%ED%96%A5"><code>launch</code> 를 사용함으로서 고려할 점들</a></p>
</blockquote>
</br>

<h2 id="두-번째-문제-→-api-과부하">두 번째 문제 → API 과부하</h2>
<p>성능은 충분히 개선되었지만, 주소 검증 API에 동시 요청이 몰리면서 <code>429 Too Many Requests</code> 의 문제가 발생하였습니다 </p>
<h3 id="ratelimiter-도입">RateLimiter 도입</h3>
<p>기존에 있던 Sliding Time WIndow 기반 RateLimiter 가 존재했고 이를 적용했습니다 </p>
<ul>
<li>초당 최대 요청 수 제한 (<code>maxRequests: 45</code>)</li>
<li><code>delay</code> 후 요청 분산 </li>
</ul>
<p><strong>결과적으로 성능은 다시 느려졌습니다..!</strong></p>
<p>코루틴 동시성 처리로 20분에서 3분까지 줄였지만 <code>RateLimiter</code> 를 도입으로 다시 <strong>9분 ~ 10분 수준까지</strong> 늘어났습니다 </p>
<p>그렇지만 외부 API 와의 안정성과 실패없는 업로드를 위해 선택했습니다 </p>
</br>

<h3 id="주소-정제를-위한-캐시-도입">주소 정제를 위한 캐시 도입</h3>
<p>주소 검증에 외부 API 를 사용하는게 문제라면 같은 주소인 경우 반복된 외부 API 호출은 불필요합니다 </p>
<p>더불어 주소 검증에 대한 패턴을 파악하여 사용자의 시간, 주문이 업로드되는 주소를 기반으로 중복된 주소를 사용하는 걸 알 수 있었습니다.</p>
<p>그래서 우리는 <code>Look-aside</code> 캐시 기반의 Redis 주소 캐싱을 도입해서 처리하게 되었습니다 </p>
<p>이후 구조는 이렇게 변했습니다 </p>
<pre><code class="language-kotlin">fun resolve() {
    val key = &quot;addr:$address&quot;

    redisTemplate.opsForValue().get(key)?.let { point -&gt;
        return deserialize(point)
    }

    val point = Point.of(address)
    point?.let {
        redisTemplate.opsForValue().set(key, serialize(it))
    }
    return point
}</code></pre>
<p>결과적으로 전체 시간은 <strong>다시 2분 ~ 3분대로 줄어들었으며 주소 검증에 캐시 히트율이 많아지면 1분대로도 줄어드는 효과를 보았습니다</strong></p>
<p>더불어 <strong>외부 API 호출 수 또한 50% 이상이 감소</strong>하게 되었습니다</p>
</br>

<h2 id="결과">결과</h2>
<ul>
<li>엑셀 업로드 시간 20분 -&gt; <strong>2분 ~ 3분</strong>대로 85% 이상 단축 </li>
<li>기존 시스템을 유지한채 코루틴 동시성 처리 및 외부 API 의 안정성 유지 </li>
<li>이후 주소 검증 캐시를 도입하여 주소 검증을 사용하는 곳에서도 간접적인 속도 개선 확보 </li>
</ul>
</br>


<h2 id="회고">회고</h2>
<p>대용량 처리 외의 단순한 속도 개선 보다 <strong>안정적으로 성공하는 구조의 중요성</strong>을 알게 되었고 더불어 문제를 해결함으로써 생겨날 사이드 이펙트를 잘 고려해서 설계 및 문제해결을 해야겠다고 느꼇습니다</p>
<h3 id="앞으로-개선-방향">앞으로 개선 방향</h3>
<p>대용량 업로드 자체를 비동기 + 배치 처리 구조로 전환하는 것도 하나의 방법이 될 것 같습니다 
<del>가용가능한 CPU 코어 수 기반 병렬 처리는 전체 리소스가 포화될 수 있는 문제가 발생할 수 있습니다. 이를 개선할 방안이 필요합니다</del>
코어 수 기반 병렬처리는 실제로 도입하기에 적절한지 다시한번 고려할 필요가 있어 보여 현재는 코루틴기반 동시성 처리를 진행하였습니다.</p>
<ul>
<li>동시실행 제한, 버퍼 큐 형태 </li>
</ul>
<p>더불어 <code>RateLimiter</code> 를 현재는 애플리케이션 단에서 처리하고 있지만 이중화되는 구조를 생각해서 재시도 개선 및 RateLimiter 의 변경이 필요해 보입니다 
(+ 추가로 <code>RateLimiter</code> 의 공유자원에 대한 동시성 처리)</p>
<p>추가로 API 에 대한 최악의 상황을 고려해 테스트하는 프로세스를 꼭 도입할 필요성을 느끼게 되었습니다</p>
</br>
</br>

<p><strong>[2025.08.06 회고]</strong></p>
<p>코루틴의 <code>launch</code> 함수는 <code>fire-and-forget</code> 스타일이므로 <code>map { launch(...) }</code> 자체가 반환하든 <code>List&lt;Job&gt;</code> 을 아무데도 저장하지 않거나 <code>join()</code> 처리를 하지 않는다면 문제가 발생할 수 있습니다.</p>
<ul>
<li>예외 전파/취소 전파가 안되는 문제 </li>
<li>부모가 먼저 종료되더라도 자식 코루틴이 계속 실행되므로 메모리 누수(OOM) 이나 예기치 않은 동작이 발생할 문제 </li>
</ul>
<p>결국 두 가지 해결책을 고려해야 합니다</p>
<p><strong>해결책 1 → <code>joinAll()</code> 로 자식 코루틴 모두 기다리기</strong></p>
<pre><code class="language-kotlin">coroutineScope {
    val jobs = orders.map { order -&gt;
        launch(Dispatchers.IO) {
            process(order)
        }
    }
    jobs.joinAll() // 모든 코루틴이 끝날때까지 기다리기
}</code></pre>
<ul>
<li><code>joinAll()</code> 을 통해 모든 코루틴이 끝날때까지 기다리는 방법 </li>
</ul>
</br>

<p><strong>해결책 2 → <code>async</code> + <code>awaitAll()</code> 을 사용</strong></p>
<pre><code class="language-kotlin">coroutineScope {
    val jobs = orders.map { order -&gt;
        async(Dispatchers.IO) {
            process(order)
        }
    }
    jobs.awaitAll()
}</code></pre>
<ul>
<li><code>async</code> 는 <code>Deferred</code> 를 반환하며 <code>awaitAll()</code> 은 예외 전파가 보장됩니다.</li>
<li><code>launch</code> 는 예외가 내부에서만 발생할 수 있으므로 예외가 감지되지 않습니다 </li>
</ul>
</br>

<p>혹은 <code>SupervisorScope</code> 를 고려 → 현재 비즈니스에 맞는 상황인 경우
(한 코루틴이 실패해도 나머지가 취소되지 않도록 할때 유용)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL 에서 DDL 과 Auto-commit]]></title>
            <link>https://velog.io/@dlaudrb09-/MySQL-%EC%97%90%EC%84%9C-DDL-%EA%B3%BC-Auto-commit</link>
            <guid>https://velog.io/@dlaudrb09-/MySQL-%EC%97%90%EC%84%9C-DDL-%EA%B3%BC-Auto-commit</guid>
            <pubDate>Sat, 24 May 2025 03:28:57 GMT</pubDate>
            <description><![CDATA[<p>MySQL 에서 DDL을 실행할 때의 트랜잭션 처리 방식과 암묵적 커밋(Implicit Commit) 에 대한 이슈를 통해 정리한 글 입니다 </p>
<hr>
<h1 id="1-ddl-은-트랜잭션에서-암묵적으로-커밋된다">1. DDL 은 트랜잭션에서 암묵적으로 커밋된다</h1>
<p>MySQL 에서는 <strong>DDL이 실행되면 자동으로 커밋</strong>이 발생합니다.
이를 &quot;암묵적 커밋 (Implicit Commit)&quot; 이라고 부릅니다 </p>
<p>예시</p>
<pre><code class="language-sql">START TRANSACTION;
INSERT INTO user VALUES(1, &#39;철수&#39;);
ALTER TABLE user ADD COLUMN email VARCHAR(255);
ROLLBACK;</code></pre>
</br>

<p>결과</p>
<p>→ <strong><code>ALTER TABLE</code> 이 실행되는 순간, 앞서 실행된 INSERT 도 함께 커밋됨</strong> 이후 <code>ROLLBACK</code> 은 무의미하게 됩니다 </p>
<p><strong>왜 이렇게 동작할까 ?</strong></p>
<ul>
<li>DDL 은 <strong>테이블의 메타데이터를 변경</strong>합니다</li>
<li>메타데이터는 여러 세션과 공유되며, 롤백되면 위험합니다</li>
<li>구조 변경은 즉시 반영되어야 하므로 <strong>트랜잭션과 무관하게 COMMIT이 강제</strong>됩니다 </li>
</ul>
<blockquote>
<p>관련 문서 : <a href="http://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html">MySQL 공식문서 - Implicit Commit</a></p>
</blockquote>
</br>

<hr>
<h1 id="2-ddl-중-lock-timeout-이-발생한-원인--→-metadata-lock-mdl">2. DDL 중 Lock Timeout 이 발생한 원인  → Metadata Lock (MDL)</h1>
<p>실무에서 DDL 을 실행하는 도중에 <code>lock wait timeout exceeded</code> 오류가 발생한 경우가 있습니다.</p>
<p>이는 해당 테이블의 메타데이터 락 (MDL)을 다른 트랜잭션이 쥐고 있어서 발생하는 문제 입니다.</p>
<p><strong>실제로 일어난 흐름</strong></p>
<ol>
<li><code>ALTER TABLE</code> 쿼리를 실행 중 </li>
<li><code>lock wait timeout exceeded</code> 에러 발생 </li>
<li><code>SHOW FULL PROCESSLIST</code> 로 확인해보니 Sleep 상태의 커넥션이 존재 </li>
<li>해당 커넥션을 <code>KILL</code> 하니 이후 DDL 실행이 정상적으로 처리됨 </li>
</ol>
</br>

<p><strong>원인 분석</strong></p>
<ul>
<li>MySQL 은 DDL 시 <code>MDL_EXCLUSIVE</code> 락을 요구함 </li>
<li>동시에 다른 커넥션이 <code>SELECT</code> 쿼리를 통해 <code>MDL_SHARED</code> 락을 잡고 있었다면 충돌 발생 </li>
<li>그 세션이 Sleep 상태였어도 MDL은 유지됨 </li>
<li><code>KILL</code> 명령으로 세션 종료 → MDL 해제 → 이후 쿼리 정상 실행 </li>
</ul>
</br>

<p><strong>이후 알게된 것</strong></p>
<ul>
<li>트랜잭션 커밋 또는 세션 종료 전에 구조 변경 시도하지 말것.</li>
<li><code>SHOW PROCESSLIST</code> + <code>performance_schema.metadata_locks</code> 로 누가 락을 가지고 있는지 확인 
(+ 실행 시점을 알기위해 <code>performance_schema.threads</code> 테이블 <code>join</code>)</li>
</ul>
<blockquote>
<p>참고</p>
<p>IntelliJ 에서 한 콘솔에 여러 DDL 을 실행하면 일반적으로 <strong>하나의 커넥션에서</strong> 처리됩니다</p>
<p>Auto-commit 이 꺼져 있어도 DDL 은 내부적으로 자동 커밋되고, 커넥션이 새로 생성되지는 않습니다</p>
<p><code>SHOW FULL PROCESSLIST</code> 결과, 여러 Sleep 커넥션이 남지 않음이 확인되었습니다</p>
<p>과거에 Sleep 커넥션이 다수 쌓였던 현상은 백그라운드 구조 동기화(Auto Sync), 여러 세션 열림, 실패한 트랜잭션이 커밋되지 않은 채 유지된 경우일 수 있습니다.</p>
</blockquote>
</br>

<h1 id="3-metadata-lock-mdl-이란">3. Metadata Lock (MDL) 이란?</h1>
<p>MDL 은 <strong>테이블, 컬럼, 인덱스 등 메타데이터에 대한 동시 접근을 제어하기 위한 락</strong>입니다.
메타데이터를 수정하거나 읽는 모든 쿼리는 이 락을 획득해야 합니다 </p>
<p><strong>종류</strong></p>
<ul>
<li><p><code>MDL_SHARED</code> </p>
<ul>
<li><strong>Shared Lock</strong> 이며 여러 트랜잭션이 동시에 획득할 수 있습니다</li>
<li>세부적으로는 <code>MDL_SHARED_READ</code> , <code>MDL_SHARED_WRITE</code> 등으로 나뉘어 있습니다</br></li>
</ul>
</li>
<li><p><code>MDL_EXCLUSIVE</code> </p>
<ul>
<li><code>Exclusive Lock</code> 은 하나의 트랜잭션만 획득 가능한 Lock</li>
<li><code>Exclusive Lock</code> 은 테이블에 대해 어떠한 Lock 도 걸려있지 않아야 획득 가능합니다</li>
<li><code>ALTER</code>, <code>DROP</code>, <code>RENAME</code> 등 구조 변경 시 사용합니다.</br>
</li>
</ul>
</li>
<li><p><code>MDL_INTENTION</code> </p>
<ul>
<li><code>Intention Lock</code> 은 잠금을 걸기 전에 먼저 의도(intent)를 표시하며 다른 트랜잭션 과의 충돌을 방지하기 위해 InnoDB 에서 사용하는 잠금 메커니즘 입니다.</li>
<li>InnoDB 엔진이 자동으로 관리하므로 사용자가 직접 제어할 일은 거의 없습니다</li>
<li>락 요청 의도가 있는 작업 시 미리 획득하게 됩니다</li>
</ul>
</li>
</ul>
<p>특징: 트랜잭션 종료와 무관하게 커넥션이 열려 있으면 락이 유지됨 </p>
<p><strong>언제 걸리는 것 인가?</strong></p>
<ul>
<li><code>SELECT * FROM table</code> → MDL_SHARED</li>
<li><code>ALTER TABLE table ...</code> → MDL_EXCLUSIVE</li>
<li><code>SHOW COLUMNS FROM table</code> → MDL_SHARED</li>
</ul>
</br>

<p>MySQL 의 InnoDB 엔진은 <strong>DML 시 내부적으로 다양한 레코드 수준의 락을 사용합니다</strong> 
이들과 MDL 은 전혀 다른 레이어 입니다 </p>
<p><strong>종류</strong></p>
<ul>
<li><code>X (Exclusive Lock)</code> <ul>
<li><code>UPDATE</code> , <code>DELETE</code> 등에서 해당 ROW 를 다른 트랜잭션이 접근하지 못하게 막음 </li>
</ul>
</li>
</ul>
<ul>
<li><code>S (Shared Lock)</code><ul>
<li><code>SELECT ... LOCK IN SHARE MODE</code> 에서 사용 </li>
</ul>
</li>
</ul>
<ul>
<li><code>IX / IS (Intention Lock</code><ul>
<li>테이블에 대해 부분적으로 row-level 락을 걸겠다는 의도를 나타낸 락 </li>
</ul>
</li>
</ul>
<ul>
<li><code>Gap Lock</code><ul>
<li>인덱스 상 존재하지 않는 값 범위에 락을 걸어 Insert 방지 </li>
</ul>
</li>
</ul>
<ul>
<li><code>Next-Key Lock</code><ul>
<li>레코드 락 + 갭 락의 조합</li>
</ul>
</li>
</ul>
</br>

<p><strong>격리 수준과 락의 결정 방식</strong></p>
<ul>
<li><code>REPEATABLE READ</code> 격리 수준에서는 기본적으로 <strong>Next-Key Lock</strong> 을 사용하여 팬텀 리드를 방지합니다 </li>
<li>하지만 <strong>정확히 유니크한 인덱스 값을 WHERE 조건으로 조회하는 경우</strong> 해당 ROW 에 대해서만 <strong>Record Lock</strong> 만 걸리고 <strong>Gap Lock</strong> 은 생략됩니다 </li>
</ul>
<p>즉, 쿼리 결과가 정확히 유니크한 하나의 레코드일 경우에는 Gap Lock 대신 Record Lock만 획득합니다 </p>
<p><strong>MDL 과의 차이점</strong></p>
<ul>
<li><p>Metadata Lock (MDL)</p>
<ul>
<li>테이블/컬럼 등의 구조 범위에서 사용 </li>
<li>쿼리 해석 시점에 발생 </li>
<li>커넥션 종료 또는 명시적 해제를 통해 락 해제 처리 </li>
</ul>
</li>
<li><p>Record Lock (InnoDB)</p>
<ul>
<li>행(ROW) 수준 데이터 범위에서 사용 </li>
<li>실행 중 트랜잭션 안에서 발생 </li>
<li>트랜잭션 커밋 또는 롤백 시점에 락 해제 처리 </li>
</ul>
</li>
</ul>
</br>

<h1 id="정리">정리</h1>
<ul>
<li>MySQL 에서 DDL 은 <strong>항상 암묵적으로 커밋되며 트랜잭션의 영향</strong>을 받지 않는다</li>
<li>DDL 중간에 <code>lock wait timeout</code> 이 발생했다면 <strong>다른 커넥션이 해당 테이블에 MDL 락을 쥐고 있어서 생긴 현상일 가능성</strong>이 높다 </li>
<li>InnoDB 의 레코드 락(Gap, Next-key, Intention Lock 등) 은 데이터 조작시 작동하며, MDL 과는 계층과 해제 시점이 다르다</li>
<li><code>REPEATABLE READ</code> 격리 수준에서 범위 조회는 Gap + Record (Next-Key Lock) 를, 유니크 단건 조회는 Record Lock 만 획득한다</li>
<li><code>SHOW PROCESSLIST</code>, <code>performance_schema.metadata_locks</code> , <code>performance_schema.threads</code> 를 통해 커넥션 및 실행 시점, 락 상태를 확인하면 원인 추적이 수월하다 </li>
</ul>
</br>

<p>참고</p>
<ul>
<li><a href="https://rnokhs.tistory.com/entry/MySQL-MDLmetadatalocks-Observability-%ED%99%95%EB%B3%B4%ED%95%98%EA%B8%B0">https://rnokhs.tistory.com/entry/MySQL-MDLmetadatalocks-Observability-%ED%99%95%EB%B3%B4%ED%95%98%EA%B8%B0</a></li>
<li><a href="https://rnokhs.tistory.com/entry/MySQL-metadatalocks">https://rnokhs.tistory.com/entry/MySQL-metadatalocks</a></li>
<li><a href="https://hyooi.github.io/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85/database/2021/11/09/mysql-metadata-lock.html">https://hyooi.github.io/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85/database/2021/11/09/mysql-metadata-lock.html</a></li>
<li><a href="https://livetodaykono.tistory.com/80">https://livetodaykono.tistory.com/80</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html">https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html?utm_source=chatgpt.com">https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html?utm_source=chatgpt.com</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Retry with Exponential Backoff & Jitter]]></title>
            <link>https://velog.io/@dlaudrb09-/Retry-with-Exponential-Backoff-Jitter</link>
            <guid>https://velog.io/@dlaudrb09-/Retry-with-Exponential-Backoff-Jitter</guid>
            <pubDate>Fri, 23 May 2025 11:15:35 GMT</pubDate>
            <description><![CDATA[<h2 id="cpu-바운드-작업으로-인해-종종-실패하는-외부-api">CPU 바운드 작업으로 인해 종종 실패하는 외부 API</h2>
<p>회사에서 사용하는 내부 최적화 엔진 API 가 특정 시간대에 자주 실패했습니다.
로그를 분석해보니 503 (Service Unavailable) 오류가 발생하는 빈도가 유독 높은 구간이 존재했고, 이는 단순히 네트워크 이슈가 아닌 <strong>서버 과부하 (CPU 바운드 작업)</strong>에 가까워 보였습니다 </p>
<p>모니터링 결과, 기사님이 작업을 배정받기 전 배차 시뮬레이션을 통해 결과를 받아보려는 오전에 해당 오류가 많이 발생하였습니다 </p>
<ul>
<li>오전 11시 ~ 12시 : 요청 급증 + 외부 엔진 CPU 사용류 약 75% 이상 → 503 오류 빈발</li>
<li>오후 시간대 요청시 정상 응답 다수 확인 </li>
</ul>
</br>

<p>→ 따라서 CPU 사용률이 약 75%를 넘어서면 처리 실패 확률이 증가하는 것을 확인했습니다. </p>
<p><strong>이러한 특정을 기반으로, CPU 부하를 회복할 시간을 갖도록하며 빠른 재시도는 실패율을 높이는 결과로 이어질 수 있으므로 Retry 전략을 설계하게 되었습니다</strong></p>
</br>
</br>

<h2 id="retry-전략---exponential-backoff--jitter">Retry 전략 - Exponential Backoff + Jitter</h2>
<p>단순 재시도는 API 서버에 더 큰 부하를 줄 수 있기 때문에 이를 방지하기 위해 <strong>지수형 백오프와 지터(Jitter)</strong> 를 함께 적용했습니다 </p>
<p><em>지수형 백오프의 구현 형태는 아래와 같습니다</em></p>
<pre><code class="language-java">private fun calculateExponentialBackoff(retryCount: Int): Long {
    val delay = (INITIAL_RETRY_DELAY_MS * BACKOFF_MULTIPLIER.pow(retryCount.toDouble())).toLong()
    return min(delay, MAX_RETRY_DELAY_MS)
}</code></pre>
<ul>
<li>초기 지연시간이 존재하고 여기에 재시도 횟수에 따라 지수적으로 증가하도록 처리되었습니다 </li>
<li>이후 최대 지연시간을 설정하여 계산된 지연시간이 최대 지연시간을 넘어간 경우 최대 지연시간을 사용하도록 구현되었습니다 </li>
</ul>
</br>
</br>

<p><em>Jitter 적용</em></p>
<pre><code class="language-java">private fun addJitter(baseDelayMs: Long): Long {
    val jitterRange = (baseDelayMs * JITTER_FACTOR).toLong()
    return baseDelayMs + Random.nextLong(-jitterRange, jitterRange)
}</code></pre>
<ul>
<li>계산된 지연시간에 무작위성을 적용하였습니다 </li>
<li>서버가 이중화되어 있으므로 동일한 지연시간을 갖게 된 경우 실패확률이 올라가게 됩니다 </li>
<li>그러므로 랜덤성을 부여하여 최대한 동일한 시간에 여러 서버가 요청하지 않도록 무작위성을 추가하였습니다 </li>
</ul>
</br>
</br>

<h2 id="실제-적용-효과">실제 적용 효과</h2>
<p>재시도 로직을 적용한 후, 다음과 같은 성과를 확인할 수 있었습니다 </p>
<p>오전 11시 ~ 12시간대 기준 재시도 로직 주요 수치</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>적용 전</th>
<th>적용 후</th>
<th>변화율</th>
</tr>
</thead>
<tbody><tr>
<td>평균 실패율</td>
<td>약 20%</td>
<td>약 2.5%</td>
<td><strong>↓ 약 87% 감소</strong></td>
</tr>
<tr>
<td>평균 처리 시간</td>
<td>약 1.5초</td>
<td>평균 4초 (2~6초)</td>
<td><strong>↑ 약 167% 증가</strong></td>
</tr>
</tbody></table>
<p>결과적으로 평균 실패율은 대략 87 % 감소했고 반대로 평균 요청 처리는 대략 167% 증가했습니다 </p>
</br>

<h3 id="후속-개선-방향">후속 개선 방향</h3>
<p>전체 성공률은 높아져 충분히 안정성을 확보할 수 있었지만 요청 처리 시간이 늘어나면서 사용자에게 즉시 결과를 보여주기 어려운 문제가 발생했습니다.</p>
<p>이를 보안하기 위해 다음과 같은 방안을 고려 중입니다.</p>
<ul>
<li>비동기 요청 + Progress UI<ul>
<li>사용자에게 응답을 블로킹하지 않도록 처리 상태 시각화</li>
</ul>
</li>
<li>대량 배차 요청에 대한 배치 처리 도입 <ul>
<li>일반적인 기사 배차는 하루 단위로 적은 양이지만, 일부 기업 고객의 경우 <strong>주간/월간 단위 대량 배차 수요</strong>가 있어 <strong>예약/배차 기반 비동기 처리 시스템</strong> 을 고려</li>
</ul>
</li>
</ul>
</br>

<h2 id="정리">정리</h2>
<p>CPU 사용률이 높은 특정 시간대에 발생했던 503 오류 문제는 <strong>Exponential Backoff + Jitter</strong> 기반 재시도 로직으로 안정성을 보안했습니다 </p>
<p>실패율은 의미있게 감소했지만 처리 시간 증가에 대한 <strong>사용자 경험 보완</strong>이 다음 과제로 남아있습니다 </p>
<p>실제 현상 분석부터 실험, 구현, 개선까지 이어져 단순 재시도와는 다른 설계적 접근의 중요성을 알게되어 좋은 경험이었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[PR에 이미 머지된 커밋이 다시 뜨는 이유 ]]></title>
            <link>https://velog.io/@dlaudrb09-/PR%EC%97%90-%EC%9D%B4%EB%AF%B8-%EB%A8%B8%EC%A7%80%EB%90%9C-%EC%BB%A4%EB%B0%8B%EC%9D%B4-%EB%8B%A4%EC%8B%9C-%EB%9C%A8%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@dlaudrb09-/PR%EC%97%90-%EC%9D%B4%EB%AF%B8-%EB%A8%B8%EC%A7%80%EB%90%9C-%EC%BB%A4%EB%B0%8B%EC%9D%B4-%EB%8B%A4%EC%8B%9C-%EB%9C%A8%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sat, 17 May 2025 06:52:35 GMT</pubDate>
            <description><![CDATA[<p>이미 머지된 커밋이 PR 에 올릴 때 새로 뜨는 이유을 알아보기 전에 먼저 커밋 해시가 어떻게 생성되는지를 알아봅시다</p>
</br>

<h2 id="1-커밋-해시는-어떻게-생성되는가-">1. 커밋 해시는 어떻게 생성되는가 ?</h2>
<p>Git 에서 커밋 해시는 단순한 ID가 아니라, 커밋의 내용과 메타데이터를 기반으로 생성된 해시 입니다 
더불어서 다음 요소들이 해시 생성에 영향을 미칩니다 </p>
<ul>
<li><strong>Tree</strong> : 현재 디렉토리와 파일들의 상태 (내용 + 이름)</li>
<li><strong>Parent</strong> : 부모 커밋 해시</li>
<li><strong>Author / Committer</strong> : 작성자와 커밋터의 이름, 이메일, 시간 </li>
<li><strong>Message</strong> : 커밋 메시지 </li>
</ul>
<p><strong>이 중 하나라도 변경되면 커밋 해시는 완전히 달라집니다.</strong></p>
</br>

<hr>
<h2 id="2-커밋-해시가-변경되는-대표적인-상황들">2. 커밋 해시가 변경되는 대표적인 상황들</h2>
<ul>
<li><code>git commit --amend</code> : 메시지나 파일을 변경하여 Tree 또는 Message 가 달라진 경우  </li>
<li><code>rebase</code>, <code>cherry-pick</code> : 부모 커밋이 달라져서 해시가 변경된 경우 </li>
<li>작성자 정보 수정 (<code>git commit --amend -author</code>) : Author / Committer 가 달라진 경우 </li>
<li>파일 수정 : Tree 객체가 변경된 경우 </li>
<li>시간 차이로 커밋 : 커밋 시간도 해시에 영향을 준다 </li>
</ul>
</br>

<hr>
<h2 id="3-실무에서-발생된-상황">3. 실무에서 발생된 상황</h2>
<p>이제 머지 PR 을 올릴 때 이미 머지된 커밋이 다시 PR 변경사항에 포함되는 이슈를 알아봅시다 </p>
<p>먼저 브랜치 구성과 그 흐름을 설명하겠습니다 </p>
<p><strong>브랜치 구성</strong></p>
<ul>
<li><code>main</code> : 운영 환경 브랜치 </li>
<li><code>tb</code> : 스테이징 환경 브랜치 </li>
<li><code>dev</code> : 개발 환경 브랜치 </li>
<li><code>release/2025-01-01</code> : 해당 날짜에 배포해야할 사항이 있는경우 <code>dev</code> 브랜치에서 따서 미리 생성 </li>
<li>작업 브랜치 : 실제 배포해야할 작업이 담긴 브랜치 </li>
</ul>
</br>

<p><strong>예시 흐름</strong></p>
<ol>
<li><p><code>release/2025-01-01</code> 브랜치가 과거 <code>dev</code> 브랜치에서 생성</p>
</li>
<li><p>이후 <code>main</code> 브랜치에 핫픽스가 머지됨 </p>
</li>
<li><p><code>main</code> → <code>tb</code> → <code>dev</code> 로 <strong>rebase</strong> 진행 </p>
<ul>
<li>(이때 <code>dev</code> 의 커밋 해시가 전부 <strong>새롭게 생성</strong>된다)</li>
</ul>
</li>
<li><p><code>release/2025-01-01</code> 브랜치에 머지하기 위한 작업 브랜
치를 PR 에 올리면, 예전 <code>dev</code> 기준이기 때문에 Git 이 보기에 <strong>공통 조상(commit base)이 달라진 것처럼</strong> 인식 </p>
</li>
<li><p>즉, 이미 병합됐던 커밋들도 &quot;새로운 커밋 처럼&quot; 다시 보이게 됨 </p>
</li>
</ol>
</br>

<h3 id="왜-해시가-변경되었을까-">왜 해시가 변경되었을까 ?</h3>
<p><code>rebase</code> 는 기존 커밋을 새로운 부모 (commit base) 위로 다시 쌓는 것 입니다.
즉, <code>main</code> 의 핫픽스 커밋을 반영하려고 <code>dev</code> 를 <code>rebase</code> 하게 되면 </p>
<ul>
<li><code>dev</code> 의 모든 커밋은 새로운 부모를 기준으로 새롭게 복제됩니다 </li>
<li>이로 인해 커밋 해시가 바뀌게 됩니다 (같은 내용이더라도 부모가 달라지면 해시가 달라짐)</li>
</ul>
<blockquote>
<p><code>rebase</code> ⇒ 과거 내용을 “복붙” 하되, 새로운 commit chain 위에 얹는 것, 커밋 해시가 달라짐</p>
</blockquote>
</br>

<h3 id="해결-방법">해결 방법</h3>
<ol>
<li><p><code>cherry-pick</code> 으로 필요한 커밋만 선별 </p>
<ul>
<li>필요한 커밋만 선택적으로 적용해야한다 </li>
<li>커밋을 일일이 찾아야 하므로 번거롭다 </li>
</ul>
</li>
<li><p><code>merge</code> 전략 변경 → <code>merge --no-ff</code> 사용 </p>
<ul>
<li><code>rebase</code> 대신 <code>merge --no-ff</code> 를 사용하면 커밋 해시는 유지되고, 병합 커밋 하나 (M) 만 추가된다 <pre><code class="language-bash">git merge --no-ff feature-branch</code></pre>
</li>
</ul>
</li>
</ol>
</br>

<h3 id="merge---no-ff-란-"><code>merge --no-ff</code> 란 ?</h3>
<blockquote>
<p>기본 병합 (merge) 은 fast-forward 가능하면 그냥 브랜치 포인터만 이동시킴 → 그래서 커밋 기록이 깔끔하지 않게 섞일 수 있음 </p>
</blockquote>
<p><code>--no-ff</code> 는 강제로 <code>merge commit</code> 을 생성해서 &quot;이게 하나의 기능/작업 단위다&quot; 라고 명확하게 남기려는 옵션입니다 </p>
<p>예를 들어 </p>
<p><strong>1. 초기 커밋</strong></p>
<pre><code class="language-bash">git init
echo A &gt; file.txt
git add .
git commit -m &quot;A&quot;</code></pre>
</br>

<p><strong>2. <code>feature</code> 브랜치에서 작업</strong></p>
<pre><code class="language-bash">git checkout -b feature
echo B &gt; file.txt
git commit -am &quot;B&quot;
echo C &gt; file.txt
git commit -am &quot;C&quot;</code></pre>
<pre><code class="language-bash">A - B - C   ← feature 브랜치</code></pre>
</br>

<p><strong>3. <code>main</code> 에서 다른 작업 진행</strong></p>
<pre><code class="language-bash">git checkout main
echo D &gt; file.txt
git commit -am &quot;D&quot;</code></pre>
<pre><code class="language-bash">A - D  ← main 브랜치</code></pre>
</br>

<p><strong>4. <code>feature</code> 를 <code>main</code> 에 병합 (<code>--no-ff</code>)</strong></p>
<pre><code class="language-bash">git merge --no-ff feature -m &quot;Merge feature branch&quot;</code></pre>
<p>→ git 은 새 커밋 M 을 만들고 B, C 는 그대로 유지한 채 D 와 병합합니다 </p>
<pre><code class="language-bash">          B - C
         /
A - D --- M     ← main</code></pre>
<blockquote>
<p>M 커밋의 부모는 D 와 C 즉 병합 대상의 HEAD 두 개를 가리킨다 (main 과 feature 의 HEAD)
B,C 는 건드리지 않기 때문에 해시가 그대로 유지 </p>
</blockquote>
</br>

<h3 id="왜-merge---no-ff-는-해시가-바뀌지-않는건가">왜 merge --no-ff 는 해시가 바뀌지 않는건가?</h3>
<p><code>merge</code> 는 기존 커밋을 그대로 두고, 새로운 <strong>merge 커밋 (M)</strong> 하나만 추가합니다 
즉, 이전 커밋들은 복사되지 않고 참조만 되기 때문에 해시가 그대로 유지됩니다 </p>
<pre><code class="language-bash">     A - B - C
    /         \
D - E --------- M  ← merge commit (new)</code></pre>
<p>→ A, B, C, D, E 커밋 해시는 변하지 않음 
→ 새로 생긴 커밋은 M 하나뿐</p>
<p><strong>반면 <code>rebase</code> 는 ?</strong>
<code>rebase</code> 는 A, B, C 커밋을 새로운 부모 E 위에 복사해서 올려 놓습니다 
복사한 커밋은 부모가 달라졌기 때문에 모두 해시가 바뀌게 됩니다 </p>
<pre><code class="language-bash">         A&#39; - B&#39; - C&#39;
       /
D - E</code></pre>
<p>→ A, B, C → A’, B’, C’ 로 바뀜 (해시도 전부 새로 생성됨)
→ Git 입장에서는 “처음보는 커밋” 이라고 생각함</p>
</br>


<blockquote>
<p>참고 </p>
<p><code>git log</code> 를 통해 확인하면 <strong>A → D → M → B → C 순</strong> 이 아닐 수 있습니다.
<code>merge commit</code> 이후의 feature 커밋들 (B, C) 은 보이지 않을 수도 있습니다</p>
<p>이유는 <code>git log</code> 는 기본적으로 &quot;first-parent&quot; 경로만 따라가기 때문입니다
<code>git log</code> 는 커밋 히스토리를 출력할 때 단일 부모 (1번 부모) 만 타고 가며 출력합니다 
이게 바로 <code>merge</code> 커밋의 첫 번재 부모만 따라가며 보여주는 기본 동작입니다</p>
<pre><code>      B---C (feature)
     /     \
A---D-------M (main)</code></pre><ul>
<li><code>M</code> 의 부모 : <code>D</code> (1st parent), <code>C</code> (2nd parent)</li>
<li><code>git log main</code> 은 보통 M → D → A 만 따라감 <pre><code>  - B, C 는 M 의 2번째 부모 방향이므로 생략 </code></pre></li>
</ul>
<p>GitHub 에서 <code>Create a merge commit</code> 옵션으로 PR을 병합했을 때는 </p>
<ul>
<li>병합 대상 브랜치 (feature) 의 커밋들 (B, C) 가 </li>
<li>merge 커밋 (M) 의 2 번째 부모가 아닌 1 번째 부모로 들어가는 경우가 있습니다 </li>
</ul>
<p>즉, GitHub 이 merge 방향을 어떻게 설정하느냐에 따라, <code>M</code> 의 부모 순서가 (D, C) 가 아니라 (C, D) 가 될 수 있습니다 </p>
</blockquote>
</br>
</br>

<h2 id="정리">정리</h2>
<ul>
<li><code>rebase</code> 는 기존 커밋을 <strong>복사해서 새로운 커밋으로 만든다</strong></li>
<li><strong>부모 커밋이 바뀌면 해시도 무조건 바뀐다</strong></li>
<li>그래서 PR 에 <strong>과거 커밋이 새 커밋처럼 다시 올라오기도 한다</strong></li>
<li><code>merge --no-ff</code> 는 커밋을 건드리지 않고 부모가 2개인 새 merge 커밋(M) 을 하나 만들 뿐 → <strong>기존 커밋 해시가 유지됨</strong></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL 실행계획 (version 8.0 이상)]]></title>
            <link>https://velog.io/@dlaudrb09-/MySQL-%EC%8B%A4%ED%96%89%EA%B3%84%ED%9A%8D-version-8.0-%EC%9D%B4%EC%83%81</link>
            <guid>https://velog.io/@dlaudrb09-/MySQL-%EC%8B%A4%ED%96%89%EA%B3%84%ED%9A%8D-version-8.0-%EC%9D%B4%EC%83%81</guid>
            <pubDate>Tue, 13 May 2025 08:54:43 GMT</pubDate>
            <description><![CDATA[<h2 id="mysql-실행계획-이란">MySQL 실행계획 이란</h2>
<p>MySQL 은 클라이언트가 요청을 하게 되면 SQL 엔진이 4가지 과정을 거쳐서 응답을 주게 된다 </p>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/ae71f860-8150-40d6-9d0e-07e60f319100/image.png" alt=""></p>
<p>그 중 실행계획은 옵티마이저 와 관련이 있다 </p>
<ul>
<li>옵티마이저는 통계 정보를 기반으로 여러 방법의 비용을 계산한다</li>
<li>가장 비용이 적은 처리 방식을 최종적으로 선택한다</li>
</ul>
<p>즉, 실행계획은 옵티마이저가 쿼리를 어떻게 처리할 것인지 계산해낸 결과</p>
<hr>
<h2 id="실행계획을-왜-알아야하는가-">실행계획을 왜 알아야하는가 ?</h2>
<ol>
<li>우리의 의도에 맞게 MySQL 이 동작하는가를 검증 </li>
<li>병목이 생기는 부분을 확인 하는 등 </li>
</ol>
<p>먼저 실습하기 앞서 <code>EXPLAIN</code> 으로 실행 계획을 확인하는 정보들은 아주 많다 </p>
<p>그러므로 <code>type</code> / <code>key</code> / <code>Extra</code> 한정해서 확인해보겠다 </p>
<p><code>type</code></p>
<ul>
<li>MySQL 이 어떻게 ROW 를 찾아내는지 를 나타낸다</li>
<li>각 종류에 따라 성능에 최적화된 순서가 존재한다</li>
</ul>
<p><code>key</code></p>
<ul>
<li>해당 쿼리에서 실제로 사용된 인덱스 이름을 보여준다</li>
</ul>
<p><code>Extra</code></p>
<ul>
<li>MySQL 이 쿼리를 어떻게 처리했는지에 대한 부가 설명</li>
<li>성능 비교시 직접적인 힌트 를 제공하며 의도에 맞게 동작하는지 등을 확인 할 수 있음</li>
</ul>
<hr>
<p>예제 데이터 </p>
<ul>
<li>사원 (30만개)</li>
<li>부서 (9개)</li>
<li>사원_부서 테이블 (30만개)</li>
</ul>
</br>

<p><strong>테이블 생성</strong></p>
<pre><code class="language-sql">CREATE TABLE departments (
    dept_no CHAR(4) PRIMARY KEY,
    name VARCHAR(40) NOT NULL
);

CREATE TABLE employees (
    emp_no INT PRIMARY KEY,
    birth_date DATE NOT NULL,
    first_name VARCHAR(14) NOT NULL,
    last_name VARCHAR(16) NOT NULL,
    gender ENUM(&#39;M&#39;, &#39;F&#39;) NOT NULL,
    hire_date DATE NOT NULL
);

CREATE TABLE dept_emp (
    emp_no INT,
    dept_no CHAR(4),
    from_date DATE NOT NULL,
    to_date DATE NOT NULL,
    PRIMARY KEY (emp_no, dept_no),
    FOREIGN KEY (emp_no) REFERENCES employees(emp_no),
    FOREIGN KEY (dept_no) REFERENCES departments(dept_no)
);</code></pre>
</br>

<p><strong>부서 추가</strong></p>
<pre><code class="language-sql">INSERT INTO departments (dept_no, name) VALUES 
    (&#39;D001&#39;, &#39;Engineering&#39;),
    (&#39;D002&#39;, &#39;Tool Design&#39;),
    (&#39;D003&#39;, &#39;Marketing&#39;),
    (&#39;D004&#39;, &#39;Finance&#39;),
    (&#39;D005&#39;, &#39;Human Resources&#39;),
    (&#39;D006&#39;, &#39;Production&#39;),
    (&#39;D007&#39;, &#39;Development&#39;),
    (&#39;D008&#39;, &#39;Research&#39;),
    (&#39;D009&#39;, &#39;Customer Service&#39;);</code></pre>
</br>

<p><strong>사원 추가</strong></p>
<pre><code class="language-sql">DELIMITER $$

CREATE PROCEDURE populate_employees()
BEGIN
    DECLARE i INT DEFAULT 1;
    WHILE i &lt;= 300000 DO
        INSERT INTO employees (
            emp_no, birth_date, first_name, last_name, gender, hire_date
        )
        VALUES (
            i,
            DATE_SUB(CURDATE(), INTERVAL FLOOR(RAND() * 15000 + 8000) DAY),
            CONCAT(&#39;First&#39;, i),
            CONCAT(&#39;Last&#39;, i),
            IF(RAND() &gt; 0.5, &#39;M&#39;, &#39;F&#39;),
            DATE_SUB(CURDATE(), INTERVAL FLOOR(RAND() * 5000) DAY)
        );
        SET i = i + 1;
    END WHILE;
END$$

DELIMITER ;

CALL populate_employees();</code></pre>
</br>

<p><strong>사원_부서 추가</strong></p>
<pre><code class="language-sql">DELIMITER $$

CREATE PROCEDURE populate_dept_emp()
BEGIN
    DECLARE i INT DEFAULT 1;
    WHILE i &lt;= 330000 DO
        INSERT IGNORE INTO dept_emp (
            emp_no, dept_no, from_date, to_date
        )
        VALUES (
            FLOOR(RAND() * 300000) + 1,
            CONCAT(&#39;D00&#39;, FLOOR(RAND() * 9 + 1)),
            DATE_SUB(CURDATE(), INTERVAL FLOOR(RAND() * 5000 + 5000) DAY),
            DATE_SUB(CURDATE(), INTERVAL FLOOR(RAND() * 2000) DAY)
        );
        SET i = i + 1;
    END WHILE;
END$$

DELIMITER ;

CALL populate_dept_emp();</code></pre>
</br>

<hr>
<h2 id="전체-테이블-스캔-풀-테이블-스캔">전체 테이블 스캔 (풀 테이블 스캔)</h2>
<pre><code class="language-sql">EXPLAIN SELECT * FROM employees;</code></pre>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/364a381d-f46e-406d-8587-649a40d104c7/image.png" alt=""></p>
<h3 id="type-all"><code>type: ALL</code></h3>
<ul>
<li>풀 테이블 스캔 (Full Table Scan)</li>
<li>MySQL 이 해당 테이블의 모든 row 를 순차적으로 읽고 있다는 의미</li>
<li>일반적으로 가장 성능이 좋지 않은 접근 방식으로 간주된다</li>
<li>조건 (<code>WHERE</code>) 이 인덱스와 무관하거나, 있어도 활용할 수 없는 등</li>
</ul>
</br>

<h3 id="key-null"><code>key: Null</code></h3>
<ul>
<li>사용된 인덱스의 이름을 나타내는 컬럼으로 해당 쿼리에 대한 어떠한 인덱스도 사용하지 않았다 라는 의미 </li>
</ul>
</br>

<h3 id="extra-null"><code>Extra: Null</code></h3>
<ul>
<li>쿼리 수행에 있어서 특별한 최적화 사항이 없다는 의미</li>
<li>아무런 조건이 없으므로 특별한 접근 방식 없이 테이블 전체를 스캔</li>
</ul>
</br>

<hr>
<h2 id="pk-값을-통한-조회">PK 값을 통한 조회</h2>
<pre><code class="language-sql">EXPLAIN SELECT * FROM employees WHERE employees.emp_no = 1;</code></pre>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/1eeffca1-e506-4c66-a2c9-a1f4929427a7/image.png" alt=""></p>
</br>

<h3 id="type-const"><code>type: const</code></h3>
<ul>
<li><code>PK</code> 또는 <code>UK</code> 조건으로 ROW 가 유일하게 결정될 때 사용</li>
<li>정확히 하나의 ROW 만 읽음 (실행 이전에 결과가 한 건 이하임을 무조건 예측할 수 있는 쿼리)</li>
</ul>
</br>

<h3 id="key-primary"><code>key: PRIMARY</code></h3>
<ul>
<li>실제 사용된 인덱스는 PRIMARY KEY (PK)</li>
</ul>
</br>

<h3 id="extra-null-1"><code>Extra: Null</code></h3>
<ul>
<li>쿼리 수행에 있어서 특별한 최적화 사항이 없다는 의미</li>
</ul>
</br>

<hr>
<h2 id="부서-사원-테이블-에서-부서-조회">부서-사원 테이블 에서 부서 조회</h2>
<ul>
<li>부서 “D005” 에 소속된 사원을 조회해보자 </li>
</ul>
<pre><code class="language-sql">EXPLAIN SELECT * FROM dept_emp WHERE dept_no = &#39;D005&#39;;</code></pre>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/d08932d1-7678-41a4-8b45-f111dcd2d2ff/image.png" alt=""></p>
</br>

<h3 id="type-ref"><code>type: ref</code></h3>
<ul>
<li>인덱스 컬럼을 사용한 비교에서 사용<ul>
<li>동등 비교 (<code>=</code>) 를 사용할때 보임</li>
<li>비 유니크 인덱스 나 외래키 등을 조건으로 여러 개의 ROW 를 가져오는 경우 사용 (PK, UNIQUE NOT NULL 아님)</li>
</ul>
</li>
<li><code>dept_no = &quot;D005&quot;</code> 조건으로 인덱스를 타고 여러 ROW 를 가져오는 상황</li>
</ul>
</br>

<h3 id="key-dept_no"><code>key: dept_no</code></h3>
<ul>
<li>실제 사용된 인덱스의 이름</li>
<li><code>dept_no</code> 에 대해 인덱스가 걸려있다는 의미<ul>
<li>실행 시 해당 인덱스를 타고 조건에 맞는 ROW 를 탐색</li>
</ul>
</li>
</ul>
</br>

<h3 id="extra--using-index-condition"><code>Extra = Using index condition</code></h3>
<ul>
<li><strong>ICP (Index Condition Pushdown)</strong> 최적화가 사용되었음을 나타냄</li>
</ul>
<p>즉, 인덱스를 통해 조회할 때 필터 조건을 스토리지 레벨까지 내려보내서 처리하는 최적화 방식을 의미한다</p>
<p>단순히 인덱스를 타고 ROW 를 찾은 후 MySQL 서버단에서 필터링하는 것이 아닌, </p>
<p><strong>인덱스 조건을 스토리지 엔진에서 먼저 처리하므로 속도가 빨라진다</strong></p>
<p>(InnoDB 스토리지 엔진이 ICP 를 지원할 경우 자동으로 활용됨)</p>
</br>

<blockquote>
<p>참고 </p>
<p>왜 “Index Condition Pushdown” 이라는 이름인 걸까 ?</p>
<p>간단히 이야기하면 MySQL 이 인덱스 조건을 “더 아래로 밀어 넣는다” 라는 의미의 최적화 기법을 설명한 용어 </p>
<p>Index Condition </p>
<ul>
<li>WHERE 조건 절에서 인덱스 컬럼에 걸린 조건 (ex. dept_no = &quot;D005&quot;) </li>
</ul>
<p>Pushdown</p>
<ul>
<li>이 조건을 MySQL 의 스토리지 엔진 레벨까지 내려보내서 먼저 평가하는 동작</li>
</ul>
<p>즉, 조건을 인덱스에서 판단 가능한 건 미리 판단해서 필요 없는 ROW 는 아예 읽지도 않음 → 성능 향상 </p>
<p>MySQL 5.5 버전까지는 조건이 인덱스에 포함된 필드라도 인덱스 범위 조건으로 사용할 수 없는 경우에는 스토리지 엔진 조건 자체를 전달 조차 못함 (ex. <code>LIKE &quot;%TEST%&quot;</code>)</p>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/622717e7-961e-45b3-ab19-c583f17168a5/image.png" alt=""></p>
</blockquote>
</br>

<hr>
<h2 id="범위-검색">범위 검색</h2>
<ul>
<li>사원일자 (<code>hire_date</code>) 컬럼에 대한 범위 검색</li>
<li>아직 사원일자 (<code>hire_date</code>) 컬럼에 대한 인덱스는 없음</li>
</ul>
<pre><code class="language-sql">EXPLAIN SELECT * FROM employees WHERE hire_date &gt; &#39;2022-01-01&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/37f802f6-1d52-447e-8c07-4da5ad7a30f2/image.png" alt=""></p>
</br>

<h3 id="type-all-1"><code>type: ALL</code></h3>
<ul>
<li>풀 테이블 스캔 (Full Table Scan)</li>
<li>MySQL 이 해당 테이블의 모든 row 를 순차적으로 읽고 있다는 의미</li>
<li>조건 (<code>WHERE</code>) 이 인덱스와 무관하거나, 있어도 활용할 수 없는 등</li>
</ul>
</br>

<h3 id="key-null-1"><code>key: Null</code></h3>
<ul>
<li>사용된 인덱스의 이름을 나타내는 컬럼으로 해당 쿼리에 대한 어떠한 인덱스도 사용하지 않았다 라는 의미</li>
</ul>
</br>

<h3 id="extra-using-where"><code>Extra: Using where</code></h3>
<ul>
<li>조건을 통한 접근 방법을 사용하여 쿼리 실행</li>
<li>범위 검색 조건을 통한 쿼리이므로 당연하다</li>
</ul>
</br>

<p><strong>이제 사원일자 (<code>hire_date</code>) 컬럼에 대해 인덱스를 생성 후 범위 검색 실행</strong></p>
</br>

<hr>
<h2 id="인덱스를-통한-범위-검색">인덱스를 통한 범위 검색</h2>
<pre><code class="language-sql">CREATE index IX_employees_hire_date ON employees (hire_date);

EXPLAIN SELECT * FROM employees WHERE hire_date &gt; &#39;2022-01-01&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/77124447-ab83-4d8c-ad91-aea19a10e3d1/image.png" alt=""></p>
</br>

<p>이 실행 결과를 보면 여전히 <strong>풀 테이블 스캔 (Full Table Scan)</strong> 을 한 걸로 보여진다 </p>
<p>인덱스가 생성되지 않았나? </p>
<p>라고 생각할 수 있는데 <code>possible_keys = IX_employees_hire_date</code> 인 것을 보면 인덱스가 생성 되어 있고 키로 사용할 가능성이 있는 것으로 판단하고 있긴 하다</p>
</br>

<p><strong>왜 풀 테이블 스캔을 처리하는걸까?</strong></p>
<ul>
<li>MySQL 옵티마이저는 아래 여러 상황을 고려하여 최종 결정을 함</li>
<li>풀 테이블 스캔 vs 인덱스<ul>
<li>조건절 - 필터링 비율</li>
<li>커버링 인덱스 여부 - 인덱스만으로 해결 가능한가</li>
<li>읽어야할 ROW 수</li>
<li>I/O 비용</li>
<li>통계 정보 - 데이터의 실제 분포</li>
</ul>
</li>
</ul>
<p>여러가지 추측을 할 수 있는데 우선</p>
</br>

<ol>
<li>커버링 인덱스 <ul>
<li>인덱스를 사용하더라도 결국에는 모든 컬럼을 조회해야 하므로 데이터 ROW 를 읽어야 한다</li>
<li>MySQL 은 오히려 <strong>인덱스를 거치지 않고 바로 테이블을 읽는 것이 빠르다고</strong> 판단할 수 있다</li>
<li>인덱스 사용시<ul>
<li>인덱스 → 일치하는 ROW 찾기</li>
<li>다시 테이블 접근 (ROW lookup) → 비용 증가</li>
</ul>
</li>
<li>반면 풀 테이블 스캔 사용시<ul>
<li>그냥 처음부터 다 읽고 조건으로 필터링 → I/O 단순화</li>
</ul>
</li>
</ul>
</li>
<li>통계 정보 기반 <ul>
<li>MySQL 은 테이블 과 인덱스에 대해 통계 정보를 유지한다<ul>
<li>시스템 테이블 <code>information_schema.tables</code> / <code>information_schema.statistics</code> 을 통해 통계 정보를 볼 수 있다</li>
</ul>
</li>
<li>이걸 기반으로 실행 계획을 세우고, 예상되는 비용(cost)이 더 적은 쪽을 선택</li>
<li>하지만 이 통계 정보가 오래되었거나, 실제 데이터 분포와 다르다면,</li>
<li>풀 테이블 스캔이 더 빠르다고 오판할 수 있다 → 이럴땐 ex. <code>ANALYZE TABLE employees</code> 로 통계를 갱신하는 것도 방법</li>
</ul>
</li>
<li>순차 I/O vs 랜덤 I/O 차이로 인해 인덱스를 사용하지 않을 수 있음 <ul>
<li>풀 테이블 스캔 = 순차 I/O<ul>
<li>테이블은 디스크에서 <strong>데이터 페이지를 순서대로</strong> 읽을 수 있음 → 순차 I/O</li>
<li>디스크 또는 버퍼 풀에서 한 번에 블록 단위로 읽을 수 있으므로 매우 빠르다<ul>
<li>MySQL 은 read-ahead 를 통해 최적화 → 처음에는 페이지를 1개 씩 들고 오다가 나중에는 2개 씩, 4개 씩 데이터 페이지를 들고오는 방식</li>
</ul>
</li>
</ul>
</li>
<li>인덱스 사용 시 = 랜덤 I/O<ul>
<li>인덱스로 조건을 만족하는 ROW 의 주소를 찾은 후,</li>
<li>다시 테이블의 실제 데이터 위치를 랜덤하게 탐색 → 랜덤 I/O</li>
</ul>
</li>
</ul>
</li>
</ol>
</br>

<p>혹은 인덱스를 활용해도 전체 레코드의 너무 많은 부분을 탐색하는 경우</p>
<p>인덱스가 걸린 컬럼의 유니크 정도가 낮은 경우 등 (카디널리티가 작은 경우)</p>
</br>

<blockquote>
<p>참고 </p>
<p>커버링 인덱스 란 ?</p>
<ul>
<li>인덱스만 보고도 쿼리를 처리할 수 있는 상황을 말한다 </li>
</ul>
<p>즉, 테이블까지 랜덤 I/O 하지 않고도 인덱스만으로 결과를 낼 수 있는 경우 </p>
<p>예시로 사원번호(emp_no) 에 대한 인덱스를 걸고 조회할 때 사원번호만 조회한다면 </p>
<p><code>SELECT emp_no FROM employees WHERE emp_no &gt; 10</code></p>
<ul>
<li>인덱스에 필터 조건 + SELECT 대상 컬럼 모두 포함됨 </li>
<li>테이블 접근 생략 (랜덤 I/O X)</li>
<li>인덱스만으로 빠르게 처리 가능 </li>
</ul>
<p>추가로 PK 혹은 커버링 인덱스 접근 방식은 랜덤 I/O 를 줄이거나 순차 I/O 로 조회할 수 있다 
(DB 의 인덱스 구조, 디스크 형식에 따라 다를 수 있음) </p>
</blockquote>
</br>

<p>그러면 이제 사원(<code>employees</code>) 테이블에 10만건의 데이터를 넣고 실행해보자 </p>
<p>데이터를 더 추가해서 아주 데이터 많은 경우에도 인덱스를 사용하지 않고 풀 테이블 스캔을 하는지</p>
</br>

<p><strong>사원 데이터 60만건 추가</strong></p>
<pre><code class="language-sql">DELIMITER $$

DROP PROCEDURE IF EXISTS populate_more_employees$$

CREATE PROCEDURE populate_more_employees()
BEGIN
  DECLARE i INT DEFAULT 300001;

  WHILE i &lt;= 859999 DO
    INSERT INTO employees (
      emp_no, birth_date, first_name, last_name, gender, hire_date
    ) VALUES (
      i,
      DATE_ADD(&#39;1970-01-01&#39;, INTERVAL FLOOR(RAND() * 15000) DAY),
      CONCAT(&#39;First&#39;, i),
      CONCAT(&#39;Last&#39;, i),
      IF(RAND() &gt; 0.5, &#39;M&#39;, &#39;F&#39;),
      DATE_ADD(&#39;1995-01-01&#39;, INTERVAL FLOOR(RAND() * 10000) DAY)
    );
    SET i = i + 1;
  END WHILE;
END$$

DELIMITER ;

-- 실행
CALL populate_more_employees();</code></pre>
</br>

<pre><code class="language-sql">EXPLAIN SELECT * FROM employees WHERE hire_date &gt; &#39;2022-01-01&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/a1586a3b-3e9a-4b27-a5c3-06a350732bda/image.png" alt=""></p>
</br>

<h3 id="type-range"><code>type: range</code></h3>
<ul>
<li>인덱스 범위 스캔이 발생했다는 의미<ul>
<li>인덱스를 사용하여 특정 범위만 스캔한 경우</li>
</ul>
</li>
<li>아래 조건인 경우 발생<ul>
<li><code>&gt;</code> / <code>&lt;</code> / <code>BETWEEN</code> / <code>IN(…)</code> 등의 조건에서 발생</li>
</ul>
</li>
</ul>
</br>

<h3 id="key-ix_employees_hire_date"><code>key: IX_employees_hire_date</code></h3>
<ul>
<li>실제 사용된 인덱스</li>
<li>사원 일자(<code>hire_date</code>) 에 대해 생성한 인덱스를 활용</li>
</ul>
</br>

<h3 id="extra-using-index-condition"><code>Extra: Using index condition</code></h3>
<ul>
<li>ICP (Index Condition Pushdown) 이 적용되었다는 의미</li>
<li>스토리지 레벨에서 인덱스만 보고도 조건을 필터링함</li>
</ul>
</br>

<hr>
<h2 id="join">Join</h2>
<ul>
<li>각 사원이 속한 부서 조회 </li>
<li>부서 사원 테이블 (<code>dept_emp</code>) 테이블 과 사원 (<code>employees</code>) 를 Join</li>
</ul>
<pre><code class="language-sql">EXPLAIN SELECT * FROM dept_emp JOIN employees ON dept_emp.emp_no = employees.emp_no AND dept_emp.dept_no = &#39;D005&#39;;</code></pre>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/fa98e85f-9071-41e3-aa26-3aa30aa0960d/image.png" alt=""></p>
<h3 id="부서-사원-테이블-dept_emp">부서 사원 테이블 (<code>dept_emp</code>)</h3>
<ul>
<li><strong><code>type: ref</code></strong><ul>
<li>인덱스 컬럼을 사용한 비교에서 사용<ul>
<li>동등 비교 (<code>=</code>) 를 사용할때 보임</li>
<li>비 유니크 인덱스 나 외래키 등을 조건으로 여러 개의 ROW 를 가져오는 경우 사용 (PK, UNIQUE NOT NULL 아님)</li>
</ul>
</li>
</ul>
</li>
<li><strong><code>key: dept_no</code></strong><ul>
<li>실제 사용된 인덱스는 <code>dept_no</code> 인덱스</li>
</ul>
</li>
<li><strong><code>Extra: Using index condition</code></strong><ul>
<li>ICP 적용됨</li>
</ul>
</li>
</ul>
</br>

<h3 id="사원-테이블-employees">사원 테이블 (<code>employees</code>)</h3>
<ul>
<li><p><strong><code>type: eq_ref</code></strong></p>
<ul>
<li>테이블의 PK 혹은 UNIQUE NOT NULL 컬럼 과 동등 비교로 JOIN 되는 경우 사용<ul>
<li>인덱스를 통해 ROW 가 하나로 특정되는</li>
<li>즉, <code>PRIMAY_KEY</code> 조인을 의미하며 하나의 레코드만 매칭된다</li>
</ul>
</li>
</ul>
</li>
<li><p><strong><code>key: PRIMARY</code></strong></p>
<ul>
<li>employees 테이블의 PK 를 사용한 것</li>
</ul>
</li>
<li><p><strong><code>Extra: Null</code></strong></p>
<ul>
<li>추가 최적화가 없다는 의미</li>
</ul>
</br>

<p><strong>동작 방식</strong></p>
<ol>
<li>부서 사원 테이블에서 부서 번호를 통해 조회 및 필터링 (<code>Index lookup on dept_emp using dept_no</code>)<ul>
<li><img src="https://velog.velcdn.com/images/dlaudrb09-/post/d07bac97-e3c6-4543-b73c-24b7e10e87fb/image.png" alt=""></li>
<li>한 번의 루프로 쭉 순회를 하면서 34,218 개의 결과물을 가져옴 <ul>
<li>`dept_emp.dept_no=&#39;D005&#39; <strong>조건으로 인덱스 검색</strong></li>
<li><strong>사용 인덱스</strong>: <code>dept_no</code></li>
<li>cost: 9542 (예상 비용)</li>
<li><strong>rows=86994</strong>: 옵티마이저가 <strong>잘못 추정</strong>함 (실제는 34,218개인데 두 배로 추정)</li>
<li>actual time=0.838..69.6: 인덱스 시작 시간 ~ 종료 시간 (총 69ms)</li>
<li>rows=34218: 실제 결과 row 수</li>
<li>loops=1: 1회 인덱스 조회로 모든 조건 row 가져옴</li>
</ul>
</li>
<li><code>dept_emp</code> 테이블에서 <code>dept_no=&#39;D005&#39;</code> 조건을 인덱스로 빠르게 가져옴.</li>
</ul>
</li>
</ol>
</li>
</ul>
<ol start="2">
<li>사원 테이블에 대한 PK 조회 (<code>Single-row index lookup on employees using PRIMARY</code>)<ul>
<li><img src="https://velog.velcdn.com/images/dlaudrb09-/post/04edf89b-fe07-4083-b583-06dcf5811151/image.png" alt=""></li>
<li>PK 를 통한 참조를 하므로 한 번의 검색해서 하나의 결과를 가져온다  </li>
<li>위 동작과정을 앞에서 나왔던 결과를 기준으로 34,218 번의 반복 수행을 하게된다 </li>
<li>참고로 cost 는 MySQL 에서 버퍼 풀 (인메모리) 에 저장된 데이터 페이지에 한 번 접근할때 MySQL 이 추산하는 비용을 의미한다<ul>
<li>employees.emp_no로 PK(primary key) 인덱스 조회 (빠름)</li>
<li>각 emp_no마다 한 건씩 조회됨</li>
<li>cost=0.25: 한 건 조회 예상 비용</li>
<li><strong>actual time=0.00153..0.00155</strong>: 한 건 조회 평균 0.00002ms (20 ns 나노초)</li>
<li>rows=1: 1건씩만 나옴 (PK니까 당연)</li>
<li><strong>loops=34218</strong>: 위에서 나온 34,218건에 대해 <strong>반복 조회</strong></li>
</ul>
</li>
<li><code>dept_emp</code> 테이블에서 <code>dept_no=&#39;D005&#39;</code> 조건을 인덱스로 빠르게 가져옴.<ol start="3">
<li><code>Nested loop inner join</code></li>
</ol>
<ul>
<li>조인 방식: Nested Loop Join (중첩 반복)</li>
</ul>
</li>
<li>cost: MySQL 옵티마이저가 추정한 총 실행 비용</li>
<li>rows=86994: 추정 결과 건수 (조인 결과 예상)</li>
<li>actual time=2.6..714: 실제 실행 시간 (시작 ~ 끝까지 2.6ms ~ 714ms)</li>
<li>rows=64433: 실제 반환된 row 수</li>
<li>loops=1: 루프 1번 → 조인 전체를 대표하는 연산, 한 번만 실행되었다는 의미 (조인 연산을 한번 시작해서 한 세트 완성, 그 하위는 여러번 반복)</li>
</ul>
</li>
</ol>
</br>

<hr>
<h2 id="type">type</h2>
<p>위 컬럼에는 각 종류에 따라 성능에 최적화된 순서가 존재한다 </p>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/38e7545d-d500-43c8-a41c-fa934622cd97/image.png" alt=""></p>
<p>위 그림이 각 <code>type</code> 에 대한 종류인데 </p>
<p>아래로 갈수록 성능에 안좋은 영향이 생기는 <code>type</code> 이다 </p>
<ul>
<li><code>index</code>  와 <code>ALL</code> 에 대해서는 주의깊게 사용해야 한다</li>
</ul>
</br>

<h2 id="type-index"><code>type: index</code></h2>
<ul>
<li>인덱스 테이블 풀 스캔</li>
<li>인덱스를 사용하므로 말만 들으면 인덱스를 잘 활용하는 구나 라고 생각할 수 있다</li>
<li>그러나 아래 처럼 여러가지 경우에 따라 성능에 안좋은 영향을 끼친다!</li>
</ul>
</br>

<p><a href="https://velog.velcdn.com/images/dlaudrb09-/post/6d1c881e-fc0f-43b6-8a7a-0cb3e4515a89/image.png"></a></p>
<p>위 그림은 인덱스 (Non-Clustered) 테이블에 대한 풀 스캔을 의미한다 </p>
<p>인덱스 테이블 (Non-Clustered Leaf) 들을 순차적으로 쭉 순회한다 → 사실 이 행동 자체는 크게 문제되지 않는다 </p>
<p>그러나 인덱스 테이블 (Non-Clustered) 에 없는 컬럼을 조회한다고 한다면 실제 데이터 레코드까지 찾아가서 참조해야하는 경우 중간에 Disk I/O 가 발생한다</p>
<p>그러므로 인덱스 테이블 풀 스캔을 할때는 </p>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/850d88ec-1465-4ac1-977c-cce62bdf6ed2/image.png" alt=""></p>
<p>위 그림처럼 인덱스 테이블을 순차적으로 쭉 순회하지만 필터링 조건에 의해 몇개만 데이터 레코드까지 참조하는 경우 </p>
<p>혹은 </p>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/02bdcb30-1f1a-4271-8bd5-1de6f6bed831/image.png" alt=""></p>
<p>위에서 이야기했던 커버링 인덱스를 통해 실제 데이터 레코드까지 조회하지 않고 인덱스 테이블만으로 결과물을 도출할 수 있는 경우에는 Disk I/O 가 많이 발생하지 않으므로 괜찮은 성능을 보인다</p>
</br>

<hr>
<h3 id="참고---실행-계획-수립은-싼-작업이-아니다">참고 - 실행 계획 수립은 싼 작업이 아니다</h3>
<h3 id="mysql-은-수립한-실행-계획을-커넥션-내에서만-캐싱하며-재사용한다">MySQL 은 수립한 실행 계획을 커넥션 내에서만 캐싱하며 재사용한다</h3>
<p>그래서 다른 커넥션으로 똑같은 쿼리를 보내도 MySQL 은 새로운 실행 계획을 계산해야한다 </p>
<p>이 과정에서 옵티마이저는 직접 인덱스 몇개를 탐색해 샘플링하는 작업을 하게되는데 이를 Index Dive 라고 한다 </p>
<p>그렇지만 실제로 보내는 쿼리들은 조합이 아주 다양하고 많은 인덱스를 사용해야하는 경우에는 실행 계획을 수립하는 과정에서 굉장히 많은 비용이 소모된다 </p>
<p>심지어는 쿼리 자체를 수행하는 것 보다 실행 계획을 수립하는데 리소스가 더 많이 들기도 하는 경우가 있다 </p>
<p>혹은 </p>
<p>실행 계획 수립에는 할당된 시스템 메모리 비용이 정해져 있는데 해당 메모리를 초과해서 실행 계획을 수립하다가 실행 계획 자체를 포기하고 풀 테이블 스캔을 하기도 한다고 함</p>
</br>

<p>참조)</p>
<ul>
<li><a href="https://www.youtube.com/watch?v=usEsrsaSSuU">10분 테코톡 - 모디의 MySQL 의 실행계획</a></li>
<li><a href="https://jojoldu.tistory.com/474">향로 (기억보단 기록을) MySQL (MariaDB) 인덱스 컨디션 푸시다운</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAR vs WAR - 그리고 스프링 부트의 JAR 실행 구조]]></title>
            <link>https://velog.io/@dlaudrb09-/JAR-vs-WAR-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%9D%98-JAR-%EC%8B%A4%ED%96%89-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@dlaudrb09-/JAR-vs-WAR-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%9D%98-JAR-%EC%8B%A4%ED%96%89-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Thu, 27 Mar 2025 01:58:11 GMT</pubDate>
            <description><![CDATA[<p>이 글은 JAR과 WAR의 차이, 전통적인 웹 애플리케이션 배포 방식의 한계, 그리고 스프링 부트에서 JAR 방식이 어떻게 작동하는지까지 정리한 글입니다.</p>
<h2 id="jar-java-archive">JAR (Java Archive)</h2>
<ul>
<li>자바는 여러 클래스와 리소스를 묶어서 <code>JAR</code> (Java Archive) 라고 하는 압축 파일을 만들 수 있다</li>
<li>이 파일은 JVM 위에서 직접 실행되거나 또는 다른 곳에서 사용하는 라이브러리로 제공된다<ul>
<li><code>JAR</code> 파일안에 main 메서드가 있어서 직접 실행하거나 OR 다른 곳에서 라이브러리로 사용하거나</li>
<li><code>java -jar abc.jar</code> 이런식으로 명령어를 통해 실행한다</li>
<li>직접 실행하려면 위에서 이야기했듯 <code>main()</code> 메서드가 필요하고, <code>MANIFEST.MF</code> 파일에 실행할 메인 메서드가 있는 클래스를 지정해두어야 한다</li>
</ul>
</li>
<li>즉, 단순하게 생각하자면 <code>JAR</code> 는 클래스와 관련 리소스를 압축한 단순한 파일이다</li>
</ul>
</br>

<h2 id="war-web-application-archive">WAR (Web Application Archive)</h2>
<ul>
<li>WAR (Web Application Archive) 라는 이름에서 알 수 있듯 WAR 파일은 웹 애플리케이션 서버(WAS) 에 배포할 때 사용하는 파일이다</li>
<li>JAR 파일이 JVM 위에서 실행된다면, WAR 는 웹 애플리케이션 서버 위에서 실행된다<ul>
<li>당연히 웹 애플리케이션 서버는 자바 (JVM) 위에서 실행된다<img src="https://velog.velcdn.com/images/dlaudrb09-/post/14e049b8-58e3-4449-affe-70eeec6b536b/image.png" alt=""></li>
</ul>
</li>
<li>웹 애플리케이션 서버 위에서 실행되어야 하며, HTML 같은 정적 리소스와 클래스 파일을 모두 함께 포함하기 때문에 JAR 와 비교해서 구조가 더 복잡하다<ul>
<li>JAR 는 필요한 <code>.class</code> 파일 몇개만 있으면 되지만 WAR 는 위 파일이 모두 포함되어야 하며 WAR 구조를 지켜야 한다</li>
</ul>
</li>
</ul>
</br>

<p>WAR 구조 </p>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/56c499c6-f7d1-4b8e-8af2-e0e0b7492495/image.png" alt=""></p>
<ul>
<li>단순한 서블릿 하나와 index.html 이 포함된 파일을 <code>gradle</code> 을 통해 빌드<ul>
<li><code>war</code> 로 빌드함</li>
</ul>
</li>
<li><code>server-0.0.1-SNAPSHOT.war</code> 파일의 압축을 풀면 아래 3가지 파일이 나옴<ul>
<li><code>index.html</code> : 정적 리소스</li>
<li><code>META-INF</code> : 메인 메서드가 담긴 클래스에 대한 정보</li>
<li><code>WEB-INF</code> : 자바 클래스와 라이브러리, 설정 정보가 들어간다</li>
</ul>
</li>
<li>파일 트리 구조
<img src="https://velog.velcdn.com/images/dlaudrb09-/post/fbd8b86d-0632-46d3-bbe3-c7fcf303104a/image.png" alt=""></li>
<li><code>WEB-INF</code> 를 제외한 나머지 영역은 HTML, CSS 같은 정적 리소스가 사용되는 영역이다</li>
<li>내장 톰캣이 아닌 외부 톰캣을 설치 및 실행 후 해당 <code>war</code> 파일을 특정 톰캣 폴더에 옮기면 우리가 만든 자바 파일이 실행된다<ul>
<li>스프링 부트, 내장 톰캣을 사용하지 않았던 옛날에는 외부에서 톰캣을 설치해서 빌드된 <code>war</code> 파일을 이런식으로 실행하여 배포함</li>
</ul>
</li>
</ul>
</br>

<p>옛날 WAR 배포 방식의 단점 </p>
<p>옛날에는 웹 애플리케이션을 개발하고 배포하려면 다음과 같은 과정을 거쳐야 한다</p>
<ol>
<li>톰캣 같은 웹 애플리케이션 서버(WAS) 를 별도로 설치해야 한다 </li>
<li>애플리케이션 코드를 WAR 로 빌드해야 한다 </li>
<li>빌드한 WAR 파일을 WAS 에 배포해야 한다 </li>
</ol>
<ul>
<li>웹 애플리케이션을 실행하고 싶으면 웹 애플리케이션 서버를 별도로 설치해야하는 구조이다</li>
<li>과거에는 이렇게 웹 애플리케이션 서버와 웹 애플리케이션 빌드 파일(WAR) 이 분리되어 있는 것이 
당연한 구조였다</li>
</ul>
<p><strong>그렇지만 이 방식에는 단점이 있다</strong></p>
<h3 id="단점">단점</h3>
<ul>
<li>톰캣 같은 WAS 를 별도로 설치해야 한다</li>
<li>개발 환경 설정이 복잡하다<ul>
<li>단순 자바라면 <code>main()</code> 을 실행하면 되지만</li>
<li>웹 애플리케이션은 WAS 를 실행하고 WAR 를 연동하는 등 복잡하게 설정해야 한다</li>
</ul>
</li>
<li>배포 과정 또한 복잡하다<ul>
<li>WAR 로 빌드하고 WAS 를 설치하고 그 안에 WAR 를 넣어서 배포하는 등</li>
</ul>
</li>
<li>톰캣의 버전을 변경하려면 톰캣을 다시 설치해야 한다</li>
</ul>
<p>한가지 제안 </p>
<ul>
<li>단순히 자바의 <code>main()</code> 메서드만 실행하면 웹 서버까지 같이 실행되도록 하면 되지 않을까 ?</li>
<li>톰캣도 자바로 만들어져 있으니 톰캣을 하나의 라이브러리 처럼 포함해서 사용해도 되지 않을까 ?</li>
<li>이런 문제를 해결하기 위해 톰캣을 라이브러리로 제공하는 내장 톰캣 (embed tomcat) 기능을 제공한다
<img src="https://velog.velcdn.com/images/dlaudrb09-/post/99986389-c0b0-4307-9e38-6ed111953b9b/image.png" alt=""></li>
<li>왼쪽은 웹 애플리케이션 서버에 <code>WAR</code> 파일을 배포하는 방식, WAS 를 실행해서 동작한다 (옛날 방식)</li>
<li>오른쪽은 애플리케이션 <code>JAR</code> 안에 다양한 라이브러리들과 <code>WAS</code> 라이브러리가 포함되는 방식<ul>
<li><code>main()</code> 메서드를 실행해서 동작한다</li>
</ul>
</li>
</ul>
<p>내장 톰캣 라이브러리 </p>
<pre><code class="language-java">dependencies {
    implementation &#39;org.apache.tomcat.embed:tomcat-embed-core:10.1.5&#39;
}</code></pre>
<p>내장 톰캣 설정 </p>
<ul>
<li><p>내장 톰캣은 이런식으로 동작하는 구나 정도만 인지</p>
<pre><code class="language-java">public class EmbedTomcatServletMain {

  public static void main(String[] args) throws LifecycleException {
      System.out.println(&quot;EmbedTomcatServletMain.main&quot;);

      // 톰캣 설정
      Tomcat tomcat = new Tomcat();
      Connector connector = new Connector();
      connector.setPort(8080);
      tomcat.setConnector(connector);

      // 스프링 컨테이너 생성
      AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
      appContext.register(HelloConfig.class);

      // 스프링 MVC 디스패쳐 서블릿 생성, 스프링 컨테이너 연결
      DispatcherServlet dispatcher = new DispatcherServlet(appContext);

      // 디스패쳐 서블릿 등록
      Context context = tomcat.addContext(&quot;&quot;, &quot;/&quot;);
      tomcat.addServlet(&quot;&quot;, &quot;dispatcher&quot;, dispatcher);
      context.addServletMappingDecoded(&quot;/&quot;, &quot;dispatcher&quot;);
      tomcat.start();
  }
}</code></pre>
<p>이 덕분에 IDE 에 복잡한 톰캣 설정도 없어지고 <code>main()</code> 메서드만 실행하면 매우 편리하게 실행된다 </p>
</li>
</ul>
<p>물론 톰캣 서버를 설치하지 않아도 된다!</p>
<p>또한 스프링 부트에서 내장 톰캣 관련된 부분을 거의 자동화 및 제공하므로 내장 톰캣을 다룰일이 거의 없다</p>
<p>그럼 이제 빌드 및 배포에 대해 알아봐야 하는데 먼저 우리는 일반적인 자바 파일의 빌드에 대해 알아보자 </p>
</br>

<h3 id="자바-빌드">자바 빌드</h3>
<ul>
<li>위에서도 이야기했듯이 자바는 <code>main()</code> 메서드안에서 코드를 작성하며 실행하기 위해서는 <code>jar</code> 형식으로 빌드해야 한다</li>
<li>그리고 <code>jar</code> 안에는 <code>META-INF/MANIFEST.MF</code> 파일에 실행할 <code>main()</code> 메서드의 클래스를 지정해주어야 한다<pre><code class="language-java">Manifest-Version: 1.0
Main-Class: hello.start.GongzaMainClass</code></pre>
</li>
</ul>
<p>그럼 이제 내장 톰캣을 통해 스프링 컨테이너를 구성한 프로젝트를 jar 로 빌드해보자 </p>
</br>

<h3 id="내장-톰캣-프로젝트-빌드">내장 톰캣 프로젝트 빌드</h3>
<ul>
<li><code>./gradlew clean buildJar</code> gradle 을 통해 jar 로 빌드해보자</li>
<li>이후 <code>./build/lib</code> 폴더 내부에 <code>embed-0.0.1-SNAPSHOT.jar</code> 파일이 생겼다</li>
<li>이를 명령어를 통해 실행해보면 !<ul>
<li><code>java -jar embed-0.0.1-SNAPSHOT.jar</code></li>
</ul>
</li>
<li>실행이 되지 않는다 !<pre><code class="language-java">Error: Unable to initialize main class hello.embed.EmbedTomcatSpringMain
Caused by: java.lang.NoClassDefFoundError: org/springframework/web/context/WebApplicationContext</code></pre>
</li>
<li>에러 내용은 <code>springframework</code> 가 존재하지 않아서 발생하는 에러이다</li>
<li><strong>분명히 스프링, 톰캣 모두 gradle 에 포함해서 빌드했지만 결과는 라이브러리 파일이 모두 사라지고 현재 프로젝트에서 내가 구성한 자바 파일만 빌드되어 있다</strong>
<img src="https://velog.velcdn.com/images/dlaudrb09-/post/75612aa7-54bc-4b1b-bf4f-1ee17149b0e6/image.png" alt=""></li>
<li>위에서 WAR 로 빌드했을 때는 라이브러리까지 포함되어서 빌드가 되었지만 jar 로 빌드하게 되면 라이브러리를 포함하지 않고 우리가 구성한 파일만 빌드하게 된다 </li>
</ul>
</br>

<h3 id="jar-파일은-jar-파일을-포함할-수-없다">JAR 파일은 JAR 파일을 포함할 수 없다</h3>
<ul>
<li>WAR 와 다르게 JAR 파일은 내부에 라이브러리 역할을 하는 JAR 파일을 포함할 수 없다<ul>
<li>포함한다고 해도 인식이 안된다</li>
</ul>
</li>
<li>이것이 JAR 파일 스펙의 한계이다</li>
<li>그렇다고 WAR 를 사용할 수도 없다, WAR 는 웹 애플리케이션 서버 (WAS) 위에서만 실행할 수 있다<ul>
<li>여태까지 내장 톰캣을 통해 기껏 개발을 했는데 다시 톰캣을 따로 설치해서 그위에서 실행하는 WAR 로 돌아가야하는가 ?</li>
</ul>
</li>
</ul>
</br>

<h3 id="fatjar">FatJar</h3>
<ul>
<li>이러한 문제에 대한 대안으로 <code>fat jar</code> 또는 <code>uber jar</code> 라고 불리는 방법이 있다</li>
<li>이름그대로 “뚱뚱한 JAR”</li>
<li>역시 JAR 안에는 JAR 를 포함할 수 없다 이는 파일 스펙의 한계이다</li>
<li>그러나 ! 클래스 파일은 얼마든지 포함할 수 있다</li>
<li>라이브러리에 사용되는 JAR 파일의 압축을 풀면 <code>.class</code> 파일이 나온다 이를 다시 우리 프로젝트와 합쳐서 새로운 JAR 파일을 만드는 방식</li>
<li>이렇게 하면 수 많은 라이브러리에서 나오는 <code>.class</code> 때문에 뚱뚱한(Fat) JAR 가 탄생한다 그래서 Fat Jar 라고 부른다</li>
</ul>
<p>빌드후 실행하면 정상동작한다</p>
<pre><code class="language-shell">-rw-r--r--  1 imyeong-gyu  staff    10M Jan 19 18:48 embed-0.0.1-SNAPSHOT.jar</code></pre>
<p>그러나 파일 크기가 10 메가 이다..</p>
<p>JAR 파일 압축을 풀면
<img src="https://velog.velcdn.com/images/dlaudrb09-/post/01c85868-4532-4425-a33d-e911d78fe5d5/image.png" alt=""></p>
<pre><code class="language-shell">         ...
                    ├── SeparatorPathElement.class
                    ├── SingleCharWildcardedPathElement.class
                    ├── WildcardPathElement.class
                    ├── WildcardTheRestPathElement.class
                    └── package-info.class

361 directories, 6017 files</code></pre>
<ul>
<li><code>jakarta</code> , <code>org</code> 등 라이브러리에 있는 자바파일도 포함되어 있으며 361개의 폴더와 6071 파일이 존재하게 된다 </li>
</ul>
</br>

<h3 id="fat-jar-단점">Fat Jar 단점</h3>
<ul>
<li>어떤 라이브러리가 포함되어 있는지 확인하기 어렵다<ul>
<li>모두 <code>.class</code> 파일로 풀려있으니 확인이 어렵다</li>
</ul>
</li>
<li>파일명 중복을 해결하기가 어렵다<ul>
<li>두 개의 라이브러리가 존재할때 두 라이브러리에 같은 이름의 파일명이 존재한다면<ul>
<li>ex. <code>META-INF</code> 파일이 두 라이브러리에 모두 포함된 경우</li>
</ul>
</li>
<li>충돌이 발생하며 두 라이브러리가 가지고 있는 파일 중 하나만 선택된다</li>
<li>결과적으로 나머지 하나는 포함되지 않으므로 정상 동작하지 않는다</li>
</ul>
</li>
</ul>
<h4 id="이런-빌드-및-배포와-관련된-단점과-문제를-스프링-부트가-해결했다-">이런 빌드 및 배포와 관련된 단점과 문제를 스프링 부트가 해결했다 !</h4>
</br>

<h2 id="스프링-부트---빌드">스프링 부트 - 빌드</h2>
<ul>
<li>단순히 스프링 부트 프로젝트를 만들어서 <code>./gradlew clean build</code> 를 통해 빌드 후</li>
<li><code>./build/lib/~~-SNAPSHOT.jar</code> 파일을 <code>java -jar</code> 를 통해 실행하면 정상 동작한다</li>
<li><code>SNAPSHOT.jar</code> 파일을 보면 크기가 18M 이다
```shell</li>
<li>rw-r--r--   1 imyeong-gyu  staff    18M Jan 19 19:28 boot-0.0.1-SNAPSHOT.jar
```</li>
<li>예상하기로 Fat JAR 형식으로 빌드한 건가 싶기도 하다</li>
<li>JAR 파일 스펙상 다른 JAR 파일을 넣을 수 없으니</li>
<li>JAR 파일의 압축을 풀면 크게 세 가지 파일이 존재한다
<img src="https://velog.velcdn.com/images/dlaudrb09-/post/0fb1aaef-ba7c-4424-a026-de3ea2a53be4/image.png" alt=""></li>
<li><code>BOOT-INF</code>  / <code>META-INF</code> / <code>org</code> 크게 3가지 파일이 존재한다<ul>
<li><code>plan.jar</code> 파일은 단순히 현재 프로젝트에서 라이브러리 파일이 포함되지 않은 JAR 파일</li>
</ul>
</li>
<li>또한 신기한 점은 <code>BOOT-INF</code> 폴더 안에 <code>lib</code> 라는 라이브러리 파일이 존재하는데 이 파일은 JAR 파일이다
<img src="https://velog.velcdn.com/images/dlaudrb09-/post/9b7cea29-1ad5-44cf-8c19-130e9453a1d6/image.png" alt=""></li>
<li>JAR 파일에는 JAR 파일이 포함될 수 없는데…?</li>
</ul>
<p>이제 각각의 파일들을 설명해보자</p>
</br>

<h2 id="meta-inf">META-INF</h2>
<ul>
<li>여기에는 단순히 <code>MENIFEST.MF</code> 파일만 존재하게 된다</li>
<li>스프링 부트가 처음시작할때 실행해야할 클래스 정보</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/9c503e22-cdce-4430-8f7a-6bf95b8ce3bf/image.png" alt=""></p>
<pre><code class="language-java">Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: hello.boot.BootApplication
Spring-Boot-Version: 3.0.2
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Build-Jdk-Spec: 17</code></pre>
</br>

<h2 id="org-파일">org 파일</h2>
<ul>
<li><code>org</code> 파일 안에는 스프링 부트의 처음 main 클래스인 <code>JarLauncher</code> 파일이 존재하게 된다<ul>
<li><code>org/springframework/boot/loader</code>
<img src="https://velog.velcdn.com/images/dlaudrb09-/post/dfccfe0a-8c36-4f6a-bb8c-72b297a7c856/image.png" alt=""></li>
</ul>
</li>
</ul>
<h2 id="boot-inf">BOOT-INF</h2>
<ul>
<li>안에는 <code>classes</code> 파일과 <code>lib</code> 라는 파일이 존재한다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/1b17a1e9-7aff-4f82-9437-5e3f243f94b0/image.png" alt=""></p>
<ul>
<li><code>classes</code> 파일<ul>
<li>우리가 개발한 class 파일과 리소스 파일</li>
<li>ex. <code>BootApplication.class</code> , <code>HelloController</code> , <code>HelloService</code> 등</li>
</ul>
</li>
<li><code>lib</code> 파일<ul>
<li>외부 라이브러리 파일이 존재한다</li>
<li><code>spring-webmvc-6.0.4.jar</code> , <code>tomcat-embed-core-10.1.5.jar</code> 등</li>
</ul>
</li>
<li>그 외에는 경로값<ul>
<li><code>classpath.idx</code> : 외부 라이브러리 경로</li>
<li><code>layers.idx</code> : 스프링 부트 구조 경로</li>
</ul>
</li>
</ul>
<p>핵심은 JAR 를 푼 결과가 Fat JAR 가 아니라 처음보는 새로운 구조로 만들어져 있다 </p>
<p>심지어 JAR 내부에 JAR 를 담아서 인식하는 것이 불가능한데, JAR 가 포함되어 있고 인식까지 되었다</p>
<h2 id="스프링-부트-실행-가능-jar">스프링 부트 실행 가능 JAR</h2>
<ul>
<li>Fat Jar 는 위에서 이야기한 단점들이 존재하게 된다 그러므로 스프링 부트에서는 Fat Jar 와 같은 방식을 사용하기 어렵다</li>
</ul>
<h3 id="실행-가능-jar">실행 가능 JAR</h3>
<ul>
<li>스프링 부트는 이러한 문제를 해결하기 위해 JAR 내부에 JAR 를 포함할 수 있는 특별한 구조의 JAR 를 만들었다</li>
<li>또한 JAR 를 내부에 포함해서 실행할 수 있게 까지 가능하게 만들었다</li>
<li>이것을 “실행 가능 JAR (Executable JAR)” 라고 한다</li>
</ul>
<p>이를 통해 문제를 해결하게 된다 </p>
<ul>
<li>어떤 라이브러리가 포함되어 있는지 확인하기 어렵다<ul>
<li>JAR 내부에 JAR 를 포함하기 때문에 어떤 라이브러리가 포함되었는지 쉽게 확인할 수 있다</li>
</ul>
</li>
<li>파일명 중복을 해결할 수 없다<ul>
<li>마찬가지로 JAR 를 통해 라이브러리 <code>.class</code> 파일이 묶여있기 때문에 내부에 같은 경로의 파일이 존재하여도 둘다 인식이 가능하다</li>
</ul>
</li>
</ul>
<p>참고로 실행 가능 JAR 는 자바 표준이 아닌 스프링 부트에서 새롭게 정의한 것이다 </p>
<h2 id="실행-가능-jar-동작-과정">실행 가능 JAR 동작 과정</h2>
<ul>
<li><code>java -jar xxx.jar</code>  를 통해 실행하게 되면 우선 <code>META-INF/MANIFEST.MF</code> 파일을 찾는다</li>
<li>그리고 여기에 있는 <code>Main-Class</code> 를 읽어서 <code>main()</code> 메서드를 실행하게 된다</li>
</ul>
<p>스프링 부트가 만든 <code>MANIFEST.MF</code></p>
<pre><code class="language-java">Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: hello.boot.BootApplication
Spring-Boot-Version: 3.0.2
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Build-Jdk-Spec: 17</code></pre>
<ul>
<li><p>파일을 보면 우리가 만든 메인 클래스 (<code>BootApplication</code>) 파일이 아닌 다른 파일이 메인 클래스로 등록되어 있다</p>
<ul>
<li><code>JarLauncher</code> 파일이 <code>Main-Class</code> 파일로 지정되어 있음</li>
</ul>
</li>
<li><p>즉, 스프링 부트가 실행되면 <code>JarLauncher</code> 가 실행된다</p>
</li>
<li><p>스프링부트는 JAR 내부에 JAR 를 읽어들이는 기능이 필요하다, 또 특별한 구조에 맞게 클래스 정보도 읽어들어야 한다</p>
</li>
<li><p>바로 <code>JarLauncher</code> 가 이런일을 처리해준다</p>
</li>
<li><p>이런 작업이 처리된 이후 <code>Start-Class:</code> 에 지정된 우리가 만든 메인 클래스의 <code>main()</code> 을 호출한다</p>
<ul>
<li>JVM → <code>JarLauncher</code> → 우리가 만든 메인 클래스 <code>main()</code></li>
</ul>
<h4 id="전체-흐름-구조">전체 흐름 구조</h4>
<pre><code>JVM
└─&gt; JarLauncher (Main-Class)
     ├─&gt; BOOT-INF/lib/*.jar 로드
     ├─&gt; BOOT-INF/classes 로드
     └─&gt; Start-Class 의 main() 실행</code></pre></li>
</ul>
<h4 id="또한-스프링-부트는-내장-톰캣을-이미-라이브러이에-포함되어-실행되어진다">또한 스프링 부트는 내장 톰캣을 이미 라이브러이에 포함되어 실행되어진다</h4>
<ul>
<li>내장 톰캣에 대한 사용또한 위 예시와 마찬가지로 내장 톰캣 설정을 통해 이루어진다
<img src="https://velog.velcdn.com/images/dlaudrb09-/post/2f59e92c-7bbe-44ae-8aa0-681db314c5d9/image.png" alt=""></li>
</ul>
<h3 id="그럼-라이브러리-jar-파일은-어떻게-실행하는가-">그럼 라이브러리 JAR 파일은 어떻게 실행하는가 ?</h3>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/4b5e228a-fb29-4e6e-b2d3-37f3fe91c88d/image.png" alt=""></p>
<ul>
<li><code>BOOT-INF</code> 폴더 안에 보면 <code>classpath.idx</code> 파일이 존재하는 걸 볼 수 있다</li>
<li>해당 파일은 <code>lib</code> 파일에 대한 클래스 파일 경로 정보 이다</li>
<li>즉 외부 라이브러리의 클래스 파일 경로 정보가 <code>classpath.idx</code> 파일에 담겨있다</li>
<li>또한 아래 <code>MANIFEST.MF</code> 파일을 보면 <code>Spring-Boot-Classpath-Index</code> 의 값에 <code>classpath</code> 파일의 경로가 포함되어 있다</li>
<li>클래스 로더는 해당 <code>MANIFEST.MF</code> 파일을 읽어들이고 <code>Spring-Boot-Classpath-Index</code> 의 값을 읽어 외부 라이브러리 Jar 파일을 로더 할 수 있게 된다<pre><code class="language-java">Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: hello.boot.BootApplication
Spring-Boot-Version: 3.0.2
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Build-Jdk-Spec: 17</code></pre>
</br>

</li>
</ul>
<h2 id="결론">결론</h2>
<p>스프링 부트는 복잡한 WAR 구조와 배포를 간소화하기 위해 실행 가능한 JAR 구조를 도입했고,
이로써 내장 톰캣, 라이브러리 포함, 독립 실행까지 모두 지원하는 환경을 만들었습니다.</p>
<blockquote>
<p>참고: 실행 가능한 JAR 구조는 자바 표준은 아니며 스프링 부트가 자체 정의한 구조입니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[문제로 알아보는 오버로딩 과 오버라이딩 (동적, 정적 디스패치)]]></title>
            <link>https://velog.io/@dlaudrb09-/%EB%AC%B8%EC%A0%9C%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%EC%98%A4%EB%B2%84%EB%A1%9C%EB%94%A9-%EA%B3%BC-%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%94%A9-%EB%8F%99%EC%A0%81-%EC%A0%95%EC%A0%81-%EB%94%94%EC%8A%A4%ED%8C%A8%EC%B9%98</link>
            <guid>https://velog.io/@dlaudrb09-/%EB%AC%B8%EC%A0%9C%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%EC%98%A4%EB%B2%84%EB%A1%9C%EB%94%A9-%EA%B3%BC-%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%94%A9-%EB%8F%99%EC%A0%81-%EC%A0%95%EC%A0%81-%EB%94%94%EC%8A%A4%ED%8C%A8%EC%B9%98</guid>
            <pubDate>Tue, 12 Nov 2024 09:30:09 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-1">문제 1.</h2>
<pre><code class="language-java">public class StaticDispatch {
        static abstract class Human {
        }

        static class Man extends Human {
        }

        static class Woman extends Human {
        }

        // 오버로딩된 메서드 (1)
        public void sayHello(Human guy) {
                System.out.println(&quot;Hello, guy!&quot;);
        }

        // 오버로딩된 메서드 (2)
        public void sayHello(Man guy) {
                System.out.println(&quot;Hello, gentleman!&quot;);
        }

        // 오버로딩된 메서드 (3)
        public void sayHello(Woman guy) {
                System.out.println(&quot;Hello, lady!&quot;);
        }

        public static void main(String[] args) {
                Human man = new Man();
                Human woman = new Woman();
                StaticDispatch sr = new StaticDispatch();

                sr.sayHello(man);
                sr.sayHello(woman);
        }
}</code></pre>
</br>

<h2 id="문제-2">문제 2.</h2>
<pre><code class="language-java">public class DynamicDispatch {
        static abstract class Human {
                protected abstract void sayHello();
        }

        static class Man extends Human {
                @Override
                protected void sayHello() {
                        System.out.println(&quot;Man said hello&quot;);
                }
        }

        static class Woman extends Human {
                @Override
                protected void sayHello() {
                        System.out.println(&quot;Woman said hello&quot;);
                }
        }

        public static void main(String[] args) {
                Human man = new Man();
                Human woman = new Woman();

                man.sayHello();
                woman.sayHello();

                man = new Woman();
                man.sayHello();
        }
}</code></pre>
</br>

<h2 id="문제-3">문제 3.</h2>
<pre><code class="language-java">import java.io.Serializable;

public class Overload {
        public static void sayHello(Object arg) {
                System.out.println(&quot;Hello Object&quot;);
        }

        public static void sayHello(int arg) {
                System.out.println(&quot;Hello int&quot;);
        }

        public static void sayHello(long arg) {
                System.out.println(&quot;Hello long&quot;);
        }

        public static void sayHello(Character arg) {
                System.out.println(&quot;Hello Character&quot;);
        }

        public static void sayHello(char arg) {
                System.out.println(&quot;Hello char&quot;);
        }

        public static void sayHello(char... arg) {
                System.out.println(&quot;Hello char ...&quot;);
        }

        public static void sayHello(Serializable arg) {
                System.out.println(&quot;Hello Serializable&quot;);
        }

        public static void main(String[] args) {
                sayHello(&#39;a&#39;);
        }
}</code></pre>
<ul>
<li>Q. <code>sayHello(char arg)</code> 메서드 주석처리시 어떻게 되는가 ?</li>
<li>Q.<code>sayHello(int arg)</code> 메서드 주석처리시 어떻게 되는가 ?</li>
<li>Q.<code>sayHello(long arg)</code> 메서드 주석처리시 어떻게 되는가 ?</li>
<li>Q. <code>sayHello(Character arg)</code> 메서드 주석처리시 어떻게 되는가 ?</li>
<li>Q. 만약 <code>sayHello(Comparable&lt;Character&gt; arg)</code> 메서드가 같이 있다면 ?</li>
<li>Q. <code>sayHello(Serializable arg)</code> 메서드 주석처리시 어떻게 되는가 ?</li>
<li>Q. <code>sayHello(Object arg)</code> 메서드 주석처리시 어떻게 되는가 ?</li>
</ul>
</br>

<h2 id="문제-4">문제 4.</h2>
<pre><code class="language-java">public class FieldHasNoPolymorphic {
        static class Father {
                public int money = 1;

                public Father() {
                        money = 2;
                        showMeTheMoney();
                }

                public void showMeTheMoney() {
                        System.out.println(&quot;I am a Father, I have $&quot; + money);
                }
        }

        static class Son extends Father {
                public int money = 3;

                public Son() {
                        money = 4;
                        showMeTheMoney();
                }

                public void showMeTheMoney() {
                        System.out.println(&quot;I am a Son, I have $&quot; + money);
                }
        }

        public static void main(String[] args) {
                Father guy = new Son();
                System.out.println(&quot;This guy has $&quot; + guy.money);
        }
}</code></pre>
</br>

<h1 id="문제-해석-전-사전-지식">문제 해석 전 사전 지식</h1>
</br>
</br>

<h2 id="자바-소스-코드">자바 소스 코드</h2>
<ul>
<li>자바 소스 코드는 <code>javac</code> 라는 컴파일러를 거치고 나면 자바 바이트 코드가 된다</li>
<li>바이트 코드 (<code>*.class</code>) → 이후 JVM 위에서 실행됨</li>
<li>자바 바이트 코드는 JVM 이 설치된 곳이 어디든 JVM 만 설치되어 있으면 실행됨 → 플랫폼 독립적</li>
</ul>
</br>

<h2 id="자바-바이트-코드">자바 바이트 코드</h2>
<p>클래스 파일 구조 </p>
<pre><code>ClassFile {
        u4                 magic;
        u2                 minor_version;
        u2                 major_version;
        u2                 constant_pool_count;
        ...
        u2                 fields_count;
        field_info         fields[fields_count];
        u2                 methods_count;
        method_info        methods[methods_count];
        u2                 attributes_count;
        attribute_info     attributes[attributes_count];
        ...
}</code></pre><ul>
<li><p>클래스 파일의 구조는 부호 없는 숫자 와 테이블로 구성되어 있다</p>
<ul>
<li>부호없는 숫자 : <code>u1</code> (1 바이트) , <code>u2</code> (2 바이트) , <code>u4</code> (4 바이트) , <code>u8</code> (8 바이트)</li>
<li>테이블 : 클래스 파일 구조 형태 → 테이블 이름은 관례적으로 <code>_info</code> 로 끝남</li>
</ul>
</br>

<p>클래스 파일을 십육진수 편집기 (HxD) 로 열어본 모습 </p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/6140b25c-97e8-4b06-9c17-21cf66e12583/image.png" alt=""></p>
<ul>
<li>자바 코드를 <code>javac</code> 로 컴파일할 때는 C, C++ 와 달리 링크 단계가 없다<ul>
<li>C, C++ 링크 단계 : 컴파일러가 생성한 obj (목적) 파일은 라이브러리 (Library) 와 연결하여 실행 파일(exe) 로 생성하는데 이 과정을 링크라고 함</li>
<li>동적링크 즉, 런타임에서의 링크단계는 존재함 </li>
</ul>
</li>
<li>자바에서 링크는 JVM 이 클래스 파일을 로그할 때 동적으로 이루어진다<ul>
<li>필드와 메서드가 메모리에서 어떤 구조로 표현되는가에 관한 정보는 클래스 파일에 저장되지 않는다는 뜻.</li>
<li>따라서 JVM 이 필드와 메서드의 참조를 런타임에 실제 심벌 참조 로 변환하지 않으면 각 항목의 실제 메모리 위치를 알 수 없다<ul>
<li>심벌 참조 : 몇가지 정해진 심벌로 참조 대상을 정함. 특정 리터럴 형태 <ul>
<li>직접 참조 : 오프셋 혹은 대상의 위치를 가리키는 핸들 </li>
<li>필드 테이블</li>
</ul>
</li>
</ul>
</li>
<li>클래스 변수와 인스턴스 변수를 뜻함</li>
<li>메서드안에 선언된 지역 변수는 필드가 아님 → 스택 프레임안에 로컬 변수 테이블에 있음 </li>
</ul>
</li>
<li>메서드 테이블 <ul>
<li>메서드 메타 데이터 (정의) 정보들이 들어있음</li>
<li>메서드 본문의 코드는 여기에 있지않고 메서드 속성 테이블 컬렉션의 Code 속성에 따로 저장된다 </li>
</ul>
</li>
</ul>
<pre><code>Code_attribute {
    u2            attribute_name_index;
    ...
    u4            code_length;
    u1.           code[code_length];  // 바이트 코드 명령어들 (명령어 각각이 1바이트임)
}</code></pre></br>

<h2 id="클래스-로더-준비">클래스 로더 준비</h2>
<ul>
<li><p>준비</p>
<ul>
<li><p>준비는 클래스 변수를 메모리에 할당하고 초기값을 설정하는 단계이다</p>
<ul>
<li>정적 변수</li>
<li>클래스 변수가 메서드 영역에 존재한다 라는 개념은 논리적으로 그렇다는 의미이며 JDK 8 부터는 클래스 변수가 클래스 객체와 함께 자바 힙에 저장된다</li>
</ul>
</li>
<li><p>첫째. 인스턴스 변수가 아닌 클래스 변수만 할당된다. 인스턴스 변수는 객체가 인스턴스화할 때 객체와 함께 자바 힙에 할당된다.</p>
</li>
<li><p>둘째. 준비 단계에서 클래스 변수에 할당하는 초기값은 해당 데이터 타입의 제로 값이다.</p>
<ul>
<li>public static int value = 123; → 준비 단계를 마친 직후 value 변수에 할당되어 있는 초기값은 123 이 아닌 0 이다.</li>
<li>준비 단계에서는 어떠한 자바 메서드도 아직 실행되지 않은 상태이기 때문이다 → 클래스 초기화 단계에서 실행 </li>
<li>123 을 할당하는 putstatic 명령어는 클래스 생성자인 <clinit>() 메서드에 포함됨 </li>
</ul>
</li>
<li><p>셋째. 지역변수는 준비단계가 없다. 즉 아래 int a 가 컴파일되지 않는 것은 시스템 초기값 (0) 이 할당되지 않는 것이다 </p>
<p>public static void main(String[] args) {
  int a;  // 컴파일 에러 발생 
  System.out.println(a);
}</p>
</li>
</ul>
</li>
</ul>
</br>

<h2 id="클래스-로더-초기화">클래스 로더 초기화</h2>
<ul>
<li>초기화 <ul>
<li>클래스 로더의 마지막 단계.</li>
<li>초기화 단계에서 초기화 되지 않은 것들은 모두 초기화를 거친다</li>
<li>new 키워드로 인스턴스 생성</li>
<li>클래스를 초기화할 때 상위 클래스가 초기화되어 있지 않다면 상위 클래스 초기화</li>
<li>총 6가지의 경우가 있음 (초기화를 능동적으로 실행하는 6가지 시나리오 → 능동참조)</li>
<li>앞서 준비 단계에서 모든 변수에 시스템이 정의한 초기값인 0을 할당했다, 초기화 단계에서는 개발자가 직접 기술한대로 초기화를 한다 → <code>&lt;clinit&gt;()</code> 메서드를 실행하는 단계<ul>
<li>부모 클래스의 생서자를 명시적으로 호출하지 않아도 자바 가상 머신은 하위 클래스의 <clinit>() 가 실행되기 전에 부모 클래스의 <clinit>() 부터 실행한다</li>
<li>그러므로 첫번째 <clinit>() 는 바로 java.lang.Object 의 <clinit>() 이다 </li>
</ul>
</li>
</ul>
</li>
<li>로딩 <ul>
<li>JVM 은 이름을 보고 해당 클래스를 정의하는 바이너리 바이트 스트림을 가져옴<ul>
<li>이 규칙들은 JVM 명세에서 구체적으로 어떻게 하라는 구체적인 룰이 명시되어 있지 않음</li>
<li>완전한 이름 (= 패키지 명을 포함한 클래스의 전체 이름) (Fully Qualified Name = FQN) </li>
</ul>
</li>
<li>바이트 스트림으로 표현된 정적인 저장 구조를 메서드 영역에서 사용하는 런타임 데이터 구조로 변환 </li>
<li>로딩 대상 클래스를 표현하는 java.lang.Class 객체를 힙 메모리에 생성한다 </li>
<li>이 Class 객체는 애플리케이션이 메서드 영역에 저장된 타입 데이터를 활용할 수 있게 하는 통로가 된다 </li>
</ul>
</li>
</ul>
  </br>


<h2 id="동적-링크">동적 링크</h2>
<ul>
<li><p>런타임 상수 풀 </p>
<ul>
<li>런타임 상수 풀은 메서드 영역의 일부이다 </li>
<li>상수 풀 테이블에는 클래스 버전, 필드, 메서드, 인터페이스 등 클래스 파일에 포함된 설명 정보에 더해 컴파일 타임에 생성된 다양한 리터럴과 심벌참조가 저장된다</li>
<li>JVM 이 클래스를 로드할 때 이러한 정보를 메서드 영역의 런타임 상수 풀에 저장한다 </li>
</ul>
</li>
<li><p>동적 링크</p>
<ul>
<li>메서드에서 이용하는 외부 객체를 가리키는 참조는 런타임 상수 풀에 담겨 있으며 각 메서드의 스택 프레임에서 런타임 상수 풀 내의 원소를 참조하는 식으로 구성된다 </li>
<li>이 참조가 동적 링크를 가능하게 하는 매개다</li>
</ul>
</br>


</li>
</ul>
<h2 id="메서드-호출">메서드 호출</h2>
<ul>
<li><p>메서드 호출은 메서드 본문 코드를 실행하는 일과 다르다</p>
</li>
<li><p>메서드 호출 단계에서 수행하는 유일한 일은 호출할 메서드의 버전을 선택하는 것이다 </p>
<ul>
<li>상속 구조에서 같은 메서드가 2개 존재할때 이를 총 2개의 버전이 있다고 이야기한다 → 어디에 정의된 메서드를 실행해야하는지 “해석” 해야한다 </li>
</ul>
</li>
<li><p>링킹 단계가 존재하지 않기 때문에 클래스 파일에 저장된 모든 메서드 호출은 심벌 참조일 뿐 메서드 주소를 담은 직접 참조가 아니다</p>
</li>
<li><p>그러나 그 중 일부는 직접 참조로 변환하는데 즉, 컴파일러가 프로그램 코드를 컴파일하는 시점에 호출 대상이 정해진다 </p>
<ul>
<li>이 처럼 호출 대상이 미리 특정되는 경우를 정적 해석이라고 한다 </li>
<li>주로 정적 메서드와 private 메서드, final 인스턴스 메서드다.</li>
<li>정적 메서드는 특정 클래스에 고정되어 있고, private 메서드는 인스턴스 바깥에서는 접근할 수 없다</li>
<li>final 인스턴스 메서드는 오버라이딩이 불가능하므로 다른 버전이 만들어질 가능성이 없다</li>
<li>따라서 두 유형의 메서드 모두 상속등을 통해 다른 버전을 만들 수 없으므로 클래스 로딩 단계에서 정적 해석을 한다 → 바이트 코드 (명령어가 다름) <code>invokestatic</code> , <code>invokespecial</code></li>
<li>정적 해석을 해야하는 메서드 외의 메서드들은 가상 메서드라고 한다</li>
</ul>
</br>


</li>
</ul>
<h2 id="디스패치">디스패치</h2>
<ul>
<li>한편 메서드 호출의 또 다른 형태로 디스패치가 있다</li>
<li>정적일 수도 있고 동적일 수도 있고 단일 디스패치 일수도 있고 다중 디스패치 일 수도 있다</li>
<li>총 네가지 유형이 생겨난다<ul>
<li>정적 단일 디스패치</li>
<li>정적 다중 디스패치</li>
<li>동적 단일 디스패치</li>
<li>동적 다중 디스패치 </li>
</ul>
</li>
</ul>
</br>

<h2 id="문제-1-해석">문제 1 해석</h2>
<ul>
<li><code>sayHello()</code> 메서드 중 JVM 이 Human 타입을 받는 버전을 선택하는 이유는 무엇일까 ?</li>
<li>문제의 이유를 알기전에 개념을 알고가자.<pre><code class="language-java">Human man = new Man();</code></pre>
</li>
<li>이 코드에서 Human 을 변수의 정적 타입 또는 겉보기 타입 이라고 한다</li>
<li>Man 을 변수의 실제 타입 또는 런타임 타입이라고 한다 <pre><code class="language-java">// 실제 타입 변경 
Human human = new Random() ? new Man() : new Woman(); // 랜덤으로 둘중 하나의 타입이 런타임에 들어갔다고 가정
</code></pre>
</li>
</ul>
<p>// 정적 타입 변경 
sr.sayHello((Man) human);
sr.sayHello((Woman) human);</p>
<pre><code>- human 객체는 랜덤으로 실제 타입이 변경될 수 있으니 알수없다 → 슈뢰딩거의 고양이 
- 실제 타입이 `Man` 인지 `Woman` 인지는 프로그램이 해당 코드 라인을 실행할때에 마침내 선택된다
- 반면 `human` 객체의 정적 타입은 `Human` 이며 사용중에는 변경가능하다
- 하지만.
- 이 변경은 컴파일타임에 알 수 있다
- 위 코드에서 `sayHello()` 메서드를 호출할 때 강제로 변환했기 때문에 변환 결과가 `Man` 인지 `Woman` 인지 컴파일 타임에 명확해진다
- 문제 1 코드 
```java
Human man = new Man();      // 정적 타입 = Human, 실제 타입 = Man
Human woman = new Woman();  // 정적 타입 = Human, 실제 타입 = Woman 

sr.sayHello(man);           // 메서드 버전 선택 필요
sr.sayHello(woman);         // 메서드 버전 선택 필요 </code></pre><ul>
<li>보다시피 <code>sayHello()</code> 메서드를 두 번 호출한다.</li>
<li>이때 메서드 수신자인 <code>sr</code> 객체의 <code>sayHello()</code> 메서드를 호출시, <strong>오버로딩된 메서드 중 어느 버전을 호출할지는 전적으로 매개변수의 수와 타입이 기준이다</strong></li>
<li>이 코드에서 <code>man</code> 과 <code>woman</code> 은 정적 타입이 같지만 실제 타입은 다르다.</li>
<li>하지만.</li>
<li>JVM (정확히 말하자면 컴파일러) 은 호출할 <code>sayHello()</code> 를 선택할 때 매개변수의 실제 타입이 아닌 정적 타입을 참고한다.</li>
<li>정적 타입은 컴파일타임에 알려지기 때문에 <code>javac</code> 컴파일러는 매개변수의 정적 타입을 보고 어떤 오버로딩 버전을 호출할지 선택한다. 따라서 <code>sayHello(Human)</code> 이 호출 대상으로 선택된다</li>
<li>메서드 버전 선택에 정적 타입을 참고하는 모든 디스패치 작업을 정적 디스패치라고한다.</li>
<li>정적 디스패치의 가장 일반적인 응용 예가 메서드 오버로딩이다</li>
<li>정적 디스패치는 컴파일타임에 이루어지므로 지금 설명한 선택 작업은 JVM 에서는 이루어지지 않는다</li>
</ul>
</br>

<h2 id="문제-3-해석">문제 3 해석</h2>
<ul>
<li><p><code>javac</code> 컴파일러가 오버로딩된 메서드 중 적절한 버전을 선택할 수 있지만, 어느 하나를 꼭 집어내지 못하여 “비교적 더 적합한” 버전으로 선택하는 경우도 많다</p>
</li>
<li><p>모호함의 주된 원인은 바로 “리터럴” 이다.</p>
</li>
<li><p>리터럴에는 명시적인 정적 타입이 없으므로 언어와 문법 규칙을 바탕으로 이해하고 유추할 수 있을 뿐이다.</p>
</li>
<li><p>문제 3번의 1번째 답은 <code>“Hello char”</code> 이다</p>
</li>
<li><p>리터럴 <code>‘a’</code> 의 타입은 <code>char</code> 이므로 자연스럽게 매개변수 타입인 <code>char</code> 인 버전을 선택했다</p>
</li>
<li><p>문제 3번의 2번째 답은 <code>“Hello int”</code> 이다</p>
</li>
<li><p>자동 형 변환이 이루어진 것 이다.</p>
<ul>
<li>문제 <code>a</code> 는 숫자 97을 나타낼 수도 있다, 따라서 매개 변수 타입인 int 인 메서드도 적합하다. </li>
</ul>
</li>
<li><p>문제 3번의 3번째 답은 <code>“Hello long”</code> 이다 </p>
</li>
<li><p>이번에는 자동 형 변환이 두번 일어난다</p>
<ul>
<li><code>a</code> 를 정수 97 로 변환한 후 매개 변수 타입인 long 과 일치시키기 위해 long 타입 정수인 97L 로 다시 변환한다 </li>
<li><code>char</code> → <code>int</code> → <code>long</code> → <code>float</code> → <code>double</code> 순서로 이루어진다. </li>
<li>char 를 byte 나 short 로 변환하는 것은 안전하지 않기 때문에 후보에 들지 않는다</li>
</ul>
</li>
<li><p>문제 3번의 4번째 답은 “Hello Character” 이다</p>
</li>
<li><p>오토박싱이 일어났다 </p>
<ul>
<li>즉 <code>a</code> 의 래퍼 타입인 <code>java.lang.Character</code> 로 박싱하여 매개 변수 타입인 <code>Character</code> 인 메서드와 일치시켰다 </li>
</ul>
</li>
<li><p>문제 3번의 5번째 답은 <code>&quot;Hello Serializable&quot;</code> 이다 </p>
</li>
<li><p>문자와 직렬화가 무슨 관련이 있을까 ?</p>
</li>
<li><p>원인은 <code>Character</code> 가 <code>java.lang.Serializable</code> 을 구현했기 때문이다 </p>
<ul>
<li>오토박싱 이후에도 매개 변수를 찾지 못하여 대신에 래퍼 클래스가 구현한 인터페이스 타입을 받는 메서드를 선택했다</li>
</ul>
</li>
<li><p>문제 3번의 6번째 답은 컴파일 오류이다</p>
</li>
<li><p><code>Character</code> 는 또 다른 인터페이스인 <code>java.lang.Comparable&lt;Character&gt;</code> 도 구현한다 </p>
</li>
<li><p>그러나 이는 <code>Serializable</code> 을 받는 메서드와 우선순위가 동일하다</p>
<ul>
<li>그러므로 컴파일러는 어느 타입으로 변환할지 선택할 수 없기에 컴파일 오류가 발생하게 되는것이다 </li>
</ul>
</li>
<li><p>문제 3번의 7번째 답은 <code>&quot;Hello Object&quot;</code> 이다 </p>
<ul>
<li>오토 박싱 이후 부모 클래스로 변환된 것 이다 </li>
</ul>
</li>
<li><p>문제 3번의 8번째 답은 <code>&quot;Hello char ...&quot;</code> 이다</p>
<ul>
<li>7개의 오버로딩된 메서드 중 가변 길이 매개 변수를 받는 메서드의 우선순위가 가장낮다</li>
</ul>
</li>
</ul>
</br>


<h2 id="문제-2-해석">문제 2 해석</h2>
<ul>
<li>오버라이딩과 밀접하게 관련된 또 다른 주제인 동적 디스패치의 작동 과정을 살펴보자. </li>
<li>문제 2의 실행 결과는 다음과 같다 <pre><code class="language-java">Man said hello
Woman said hello
Woman said hello</code></pre>
</li>
<li>자연스러운 결과이다</li>
<li>그러나 위와 같이 JVM 이 어떤 메서드 버전을 선택하는지를 살펴보자</li>
<li>이번 문제는 호출할 메서드의 버전을 정적 타입만으로 결정하기가 불가능하다.</li>
<li>변수 <code>man</code> 과 <code>woman</code> 의 정적 타입은 모두 <code>Human</code> 으로 똑같지만, <code>sayHello()</code> 를 호출하면 서로 다른 메서드를 호출하기 때문이다</li>
<li>바이트 코드에서 메서드 호출은 <code>invokevirtual</code> 이고 실제로 심벌 참조 (<code>Human.sayHello()</code>) 도 모두 동일하다</li>
<li>그러나 실제 실행된 메서드는 다르다</li>
<li><code>invokevirtual</code> 명령어는 실행의 첫 단계에서 런타임 수신 객체의 실제 타입을 해석한다는 점이 중요하다</li>
<li>이런 이유로 앞에 두 메서드는 상수 풀에 있는 메서드의 심벌 참조를 직접 참조로 변환하는데서 그치지 않고 메서드 수신 객체의 실제 타입을 보고 메서드 버전을 선택한다</li>
<li>런타임에 실제 타입을 보고 메서드 버전을 선택하는 이러한 디스패치 방식을 동적 디스패치라고 한다</li>
<li>다형성의 뿌리는 가상 메서드 호출 명령어인 <code>invokevirtual</code> 의 실행 로직에 있다</li>
</ul>
</br>

<h2 id="문제-4-해석">문제 4 해석</h2>
<ul>
<li>실행 결과는 다음과 같다 <pre><code class="language-java">I am a Son, I have $0
I am a Son, I have $4
This guy has $2</code></pre>
</li>
<li>“I am a Son” 문장이 두 번 출력되었다 </li>
<li>첫 번째 줄은 부모 클래스인 <code>Father</code> 의 생성자에서 출력하고 두 번째 줄은 <code>Son</code> 클래스의 생성자에 출력한 결과이다 </li>
<li>그런데 왜 둘 다 <code>“Son”</code> 이라는 결과가 나왔을까 ?</li>
<li>코드는 <code>Father</code> 타입의 <code>guy</code> 는 <code>Son</code> 의 객체로 초기화 된다. 따라서 <code>guy</code> 는 <code>Father</code> 타입을 참조하지만 실제 인스턴스는 <code>Son</code> 이며 이 과정에서 <code>Father</code> 와 <code>Son</code> 클래스의 생성자가 모두 호출된다</li>
<li><code>Son</code> 클래스가 생성될 때 암묵적으로 <code>Father</code> 의 생성자를 먼저 호출하는데, <code>Father</code> 의 생성자에서 <code>money = 2</code> 가 수행되고 <code>showMeTheMoney()</code> 가 호출된다 </li>
<li>하지만 해당 메서드는 <code>Son</code> 자식클래스에서 오버라이딩 되었으므로 실제 실행되는 것은 <code>Son</code> 의 <code>showMeTheMoney</code> , 이는 가상 메서드 호출을 실행합니다</li>
<li>이때 부모 클래스의 <code>money</code> 필드는 <code>2</code> 로 초기화되었지만 <code>Son.showMeTheMoney()</code> 메서드는 자식 클래스의 <code>money</code> 필드를 이용한다</li>
<li>자식 클래스의 <code>money</code> 필드는 자식 클래스의 생성자가 실행될 때에 초기화되기 때문에 아직은 값이 <code>0</code>인 상태이다 <ul>
<li>즉 <code>Son</code> 생성자가 실행되어야지만 값을 할당함 </li>
</ul>
</li>
<li>마지막은 정적 타입을 통해 부모 클래스인 <code>money</code> 로부터 값을 직접 가져왔기 때문에 <code>2</code> 를 출력한다<ul>
<li>필드 접근은 정적 타입에 의해 결정되며 필드는 오버라이드 되지 않는다.</li>
<li>필드는 메서드와 다르게 다형성을 지원하지 않기 때문에 동적 디스패치는 오직 메서드에만 적용된다<ul>
<li>즉 필드에는 <code>invokevirtual</code> 이라는 명령어를 사용하지 않음</li>
</ul>
</li>
<li>가상 필드라는 개념은 존재하지 않음 </li>
<li>그러므로 객체의 정적 타입을 기준으로 접근한다. </li>
<li>추가로 필드의 이름은 클래스의 메서드가 그 필드에 담겨있는 값에 접근할 수 있는 수단이다 </li>
</ul>
</li>
</ul>
</br>


<h2 id="단일-디스패치-와-다중-디스패치">단일 디스패치 와 다중 디스패치</h2>
<ul>
<li><p>메서드의 수신 객체 (호출된 메서드의 주인) 와 매개 변수를 합쳐 메서드 볼륨이라고 한다 </p>
</li>
<li><p>디스패치의 기준이 되는 볼륨 수에 따라 디스패치는 단일 디스패치와 다중 디스패치로 나뉜다 </p>
<pre><code class="language-java">public class Dispatch {
      static class QQ {}
      static class _360 {}

      public static class Father {
              public void choice(QQ arg) {
                      System.out.println(&quot;Father chose a qq&quot;);
              }

              public void choice(_360 arg) {
                      System.out.println(&quot;Father chose a 360&quot;);
              }
      }

      public static class Son extends Father {
              public void choice(QQ arg) {
                      System.out.println(&quot;Son chose a qq&quot;);
              }

              public void choice(_360 arg) {
                      System.out.println(&quot;Son chose a 360&quot;);
              }
      }

      public static void main(String[] args) {
              Father father = new Father();
              Father son = new Son();

              father.choice(new _360());
              son.choice(new QQ());
      }
}</code></pre>
</li>
<li><p>가장 먼저 집중할 부분은 컴파일 단계에서 컴파일러의 선택 과정 즉, 정적 디스패치 과정이다 </p>
</li>
<li><p>이때 대상 메서드를 선택하는데는 두 가지를 고려한다 </p>
</li>
<li><p>하나는 변수는 정적 타입이 <code>Father</code> 이냐 <code>Son</code> 이냐이고 다른 하나는 매개 변수 타입이 <code>QQ</code> 이나 <code>_360</code> 이냐이다</p>
</li>
<li><p>두 가지를 조합해 내린 결론이 두 개의 invokevirtual 명령어를 생성하는데 이용된다</p>
<ul>
<li><code>Father</code> 의 두 개의 메서드를 실행할 명령어를 생성하는데 사용된다.</li>
</ul>
</li>
<li><p>두 명령어의 매개 변수는 각각 상수 풀에 있는 <code>Father.choice(_360)</code> 과 <code>Father.choice(QQ)</code> 메서드이다</p>
<ul>
<li>이 처럼 선택에 이용된 볼륨이 두 개라서 자바의 정적 디스패치는 다중 디스패치다</li>
</ul>
</li>
<li><p>이어서 동적 디스패치, 실행단계의 메서드 선택을 살펴보자 </p>
</li>
<li><p><code>son.choice(QQ)</code> 가 실행될 때 컴파일 타임에 이미 <code>choice(QQ)</code> 로 결정되었다 </p>
</li>
<li><p>따라서 매개 변수 <code>QQ</code> 로 전달되는 인수의 실제 타입이 무엇인지는 상관이 없다 </p>
<ul>
<li>이시점에서 매개 변수 <code>QQ</code> 로 전달되는 인수의 실제 타입이 무엇인지는 상관하지 않는다. 이처럼 정적 타입과 실제 타입은 메서드 선택에 관여하지 않는다</li>
<li>그렇다면 JVM 선택에 영향을 주는 유일한 요소는 메서드 수신 객체의 실제 타입이 <code>Father</code> 이냐 <code>Son</code> 이냐 뿐.</li>
<li>즉, 선택 기준 볼륨이 하나뿐이므로 자바의 동적 디스패치는 단일 디스패치이다 </li>
</ul>
</li>
</ul>
<pre><code class="language-java">출력. 

Father chose a 360
Son chose a qq</code></pre>
<ul>
<li>위 내용을 좀 더 간단히 이야기하자면,<ul>
<li>결정하는 시점이 다르다는 것에 초점을 맞춰야한다 </li>
<li>정적 디스패치는 컴파일 시점에 결정을 하는 것 이고 동적 디스패치는 실행 시점에 결정을 하는 것이다 </li>
<li>위에서 이야기했듯 볼륨 즉 결정 (선택) 할 때 기준이 되는 요소는 두가지 <ol>
<li>메서드 매개변수 타입 </li>
<li>메서드 수신 객체 타입 → 호출한 메서드의 주인 타입</li>
</ol>
</li>
<li>정적 디스패치 가 왜 다중 디스패치 인지는 컴파일 시점의 결정을 생각해야 한다 <ul>
<li>정적 디스패치는 컴파일 시점에 위에서의 두 가지 기준을 모두 고려해야 한다 </li>
<li>이 때문에 다중 디스패치라고 하고 </li>
<li>실행시점에는 이미 메서드 호출이 명확해 졌기 때문에 선택하지 않는다 </li>
<li>동적 디스패치는 컴파일 시점에 두 가지 기준이 아닌 한 가지 기준 즉, 메서드 매개변수 타입만을 고려해야 한다 <ul>
<li>이유는 수신 객체의 타입이 컴파일 시점에 알 수 없는 슈뢰딩거의 고양이 이니 말이다 </li>
</ul>
</li>
<li>그렇게 동적 디스패치는 컴파일 시점에 한 가지 기준을 두고 선택을 한다 이후 </li>
<li>실행 시점에서 선택을 해야한다 이때는 메서드 수신 객체의 타입을 가지고 메서드 선택을 해야한다 </li>
<li>이를 단일 디스패치라고 한다  </li>
</ul>
</li>
</ul>
</li>
</ul>
</br>

<h2 id="jvm-의-동적-디스패치-구현">JVM 의 동적 디스패치 구현</h2>
<ul>
<li>동적 디스패치는 매우 자주 일어난다 </li>
<li>또한 동적 디스패치 중 메서드 버전 선택시에는 런타임에 수신 객체 타입의 메서드 메타데이터를 보고 적절한 대상 메서드를 찾는 작업이 이루어진다</li>
<li>그래서 실행 성능을 중요시하는 JVM 구현에는 일반적으로 타입 메타데이터를 그리 자주 검색하지 않는다 <ul>
<li>타입 메타데이터는 메서드 정보외의 객체의 복잡한 정보가 담겨있음 (클래스 이름, 필드 목록, 상속관계 등)</li>
<li>계층적으로 구성되어 있으며 여러 단계를 거쳐서 특정 메서드를 찾는 과정에 많은 검색 작업이 필요함</li>
</ul>
</li>
<li>해당 타입에 대한 가상 메서드 테이블 (메서드 영역에 존재하는 vtable) 을 만들어 최적화하는 것이다 </li>
<li>이처럼 일반적으로 메타데이터 조회 대신 가상 메서드 테이블 인덱스를 사용해 성능 향상을 꾀한다.
<img src="https://velog.velcdn.com/images/dlaudrb09-/post/d2111dc5-7d3c-473c-a70e-a8d2fc66f521/image.png" alt=""></li>
<li>가상 메서드 테이블에는 각 메서드의 실제 시작 주소가 담긴다 </li>
<li>하위 클래스에서 메서드가 오버라이딩되지 않으면 하위 클래스 가상 메서드 테이블의 주소 항목은 부모 클래스에 있는 동일한 메서드의 주소 항목과 같다 </li>
<li>즉, 둘 다 부모 클래스의 구현 시작점을 가리킨다 </li>
<li>하위 클래스에서 메서드를 오버라이딩하면 하위 클래스 가상 메서드 테이블의 주소 항목은 하위 클래스의 구현 시작점을 가리키게 바뀐다 </li>
<li>자바에서 메서드는 final 로 선언하지 않는 이상 기본적으로 가상 메서드이다 </li>
</ul>
</br>
</br>

<p>참고 ) </p>
<ul>
<li>JDK 10 에서 추가된 <code>var</code> 라는 키워드는 오른쪽 표현식의 타입을 보고 컴파일 타임에 추론하기 때문에 정적 디스패치를 한다</li>
</ul>
</br>
</br>

<p>답.</p>
<p>문제1</p>
<pre><code class="language-java">Hello, guy!
Hello, guy!</code></pre>
<p>문제2</p>
<pre><code class="language-java">Man said hello
Woman said hello
Woman said hello</code></pre>
<p>문제3</p>
<pre><code class="language-java">Hello char </code></pre>
<p>문제3-1</p>
<pre><code class="language-java">Hello int</code></pre>
<p>문제 3-2</p>
<pre><code class="language-java">Hello long</code></pre>
<p>문제 3-3</p>
<pre><code class="language-java">Hello Character</code></pre>
<p>문제 3-4</p>
<pre><code class="language-java">Hello Serializable</code></pre>
<p>문제 3-5</p>
<pre><code class="language-java">&quot;The method sayHello(Object) is ambiguous for the type overload&quot; 

라는 메시지를 뿌리며 컴파일을 거부한다.</code></pre>
<p>문제 3-6</p>
<pre><code class="language-java">Hello Object</code></pre>
<p>문제 3-7</p>
<pre><code class="language-java">Hello char ...</code></pre>
<p>문제 4</p>
<pre><code class="language-java">I am a Son, I have $0
I am a Son, I have $4
This guy has $2</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Spring 빈 조회 및 등록, AutoWired 정리 (Spring Boot 3.3.3 기준)]]></title>
            <link>https://velog.io/@dlaudrb09-/Spring-%EB%B9%88-%EC%A1%B0%ED%9A%8C-%EB%B0%8F-%EB%93%B1%EB%A1%9D-AutoWired-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dlaudrb09-/Spring-%EB%B9%88-%EC%A1%B0%ED%9A%8C-%EB%B0%8F-%EB%93%B1%EB%A1%9D-AutoWired-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 17 Sep 2024 14:54:09 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>Spring에 대한 정리글입니다.</p>
<p>이번 글에서는 빈 조회 방식, 빈 등록 흐름, 그리고 <code>@Autowired</code> 어노테이션이 실제로 어떻게 동작하는지에 대해 깊이 있게 설명합니다.</p>
<p>버전은 Spring Boot 3.3.3을 기준으로 하며, <strong>SpringApplication 내부에서 빈을 어떻게 스캔하고 등록하고 주입하는지에 중점을 둡니다.</strong></p>
</br>

<h1 id="springapplication">SpringApplication</h1>
<p><code>SpringApplication.run()</code> 의 전체 흐름 중 실제로 빈을 등록하고 주입하는 데 영향을 주는 두 메서드를 깊이 파고듭니다.</p>
<p>실행 순서는 다음과 같습니다 </p>
<ol>
<li><code>prepareContext()</code></li>
<li><code>refreshContext()</code> </li>
</ol>
</br>

<hr>
<h1 id="preparecontext"><code>prepareContext()</code></h1>
<pre><code class="language-java">    private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
        context.setEnvironment(environment);

        // 생략된 다양한 작업들...

        if (!AotDetector.useGeneratedArtifacts()) {
            Set&lt;Object&gt; sources = this.getAllSources();
            Assert.notEmpty(sources, &quot;Sources must not be empty&quot;);
            this.load(context, sources.toArray(new Object[0]));
        }

        listeners.contextLoaded(context);
    }
</code></pre>
<ul>
<li>이 메서드는 <code>ApplicationContext</code> 가 아직 완전히 초기화되기 전 단계에서 실행됩니다.<ul>
<li>즉 컨텍스트가 완전히 리프레시되지 않은 상태에서 실행</li>
</ul>
</li>
<li>중요한 핵심은 <code>this.load()</code> 메서드를 호출하는 시점입니다.</li>
<li>이 <code>load()</code> 가 실제로 빈 정의(BeanDefinition)를 생성하고 등록하는 역할을 합니다.<ul>
<li>애플리케이션 코드 즉, <code>@Configuration</code> 클래스나 <code>@Component</code> 가 붙은 클래스를 인자로 받아서 실행합니다</li>
</ul>
</li>
</ul>
<blockquote>
<p>참고 </p>
</blockquote>
<ul>
<li>리프레시 상태 : 애플리케이션 컨텍스트가 완전히 초기화되고 모든 빈이 생성되고 설정이 끝난 상태를 의미합니다</li>
</ul>
<h3 id="aot-조건">AOT 조건</h3>
<pre><code class="language-java">if (!AotDetector.useGeneratedArtifacts())</code></pre>
<ul>
<li>AOT(Ahead-of-Time)는 Spring Native와 관련된 컴파일 최적화 기술입니다.</li>
<li>일반적으로는 <code>false</code>이므로, 위 조건은 항상 <code>load()</code>를 실행하게 됩니다.</li>
<li>만약 설정을 통해 <code>spring.aot.enabled=true</code> 로 변경하면 이 루트가 달라집니다.</li>
</ul>
</br>

<hr>
<h1 id="applicationcontext-기본-구현체-생성-흐름"><code>ApplicationContext</code> 기본 구현체 생성 흐름</h1>
<ul>
<li><code>SpringApplication.run()</code> 내부에서는 다음과 같이 <code>ApplicationContext</code> 를 생성합니다</li>
</ul>
<pre><code class="language-java">context = this.createApplicationContext();</code></pre>
<p>해당 메서드 호출은 다음과 같이 진행됩니다 </p>
<ul>
<li><code>ApplicationContextFactory.DEFAULT</code> → <code>DefaultApplicationContextFactory</code></li>
<li><code>create(WebApplicationType)</code> 호출</li>
<li><code>createDefaultApplicationContext()</code> 호출</li>
</ul>
<p>기본 설정 Spring Boot 에서의 <code>BeanFactory</code> 구현체를 알아야 합니다</p>
<ul>
<li>기본값인 <code>DefaultApplicationContextFactory</code> 를 실행하게 됩니다</li>
</ul>
<pre><code class="language-java">public interface ApplicationContextFactory {
    ApplicationContextFactory DEFAULT = new DefaultApplicationContextFactory();
    ...
}</code></pre>
<ul>
<li>결국 <code>DefaultApplicationContextFactory.create()</code> 메서드를 실행하게 됩니다</li>
</ul>
<pre><code class="language-java">class DefaultApplicationContextFactory implements ApplicationContextFactory {

    public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {
        try {
            return (ConfigurableApplicationContext)this.getFromSpringFactories(webApplicationType, ApplicationContextFactory::create, this::createDefaultApplicationContext);
        } catch (Exception var3) {
            Exception ex = var3;
            throw new IllegalStateException(&quot;Unable create a default ApplicationContext instance, you may need a custom ApplicationContextFactory&quot;, ex);
        }
    }

    private ConfigurableApplicationContext createDefaultApplicationContext() {
        return (ConfigurableApplicationContext)(!AotDetector.useGeneratedArtifacts() ? new AnnotationConfigApplicationContext() : new GenericApplicationContext());
    }

    private &lt;T&gt; T getFromSpringFactories(WebApplicationType webApplicationType, BiFunction&lt;ApplicationContextFactory, WebApplicationType, T&gt; action, Supplier&lt;T&gt; defaultResult) {
        Iterator var4 = SpringFactoriesLoader.loadFactories(ApplicationContextFactory.class, this.getClass().getClassLoader()).iterator();

        Object result;
        do {
            if (!var4.hasNext()) {
                return defaultResult != null ? defaultResult.get() : null;
            }

            ApplicationContextFactory candidate = (ApplicationContextFactory)var4.next();
            result = action.apply(candidate, webApplicationType);
        } while(result == null);

        return result;
    }
}</code></pre>
<ul>
<li>코드를 보면  <code>getFromSpringFactories()</code> 메서드를 호출합니다 </li>
<li><code>SpringFactoriesLoader.loadFactories()</code> 메서드에서 반환된 <code>ApplicationContextFactory</code> 구현체들을 순회하면서 적절한 <code>ApplicationContext</code> 를 생성하게 됩니다 </li>
<li>기본 Spring Boot 설정에서는 <code>createDefaultApplicationContext()</code> 메서드에 의해서 <code>AnnotationConfigApplicationContext</code> 객체가 <code>ApplicationContext</code> 로 설정됩니다</li>
</ul>
</br>


<h3 id="annotationconfigapplicationcontext"><code>AnnotationConfigApplicationContext</code></h3>
<pre><code class="language-java">public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {
    ...
}</code></pre>
<ul>
<li>기본 Spring Boot 설정으로 인해 생성된 <code>AnnotationConfigApplicationContext</code> 는 <code>GenericApplicationContext</code> 를 상속받아서 구현하고 있습니다</li>
</ul>
<pre><code class="language-java">public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
    private final DefaultListableBeanFactory beanFactory;

        public final ConfigurableListableBeanFactory getBeanFactory() {
            return this.beanFactory;
        }
        // 그외 많은 기능들
}</code></pre>
<ul>
<li><code>GenericApplicationContext</code> 안에는 <code>DefaultListableBeanFactory</code> 타입의 <code>beanFactory</code> 를 가지고 있습니다</li>
<li><code>getBeanFactory()</code> 를 통해 외부에서 꺼내서 사용할 수 있습니다 </li>
<li>해당 <code>beanFactory</code> 가 결국 모든 빈 정의를 등록/조회/생성하는 역할을 담당합니다</li>
</ul>
<blockquote>
<p>참고
<code>ApplicationContext</code> 가 곧 <code>beanFactory</code> 를 확장한 것 인데 내부에 <code>beanFactory</code> 필드가 또 있는 이유는 ?</p>
</blockquote>
<ul>
<li><code>ApplicationContext</code> 는 기능이 많기 때문에, 핵심 빈 관리 책임을 <code>beanFactory</code> 에 위임하는 구조로 설계되었다</li>
</ul>
</br>

<hr>
<h1 id="load"><code>load()</code></h1>
<pre><code class="language-java">protected void load(ApplicationContext context, Object[] sources) {
    if (logger.isDebugEnabled()) {
        logger.debug(&quot;Loading source &quot; + StringUtils.arrayToCommaDelimitedString(sources));
    }

    BeanDefinitionLoader loader = this.createBeanDefinitionLoader(this.getBeanDefinitionRegistry(context), sources);
    if (this.beanNameGenerator != null) {
        loader.setBeanNameGenerator(this.beanNameGenerator);
    }

    if (this.resourceLoader != null) {
        loader.setResourceLoader(this.resourceLoader);
    }

    if (this.environment != null) {
        loader.setEnvironment(this.environment);
    }

    loader.load();
}

private BeanDefinitionRegistry getBeanDefinitionRegistry(ApplicationContext context) {
    if (context instanceof BeanDefinitionRegistry registry) {
        return registry;
    } else if (context instanceof AbstractApplicationContext abstractApplicationContext) {
        return (BeanDefinitionRegistry)abstractApplicationContext.getBeanFactory();
    } else {
        throw new IllegalStateException(&quot;Could not locate BeanDefinitionRegistry&quot;);
    }
}

protected BeanDefinitionLoader createBeanDefinitionLoader(BeanDefinitionRegistry registry, Object[] sources) {
    return new BeanDefinitionLoader(registry, sources);
}</code></pre>
<ul>
<li><code>getBeanDefinitionRegistry(context)</code> 를 통해 빈 정의 레지스트리를 가져옵니다.<ul>
<li>대부분의 경우 <code>DefaultListableBeanFactory</code> 가 됩니다.</li>
<li><code>registry</code> 는 간단히 이야기해서 &quot;빈에 대한 정의를 생성하는 객체&quot; 라고 이해하면 될 것 같습니다</li>
</ul>
</li>
<li>이후 <code>BeanDefinitionLoader</code> 를 생성합니다</li>
<li>마지막에 <code>loader.load()</code> 를 호출하며 <code>BeanDefinitionLoader</code> 는 <code>AnnotatedBeanDefinitionReader</code> 와 <code>ClassPathBeanDefinitionScanner</code> 를 활용하여 애플리케이션의 클래스 경로나 어노테이션을 스캔하여 빈 정의를 생성합니다<ul>
<li><code>AnnotatedBeanDefinitionReader</code> : 주로 <code>@Configuration</code>, <code>@Component</code> 등의 어노테이션을 읽고 빈 정의를 생성합니다</li>
<li><code>ClassPathBeanDefinitionScanner</code> : 특정 패키지를 스캔하여 <code>@Component</code>, <code>@Service</code>, <code>@Repository</code>, <code>@Controller</code> 등으로 선언된 클래스를 빈으로 등록합니다</li>
</ul>
</li>
</ul>
<pre><code class="language-java">class BeanDefinitionLoader {
    private static final Pattern GROOVY_CLOSURE_PATTERN = Pattern.compile(&quot;.*\\$_.*closure.*&quot;);
    private final Object[] sources;
    private final AnnotatedBeanDefinitionReader annotatedReader;
    private final AbstractBeanDefinitionReader xmlReader;
    private final BeanDefinitionReader groovyReader;
    private final ClassPathBeanDefinitionScanner scanner;
    private ResourceLoader resourceLoader;

    BeanDefinitionLoader(BeanDefinitionRegistry registry, Object... sources) {
        Assert.notNull(registry, &quot;Registry must not be null&quot;);
        Assert.notEmpty(sources, &quot;Sources must not be empty&quot;);
        this.sources = sources;
        ...
    }

    void load() {
        for(Object source : this.sources) {
            this.load(source);
        }
    }

    ...
 }</code></pre>
<ul>
<li><code>BeanDefinitionLoader</code> 는 <code>load()</code> 메서드에서 각 타입에 맞게 <code>load()</code> 메서드를 오버로딩하여 타입에 맞는 <code>Scanner</code> 혹은 <code>Reader</code> 를 호출. </li>
<li>결국에는 <code>Reader</code> , <code>Scanner</code> 둘다 <code>BeanDefinitionReaderUtils.registerBeanDefinition()</code> 를 호출하게 됩니다 </li>
</ul>
<pre><code class="language-java">public abstract class BeanDefinitionReaderUtils {
    public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException {
        String beanName = definitionHolder.getBeanName();
        registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
        String[] aliases = definitionHolder.getAliases();
        if (aliases != null) {
            String[] var4 = aliases;
            int var5 = aliases.length;

            for(int var6 = 0; var6 &lt; var5; ++var6) {
                String alias = var4[var6];
                registry.registerAlias(beanName, alias);
            }
        }
    }
}</code></pre>
<ul>
<li>여기서 <code>registry.registerBeanDefinition()</code> 이 <code>DefaultListableBeanFactory.registerBeanDefinition()</code> 를 호출하고, 인자로 beanName 의 문자열을 받아서 <code>beanDefinitionNames</code> 에 추가하는 작업을 합니다 </li>
<li>이름을 통해 <code>beanDefinitionMap</code> 을 생성하고 이 맵은 모든 빈의 정의들의 중앙 저장소 역할을 합니다 </li>
<li>이로 인해 스프링 컨텍스트는 빈 정의를 쉽게 조회하고 인스턴스화할 수 있습니다</li>
</ul>
</br>

<hr>
<h1 id="빈-등록-및-빈-후처리-작업">빈 등록 및 빈 후처리 작업</h1>
<ul>
<li><code>prepareContext()</code> 를 통해 빈 정의를 생성하고 현재 컨텍스트 기준으로 <code>beanDefinitionMap</code> 을 생성했습니다 </li>
<li>이후 <code>refreshContext()</code> 를 통해 빈 등록 및 빈 후처리 작업을 시작하게 됩니다 </li>
</ul>
</br>

<hr>
<h2 id="beanpostprocessor"><code>BeanPostProcessor</code></h2>
<ul>
<li><p>빈 후처리기 (<code>BeanPostProcessor</code>)를 알아보겠습니다 </p>
</li>
<li><p><code>Bean</code> 의 인스턴스를 만든 이후 <code>Bean</code> 의 <code>Initialization Life Cycle</code> 의 이전, 이후에 실행되는 작업들을 의미합니다</p>
<ul>
<li><code>Bean</code> 초기화 전<ul>
<li><code>postProcessBeforeInitialization()</code> : <code>Bean</code> 이 초기화되거 전에 호출</li>
</ul>
</li>
<li><code>Bean</code> 초기화 후<ul>
<li><code>postProcessAfterInitialization()</code> : <code>Bean</code> 의 초기화가 완료된 후에 호출</li>
<li>예시) <code>@PostConstruct</code>, <code>InitializingBean.afterPropertiesSet()</code></li>
</ul>
</li>
</ul>
</li>
<li><p><code>@Autowired</code> 는 <code>AutowiredAnnotationBeanPostProcessor</code> 를 통해 구현되어 있습니다</p>
</br>

<hr>
</li>
</ul>
<h2 id="refreshcontext"><code>refreshContext()</code></h2>
<ul>
<li><code>refreshContext()</code> 는 <code>ApplicationContext</code> 의 <code>refresh()</code> 메서드를 실행하게 되는데 기본 설정으로는 <code>AbstractApplicationContext.refresh()</code> 메서드를 실행하게 됩니다 <ul>
<li><img src="https://velog.velcdn.com/images/dlaudrb09-/post/317f69a5-fab0-4008-9e00-2d080a996058/image.png" alt=""></li>
</ul>
</li>
</ul>
<pre><code class="language-java">public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
    public void refresh() throws BeansException, IllegalStateException {
        ...
        try {
            ...
            this.registerBeanPostProcessors(beanFactory);
            this.finishBeanFactoryInitialization(beanFactory);
            ...
        } catch (Error | RuntimeException var12) {
            ...
        }
    }
}</code></pre>
<ul>
<li>현재 설명할 중요한 메서드만 명시했습니다<ul>
<li><code>registerBeanPostProcessors()</code></li>
<li><code>finishBeanFactoryInitialization()</code></li>
</ul>
</li>
</ul>
</br>

<h3 id="registerbeanpostprocessors"><code>registerBeanPostProcessors()</code></h3>
<ul>
<li><p>해당 메서드는 <code>PostProcessorRegistrationDelegate.registerBeanPostProcessors()</code> 를 호출하고 있습니다</p>
<pre><code class="language-java">final class PostProcessorRegistrationDelegate {
  public static void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {
      String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);
      ...
      List&lt;BeanPostProcessor&gt; priorityOrderedPostProcessors = new ArrayList();
      List&lt;BeanPostProcessor&gt; internalPostProcessors = new ArrayList();
      List&lt;String&gt; orderedPostProcessorNames = new ArrayList();
      List&lt;String&gt; nonOrderedPostProcessorNames = new ArrayList();
      ...

      String ppName;
      BeanPostProcessor pp;
      for(int var10 = 0; var10 &lt; var9; ++var10) {
          ppName = var8[var10];
          if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
              pp = (BeanPostProcessor)beanFactory.getBean(ppName, BeanPostProcessor.class);
              priorityOrderedPostProcessors.add(pp);
           }
       }

       ...
      registerBeanPostProcessors(beanFactory, (List)nonOrderedPostProcessors);
      sortPostProcessors(internalPostProcessors, beanFactory);
      registerBeanPostProcessors(beanFactory, (List)internalPostProcessors);
  }

  private static void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory, List&lt;? extends BeanPostProcessor&gt; postProcessors) {
      if (beanFactory instanceof AbstractBeanFactory abstractBeanFactory) {
          abstractBeanFactory.addBeanPostProcessors(postProcessors);
      } else {
          Iterator var3 = postProcessors.iterator();

          while(var3.hasNext()) {
              BeanPostProcessor postProcessor = (BeanPostProcessor)var3.next();
              beanFactory.addBeanPostProcessor(postProcessor);
          }
      }

  }
}</code></pre>
</li>
<li><p>위 메서드는 <code>registerBeanPostProcessors()</code> 를 두가지 형태로 오버로딩한 메서드입니다 </p>
<ul>
<li><code>public</code> 메서드</li>
<li><code>private</code> 메서드</li>
</ul>
</li>
<li><p>맨 처음 (<code>public</code> 메서드) <code>registerBeanPostProcessors()</code> 의 시작은 위의 첫번째 메서드부터 시작되며 <code>beanFactory.getBeanNamesForType()</code> 를 통해 <code>BeanPostProcessor</code> 타입을 모두 조회합니다 </p>
<ul>
<li>모든 <code>BeanPostProcessor</code> 타입을 가져옵니다 </li>
<li><strong>이때 <code>@Autowired</code> 의 빈 후처리기인 <code>AutowiredAnnotationBeanPostProcessor</code> 구현체를 가져오게 됩니다</strong> </li>
</ul>
</li>
<li><p>모든 빈 후처리기를 가져와서 각 빈의 순서에 따라 정렬을 합니다 </p>
</li>
<li><p>이후 순서대로 순회하면서 <code>beanFactory.getBean()</code> 을 호출하게 됩니다 </p>
</li>
<li><p>구현체인 <code>AbstractBeanFactory.getBean()</code> 를 호출하게 되며 이는 <code>doGetBean()</code> 인 내부 메서드를 호출하며 빈을 가져오는 과정에서 빈을 생성할지, 기존의 인스턴스를 반환할지를 결정합니다 </p>
<ul>
<li><img src="https://velog.velcdn.com/images/dlaudrb09-/post/d0d0de74-17d6-4cf3-8221-3086a6488a78/image.png" alt=""></li>
<li>이에 대한 내용은 아래 따로 구별해놓았습니다 </li>
</ul>
</li>
<li><p>이후 두번째 (<code>private</code> 메서드)  <code>registerBeanPostProcessors()</code> 메서드를 통해 <code>abstractBeanFactory.addBeanPostProcessor(postProcessors)</code> 를 실행하게 됩니다 </p>
</li>
</ul>
</br>

<h3 id="addbeanpostprocessors"><code>addBeanPostProcessors()</code></h3>
<ul>
<li>이어서 빈 후처리기를 등록하는 메서드를 확인해봅시다 <pre><code class="language-java">public void addBeanPostProcessors(Collection&lt;? extends BeanPostProcessor&gt; beanPostProcessors) {
  synchronized(this.beanPostProcessors) {
      this.beanPostProcessors.removeAll(beanPostProcessors);
      this.beanPostProcessors.addAll(beanPostProcessors);
  }
}</code></pre>
</li>
<li>메서드 내용은 간단합니다, 기존의 등록된 빈 후처리기들이 있으면 모두 삭제하고 다시 등록합니다. </li>
</ul>
<pre><code class="language-java">public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
    private final List&lt;BeanPostProcessor&gt; beanPostProcessors = new BeanPostProcessorCacheAwareList();
    @Nullable
    private BeanPostProcessorCache beanPostProcessorCache;
}</code></pre>
<ul>
<li><p><code>beanPostProcessors</code> 를 살펴보면 <code>BeanPostProcessorCacheAwareList</code> 를 사용한걸로 보여집니다</p>
</li>
<li><p>여기 <code>BeanPostProcessorCacheAwareList</code> 는 <code>add</code> 메서드를 통해 빈 후처리기를 등록하는데 등록하는 부분을 보면 <code>beanProcessorCache</code> 를 삭제한 후 등록하는 것으로 구현되어 있다 </p>
<pre><code class="language-java">private class BeanPostProcessorCacheAwareList extends CopyOnWriteArrayList&lt;BeanPostProcessor&gt; {
      private BeanPostProcessorCacheAwareList() {
      }

      public BeanPostProcessor set(int index, BeanPostProcessor element) {
          BeanPostProcessor result = (BeanPostProcessor)super.set(index, element);
          AbstractBeanFactory.this.resetBeanPostProcessorCache();
          return result;
      }

      public boolean add(BeanPostProcessor o) {
          boolean success = super.add(o);
          AbstractBeanFactory.this.resetBeanPostProcessorCache();
          return success;
      }

      public void add(int index, BeanPostProcessor element) {
          super.add(index, element);
          AbstractBeanFactory.this.resetBeanPostProcessorCache();
      }

      public BeanPostProcessor remove(int index) {
          BeanPostProcessor result = (BeanPostProcessor)super.remove(index);
          AbstractBeanFactory.this.resetBeanPostProcessorCache();
          return result;
      }

      public boolean remove(Object o) {
          boolean success = super.remove(o);
          if (success) {
              AbstractBeanFactory.this.resetBeanPostProcessorCache();
          }

          return success;
      }
}
</code></pre>
</li>
</ul>
<p>public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
        private void resetBeanPostProcessorCache() {
            synchronized(this.beanPostProcessors) {
                this.beanPostProcessorCache = null;
            }
        }
}</p>
<pre><code>- `BeanPostProcessorCacheAwarList` 는 일반적인 리스트가 아니라 `BeanPostProcessor` 객체들을 효율적으로 관리하기 위한 Spring 의 특별한 리스트 클래스입니다 
  - `CopyOnWriteArrayList&lt;BeanPostProcessor&gt;` 는 `java.util.concurrent` 패키지에 포함되어 있으며 `CopyOnWriteArrayList` 는 스레드 안전한 리스트로 쓰기 작업 (추가, 수정, 삭제) 이 일어날 때마다 기존 배열을 복사하여 새로운 배열을 생성하는 방식으로 동작합니다 
- 내부적으로 `BeanPostProcessor` 들을 분류하고 캐시처럼 활용합니다 
  - `BeanPostProcessor` 들은 빈 생성 과정에서 빈의 초기화 전후로 자주 호출됩니다, 매번 `beanFactory` 에서 `BeanPostProcessor`를 조회하는 것은 성능에 영향을 줄 수 있기때문에 Spring 은 `BeanPostProcessor`들을 한 번 가져와 캐시에 저장하고, 이후에는 이 캐시에서 `BeanPostProcessor`들을 사용합니다 

&lt;/br&gt;

---

# `getBean()`

- `BeanPostProcessor` 를 가져오기 위해 `beanFactory.getBean()` 을 호출합니다 
```java
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
    public Object getBean(String name) throws BeansException {
        return this.doGetBean(name, (Class)null, (Object[])null, false);
    }

    public &lt;T&gt; T getBean(String name, Class&lt;T&gt; requiredType) throws BeansException {
        return this.doGetBean(name, requiredType, (Object[])null, false);
    }

    public Object getBean(String name, Object... args) throws BeansException {
        return this.doGetBean(name, (Class)null, args, false);
    }

    public &lt;T&gt; T getBean(String name, @Nullable Class&lt;T&gt; requiredType, @Nullable Object... args) throws BeansException {
        return this.doGetBean(name, requiredType, args, false);
    }

    protected &lt;T&gt; T doGetBean(String name, @Nullable Class&lt;T&gt; requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
        String beanName = this.transformedBeanName(name);
        Object sharedInstance = this.getSingleton(beanName);
        Object beanInstance;
        if (sharedInstance != null &amp;&amp; args == null) {
            beanInstance = this.getObjectForBeanInstance(sharedInstance, name, beanName, (RootBeanDefinition)null);
        } else {
            if (this.isPrototypeCurrentlyInCreation(beanName)) {
                throw new BeanCurrentlyInCreationException(beanName);
            }

            BeanFactory parentBeanFactory = this.getParentBeanFactory();
            if (parentBeanFactory != null &amp;&amp; !this.containsBeanDefinition(beanName)) {
                String nameToLookup = this.originalBeanName(name);
                if (parentBeanFactory instanceof AbstractBeanFactory) {
                    AbstractBeanFactory abf = (AbstractBeanFactory)parentBeanFactory;
                    return abf.doGetBean(nameToLookup, requiredType, args, typeCheckOnly);
                }
                if (args != null) {
                    return parentBeanFactory.getBean(nameToLookup, args);
                }
                if (requiredType != null) {
                    return parentBeanFactory.getBean(nameToLookup, requiredType);
                }
                return parentBeanFactory.getBean(nameToLookup);
            }

            if (!typeCheckOnly) {
                this.markBeanAsCreated(beanName);
            }

            try {
                RootBeanDefinition mbd = this.getMergedLocalBeanDefinition(beanName);
                this.checkMergedBeanDefinition(mbd, beanName, args);
                String[] dependsOn = mbd.getDependsOn();
                if (dependsOn != null) {
                    for(int var14 = 0; var14 &lt; var13; ++var14) {
                        String dep = prototypeInstance[var14];
                        ...
                        try {
                            this.getBean(dep);
                        } catch (NoSuchBeanDefinitionException var33) {
                            ...
                        } catch (BeanCreationException var34) {
                            ...
                        }
                    }
                }

                if (mbd.isSingleton()) {
                    sharedInstance = this.getSingleton(beanName, () -&gt; {
                        try {
                            return this.createBean(beanName, mbd, args);
                        } catch (BeansException var5) {
                            BeansException ex = var5;
                            this.destroySingleton(beanName);
                            throw ex;
                        }
                    });
                    beanInstance = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
                } else if (mbd.isPrototype()) {
                    prototypeInstance = null;

                    Object prototypeInstance;
                    try {
                        this.beforePrototypeCreation(beanName);
                        prototypeInstance = this.createBean(beanName, mbd, args);
                    } finally {
                        this.afterPrototypeCreation(beanName);
                    }

                    beanInstance = this.getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
                } else {
                    String scopeName = mbd.getScope();
                    if (!StringUtils.hasLength(scopeName)) {
                        throw new IllegalStateException(&quot;No scope name defined for bean &#39;&quot; + beanName + &quot;&#39;&quot;);
                    }

                    Scope scope = (Scope)this.scopes.get(scopeName);
                    if (scope == null) {
                        throw new IllegalStateException(&quot;No Scope registered for scope name &#39;&quot; + scopeName + &quot;&#39;&quot;);
                    }

                    try {
                        Object scopedInstance = scope.get(beanName, () -&gt; {
                            this.beforePrototypeCreation(beanName);

                            Object var4;
                            try {
                                var4 = this.createBean(beanName, mbd, args);
                            } finally {
                                this.afterPrototypeCreation(beanName);
                            }

                            return var4;
                        });
                        beanInstance = this.getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
                    }
                }
            }
        }

    }

}</code></pre><ul>
<li>최대한 필요한 부분만 추려보았습니다</li>
</ul>
<h3 id="1-빈-이름-변환">1. 빈 이름 변환</h3>
<ul>
<li>주어진 이름을 실제 빈 이름으로 변환하는 과정이며 빈 이름이 별칭일 수 있기 때문입니다 </li>
</ul>
<h3 id="2-싱글톤-캐시에서-빈-인스턴스-확인">2. 싱글톤 캐시에서 빈 인스턴스 확인</h3>
<ul>
<li><p>Spring 에서 싱글톤 객체는 디자인 패턴의 생성자를 이용한 싱글톤 패턴을 구현한 것이 아닙니다 </p>
</li>
<li><p>Spring 의 싱글톤 빈은 객체에 대한 전통적인 싱글톤 패턴을 코드로 구현한 것이 아니라 <strong><em>Spring 컨테이너가 싱글톤 범위로 관리</em></strong> 하여 구현된 것 입니다 </p>
</li>
<li><p>즉, 캐시를 통해서 싱글톤을 구현한 것 입니다 </p>
<ul>
<li><p><code>getBean()</code> 을 통해 Spring은 빈의 라이프사이클에 따라 객체를 생성합니다 </p>
</li>
<li><p>생성된 인스턴스는 <code>addSingleton()</code> 메서드를 통해 <code>singletoneObjects</code> 라는 캐시에 저장됩니다</p>
<pre><code class="language-java">public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100;
    private final Map&lt;String, Object&gt; singletonObjects = new ConcurrentHashMap(256);

  protected void addSingleton(String beanName, Object singletonObject) {
        synchronized(this.singletonObjects) {
            this.singletonObjects.put(beanName, singletonObject);
            this.singletonFactories.remove(beanName);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }

  @Nullable
  protected Object getSingleton(String beanName, boolean allowEarlyReference) {
      Object singletonObject = this.singletonObjects.get(beanName);
      if (singletonObject == null &amp;&amp; this.isSingletonCurrentlyInCreation(beanName)) {
          singletonObject = this.earlySingletonObjects.get(beanName);
          if (singletonObject == null &amp;&amp; allowEarlyReference) {
              synchronized(this.singletonObjects) {
                  singletonObject = this.singletonObjects.get(beanName);
                  if (singletonObject == null) {
                      singletonObject = this.earlySingletonObjects.get(beanName);
                      if (singletonObject == null) {
                          ObjectFactory&lt;?&gt; singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                      if (singletonFactory != null) {
                          singletonObject = singletonFactory.getObject();
                          this.earlySingletonObjects.put(beanName, singletonObject);
                          this.singletonFactories.remove(beanName);
                      }
                  }
              }
          }
      }
  }

  return singletonObject;
}</code></pre>
</li>
</ul>
</li>
<li><p>그래서 이미 생성된 싱글톤 빈이 캐시에 존재하는지 확인한 후 존재한다면 캐시된 인스턴스를 사용하게 되도록 합니다 (<code>this.getSingleton()</code>)</p>
</li>
</ul>
<h3 id="3-싱글톤-인스턴스-반환">3. 싱글톤 인스턴스 반환</h3>
<pre><code class="language-java">if (sharedInstance != null &amp;&amp; args == null) {
    // 캐시된 인스턴스를 반환 
}</code></pre>
<ul>
<li>만약 싱글톤이 존재하고 추가적인 생성자 인자가 없다면, 이미 존재하는 인스턴스를 반환합니다 </li>
<li>이 과정은 순한 참조를 방지하고 생성된 빈을 재사용하기 위해 사용됩니다 <ul>
<li>순환 참조 : 두 개 이상의 빈이 서로를 의존하고 있어 초기화할 때 무한 루프에 빠지는 상황 </li>
</ul>
</li>
</ul>
<h3 id="4-프로토타입-빈-생성-여부-확인">4. 프로토타입 빈 생성 여부 확인</h3>
<pre><code class="language-java">if (this.isPrototypeCurrentlyInCreation(beanName)) {
    // 예외
}</code></pre>
<ul>
<li>만약 프로토타입 빈이 현재 생성 중인 경우, 순환 의존성을 피하기 위해 예외를 던집니다 <ul>
<li>현재 스레드의 프로토타입 빈 정보를 가져온 후 <code>beanName</code> 과 같으면 순환 의존성이 생겼다고 파악합니다</li>
</ul>
</li>
</ul>
<h3 id="5-부모-빈-팩토리에서-조회">5. 부모 빈 팩토리에서 조회</h3>
<ul>
<li>현재 빈 팩토리에 빈 정의가 없고 부모 팩토리가 존재한다면, 부모 팩토리에서 해당 빈을 조회합니다 </li>
</ul>
<h3 id="6-새로운-빈-생성-준비">6. 새로운 빈 생성 준비</h3>
<pre><code class="language-java">if (!typeCheckOnly) {
    this.markBeanAsCreated(beanName);
}</code></pre>
<ul>
<li>빈을 생성할 준비를 합니다. 이는 빈이 처음으로 생성되고 있음을 표시하는 작업입니다</li>
</ul>
<h3 id="7-빈-정의-beandefinition-가져오기-및-종속성-확인">7. 빈 정의 (BeanDefinition) 가져오기 및 종속성 확인</h3>
<pre><code class="language-java">RootBeanDefinition mbd = this.getMergedLocalBeanDefinition(beanName);
this.checkMergedBeanDefinition(mbd, beanName, args);</code></pre>
<ul>
<li>빈 정의를 가져오고 해당 빈 정의가 유효한지 확인합니다. 종속성이 있는 경우, 필요한 종속성 빈들을 미리 가져와야 합니다 <ul>
<li><code>getMergedLocalBeanDefinition()</code> 은 특정 빈의 최종 <code>BeanDefinition</code> 을 반환하는 역할을 합니다 </li>
<li>이 과정에서 상속된 부모 빈 정의와 병합 작업을 수행합니다 </li>
<li><code>RootBeanDefinition</code> 과 같은 실제 빈 정의 객체를 반환합니다 </li>
</ul>
</li>
</ul>
<h3 id="8-종속성-처리">8. 종속성 처리</h3>
<pre><code class="language-java">String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
    // 종속된 빈을 순회하며 가져옵니다.
}</code></pre>
<ul>
<li><code>dependsOn</code> 속성을 통해 다른 빈에 대한 종속성을 확인하고, 필요한 빈들을 먼저 생성하여 등록합니다 </li>
<li>하지만 이것은 <code>@Autowired</code> 와는 다릅니다 </li>
<li><code>dependsOn</code> 은 일반적으로 빈 정의 수준에서 사용되며 특정 빈이 먼저 초기화되어야 할 때 사용됩니다 <ul>
<li>예를 들어, 데이터베이스 연결이 필요한 서비스 빈은 데이터 소스 빈에 의존할 수 있고, 이를 <code>dependsOn</code> 으로 설정할 수 있습니다 </li>
<li>즉, 의존성 주입이 아닌 빈에 대한 초기화 순서를 조정하기 위한 것 입니다 </li>
</ul>
</li>
</ul>
<h3 id="9-빈-생성-방식-결정">9. 빈 생성 방식 결정</h3>
<ul>
<li>싱글톤 빈의 경우 <code>createBean()</code> 을 호출하여 새로 생성하고 생성된 인스턴스를 싱글톤 캐시에 저장합니다 </li>
<li>프로토타입 빈은 매번 새로 생성하여 반환합니다 </li>
<li>커스텀 스코프가 정의된 경우 해당 스코프에서 빈을 생성하여 관리합니다 </li>
</ul>
</br>

<hr>
<h1 id="finishbeanfactoryinitialization"><code>finishBeanFactoryInitialization()</code></h1>
<ul>
<li>이제 모든 싱글톤 빈을 초기화하고 의존성을 주입합니다 </li>
<li>이 단계에서 <code>@Autowired</code> 가 실제로 동작합니다 <pre><code class="language-java">protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
  ...
  beanFactory.freezeConfiguration();
  beanFactory.preInstantiateSingletons();
}</code></pre>
</li>
<li><code>freezeConfiguration()</code> : 빈 팩토리의 설정을 &quot;동결&quot;합니다, 이 설정이 완료되면 빈 정의를 더 이상 변경할 수 없으며 애플리케이션의 안정적인 동작을 보장합니다 </li>
<li><code>preInstantiateSingletons()</code> : 메서드의 핵심으로 모든 싱글톤 빈을 미리 인스턴화합니다, 이 과정에서 <code>@Autowired</code> 어노테이션에 의한 의존성 주입이 실제로 발생합니다 </li>
</ul>
</br>

<h3 id="preinstantiatesingletons"><code>preInstantiateSingletons()</code></h3>
<ul>
<li><p><code>DefaultListableBeanFactory.preInstantiateSingletons()</code> 를 실행합니다 </p>
<pre><code class="language-java">  public void preInstantiateSingletons() throws BeansException {
      if (this.logger.isTraceEnabled()) {
          this.logger.trace(&quot;Pre-instantiating singletons in &quot; + this);
      }

      List&lt;String&gt; beanNames = new ArrayList(this.beanDefinitionNames);
      Iterator var2 = beanNames.iterator();

      String beanName;
      while(var2.hasNext()) {
          beanName = (String)var2.next();
          RootBeanDefinition bd = this.getMergedLocalBeanDefinition(beanName);
          if (!bd.isAbstract() &amp;&amp; bd.isSingleton() &amp;&amp; !bd.isLazyInit()) {
              if (this.isFactoryBean(beanName)) {
                  Object bean = this.getBean(&quot;&amp;&quot; + beanName);
                  if (bean instanceof SmartFactoryBean) {
                      SmartFactoryBean&lt;?&gt; smartFactoryBean = (SmartFactoryBean)bean;
                      if (smartFactoryBean.isEagerInit()) {
                          this.getBean(beanName);
                      }
                  }
              } else {
                  this.getBean(beanName);
              }
          }
      }

      var2 = beanNames.iterator();

      while(var2.hasNext()) {
          beanName = (String)var2.next();
          Object singletonInstance = this.getSingleton(beanName);
          if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) {
              StartupStep smartInitialize = this.getApplicationStartup().start(&quot;spring.beans.smart-initialize&quot;).tag(&quot;beanName&quot;, beanName);
              smartSingleton.afterSingletonsInstantiated();
              smartInitialize.end();
          }
      }
  }</code></pre>
</li>
<li><p><code>this.beanDefinitionNames</code> 는 이전에 <code>load()</code> 를 통해서 이미 <code>beanDefinitionMap</code> 을 만들고 <code>beanDefinitionNames</code>에 모든 빈을 추가했습니다 </p>
</li>
<li><p><code>getBean()</code> 을 찾아가 보면 <code>createBean()</code> 을 실행하는 걸 볼 수 있습니다.</p>
</li>
<li><p>구현체는 <code>AbstractAutowireCapableBeanFactory.createBean()</code> 입니다 </p>
<ul>
<li>DefaultListableBeanFactory -&gt; AbstractAutowireCapableBeanFactory</li>
</ul>
</li>
<li><p>이후 <code>createBean()</code> 은 내부 메서드인 <code>doCreateBean()</code> 을 호출하게 됩니다 </p>
</li>
<li><p><code>doCreateBean()</code> 은 <code>populateBean()</code> 메서드를 호출하게 됩니다 </p>
<ul>
<li>빈을 채운다 (&quot;populate&quot;), 이는 빈의 내부 상태를 설정하고 필요한 의존성을 채워 넣는 과정을 표현함 <pre><code class="language-java">if (this.hasInstantiationAwareBeanPostProcessors()) {
if (pvs == null) {
    pvs = mbd.getPropertyValues();
}
PropertyValues pvsToUse;
for (Iterator var11 = this.getBeanPostProcessorCache().instantiationAware.iterator(); var11.hasNext(); pvs = pvsToUse) {
    InstantiationAwareBeanPostProcessor bp = (InstantiationAwareBeanPostProcessor)var11.next();
    pvsToUse = bp.postProcessProperties((PropertyValues)pvs, bw.getWrappedInstance(), beanName);
    if (pvsToUse == null) {
        return;
    }
}
}</code></pre>
</li>
</ul>
</li>
<li><p><code>InstantiationAwareBeanPostProcessor</code> 를 통해 <code>@Autowired</code> 와 같은 어노테이션이 있는 필드에 값을 주입하는 작업이 시작됩니다 </p>
</li>
<li><p><code>getBeanPostProssorCache()</code> 를 통해 이전에 가져온 <code>BeanPostProcessor</code> 들을 모두 가져옵니다 </p>
<pre><code class="language-java">static class BeanPostProcessorCache {
  final List&lt;InstantiationAwareBeanPostProcessor&gt; instantiationAware = new ArrayList();
  final List&lt;SmartInstantiationAwareBeanPostProcessor&gt; smartInstantiationAware = new ArrayList();
  final List&lt;DestructionAwareBeanPostProcessor&gt; destructionAware = new ArrayList();
  final List&lt;MergedBeanDefinitionPostProcessor&gt; mergedDefinition = new ArrayList();

  BeanPostProcessorCache() {
  }
}</code></pre>
</li>
<li><p>각각의 List 는 빈의 라이프 사이클과 연관되어 있습니다 </p>
<ul>
<li><code>InstantiationAwareBeanPostProcessor</code> : 빈이 인스턴스화될 때와 프로퍼티가 설정될 때 개입됨, 이 프로세서가 빈의 인스턴스화와 의존성 주입 시점에 직접적으로 영향을 미침 </li>
<li><code>SmartInstantiationAwareBeanPostProcessor</code> : 위 인터페이스의 하위 인터페이스이며 인스턴스화 과정에서 더욱 세밀하게 제어할 수 있는 기능을 제공함 </li>
<li><code>DestructionAwareBeanPostProcessor</code> : 빈의 소멸 단계에서 개입하는 프로세서이며 주로 <code>destroy</code> 메서드를 호출하거나, 빈이 컨테이너에서 제거될 때 필요한 작업을 처리함 </li>
<li><code>MergedBeanDefinitionPostProcessor</code> : 빈 정의가 병합된 후, 정의를 수정하거나 추가 작업을 수행하는 프로세서, 빈의 메타 데이터를 변경하거나 어노테이션 정보를 처리하는데 사용함 <pre><code class="language-java">pvsToUse = bp.postProcessProperties((PropertyValues)pvs, bw.getWrappedInstance(), beanName);</code></pre>
</li>
</ul>
</li>
<li><p><code>postProcessProperties()</code> 메서드를 통해 빈 후처리가 시작되며 <code>AutowiredAnnotationBeanPostProcessor.postProcessProperties()</code> 메서드가 실행되며 이때 의존성 주입이 시작됩니다 </p>
<pre><code class="language-java">public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
  InjectionMetadata metadata = this.findAutowiringMetadata(beanName, bean.getClass(), pvs);

  try {
      metadata.inject(bean, beanName, pvs);
      return pvs;
  } catch (BeanCreationException var6) {
      BeanCreationException ex = var6;
      throw ex;
  } catch (Throwable var7) {
      Throwable ex = var7;
      throw new BeanCreationException(beanName, &quot;Injection of autowired dependencies failed&quot;, ex);
  }
}</code></pre>
</li>
<li><p><code>InjectionMetadata</code> 클래스의 <code>inject()</code> 메서드를 통해 실제 의존성 주입을 하게됩니다 </p>
<pre><code class="language-java">public class InjectionMetadata {

  public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
      Collection&lt;InjectedElement&gt; checkedElements = this.checkedElements;
      Collection&lt;InjectedElement&gt; elementsToIterate = checkedElements != null ? checkedElements : this.injectedElements;
      if (!((Collection)elementsToIterate).isEmpty()) {
          Iterator var6 = ((Collection)elementsToIterate).iterator();

          while(var6.hasNext()) {
              InjectedElement element = (InjectedElement)var6.next();
              element.inject(target, beanName, pvs);
          }
      }

  }
}</code></pre>
</li>
<li><p><code>inject()</code> 메서드에서는 빈들을 순환하면서 <code>@Autowired</code> 등의 어노테이션이 적용된 필드나 메서드에 의존성을 주입합니다</p>
</li>
</ul>
</br>

<hr>
<h1 id="정리">정리</h1>
<ul>
<li><code>load()</code> 를 통해 빈을 조회하기 쉽도록 mapping table을 생성합니다<ul>
<li>이때 빈 정의 (<code>BeanDefinition</code>)을 모두 조회함 </li>
</ul>
</li>
<li><code>refreshContext()</code> 를 통해 빈 후처리기 (<code>BeanPostProcessor</code>) 를 등록합니다 <ul>
<li>이때 <code>@Autowired</code> 의 후처리기인 <code>AutoWiredAnnotationBeanPostProcessor</code> 가 등록됩니다 </li>
</ul>
</li>
<li><code>finishBeanFactoryInitialization()</code> 를 통해 미리 조회한 빈을 가져온 후 빈으로 등록하며 생성하면서 빈 후처리기를 실행합니다 <ul>
<li><code>beanFactory.getBean()</code> </li>
<li><code>InjectionMetadata.inject()</code></li>
</ul>
</li>
</ul>
</br>

<blockquote>
<p>참고.</p>
</blockquote>
<h4 id="beanfactory--prefix">BeanFactory, <code>&amp;</code> Prefix</h4>
<ul>
<li>빈 이름에는 <code>&amp;</code> 프리픽스가 붙어있는 빈 이름들이 있는데 이는 <code>getBean(&quot;&amp;test&quot;)</code> 호출 시 <code>getFactoryBean(&quot;test&quot;)</code> 를 통해 <code>FactoryBean</code> 을 직접 반환하는 역할하는 하는 프리픽스입니다 <ul>
<li><code>FactoryBean</code> 은 빈을 생성하는 팩토리 역할을 하는 특별한 스프링 빈입니다<pre><code class="language-java">public interface BeanFactory {
String FACTORY_BEAN_PREFIX = &quot;&amp;&quot;;
...
}</code></pre>
<h4 id="value-어노테이션-처리"><code>@Value</code> 어노테이션 처리</h4>
</li>
</ul>
</li>
<li><code>finishBeanFactoryInitialization()</code> 메서드 과정에서 <code>@Value(&quot;${mail.host}&quot;)</code> 와 관련된 로직도 구현되어 있다 <pre><code class="language-java">if (!beanFactory.hasEmbeddedValueResolver()) {
  beanFactory.addEmbeddedValueResolver((strVal) -&gt; { 
      return this.getEnvironment().resolvePlaceholders(strVal); 
  }
);</code></pre>
</li>
<li>이 리졸버는 <code>${}</code> 형식의 플레이스 홀더를 실제 값으로 치환하는 역할을 하며, 환경 변수나 설정 파일의 값을 해결하는데 사용됩니다<ul>
<li><code>hasEmbeddedValueResolver()</code> 는 <code>BeanFactory</code> 에 값 리졸버가 등록되어 있는지 확인하는 부분이며 만약, 등록되어 있지 않다면 새로운 리졸버를 추가합니다 </li>
<li><code>addEmbeddedValueResolver()</code> 는 <code>${}</code> 형식의 플레이스 홀더를 처리하기 위해 리졸버를 추가하는 작업이며, 이 리졸버는 주로 <code>@Value</code> 어노테이션에서 사용되는 플레이스홀더를 실제 값으로 변환하는 역할을 합니다 </li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Spring IoC 컨테이너와 SpringApplication 실행 흐름 정리 (Spring Boot 3.3.3 기준)]]></title>
            <link>https://velog.io/@dlaudrb09-/Spring-IoC-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-BeanFactory-SpringApplication-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dlaudrb09-/Spring-IoC-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-BeanFactory-SpringApplication-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 16 Sep 2024 11:57:29 GMT</pubDate>
            <description><![CDATA[<p>Spring에 대한 정리글입니다. 주로 IoC 컨테이너, BeanFactory, 그리고 Spring Boot 애플리케이션의 시작 흐름에 대해 다룹니다.</p>
<blockquote>
<p>참고: SpringApplication 클래스는 Spring Boot에서만 사용되며, 일반 Spring에서는 수동으로 초기화가 필요합니다.</p>
</blockquote>
<hr>
<h1 id="스프링-ioc-컨테이너">스프링 IoC 컨테이너</h1>
<h3 id="beanfactory">BeanFactory</h3>
<ul>
<li>Spring IoC 컨테이너의 최상위 인터페이스</li>
<li><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/BeanFactory.html">BeanFactory 공식 문서</a></li>
<li>애플리케이션 구성 요소의 중앙 레지스트리 역할</li>
</ul>
<h3 id="applicationcontext">ApplicationContext</h3>
<ul>
<li>가장 많이 사용하는 IoC 컨테이너</li>
<li><code>BeanFactory</code>를 확장하며 다양한 기능 포함: <code>EnvironmentCapable</code>, <code>ListableBeanFactory</code>, <code>HierarchicalBeanFactory</code>, <code>ResourceLoader</code></li>
<li><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ApplicationContext.html">ApplicationContext 공식 문서</a></li>
</ul>
<h3 id="listablebeanfactory">ListableBeanFactory</h3>
<ul>
<li>여러 빈을 나열하거나 조회 가능</li>
<li><code>getBeanNamesForType()</code>, <code>getBeansOfType()</code> 등 제공<ul>
<li>특정 유형의 빈 목록 조회 가능</li>
</ul>
</li>
<li><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/ListableBeanFactory.html">ListableBeanFactory 공식 문서</a></li>
</ul>
<h3 id="hierarchicalbeanfactory">HierarchicalBeanFactory</h3>
<ul>
<li>부모 빈 팩토리를 참조할 수 있는 기능</li>
<li>계층적 구조로 빈을 탐색하며 존재하지 않을 경우 부모 팩토리에서 탐색<ul>
<li><code>getParentBeanFactory()</code> 메서드를 통해 현재 빈 팩토리의 부모 빈 팩토리를 알 수 있으며 만약, 없을 경우 <code>null</code> 을 반환</li>
<li>계층적인 구조로 되어 있으므로 빈을 먼저 현재 빈 팩토리에서 찾고 없을 경우 부모 빈 팩토리에서 계속해서 탐색(<code>setParentBeanFactory(parentFactory)</code>)</li>
</ul>
</li>
<li><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/HierarchicalBeanFactory.html">HierarchicalBeanFactory 공식 문서</a></li>
</ul>
<h3 id="resourceloader">ResourceLoader</h3>
<ul>
<li>리소스 로딩 기능 (클래스패스, 파일 시스템 등)<ul>
<li>웹에서의 파일 등 리소스를 로드하게끔 하는 기능을 가진 인터페이스</li>
</ul>
</li>
<li><code>ResourcePatternResolver</code>를 통해 여러 리소스를 위치 패턴으로 검색 가능</li>
<li><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/ResourceLoader.html">ResourceLoader 공식 문서</a></li>
</ul>
</br>

<hr>
</br>

<h1 id="spring-boot-의-시작점">Spring Boot 의 시작점</h1>
<p>Spring Boot 애플리케이션은 <code>main()</code> 메서드 내에서 <code>SpringApplication.run()</code>을 호출하면서 시작됩니다.</p>
<pre><code class="language-java">public static void main(String[] args) {
    SpringApplication.run(MyApplication.class, args);
}</code></pre>
<ul>
<li><p><code>ApplicationContext</code> 인스턴스를 생성하며 빈들을 스캔하고, 설정에 따라 각 빈을 생성하고 구성합니다.</p>
</li>
<li><p>그 과정에서 다양한 이벤트가 발생하며, 미리 등록된 리스너들이 해당 이벤트를 수신하여 처리합니다.</p>
</li>
<li><p><code>application.yml</code> 또는 <code>application.properties</code> 파일, 그리고 커맨드라인 인자에서 환경설정을 읽고 이를 환경 변수로 등록합니다.</p>
</li>
</ul>
<p>이 전체 과정은 내부적으로 <code>BeanFactory</code>와 연결되며, IoC 컨테이너가 실제로 동작하는 핵심 흐름과도 밀접합니다</p>
</br>

<ul>
<li><p><code>HierarchicalBeanFactory</code>는 현재 빈 팩토리뿐 아니라 상위 빈 팩토리를 참조할 수 있는 구조를 가지고 있습니다.</p>
</li>
<li><p>빈 조회는 <code>getBean()</code> 메서드를 통해 수행되며, 대부분의 실제 운영 환경에서는 이 메서드를 직접 호출하지 않고, 주입받는 방식(<code>@Autowired</code> 등)을 통해 사용됩니다.</p>
</li>
<li><p>빈을 주입받는 과정에서, 컨테이너는 이름이나 타입에 기반하여 알맞은 빈을 검색합니다.</p>
<ul>
<li><strong>빈 조회 방식</strong> : 
주입할 빈을 찾을 때, 먼저 현재 컨텍스트에서 우선적으로 검색한 후 해당 빈이 존재하지 않을 경우 상위 컨텍스트 (부모 빈 팩토리) 로 검색을 확장
이는 <code>HierarchicalBeanFactory</code> 가 제공하는 계층적 탐색 구조에 기반합니다 </li>
</ul>
</li>
<li><p>이후, <code>refreshContext()</code> 메서드가 호출되어 IoC 컨테이너의 최종 초기화가 이루어지며, 모든 빈이 등록되고 초기화됩니다.</p>
</li>
</ul>
</br>

<h3 id="번외-listablebeanfactory-는-언제-사용될까-">번외) <code>ListableBeanFactory</code> 는 언제 사용될까 ?</h3>
<ul>
<li>여러 개의 빈을 동시에 조회하거나, 특정 조건에 맞는 빈을 나열할 때 유용합니다.</li>
<li>대부분의 Spring 애플리케이션에서는 개발자가 직접 사용하는 경우는 드물고, Spring 내부 매커니즘 또는 자동 구성 기능에서 활용됩니다.</li>
</ul>
<p>예시:</p>
<ul>
<li><strong>자동 구성 (Auto-Configuration)</strong><ul>
<li>자동 구성 과정에서 특정 타입의 빈을 일괄적으로 탐색해야 할 때 <code>getBeansOfType()</code> 메서드가 활용됩니다. </li>
</ul>
</li>
<li><strong>조건부 구성 (Conditional Configuration)</strong><ul>
<li><code>@Conditional</code> 어노테이션과 함께 사용되며, 특정 조건에 맞는 빈을 활성화하거나 비활성화할 때 내부적으로 빈 목록을 확인해야 할 필요가 있습니다.</li>
</ul>
</li>
<li><strong>초기화 시 빈 확인</strong><ul>
<li>애플리케이션 컨텍스트 초기화 시 모든 빈이 제대로 등록되었는지 확인하는 과정에서 <code>getBeanDefinitionNames()</code> 등의 메서드가 호출되며, 설정 누락이나 충돌 여부를 검증하는 데 사용됩니다.</li>
</ul>
</li>
</ul>
</br>

<blockquote>
<p>참고 : 왜 객체를 빈으로 등록할까 ?</p>
</blockquote>
<ul>
<li><strong>의존성 관리 (IoC)</strong><ul>
<li>외부 객체에 의존하는 로직을 테스트하기 쉬움 (예: Mock 주입)</li>
<li>비즈니스 도메인이 특정 객체에 의존하고 있다면, 의존성 주입을 통해 가짜 객체(Mock)로 대체하여 독립적인 테스트가 가능함</li>
</ul>
</li>
<li><strong>스코프 관리</strong><ul>
<li><code>singleton</code>: 동일한 인스턴스를 재사용해야 하는 경우 유용 </li>
<li><code>prototype</code>: 요청마다 새로운 인스턴스를 생성해야 할 때 사용</li>
</ul>
</li>
<li><strong>라이프사이클 관리</strong><ul>
<li>객체 초기화 및 소멸 단계에서 필요한 작업을 자동으로 처리할 수 있음</li>
<li><code>@PostConstruct</code>, <code>@PreDestroy</code>, 혹은 <code>InitializingBean</code>, <code>DisposableBean</code> 인터페이스를 통해 정의 가능</li>
</ul>
</li>
</ul>
</br>

<h1 id="springapplicationrun">SpringApplication.run()</h1>
<pre><code class="language-java">public ConfigurableApplicationContext run(String... args) {
    Startup startup = SpringApplication.Startup.create();

    if (this.registerShutdownHook) {
        shutdownHook.enableShutdownHookAddition();
    }

    DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
    ConfigurableApplicationContext context = null;
    this.configureHeadlessProperty();
    SpringApplicationRunListeners listeners = this.getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass);

    Throwable ex;
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        Banner printedBanner = this.printBanner(environment);
        context = this.createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);
        this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
        this.refreshContext(context);
        this.afterRefresh(context, applicationArguments);
        startup.started();
        if (this.logStartupInfo) {
            (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), startup);
        }

        listeners.started(context, startup.timeTakenToStarted());
        this.callRunners(context, applicationArguments);
    } catch (Throwable var10) {
        ex = var10;
        throw this.handleRunFailure(context, ex, listeners);
    }

    try {
        if (context.isRunning()) {
            listeners.ready(context, startup.ready());
        }

        return context;
    } catch (Throwable var9) {
        ex = var9;
        throw this.handleRunFailure(context, ex, (SpringApplicationRunListeners)null);
    }
}</code></pre>
<p>위 <code>run()</code> 메서드는 Spring Boot 애플리케이션이 시작될 때 내부적으로 호출되는 핵심 메서드입니다. 
이 메서드 내부에서는 애플리케이션의 컨텍스트를 구성하고, 필요한 설정을 초기화하며, 사용자 정의 로직을 실행하기 위한 준비 과정을 수행합니다. </p>
<p>이후의 설명에서는 각 단계별 역할과 작동 방식에 대해 소스 기반으로 자세히 설명합니다.</p>
</br>

<h2 id="1-startup-초기화">1. <code>Startup</code> 초기화</h2>
<ul>
<li><p>애플리케이션의 실행 시간 측정을 위해 <code>Startup</code> 객체를 생성합니다.</p>
</li>
<li><p>이 객체는 애플리케이션의 시작 시간, 프로세스 가동 시간 등의 정보를 기록하며, 나중에 로깅 등에 활용됩니다.</p>
<pre><code class="language-java">abstract static class Startup {
  private Duration timeTakenToStarted;

  Startup() {
  }

  protected abstract long startTime();
  protected abstract Long processUptime();
  protected abstract String action();
  ...
}     </code></pre>
</li>
</ul>
<p>사용 예시:</p>
<pre><code class="language-java">public ConfigurableApplicationContext run(String... args) {
    Startup startup = SpringApplication.Startup.create();
    ...
}</code></pre>
<ul>
<li><code>run()</code> 메서드의 가장 첫 줄에서 <code>Startup.create()</code> 를 통해 해당 인스턴스를 생성합니다.</li>
</ul>
</br>


<h2 id="2-종료-후크-설정shutdown-hook">2. 종료 후크 설정(Shutdown Hook)</h2>
<pre><code class="language-java">if (this.registerShutdownHook) {
    shutdownHook.enableShutdownHookAddition();
}</code></pre>
<ul>
<li><p><code>registerShutdownHook</code> 플래그가 true일 경우, 애플리케이션 종료 시 실행될 후처리 작업을 등록합니다.</p>
</li>
<li><p>이는 JVM이 종료될 때 실행되는 스레드이며, 스프링 컨텍스트가 종료되지 않은 경우 이를 안전하게 종료하고 관련 리소스를 정리합니다.</p>
</li>
<li><p>내부적으로는 <code>AbstractApplicationContext.registerShutdownHook()</code> 메서드가 호출되어 <code>doClose()</code> 를 수행하게 되며, 이 과정에서 모든 싱글톤 빈의 <code>@PreDestroy</code> 메서드나 <code>DisposableBean</code> 구현체가 호출됩니다.</p>
</li>
</ul>
</br>

<h2 id="3--bootstrapcontext-생성">3.  BootstrapContext 생성</h2>
<ul>
<li><p>createBootstrapContext()를 호출하여 초기 설정에 사용할 임시 컨텍스트를 생성합니다.</p>
</li>
<li><p>Spring Boot 애플리케이션의 초기화 과정에서 사용되는 일종의 임시 컨텍스트이며 <code>ApplicationContext</code> 가 완전히 준비되기 전에 초기화 작업에 필요한 리소스와 설정을 관리, 애플리케이션 시작 시 여러 설정 작업을 돕는 역할을 수행합니다.</p>
</li>
<li><p>부트스트랩 컨텍스트는 전체 컨텍스트에 공유되어야 하는 초기 컴포넌트를 저장하고, 이후 실제 컨텍스트 초기화 시 함께 넘겨집니다.</p>
</li>
</ul>
</br>

<h2 id="4-사전-준비-작업">4. 사전 준비 작업</h2>
<ul>
<li><p><code>context</code> 변수 선언 (실제 <code>ApplicationContext</code> 가 나중에 할당될 자리)</p>
</li>
<li><p><code>configureHeadlessProperty()</code> 호출: 서버 환경(GUI 없는 환경)에서 문제 없이 실행될 수 있도록 AWT 환경 설정을 비활성화합니다.</p>
</li>
<li><p>실행 과정에서 발생하는 다양한 이벤트 처리를 위한 리스너 초기화 </p>
</li>
<li><p>초기화된 리스너들에게 애플리케이션이 시작되고 있음을 알리기 위해 <code>starting</code> 이벤트를 발생 </p>
</li>
<li><p>애플리케이션 실행 시 전달된 커맨드라인 인자를 처리하기 위해 <code>ApplicationArguments</code> 객체를 생성 </p>
<ul>
<li>애플리케이션이 시작할 때 전달된 인자들을 파싱하고 관리하는 역할 </li>
</ul>
</li>
</ul>
</br>

<h2 id="5-환경environment-설정">5. 환경(Environment) 설정</h2>
<pre><code class="language-java">ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
</code></pre>
<ul>
<li><code>prepareEnvironment()</code> 는 다음과 같은 여러 환경 소스를 통합하여 Environment 객체를 구성합니다 (런타임시에 이루어짐)<ul>
<li>시스템 환경 변수</li>
<li>JVM 시스템 프로퍼티</li>
<li><code>application.yml</code> 혹은 <code>application.properties</code></li>
<li><code>-key=value</code> 형식의 커맨드라인 인자</li>
</ul>
</li>
<li>설정이 완료되면 <code>ApplicationEnvironmentPreparedEvent</code> 이벤트가 발행되며, 이를 통해 리스너들이 환경 관련 후처리를 진행할 수 있게 됩니다. </li>
</ul>
</br>

<h2 id="6-배너-출력">6. 배너 출력</h2>
<pre><code class="language-java">Banner printedBanner = this.printBanner(environment);</code></pre>
<ul>
<li>기본적으로 Spring Boot 로고가 ASCII 아트 형태로 출력되며, 환경 설정 또는 커스텀 배너 클래스를 통해 변경 가능함.</li>
</ul>
</br>


<h2 id="7-applicationcontext-생성">7. <code>ApplicationContext</code> 생성</h2>
<pre><code class="language-java">context = this.createApplicationContext();</code></pre>
<ul>
<li><p><code>ApplicationContextFactory</code> 인터페이스를 통해 생성되며 <code>DEFAULT = new DefaultApplicationContextFactory();</code> 로 생성 (커스텀 팩토리 추가 가능)</p>
<ul>
<li><code>webApplicationType</code> 에 맞는 컨텍스트 생성<ul>
<li><code>NONE</code>: CLI 또는 백그라운드 작업 (서블릿 X)</li>
<li><code>SERVLET</code>: 서블릿 기반 웹 애플리케이션 (Tomcat, Jetty 등)</li>
<li><code>REACTIVE</code>: WebFlux 기반의 비동기/논블로킹 애플리케이션</li>
</ul>
</li>
</ul>
</li>
<li><p><code>webApplicationType</code> 은 클래스패스에 있는 의존성을 통해 자동으로 감지되며, 우선순위는 <code>REACTIVE</code> &gt; <code>SERVLET</code> &gt; <code>NONE</code> 순서입니다.</p>
<ul>
<li><code>Reactor Netty</code> 와 같은 반응형 서버가 있다면 <code>REACTIVE</code>로 설정</li>
<li><code>spring-boot-starter-web</code> 의존성이라면 서블릿 기반 애플리케이션을 만듦 </li>
</ul>
</li>
</ul>
</br>

<h2 id="8-context-초기화-및-실행-준비-완료">8. context 초기화 및 실행 준비 완료</h2>
<pre><code class="language-java">this.prepareContext(...);</code></pre>
<ul>
<li>생성된 <code>ApplicationContext</code> 에 필요한 정보(환경, 리스너, 설정값 등)를 주입하는 단계 </li>
<li>아직 컨텍스트는 리프레시 (<code>refresh</code>) 되지 않은 상태이며, 다음과 같은 작업이 수행됨 <ul>
<li>Environment 주입</li>
<li>리스너 등록</li>
<li>초기화할 빈 설정</li>
<li>부트스트랩 컨텍스트에서 필요한 <code>Bean</code> 전달</li>
</ul>
</li>
</ul>
<blockquote>
<p>참고 
<strong>환경 설정이 다시 필요한 이유 ?</strong> 
부트스트랩 컨텍스트에서 이미 일부 환경 설정을 수행했지만 애플리케이션 컨텍스트의 환경을 설정하고 보강하는 작업이 추가로 필요하다. </p>
</blockquote>
<ul>
<li><p>전체 애플리케이션의 컨텍스트의 환경(<code>Environment</code>) 을 설정하며 부트스트랩에서 설정되지 않은 추가 환경 변수, 프로파일, 설정 파일을 처리함 </p>
</li>
<li><p><strong>리스너 설정은 왜 또 하는건가 ?</strong>  </p>
<ul>
<li><code>prepareContext</code> 에서는 리스너가 애플리케이션 실행 과정에서 발생하는 이벤트들을 처리하도록 세팅하는 부분 </li>
<li><code>listeners.starting()</code> 은 애플리케이션 시작 시의 이벤트만을 처리하는 초기 단계이며 이후 단계에서 리스너들은 다른 종류의 이벤트 (<code>ContextRefreshedEvent</code> 등) 을 처리할 준비를 함 </li>
</ul>
</li>
<li><p><strong>리프레시 상태란 ?</strong> </p>
<ul>
<li>애플리케이션 컨텍스트가 완전히 초기화되고 모든 빈이 생성되고 설정이 끝난 상태를 말함 </li>
<li><code>refresh()</code> 가 완료되면 모든 빈이 준비되고 이벤트가 발행되며 애플리케이션이 실행될 준비가 완전히 완료된 상태가 됨 </li>
<li>이 과정이 끝나야 애플리케이션이 정상적으로 작동할 수 있음 </li>
</ul>
</br>

</li>
</ul>
<h1 id="9-context-리프레시">9. Context 리프레시</h1>
<pre><code class="language-java">this.refreshContext(context);</code></pre>
<ul>
<li><p>내부적으로 <code>AbstractApplicationContext.refresh()</code>가 호출되며 본격적인 IoC 컨테이너 초기화가 진행됨</p>
</li>
<li><p>주요 단계</p>
<ul>
<li>빈 팩토리 생성 및 설정 정보 로딩</li>
<li><code>@ComponentScan</code> 및 수동 등록 빈 처리</li>
<li><code>BeanPostProcessor</code>, <code>BeanFactoryPostProcessor</code> 실행<ul>
<li>이 단계에서 빈의 설정을 변경하거나 추가적인 처리를 할 수 있음 </li>
</ul>
</li>
<li>싱글톤 빈 초기화</li>
<li><code>ContextRefreshedEvent</code> 발행<ul>
<li>해당 이벤트 리스너들이 이벤트를 처리</li>
</ul>
</li>
</ul>
</br>

</li>
</ul>
<h1 id="10-후처리-및-실행">10. 후처리 및 실행</h1>
<ul>
<li><code>afterRefresh()</code> : 컨텍스트가 완전히 초기화된 후 추가 작업을 수행하기 위해 호출, 기본 구현은 비어 있음</li>
<li><code>Startup.started()</code> : 실행 시간 측정을 종료</li>
<li><code>StartupInfoLogger</code> 를 통해 애플리케이션 시작 로그 출력</li>
<li><code>listeners.started()</code> : 시작 완료 이벤트 발행</li>
<li><code>callRunners()</code> : <code>ApplicationRunner</code>, <code>CommandLineRunner</code> 를 구현한 Bean들을 호출</li>
</ul>
</br>

<h1 id="11-애플리케이션-준비-완료">11. 애플리케이션 준비 완료</h1>
<pre><code class="language-java">if (context.isRunning()) {
    listeners.ready(context, startup.ready());
}</code></pre>
<ul>
<li>애플리케이션이 정상적으로 실행되고 있는 경우 <code>ApplicationReadyEvent</code> 를 발행하여 모든 설정 및 로딩이 완료되었음을 알립니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[QueryDSL] SemanticException: Query specified join fetching, but the owner of the fetched association was not present in the select list ]]></title>
            <link>https://velog.io/@dlaudrb09-/QueryDSL-SemanticException-Query-specified-join-fetching-but-the-owner-of-the-fetched-association-was-not-present-in-the-select-list</link>
            <guid>https://velog.io/@dlaudrb09-/QueryDSL-SemanticException-Query-specified-join-fetching-but-the-owner-of-the-fetched-association-was-not-present-in-the-select-list</guid>
            <pubDate>Thu, 05 Sep 2024 14:45:54 GMT</pubDate>
            <description><![CDATA[<p>쿼리 DSL 을 사용하다가 N+1 문제가 발생하였다 
원인은 <code>fetchJoin</code> 을 실행하지 않고 엔티티의 연관관계 엔티티를 바로 가져오기 때문에 발생한 이슈였다 </p>
<p>원인을 알고있으니 바로 수정후 쿼리를 실행해보았다 </p>
<p>그러나 또 다른 이슈가 발생하였다 </p>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/f1446e46-7828-49fd-bdf8-6269a850b447/image.png" alt=""></p>
<p>조금 찾아보니 </p>
<p>fetch된 엔티티의 소유자가 select 리스트에 포함되지 않아서 발생한 문제이며 쿼리에서 fetch join을 사용하면서 fetch된 연관관계의 주인이 쿼리의 select 리스트에 포함되지 않은 경우 발생하는 Hibernate의 SemanticException 이라고 한다.</p>
<p>그래서 쿼리 DSL 코드를 쭉 훑어보니 현재 pagination 쿼리를 사용하고 있는 API 이며, 관련해서 total 개수를 가져오기 위해 쿼리를 사용할 때 fetch join 을 하지만 사실 개수만 조회하기 때문에 select 절에는 join 된 엔티티가 포함되지 않는 쿼리가 있었고 이 부분이 범인이었다 </p>
<pre><code class="language-kotlin">val total =
    queryFactory
        .select(task.count())
        .from(task)
        ...
        .join(task.orderDetail, orderDetail)
        .fetchJoin()
        .where(task.routing.eq(routing))
        .fetchOne() ?: 0L
</code></pre>
<p>아래처럼 수정하니 정상 동작하였다 </p>
<pre><code class="language-kotlin"> val total =
     queryFactory
         .select(task)
         .from(task)
         .join(task.orderDetail, orderDetail)
         .where(task.routing.eq(routing))
         .fetchCount()</code></pre>
<p>사실 total 쿼리에 대해서는 fetch join 이 굳이 필요하지 않았다 
이외에도 찾아보니 </p>
<p>QueryProjection 을 이용하여 DTO 를 사용할때도 fetch join 을 쓰는것은 같은 예외가 발생하게 된다</p>
<p>fetch join 을 사용하는 이유는 엔티티 상태에서 엔티티 그래프를 참조 및 엔티티 그래프를 효율적으로 로딩하기 위해 사용하는 것이며, 따라서 엔티티가 아닌 DTO 상태로 조회하는 것은 불가능하다고한다.</p>
<p>그러면 Projection 만 사용할 때의 동작을 알아보자</p>
<p>Projection만 사용할 때의 동작</p>
<ul>
<li><p>Projection만 사용하고 fetch join을 하지 않으면, QueryDSL이 자동으로 필요한 필드에 대해서만 join을 수행, 하지만 이는 fetch join이 아니라 일반적인 join으로 실행된다 </p>
</li>
<li><p>QueryDSL은 Projection에서 요청한 필드들을 분석하여 필요한 join을 자동으로 생성</p>
</li>
<li><p>이 join은 필요한 데이터만 가져오는 일반 join이며, 연관 엔티티 전체를 즉시 로딩하는 fetch join과는 다르다 </p>
</li>
<li><p>이는 fetch join과 달리 연관 엔티티 전체를 메모리에 로딩하지 않음, Projection을 사용할 때는 fetch join이 필요하지 않으며, QueryDSL이 필요한 데이터만을 효율적으로 가져오도록 쿼리를 최적화</p>
</li>
</ul>
<p>또한 Projection을 사용할 때는 N+1 문제에 주의해야 한다, 연관 엔티티의 데이터를 가져올 때 추가적인 쿼리가 발생할 수 있기 때문.</p>
<p>번외로 일반 Join 과 Fetch Join 의 비교 </p>
<ul>
<li>일반 Join: 연관된 엔티티의 데이터를 가져오지만, 실제 엔티티 객체로 변환하지 않는다</li>
<li>Fetch Join: 연관된 엔티티를 함께 조회하여 영속 상태의 엔티티 객체로 변환함<ul>
<li>Fetch Join을 사용하면, 연관된 엔티티들이 모두 영속 상태로 로딩되어 엔티티 그래프를 완전히 구성, 이는 N+1 문제를 해결하고, 성능을 최적화하는 데 도움이 됨</li>
</ul>
</li>
</ul>
<p>DTO 는 단순히 데이터 전송 객체이며 엔티티가 아니기 때문에 Fetch Join 의 개념이 적용되지 않음 
(엔티티간의 관계를 유지하지 않음)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[주문 서비스 성능 개선: 주문 데이터 조회 최적화]]></title>
            <link>https://velog.io/@dlaudrb09-/%EC%BF%BC%EB%A6%AC-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@dlaudrb09-/%EC%BF%BC%EB%A6%AC-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Wed, 04 Sep 2024 14:46:57 GMT</pubDate>
            <description><![CDATA[<p>우리 회사에서는 주문 도메인을 운영하고 있으며, 주문 개수가 증가하면서 점차 조회 성능이 저하되는 문제가 발생했다.
특히, 주문 목록을 조회할 때 응답 시간이 길어져 운영에 불편함이 발생했다.</p>
<p>이에 따라, MySQL 쿼리 최적화를 통해 성능을 개선하는 작업을 수행했고, 그 과정과 결과를 공유하고자 한다.</p>
</br>

<h2 id="문제-상황">문제 상황</h2>
<ul>
<li>주문 개수가 증가하면서 주문 조회 API의 실행 속도가 느려짐</li>
<li>운영 중 관리자가 특정 기간의 주문 데이터를 조회하는 데 6초 이상 소요</li>
<li>개선 목표: 응답 시간을 최소한 1초 이내로 줄이는 것</li>
</ul>
</br>

<h3 id="기존-쿼리-구조">기존 쿼리 구조</h3>
<p>테이블은 <code>orders</code>, <code>orders_detail</code> 이 존재한다. </p>
<pre><code class="language-sql">SELECT
    o.id,
    od.id,
    ...
    od.created_at
FROM
    orders_detail od
JOIN
    orders o ON od.order_id = o.id
WHERE
    od.created_at BETWEEN :ordered_start_date AND :ordered_end_date;</code></pre>
<p>조금 간추렸지만 대략적으로 주문 조회시 해당 쿼리를 사용하고 있다 </p>
<p>주문이 많을 경우를 테스트하기 위해여 100만 정도의 주문 개수를 넣었다
(created_at 은 2024-01-01 부터 2024-12-29 까지 랜덤)</p>
<p>전체 테스트 상황은 아래와 같다.</p>
<ul>
<li>orders_detail(주문 상세)과 orders(주문) 테이블을 JOIN하여 조회</li>
<li>created_at 범위 조건을 사용하여 특정 날짜의 주문만 조회</li>
<li>테스트 데이터: 약 100만 개의 주문 데이터 삽입 후 테스트 진행</li>
</ul>
</br>

<h3 id="2일치-데이터-테스트">2일치 데이터 테스트</h3>
<ul>
<li>조회 조건: created_at 기준 2일치 데이터 (약 5,553건)</li>
<li>평균 실행 시간: 6초 511ms</li>
</ul>
<h4 id="explain-결과-분석">EXPLAIN 결과 분석</h4>
<p><code>EXPLAIN</code> 결과를 보면, 주요 성능 저하 원인이 <code>orders_detail</code> 테이블의 Full Table Scan(전체 테이블 스캔) 에 있었다.</p>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/8e1a910e-4b20-4276-b457-467aced48e1f/image.png" alt=""></p>
<p><strong>[분석 결과]</strong></p>
<pre><code class="language-sql">-&gt; Limit: 10000 row(s)  (cost=227608 rows=10000) (actual time=11.4..6888 rows=5553 loops=1)
    -&gt; Nested loop inner join  (cost=227608 rows=106021) (actual time=11.4..6888 rows=5553 loops=1)
        -&gt; Filter: (od.created_at between &#39;2024-09-01&#39; and &#39;2024-09-02&#39;)  (cost=111017 rows=106021) (actual time=11.2..6884 rows=5553 loops=1)
            -&gt; Table scan on od  (cost=111017 rows=954288) (actual time=0.817..6734 rows=1e+6 loops=1)
        -&gt; Single-row covering index lookup on o using PRIMARY (id=od.order_id)  (cost=1 rows=1) (actual time=356e-6..380e-6 rows=1 loops=5553)</code></pre>
<h3 id="문제점">문제점</h3>
<ul>
<li>orders_detail 의 테이블 스캔 시간 : 6734 ms (끝) - 0.817 ms (시작) = 6733.183 ms (약 6.733초)<ul>
<li>주요 원인</li>
</ul>
</li>
<li>필터링 시간 : 6884 ms - 11.2 ms = 6872.8 ms 그러나, 시간이 누적되므로 6.733 초를 빼야한다 -&gt; 약 0.140초</li>
</ul>
</br>

<h2 id="개선과정">개선과정</h2>
</br>

<h3 id="1-쿼리-순서-변경">1. 쿼리 순서 변경</h3>
<p>처음에는 새로운 인덱스를 생성하지 않고도 성능을 개선할 수 있는 방법을 우선적으로 고려했다.</p>
<p>그 이유는 인덱스 생성에는 비용이 따르기 때문이다.
(인덱스 추가시 데이터 변경에 대한 부가적인 성능 오버헤드가 발생)</p>
<p>첫째로 데이터 접근 수를 줄이는 방법을 생각해보았다</p>
<p>이유는, 두 테이블에 대한 <code>Join</code> 을 했을 때 분석결과를 보면 풀 테이블 스캔 결과 전체를 조인하는 결과가 나왔다 </p>
<p>쿼리 순서를 필터링 후 필요한 테이블만 조인하도록 변경해보았다</p>
<pre><code class="language-sql">SELECT *
FROM (
    SELECT od.id, od.order_id, od.created_at
    FROM orders_detail od
    WHERE od.created_at BETWEEN &#39;2024-09-01&#39; AND &#39;2024-09-02&#39;
    LIMIT 10000
) AS filtered_orders
JOIN orders o ON filtered_orders.order_id = o.id;</code></pre>
<h3 id="테스트-결과">테스트 결과</h3>
<ul>
<li>테이블 스캔 이후 필터링이 먼저 적용됨</li>
<li>이후 임시테이블로 생성된 5,553건의 orders 테이블과 <code>Join</code></li>
</ul>
<p><strong>[분석 결과]</strong></p>
<pre><code class="language-SQL">-&gt; Nested loop inner join  (cost=123141 rows=10000) (actual time=6483..6485 rows=5553 loops=1)
    -&gt; Table scan on od  (cost=112017..112144 rows=10000) (actual time=6483..6484 rows=5553 loops=1)
        -&gt; Materialize  (cost=112017..112017 rows=10000) (actual time=6483..6483 rows=5553 loops=1)
            -&gt; Limit: 10000 row(s)  (cost=111017 rows=10000) (actual time=21.5..6457 rows=5553 loops=1)
                -&gt; Filter: (orders_detail.created_at between &#39;2024-09-01&#39; and &#39;2024-09-02&#39;)  (cost=111017 rows=106021) (actual time=21.5..6456 rows=5553 loops=1)
                    -&gt; Table scan on orders_detail  (cost=111017 rows=954288) (actual time=1.71..6309 rows=1e+6 loops=1)
    -&gt; Single-row covering index lookup on o using PRIMARY (id=od.order_id)  (cost=1 rows=1) (actual time=75.9e-6..95.3e-6 rows=1 loops=5553)</code></pre>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/759520f9-4fd7-4584-8987-351262eed9a5/image.png" alt=""></p>
<p>결과를 보면 전체 실행 시간은 6초에서 크게 변하지 않았다 (약 6.4초)
분석 결과를 확인해보니 <code>orders_detail</code> 의 풀 테이블 스캔 자체가 90% 이상의 시간을 차지하고 있다
(임시 테이블 또한 인덱스를 활용하지 못할 가능성이 존재함)</p>
</br>

<h3 id="2-인덱스-생성">2. 인덱스 생성</h3>
<p>주요 원인을 파악했으므로 <code>orders_detail</code> 테이블 스캔 부분을 최적화해보았다</p>
<p>테이블 스캔 최적화에서는 여러가지 방법이 존재한다 </p>
<ul>
<li>테이블 파티셔닝 </li>
<li>특정 컬럼에 대한 인덱스 추가 </li>
</ul>
<p>테이블 파티셔닝 방법은 기존 테이블을 직접 파티셔닝하기 어렵고 데이터 이관 작업이 필요하다 더불어 <code>Join</code> 을 통한 쿼리시 고려해야 할 점들이 많다 </p>
<p>당장에 적용하기에 오버 엔지니어링이라 생각하여 인덱스 생성 방법을 적용하기로 했다 </p>
<h4 id="created_at-인덱스-추가"><code>created_at</code> 인덱스 추가</h4>
<p>해당 쿼리의 조건이 <code>created_at</code> 컬럼이기 때문에 해당 컬럼에 대해 인덱스를 생성하면 좋겠다 
그러나 단순히 조회 조건에 있다고 해서 인덱스를 생성하기로 결정한 것은 아니다 </p>
<p>특정 컬럼에 대한 인덱스 생성시 고려사항으로 </p>
<ol>
<li>중복도 </li>
<li>해당 쿼리를 자주 사용하는가 </li>
</ol>
<p>위 기준을 통해 해당 컬럼을 선택했다 </p>
<blockquote>
<p>참고) 중복도가 낮은 컬럼에 대한 인덱스 생성시 장점 </p>
</blockquote>
<ul>
<li>고유값이 많을수록 원하는 데이터를 빠르게 찾아내기 때문에 인덱스 효율성이 증가함 </li>
<li>중복이 많으면 조회해야할 데이터 범위가 너무 넓으며 전체 테이블 스캔하는 효율과 비슷하게 결과가 나올수 있다 또한 비슷한 결과가 나올 경우 실행계획을 세울때 굳이 <code>range</code> 스캔이 아닌 <code>ALL</code> (테이블 스캔) 을 실행할 것이다</li>
<li>중복도가 많으면 데이터가 많으므로 I/O 작업이 많이 발생한다 
(그러나 MySQL 에서는 <a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-performance-read_ahead.html">read-ahaed</a> 기법을 사용해 필요한 데이터 페이지를 미리 읽는 방식을 사용해 최적화하며 버퍼를 통해 미리 캐싱하기도 한다)</li>
</ul>
<h4 id="컬럼-중복도">컬럼 중복도</h4>
<p>주문 도메인의 <code>created_at</code> 컬럼은 주문이 생성될 때의 시간이며 이는 카디널리티가 높은, 중복도가 낮은 컬럼이다 
(테스트 데이터에서는 2024-09-01 ~ 2024-09-02 의 데이터는 대략 전체에서 0.5%의 비율을 차지함)</p>
<h4 id="사용자-사용률">사용자 사용률</h4>
<pre><code class="language-sql">SELECT DIGEST_TEXT, COUNT_STAR AS execution_count
FROM performance_schema.events_statements_summary_by_digest
ORDER BY execution_count DESC
LIMIT 10;</code></pre>
<p>위 쿼리를 통해 어느 쿼리가 가장 많이 사용되었는데 최대 10개를 확인해보았다 </p>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/3de8787d-18ed-4e48-b89b-d09ef46dac6a/image.png" alt=""></p>
<p>주문에 대한 조회 사용률이 다른 도메인 보다 많은 결과를 기록했다 </p>
<h4 id="created_at-컬럼에-대해-인덱스-생성"><code>created_at</code> 컬럼에 대해 인덱스 생성</h4>
<p>(<code>idx_created_at</code>)
<img src="https://velog.velcdn.com/images/dlaudrb09-/post/784edbf2-cdd8-453c-83a1-5eaf08fe4aef/image.png" alt=""></p>
</br>

<ul>
<li>결과는 평균적으로 <strong>126ms</strong>  </li>
<li>대략 50배 이상의 성능 개선이 이루어졌다</li>
</ul>
</br>

<p>인덱스를 활용한 실행 계획 변경</p>
<ul>
<li>기존: ALL (Full Table Scan) → 6.7초</li>
<li>최적화 후: range (Index Range Scan) → 126ms </li>
</ul>
<p><strong>[분석 결과]</strong>
<img src="https://velog.velcdn.com/images/dlaudrb09-/post/71dc754e-0007-40bb-8f6c-48c1724db65a/image.png" alt=""></p>
<p>실제로 임시 테이블을 만들때 <code>idx_created_at</code> 인덱스를 활용했으며 <a href="https://dev.mysql.com/doc/refman/8.4/en/range-optimization.html">range</a> 스캔을 통해 가져왔다 
(정렬된 리프노드 인덱스키를 범위 탐색하여 가져옴)</p>
</br>


<p>EXPLAIN ANALYZE</p>
<pre><code class="language-SQL">-&gt; Nested loop inner join  (cost=11936 rows=5553) (actual time=40.5..43.5 rows=5553 loops=1)
    -&gt; Table scan on od  (cost=5757..5829 rows=5553) (actual time=40.5..42.2 rows=5553 loops=1)
        -&gt; Materialize  (cost=5757..5757 rows=5553) (actual time=40.5..40.5 rows=5553 loops=1)
            -&gt; Limit: 10000 row(s)  (cost=5202 rows=5553) (actual time=3.94..25.9 rows=5553 loops=1)
                -&gt; Index range scan on orders_detail using idx_created_at over (&#39;2024-09-01 00:00:00&#39; &lt;= created_at &lt;= &#39;2024-09-02 00:00:00&#39;), with index condition: (orders_detail.created_at between &#39;2024-09-01&#39; and &#39;2024-09-02&#39;)  (cost=5202 rows=5553) (actual time=3.94..25.6 rows=5553 loops=1)
    -&gt; Single-row covering index lookup on o using PRIMARY (id=od.order_id)  (cost=1 rows=1) (actual time=87.1e-6..112e-6 rows=1 loops=5553)</code></pre>
<p>분석결과를 보면 따로 필터링하는 부분이 사라져있고 Index range scan 을 통해 범위 최적화를 진행했다 </p>
<p>Range Scan 시간 : 25.6ms − 3.94ms = 21.66ms</p>
<p>그외에는 크게 걸린 시간은 없었다 </p>
<ul>
<li>Table scan on od : 42.2 ms - 40.5 ms = 1.7 ms</li>
<li>Nested loop inner join : 43.5 ms - 40.5 ms = 3 ms</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[재고 시스템의 동시성 제어와 락 처리 실험기]]></title>
            <link>https://velog.io/@dlaudrb09-/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%B2%98%EB%A6%AC-%EC%98%88%EC%99%B8</link>
            <guid>https://velog.io/@dlaudrb09-/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%B2%98%EB%A6%AC-%EC%98%88%EC%99%B8</guid>
            <pubDate>Sun, 01 Sep 2024 15:48:02 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-인식">문제 인식</h2>
<p>현재 서비스에서는 재고 수량을 관리하는 로직이 존재하며,
여러 요청이 동시에 들어올 경우 동시성 이슈가 발생하는 구조다.</p>
<p>이를 직접 눈으로 확인하고 싶어서,
재고 수량을 단순히 1 감소시키는 API를 작성하고 <a href="https://k6.io/docs/"><code>K6</code></a>를 활용해 부하 테스트를 진행했다.</p>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/cfbd3313-667b-4ca1-8322-081d882aea88/image.png" alt=""></p>
</br>

<h4 id="동시성-테스트-with-k6">동시성 테스트 with K6</h4>
<p>테스트 스크립트</p>
<ul>
<li>아래 가상 유저 10명이 동시에 요청하는 스크립트</li>
</ul>
<pre><code class="language-javascript">import http from &#39;k6/http&#39;;
import { sleep } from &#39;k6&#39;;

export const options = {
  scenarios: {
    simultaneous_requests: {
      executor: &#39;per-vu-iterations&#39;, // 각 VU가 동시에 시작
      vus: 10, // VU 수
      iterations: 1, // 각 VU가 한 번만 실행
      maxDuration: &#39;1s&#39;, // 전체 시뮬레이션 시간
    },
  },
};


export default function() {
    http.post(&#39;http://localhost:8080/inventories/test&#39;);
}</code></pre>
<p>k6 스크립트 실행 </p>
<pre><code class="language-shell"># k6 run script.js</code></pre>
<h4 id="실행-결과">실행 결과</h4>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/928b2b51-f1f4-4048-bf9b-85c1e7f0dcec/image.png" alt=""></p>
<p>테스트 결과, 10명의 요청 중 1건만 반영</p>
<p>예상대로 <strong>Lost Update</strong> 현상 발생
→ 가장 마지막에 커밋된 요청만 반영되고, 나머지는 모두 무시됨</p>
<h4 id="🤔-간단한-synchronized-→-실패">🤔 간단한 synchronized? → 실패</h4>
<p><code>@Transactional</code> 환경에서 단순 <code>synchronized</code> 블록을 사용해 보았지만,
스레드 락은 한 프로세스 내에서만 유효하기 때문에 멀티 프로세스/분산 환경에서는 무의미했다.</p>
<p>또한, 트랜잭션 내부에서 락을 걸더라도,
커밋 시점 이전에 다른 스레드가 과거 값을 읽는 문제가 생김 → 실패</p>
<blockquote>
<p> synchronized는 JVM 레벨의 락
synchronized 키워드는 Java 객체 수준에서 락을 거는 메커니즘이며
이 락은 JVM 내부에서 관리되며, JVM 프로세스 안에 있는 여러 스레드 간의 동시 접근을 막는 용도로 동작
따라서 JVM 밖, 즉 다른 프로세스에서 실행되는 코드와는 락이 공유되지 않아 전혀 제어할 수 없음.
synchronized는 <strong>JVM 내부(= 단일 프로세스 내)</strong>에서만 유효한 락이다.
즉, 멀티 프로세스 환경이나 분산 시스템에서는 동시성 보장이 되지 않는다.</p>
</blockquote>
<pre><code class="language-kotlin">@Transactional
fun updateQty(id: Long) {
     synchronized(lock) {
            // synchronized 블록 안의 코드는 하나의 스레드만 접근 가능
            ...
        }
}</code></pre>
<h4 id="동시성-해결-방안-탐색">동시성 해결 방안 탐색</h4>
<p>재고 수량 감소 로직에서 사용할 수 있는 대표적인 동시성 제어 방식은 다음과 같다</p>
<ul>
<li>Pessimistic Lock (비관적 락)</li>
<li>Optimistic Lock (낙관적 락)</li>
<li>Named Lock (DB 메타데이터 락)</li>
<li>Redis 기반 분산 락 (Lettuce / Redisson)</li>
</ul>
</br>

<h2 id="1-pessimistic-lock">1. Pessimistic Lock</h2>
<ul>
<li>실제로 DB 에 Lock 을 거는 방법 </li>
<li>배타적 Lock, 즉 Lock 을 가져온 이후 다른 트랜잭션이 해당 Lock 이 해제되기까지 대기하게됨. </li>
</ul>
<p>조회시 Lock 을 걸고 테스트. </p>
<pre><code class="language-kotlin">@Repository
interface InventoryDetailRepository :
    JpaRepository&lt;InventoryDetail, Long&gt;,
    InventoryDetailRepositoryDSL {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;SELECT i FROM InventoryDetail i WHERE i.id = :id&quot;)
    fun findByIdWithLock(id: Long): Optional&lt;InventoryDetail&gt;
}</code></pre>
<p>결과를 보면 잘 적용된 것 같다.  </p>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/765d4d59-6680-470a-bef6-d1aeb5924ce8/image.png" alt=""></p>
<pre><code class="language-SQL">    select
    ...
    id1_0.updated_at 
    from
        inventory_detail id1_0 
    where
        (
            id1_0.deleted_at IS NULL
        ) 
        and id1_0.id=? for update</code></pre>
<p>하지만 락을 획득할 때까지 트랜잭션이 대기하기 때문에,
성능상 병목이 발생할 가능성이 높고 일반적으로 추천되지는 않는다.
(충돌이 많이 발생하는 DB 테이블에 한해서는 권장한다고 한다) </p>
</br>

<h2 id="2-optimistic-lock">2. Optimistic Lock</h2>
<ul>
<li>Lock 을 사용하지 않고 버전을 따로 명시함으로써 데이터 정합성을 맞추는 방법 </li>
<li>데이터를 읽은 후 update 할 때 현재 버전이 맞는지 확인 → 즉 재시도 로직이 필요함 </li>
</ul>
<p><code>@Version</code> 추가</p>
<pre><code class="language-kotlin">@Entity
@Table(
    name = &quot;inventory_detail&quot;,
)
class InventoryDetail : AutoIncrementIdEntity() {
    ...

    @Version
    var version: Long? = null
}
</code></pre>
<p>그리고 기존 DB 테이블의 버전을 0으로 초기값 세팅하고 한번 요청해보자. </p>
</br>

<p>확인해보니 충돌처리에 대한 부분을 따로 처리하지 않으면 <code>ObjectOptimisticLockingFailureException</code> 이 에러가 발생한다고 한다 </p>
<pre><code class="language-kotlin">2024-09-01T21:46:41.349+09:00 ERROR 50605 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.~~.InventoryDetail#1]] with root cause</code></pre>
<p>이후 좀 더 방안을 찾아보고 재시도 로직을 추가했다 </p>
<pre><code class="language-kotlin">
    @Lock(LockModeType.OPTIMISTIC)
    @Query(&quot;SELECT i FROM InventoryDetail i WHERE i.id = :id&quot;)
    fun findByIdWithLock(id: Long): Optional&lt;InventoryDetail&gt;

    @Retryable(
        value = [Exception::class],
        maxAttempts = 50,
        backoff = Backoff(delay = 1000),
    )
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    fun updateQty(id: Long) {
        try {
            val inventoryDetail =
                inventoryDetailRepository.findByIdWithLock(id).orElseThrow {
                    throw GeneralException.with(GeneralMsgType.NOT_FOUND_INVENTORY)
                }

            inventoryDetail.updateQty(inventoryDetail.getQty() - 1)

            inventoryDetailRepository.saveAndFlush(inventoryDetail)
        } catch (e: ObjectOptimisticLockingFailureException) {
            log.error(&quot;Optimistic locking 충돌 발생 !! : ${e.message}&quot;)
            throw e 
        } catch (e: PersistenceException) {
            log.error(&quot;PersistenceException 발생: ${e.message}&quot;)
            throw RuntimeException(&quot;PersistenceException 발생&quot;, e)
        } catch (e: Exception) {
            log.error(&quot;기타 예외 발생: ${e.message}&quot;, e)
            throw e
        }
    }</code></pre>
<p>하지만 실제 테스트에서는 StaleObjectStateException 예외가 계속 발생했고,
예외가 트랜잭션 경계 밖에서 발생하는 경우가 많아 catch로 포착되지 않았다.</p>
<p>→ Spring Retry, 예외 핸들링 등을 복잡하게 구성했지만 결국 실패로 결론지었다.</p>
<p>(<a href="https://developer.jboss.org/thread/131217">https://developer.jboss.org/thread/131217</a>)
(<a href="https://stackoverflow.com/questions/30236145/not-able-to-catch-org-hibernate-staleobjectstateexception">https://stackoverflow.com/questions/30236145/not-able-to-catch-org-hibernate-staleobjectstateexception</a>)</p>
</br>

<h2 id="3-named-lock">3. Named Lock</h2>
<p>MySQL의 GET_LOCK, RELEASE_LOCK을 활용하여
이름 기반으로 락을 획득하고 해제하는 방식이다.
이름을 가진 Lock 을 획득한 후 해제할 때까지 다른 세션은 이 Lock 을 획득할 수 없음 </p>
<ul>
<li>주의. 트랜잭션이 종료될 때 Lock 이 자동으로 해제되지 않으며 별도의 명령어로 해제를 수행해주어야 함</li>
<li><code>Pessimistic Lock</code> 과 비슷하지만 Pessimistic Lock 은 테이블의 Row, Table 단위로 Lock 을 거는 것이며 <code>Named Lock</code> 은 metadata 에 Lock 을 거는 방법 즉, 공유자원 (Name) 에 대한 Lock 을 거는 것 </li>
</ul>
<p>이 방식은 데이터 소스를 서로 다른 것으로 사용하는게 좋다고 이야기한다 
이유는 커넥션 풀이 부족해지는 이슈가 생긴다</p>
<h4 id="구조-설계">구조 설계</h4>
<ul>
<li>InventoryWithNamedLockService: 락 획득/해제 담당</li>
<li>InventoryUpdater: 실제 재고 수량 감소 처리</li>
</ul>
<p>두개의 서비스로 구현
(트랜잭션의 경계와 락의 생명주기를 명확히 관리하기 위함)</p>
<p><strong>부모-자식 구조로 구현</strong></p>
<ul>
<li>부모 : 락을 획득 및 해제하는 책임을 가짐, 트랜잭션 경계를 넘어 락을 관리 </li>
<li>자식 : 실제 트랜잭션의 비즈니스 로직 처리 </li>
</ul>
<pre><code class="language-kotlin">interface InventoryDetailRepository :
    JpaRepository&lt;InventoryDetail, Long&gt;,
    InventoryDetailRepositoryDSL {
    @Query(
        value = &quot;select get_lock(:key, 3000)&quot;,
        nativeQuery = true,
    )
    fun getLock(key: String)

    @Query(
        value = &quot;select release_lock(:key)&quot;,
        nativeQuery = true,
    )
    fun releaseLock(key: String)
}

@Service
class InventoryWithNamedLockService(
    private val inventoryDetailRepository: InventoryDetailRepository,
    private val inventoryUpdater: InventoryUpdater,
) {
    private val log = logger()

    @Transactional
    fun updateQty(id: Long) {
        try {
            inventoryDetailRepository.getLock(id.toString())
            inventoryUpdater.updateQty(id)
        } finally {
            inventoryDetailRepository.releaseLock(id.toString())
        }
    }
}

@Service
class InventoryUpdater(
    private val inventoryDetailRepository: InventoryDetailRepository,
) {
    private val log = logger()

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    fun updateQty(id: Long) {
        val inventoryDetail =
            inventoryDetailRepository.findById(id).orElseThrow {
                throw GeneralException.with(GeneralMsgType.NOT_FOUND_INVENTORY)
            }

        inventoryDetail.updateQty(
            inventoryDetail.getQty() - 1,
        )
        log.info(&quot;qty : {}&quot;, inventoryDetail.getQty())
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/a372b325-287f-445d-9021-054858aec3d3/image.png" alt=""></p>
<p>동시성 문제 해결. 안정적으로 동작.
그러나, 트랜잭션 종료 시 락이 자동 해제되지 않음 → 직접 해제 필요</p>
</br>

<h2 id="4-redis-분산-락">4. Redis 분산 락</h2>
<p>대표적으로 두 가지 방식이 있다고 한다 </p>
<ol>
<li>Lettuce <ul>
<li>setnx 명령어로 분산락 구현, <del>spin lock 방식 (계속 lock을 확인)</del></li>
<li>별도의 재시도 로직 필요 </li>
</ul>
</li>
<li>Redisson<ul>
<li>pub-sub 기반으로 lock 구현 </li>
<li>채널을 만들고 lock 을 획득하려는 스레드가 구독하여 lock 을 해제하려는 스레드 쪽에서 알려주면 안내를 받은 스레드가 lock 을 획득하는 방식</li>
<li>별도의 재시도 로직 필요하지 않음 </li>
</ul>
</li>
</ol>
<h3 id="4-1-lettuce">4-1. Lettuce</h3>
<p>이것도 <code>Named Lock</code> 과 비슷한 방식으로 구현된다 단지 Redis 를 활용할 뿐. </p>
<p>일단 Redis <code>setnx</code> 명령어를 통해 key 와 value 를 설정해주는 RedisRepository 를 구현하자 </p>
<pre><code class="language-kotlin">@Component
class RedisLockRepository(
    private val redisTemplate: RedisTemplate&lt;String, String&gt;,
) {
    fun lock(key: Long): Boolean? =
        redisTemplate
            .opsForValue()
            .setIfAbsent(generateKey(key), &quot;lock&quot;, Duration.ofMillis(3000))

    fun unlock(key: Long): Boolean = redisTemplate.delete(generateKey(key))

    fun generateKey(key: Long) = key.toString()
}</code></pre>
<p>이후 부모-자식 구조를 통해 
부모 레이어는 Redis 의 key 에 대한 Lock 해제 및 획득을 구현 (spin lock)</p>
<p>자식 레이어는 기존 재고 수량 처리 로직 </p>
<pre><code class="language-kotlin">@Service
class InventoryLettuceLockService(
    private val redisLockRepository: RedisLockRepository,
    private val inventoryUpdater: InventoryUpdater,
) {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    fun updateQty(id: Long) {
        while (!redisLockRepository.lock(id)!!) {
            Thread.sleep(100)
        }

        try {
            inventoryUpdater.updateQty(id)
        } finally {
            redisLockRepository.unlock(id)
        }
    }
}</code></pre>
<p>이후 Redis io 관련 로그 </p>
<pre><code class="language-kotlin">2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] o.s.d.redis.core.RedisConnectionUtils    : Fetching Redis Connection from RedisConnectionFactory
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] io.lettuce.core.RedisChannelHandler      : dispatching command AsyncCommand [type=DEL, output=IntegerOutput [output=null, error=&#39;null&#39;], commandType=io.lettuce.core.protocol.Command]
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] i.lettuce.core.protocol.DefaultEndpoint  : [channel=0x26ca5e1a, /127.0.0.1:50710 -&gt; localhost/127.0.0.1:6379, epid=0x1] write() writeAndFlush command AsyncCommand [type=DEL, output=IntegerOutput [output=null, error=&#39;null&#39;], commandType=io.lettuce.core.protocol.Command]
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] i.lettuce.core.protocol.DefaultEndpoint  : [channel=0x26ca5e1a, /127.0.0.1:50710 -&gt; localhost/127.0.0.1:6379, epid=0x1] write() done
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler   : [channel=0x26ca5e1a, /127.0.0.1:50710 -&gt; localhost/127.0.0.1:6379, epid=0x1, chid=0x1] write(ctx, AsyncCommand [type=DEL, output=IntegerOutput [output=null, error=&#39;null&#39;], commandType=io.lettuce.core.protocol.Command], promise)
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandEncoder   : [channel=0x26ca5e1a, /127.0.0.1:50710 -&gt; localhost/127.0.0.1:6379] writing command AsyncCommand [type=DEL, output=IntegerOutput [output=null, error=&#39;null&#39;], commandType=io.lettuce.core.protocol.Command]
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler   : [channel=0x26ca5e1a, /127.0.0.1:50710 -&gt; localhost/127.0.0.1:6379, epid=0x1, chid=0x1] Received: 4 bytes, 1 commands in the stack
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler   : [channel=0x26ca5e1a, /127.0.0.1:50710 -&gt; localhost/127.0.0.1:6379, epid=0x1, chid=0x1] Stack contains: 1 commands
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.l.core.protocol.RedisStateMachine      : Decode done, empty stack: true
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [ioEventLoop-4-1] i.lettuce.core.protocol.CommandHandler   : [channel=0x26ca5e1a, /127.0.0.1:50710 -&gt; localhost/127.0.0.1:6379, epid=0x1, chid=0x1] Completing command AsyncCommand [type=DEL, output=IntegerOutput [output=1, error=&#39;null&#39;], commandType=io.lettuce.core.protocol.Command]
2024-09-02T00:15:25.310+09:00 DEBUG 55356 --- [nio-8080-exec-4] o.s.d.redis.core.RedisConnectionUtils    : Closing Redis Connection</code></pre>
<p>그치만 해당 방식은 예상하듯 spin lock 방식이므로 redis 의 부하를 줄 수 있음 
추천하지 않음.</p>
</br>

<h3 id="42-redisson">4.2 Redisson</h3>
<p>기존 Observer 패턴과 비슷하게 채널을 구독한 이후 다른 세션이 Lock 을 해제할 경우 관련 이벤트에 대한 알림을 주고,
구독한 다른 세션이 해당 이벤트를 받아서 Lock 을 획득하는 방식 </p>
<p>Redis 명령어로 알아보자. </p>
<pre><code class="language-shell">구독
127.0.0.1:6379&gt; subscribe ch1
1) &quot;subscribe&quot;
2) &quot;ch1&quot;
3) (integer) 1

publish
127.0.0.1:6379&gt; publish ch1 hello
(integer) 1
127.0.0.1:6379&gt;

구독
1) &quot;message&quot;
2) &quot;ch1&quot;
3) &quot;hello&quot;</code></pre>
<p>pub_sub 기반이므로 Lettuce 보다 Redis 부하가 줄어든다, Redisson 라이브러리에서 이미 Lock 획득 및 해제가 구현되어 있으므로 명령어를 사용하는 RedisRepository 는 필요 없다 </p>
<pre><code class="language-kotlin">@Service
class InventoryRedissonLockService(
    private val redissonClient: RedissonClient,
    private val inventoryUpdater: InventoryUpdater,
) {
    private val log = logger()

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    fun updateQty(id: Long) {
        val lock = redissonClient.getLock(id.toString())

        try {
            val enabled = lock.tryLock(10, 1, TimeUnit.SECONDS)

            if (!enabled) {
                log.info(&quot;Redis Lock 획득 실패 Key : {}&quot;, id)
                return
            }

            inventoryUpdater.updateQty(id)
        } catch (e: InterruptedException) {
            throw RuntimeException(e)
        } finally {
            lock.unlock()
        }
    }
}</code></pre>
<p>비교적 비즈니스 로직이나 관련된 에러 처리가 없어서 간단하다.
테스트 결과는 정상 작동하며 구현 또한 간단하다</p>
<p>그러나 락 해제 실패 문제, 락이 획득하지 못했을 경우 자동 해제 시간 설정 등 여러가지 요인들을 생각해야 한다 그리고 Redis 라는 외부 자원을 활용하는 만큼 예상하지 못하는 이슈들이 생겨날 수도 있다 </p>
<p>이외에도 메시지 큐를 활용한 동시성 제어 등이 존재한다 </p>
<p>추가로 <code>K6</code> 를 활용하면서 동시성 테스트를 진행했는데 좀 찾아보니 TPS (Transaction Per Seconds) 에 대한 테스트도 가능해 보인다 </p>
<ul>
<li>동시 요청에 대한 처리량 측정 </li>
</ul>
<p>간단히 여태까지의 동시성 테스트 스크립트를 돌려서 결과를 보면,</p>
<pre><code class="language-shell">execution: local
        script: script.js
        output: -

     scenarios: (100.00%) 1 scenario, 10 max VUs, 40s max duration (incl. graceful stop):
              * simultaneous_requests: 1 iterations for each of 10 VUs (maxDuration: 10s, gracefulStop: 30s)


     data_received..................: 1.6 kB 1.3 kB/s
     data_sent......................: 1.2 kB 934 B/s
     http_req_blocked...............: avg=1.87ms   min=1.82ms   med=1.85ms   max=2.02ms p(90)=1.92ms   p(95)=1.97ms
     http_req_connecting............: avg=618.8µs  min=553µs    med=615.49µs max=704µs  p(90)=675.2µs  p(95)=689.6µs
     http_req_duration..............: avg=766.22ms min=292.99ms med=761.23ms max=1.23s  p(90)=1.14s    p(95)=1.18s
       { expected_response:true }...: avg=766.22ms min=292.99ms med=761.23ms max=1.23s  p(90)=1.14s    p(95)=1.18s
     http_req_failed................: 0.00%  ✓ 0        ✗ 10
     http_req_receiving.............: avg=77.89µs  min=59µs     med=68.5µs   max=131µs  p(90)=110.3µs  p(95)=120.65µs
     http_req_sending...............: avg=293.3µs  min=177µs    med=316µs    max=366µs  p(90)=346.19µs p(95)=356.1µs
     http_req_tls_handshaking.......: avg=0s       min=0s       med=0s       max=0s     p(90)=0s       p(95)=0s
     http_req_waiting...............: avg=765.85ms min=292.64ms med=760.96ms max=1.23s  p(90)=1.14s    p(95)=1.18s
     http_reqs......................: 10     8.050875/s
     iteration_duration.............: avg=770.42ms min=297.84ms med=765.39ms max=1.24s  p(90)=1.14s    p(95)=1.19s
     iterations.....................: 10     8.050875/s
     vus............................: 3      min=3      max=3
     vus_max........................: 10     min=10     max=10</code></pre>
<p>여기의 <code>http_reqs</code> 를 보면 10개의 요청이 들어갔고 1초에 8번의 요청을 처리할 수 있다고 볼 수 있다 </p>
<hr>
<h3 id="회고-및-정리">회고 및 정리</h3>
<ul>
<li>Pessimistic Lock : 구현은 간단하며 강력한 락을 통해 동시성 제어 그러나 성능 저하 및 병목현상 발생 </li>
<li>Optimistic Lock : 트랜잭션 병렬성 확보 및 충돌시 예외 처리가 복잡하다 </li>
<li>Named Lock : 간단한 DB 락을 통한 구현 락 수동해제 필요 및 커넥션을 점유함 </li>
<li>Redis Lettuce    : 쉬운 분산 락 구현, <del>Redis 부하 및 SpinLock 구조</del></li>
<li>Redis Redisson : 안정적인 분산 락 구현, 외부 의존성 과 설정이 필요 </li>
</ul>
</br>
</br>

<hr>
<h3 id="20251005-회고-정리">2025.10.05 회고 정리</h3>
<ul>
<li><strong>잘못된 사실이 하나 있었다 바로 <code>Redis Lettuce</code> 라이브러리가 SpinLock 구조로 구현되었다기 보다는 그렇게 개발자가 사용하는 것 이다</strong> </li>
<li>즉, 동작 방식은 개발자가 그렇게 사용한 것 라이브러리는 그런 동작방식의 강제성이 없었다</li>
<li>참고한 블로그 <ul>
<li><a href="https://myvelop.tistory.com/m/260">https://myvelop.tistory.com/m/260</a> </li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[비동기 처리  예외] 스케줄러 & 이메일 발송 실패 대응기]]></title>
            <link>https://velog.io/@dlaudrb09-/%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%9F%AC-%EC%99%80-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%B0%9C%EC%86%A1%EC%8B%9C-%EC%8B%A4%ED%8C%A8-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@dlaudrb09-/%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%9F%AC-%EC%99%80-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%B0%9C%EC%86%A1%EC%8B%9C-%EC%8B%A4%ED%8C%A8-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Sat, 31 Aug 2024 13:05:14 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-인식">문제 인식</h2>
</br>

<p>회사 서비스에는 외부 API와 연동되어 데이터를 동기화하는 작업이 있고, 이 작업은 스케줄러로 처리되고 있다.
그런데 문득, &quot;이 스케줄러가 실패하면 디버깅이 어렵고, 우리가 그걸 인지하지 못하면 어떻게 하지?&quot;라는 생각이 들었다.</p>
<p>사실 이 문제는 이미 가끔 발생 중이었고,
그래서 <strong>“이슈가 터지기 전에 알 수 있도록 경고 시스템을 넣자”</strong>는 결론에 도달했다.</p>
</br>

<h3 id="1-slack-알림-시스템-도입">1. Slack 알림 시스템 도입</h3>
<p>가장 먼저 떠오른 해결책은 Slack 알림이었다.
<a href="https://velog.io/@abh0920one/%EC%96%B8%EC%A0%9C%EA%B9%8C%EC%A7%80-%EC%88%98%EB%8F%99%EC%9C%BC%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%A0%EB%9E%98%EC%9A%94-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0#%EC%8A%AC%EB%9E%99%EB%B4%87%EC%9C%BC%EB%A1%9C-%EB%85%B8%ED%8B%B0%EA%B9%8C%EC%A7%80-%EC%9E%90%EB%8F%99%ED%99%94">회사 동료들이 작성한 레퍼런스</a>를 참고해 Slack API를 기반으로 Webhook 연동을 구현했다.</p>
<p>구성한 내용은 아래와 같다</p>
<ul>
<li>Slack App 생성 및 Webhook 발급</li>
<li>Spring yml에 Webhook URL 추가</li>
<li>SlackNotifier 서비스 추가<pre><code class="language-yml">slack:
  webhook:
      url: &quot;https://hooks.slack.com/services/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&quot;</code></pre>
</li>
</ul>
<pre><code class="language-java">@Service
class SlackNotifier(
    @Value(&quot;\${slack.webhook.url}&quot;)
    private val webhookUrl: String,
) {

    fun sendMessage(message: String) {
        restTemplate.postForEntity(webhookUrl, requestEntity, String::class.java)
        ....
    }

}</code></pre>
<ul>
<li>테스트 결과는 아래와 같다.
<img src="https://velog.velcdn.com/images/dlaudrb09-/post/c1ced7a3-0fd3-4f6a-950d-c4d7bd1b63b9/image.png" alt=""></li>
</ul>
</br>

<hr>
</br>

<h3 id="2-스케줄러-실패-대응-전략">2. 스케줄러 실패 대응 전략</h3>
<p>Slack 알림 외에도, 실패 케이스를 다각도로 대응하기 위해 여러 방안을 정리해보았다</p>
<ul>
<li>Spring Retry (재시도 매커니즘)</li>
<li>Exponential Backoff (지수 백오프)
  (재시도 간격을 지수적으로 늘려가는 방식)</li>
<li>Circuit Breaker<ul>
<li>재시도가 반복적으로 실패할 경우, 일정 시간 동안 해당 작업을 멈추고 시스템을 보호하는 방법, 무의미한 재시도를 방지</li>
</ul>
</li>
<li>실패 이력 저장 및 로깅 강화</li>
</ul>
</br>

<p>이 중 내가 택한 방법은 실패 로그 저장 + 슬랙 알림이었다.
선택 이유는 다음과 같다</p>
<ol>
<li>대부분의 코드가 외부 API 요청이므로 재시도 로직이 크게 의미 없을 수 있다.</li>
<li>데이터 동기화 작업이므로 실패할 경우 빠른 대응이 필요하다  </li>
</ol>
</br>

<hr>
</br>

<h3 id="3-실패-로그-저장-히스토리-테이블">3. 실패 로그 저장 (히스토리 테이블)</h3>
<ul>
<li><p><code>SchedulerFailedLog</code> 로깅 관련 엔티티 추가 </p>
<pre><code class="language-java">@Entity
@Table(name = &quot;scheduler_failed_log&quot;)
@SQLDelete(sql = &quot;UPDATE routing SET deleted_at = NOW() WHERE id = ?&quot;)
@SQLRestriction(&quot;deleted_at IS NULL&quot;)
class SchedulerFailedLog() : AutoIncrementIdEntity() {
  constructor(
      exceptionSource: String,
      exceptionMessage: String,
      requestedUrl: String? = null,
  ) : this() {
      this.exceptionSource = exceptionSource
      this.exceptionMessage = exceptionMessage
      this.requestedUrl = requestedUrl
  }

  @Comment(&quot;예외 발생 이름&quot;)
  @Column(name = &quot;exception_source&quot;, nullable = false)
  private var exceptionSource: String = &quot;&quot;

  @Comment(&quot;예외 메시지&quot;)
  @Column(name = &quot;exception_message&quot;, nullable = false)
  private var exceptionMessage: String = &quot;&quot;

  @Comment(&quot;외부 요청 url&quot;)
  @Column(name = &quot;requested_url&quot;, nullable = true)
  private var requestedUrl: String? = &quot;&quot;
}</code></pre>
<ul>
<li>예외에 대한 더 상세한 컬럼을 추가할 수도 있지만 당장에는 예외 메시지와 요청 URL 을 보고 싶었다, 추후 추가가 필요할 경우 고려해야할 사항이다</li>
<li>스케줄러 실패 시 새로운 트랜잭션으로 에러 핸들링 및 저장되도록  <code>SchedulerFailedHandler</code> 를 구현했다</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Component
class SchedulerFailedHandler(
    private val schedulerFailedLogRepository: SchedulerFailedLogRepository,
    private val schedulerFailedNotifier: SchedulerFailedNotifier,
) {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    fun httpFailed(failedLog: SchedulerFailedLog) {
        log.info(&quot;동기화 스케줄러 실패 : {}&quot;, failedLog.getExceptionMessage())

        schedulerFailedLogRepository.save(failedLog)

        schedulerFailedNotifier.alert(failedLog.getExceptionMessage())
    }
}</code></pre>
<p><strong>결과적으로, 슬랙 알림과 함께 실패 로그가 잘 쌓이도록 구성되었고,</strong></p>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/89953158-7b98-453e-9f3a-4af642054919/image.png" alt=""></p>
<p>향후 이 로그를 기반으로 모니터링 지표나 리포트 형태로 확장할 계획이다.</p>
</br>

<hr>
</br>

<h3 id="4-이메일-발송-실패">4. 이메일 발송 실패</h3>
<p>이메일 발송 실패는 스케줄러 실패보다 더 문제였다.
이유는 명확하다</p>
<blockquote>
<p>“스케줄러는 내부의 실패지만, 이메일은 사용자와의 접점이다.”</p>
</blockquote>
<p>현재 이메일은 다음과 같은 방식으로 처리되고 있었다:</p>
<p>비즈니스 로직 (예: 임시 비밀번호 발급)이 완료된 후</p>
<p><code>Spring Event</code> + <code>@Async</code> 조합으로 이메일 발송</p>
<p>즉, 트랜잭션이 커밋된 후 비동기적으로 발송되는 구조였고,
이 구조에선 실패 시 대응이 어렵다.</p>
</br>

<hr>
<h3 id="5-이메일-실패-대응-전략">5. 이메일 실패 대응 전략</h3>
<p>검토한 방법들</p>
<ul>
<li><code>Fallback</code> 패턴 적용 <ul>
<li><code>@HystrixCommand(fallbackMethod = &quot;sendEmailFallback&quot;)</code></li>
</ul>
</li>
<li>메시지 큐 기반 재시도 구조 <ul>
<li>실패시 메시지 Produce, 이후 메시지 Consume </li>
</ul>
</li>
<li>유저 SMS 를 통해 알림 </li>
<li>보상 트랜잭션 <ul>
<li>커밋한 트랜잭션에 대한 롤백</li>
</ul>
</li>
<li>상태 플래그를 통한 롤백 </li>
</ul>
<h4 id="실제-적용한-방식">실제 적용한 방식</h4>
<ul>
<li>이메일 실패 시 실패 로그 저장 + 슬랙 알림</li>
<li>상태 플래그를 저장하여 실패 여부 추적 가능하도록 처리</li>
</ul>
<p>이유는 다음과 같다</p>
<blockquote>
<p>&quot;비즈니스 로직은 성공했지만, 이메일 발송이 실패했다고 그걸 되돌리는 건 신중해야 한다.&quot;</p>
</blockquote>
<p>다만, 발송 실패를 원자적 단위로 본다면, 오히려 비동기가 아닌 동일 트랜잭션에서 처리하는 게 맞지 않나? 라는 고민도 들었다.
이 부분은 추후 리팩토링에서 더 깊이 검토할 예정이다.</p>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/21ca9bec-2fa4-4d59-8908-607739e89faa/image.png" alt=""></p>
<hr>
</br>

<h3 id="마무리">마무리</h3>
<p>이번 작업은 단순히 실패를 잡는 것에서 끝나지 않았다.
오히려 이 과정을 통해 기존 아키텍처와 처리 방식이 적절했는지를 되돌아보게 해주는 계기가 되었다.</p>
<p>스케줄러와 이메일 발송처럼 실패 시점이 사용자 혹은 외부 시스템과 연결되는 경우엔,
단순한 로그 이상의 대응이 필요하다.</p>
<p>이번 개선으로 &#39;문제가 발생하면 알게 된다&#39; → &#39;문제가 생기기 전에 인지할 수 있다&#39;로 한 단계 나아갔다고 생각한다.
다음 리팩토링에선 모니터링, 시각화, 재시도 큐 구조 등을 포함해 더 개선해볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[계층형(Layer) 아키텍쳐의 문제점 ?]]></title>
            <link>https://velog.io/@dlaudrb09-/%EA%B3%84%EC%B8%B5%ED%98%95Layer-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90</link>
            <guid>https://velog.io/@dlaudrb09-/%EA%B3%84%EC%B8%B5%ED%98%95Layer-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90</guid>
            <pubDate>Sun, 18 Aug 2024 09:35:14 GMT</pubDate>
            <description><![CDATA[<h3 id="서론">서론.</h3>
<p>최근 인프런에서 주최한 2024 인프콘을 다녀왔는데 거기서 토비님의 발표내용 중에 헥사고날 아키텍쳐는 스프링에 있어서 반드시 필요한 아키텍쳐라고 이야기하던 것이 기억이나서 클린 아키텍쳐의 얇은 버전(?)을 사서 읽고 있다 </p>
<p>그 중 1장에 대한 공감가는 내용, 좋은 아이디어를 찾아서 정리한 내용이다 </p>
</br>


<h2 id="데이터-베이스-주도-설계-">데이터 베이스 주도 설계 ?</h2>
<p><img src="https://velog.velcdn.com/images/dlaudrb09-/post/b00564f6-f655-4700-86ec-bdadf8d9797e/image.png" alt=""></p>
<ul>
<li>계층형 아키텍쳐 구조 </li>
</ul>
<p>웹 계층은 요청을 받아 도메인 로직에 요청한다. 도메인은 다시 영속성 계층에게 요청한다 
이 내용의 결론은 결국 모든 것이 영속성 계층을 토대로 만들어야 한다는 내용이다 또한 이런 방식은 여러가지 문제를 초래한다</p>
<p>개발을 할 때 도메인의 목적 즉, 만들고 있는 제품의 목적에 대해 생각하는데 코드상에서는 보통 상태(State, 객체의 로컬 변수) 가 아닌 행동(behavior, 객체의 메시지, 메서드) 를 중심으로 모델링 한다 -&gt; 중요!</p>
<p>어떤 앱이든 상태도 중요하지만 행동이 상태를 변경하며 또 다른 객체에게 메시지를 전달하기 때문에 중심이 된다 </p>
<p>그러나 계층형 아키텍쳐의 구조는 이런 관점보다 계층 ! 이라는 특정 구조에 의해 데이터베이스의 구조를 먼저 생각하고 구현하게 되도록 유도한다 
여기서는 다른 무엇보다도 도메인 로직을 먼저 만들어야 만드는 자신이 로직을 제대로 이해했는지 빠른 검증이 된다고 이야기한다 
(맞는 말 같다, 나역시도 ERD 먼저 설계하고 진행하며 책의 내용을 보고 이해한 내용으로는 의사코드라도 정리해서 도메인 로직을 만들어야 한다는 내용 같다)</p>
<p>계층은 아래 방향으로만 접근가능하므로 도메인 계층에서는 엔티티에 접근이 쉽다
하지만 이러한 구조는 영속성 계층과 도메인 계층이 강한 결합으로 묶여있게된다 
(즉, 도메인 로직에서 트랜잭션 처리, 지연로딩등 영속성 계층에 대한 로직을 추가작성해야함)</p>
<p>영속성 코드가 사실상 도메인 코드에 녹아들어가서 둘 중 하나만 바꾸는 것이 어려워진다 (공감)</p>
</br>

<h2 id="구조상-지름길을-택하기-쉬워진다-">구조상 지름길을 택하기 쉬워진다 ?</h2>
<ul>
<li>여기서 지름길은 부정적인 의미를 이야기함 </li>
</ul>
<p>계층적 아키텍쳐는 구조상 같은 계층에 있는 컴포넌트나 아래에 있는 계층에만 접근이 가능하다는 것이다 
따라서 만약 상위 계층에 위치한 컴포넌트에 접근해야 한다면 간단하게 컴포넌트를 계층 아래로 내려버리면 된다, 그러면 접근 가능하게 되고 깔끔하게 문제가 해결된다 (지름길)</p>
<p>위 내용처럼 하지말라는 뜻이다, 심리적으로 하지 않겠다고 다짐해도 인간은 하게되어있다 범죄가 아니니까. 
그러므로 이런 지름길을 택할시 강제해야 한다. 여기서는 코드리뷰에서의 강제가 아닌 빌드자체를 하지 못하게 규칙을 만들어야 한다고 이야기한다</p>
</br>

<h2 id="테스트-하기-어려워진다-유스케이스를-숨긴다">테스트 하기 어려워진다, 유스케이스를 숨긴다</h2>
<p>만약 엔티티의 특정 필드 하나만 변경하는 요구사항이 생긴다면 컨트롤러에서 차라리 엔티티의 속성을 변경해서 기능을 만드는게 쉽지 않을까? 간단한 기능 이니까!</p>
<ul>
<li>당연히 이렇게 하면 안된다 </li>
</ul>
<p>도메인 계층이 파편화되며 컨트롤러 테스트를 작성할 때 해당 영속성 계층까지 모킹해야하는 일이 발생한다 
그리고 테스트 설정이 복잡해지면 점점 테스트를 전혀 작성하지 않게 되는 방향으로 간다 (공감)</p>
<p>혹은 계층형 아키텍쳐에서의 좋은 방향성은 
좁은 도메인 서비스를 만들도록 지향하자 이다. 즉, 하나씩만 담당하게 한다는 것 <code>UserService</code> 에서 사용자 등록 유스케이스를 추가하는 대신 <code>RegisterUserService</code> 를 추가해보는 것도 나쁘지 않다 -&gt; 좋은 생각같다 !</p>
</br>

<p>출처 </p>
<ul>
<li>만들면서 배우는 클린 아키텍쳐 01장</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>