<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>빠르게 보단 꾸준히</title>
    <link>https://jminc00.tistory.com/</link>
    <description>Github: https://github.com/MinChangJeong</description>
    <language>ko</language>
    <pubDate>Thu, 16 Jul 2026 07:13:58 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>min.c00</managingEditor>
    <image>
      <title>빠르게 보단 꾸준히</title>
      <url>https://tistory1.daumcdn.net/tistory/5148688/attach/1cafd4966aee40f8b931cdc6a47b85b7</url>
      <link>https://jminc00.tistory.com</link>
    </image>
    <item>
      <title>[Kotlin] 코루틴으로 외부 API 병렬 호출 (Dispatchers.IO+MDCContext)</title>
      <link>https://jminc00.tistory.com/112</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 코루틴을 활용해서 API를 병렬로 호출함으로써 스레드를 절약해 성능을 개선하고, 코루틴을 사용함에 따른 로그 개선 필요성을 MDCContext로 해결한 과정에 대해서 공유 하려고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 코루틴으로 외부 API를 병렬 호출하면 성능이 개선되는가?&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API를 호출하는 방식을 기존의 전통적인 방식 (Spring MVC의 동기 방식)과 코루틴을 활용해서 병렬로 호출하는 방식의 차이는 크게 두 가지 개선점을 갖습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 하드웨어 자원 효율 극대화 (스레드 절약)&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 방식에서는 API 3개를 호출하면, 호출자가 결과를 받을 때까지 스레드는 아무것도 못하고 기다려야하는데 이를 &quot;Blocking&quot;이라고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;기존 (Thread-per-request):&lt;/b&gt; 100개의 요청이 오면 100개의 스레드가 필요합니다. 각 스레드는 메모리를 점유하며, 스레드가 많아질수록 서버는 컨텍스트 스위칭 비용 때문에 지치게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;개선 (Coroutine + Dispatchers.IO):&lt;/b&gt; 코루틴은 &lt;b data-index-in-node=&quot;38&quot; data-path-to-node=&quot;5,1,0&quot;&gt;&quot;기다리는 시간&quot;&lt;/b&gt; 동안 스레드를 해제(Suspend)합니다. 일꾼(스레드)은 API 응답을 기다리는 동안 다른 요청을 처리하러 떠날 수 있습니다. 결과적으로 &lt;b data-index-in-node=&quot;126&quot; data-path-to-node=&quot;5,1,0&quot;&gt;훨씬 적은 수의 스레드로 훨씬 많은 요청을 처리&lt;/b&gt;할 수 있게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 응답 시간(Latency)의 획기적인 단축&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;사용자에게 결과를 보여주기 위해 3개의 API(사용자, 주문, 결제)를 호출해야 한다고 가정해 봅시다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;기존 (순차 호출):&lt;/b&gt; API A(1초) + API B(1초) + API C(1초) = &lt;b data-index-in-node=&quot;48&quot; data-path-to-node=&quot;9,0,0&quot;&gt;총 3초&lt;/b&gt; 소요.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;개선 (async 병렬 호출):&lt;/b&gt; API A, B, C를 동시에 호출합니다. 셋 중 가장 오래 걸리는 API 시간만큼만 기다리면 됩니다. = &lt;b data-index-in-node=&quot;78&quot; data-path-to-node=&quot;9,1,0&quot;&gt;총 1초&lt;/b&gt; 소요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 사용자의 경험이 3배 빨라지는 사용성 향상 효과를 얻습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장점만 있는가?&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴을 사용해서 외부 API를 병렬로 호출하는 방식으로 전환하면서 발생할 수 있는 문제가 있습니다. 병렬 호출을 위해 스레드를 여러 개 쓰다 보면 로그가 엉망이 되기 쉽다는 단점이 그것입니다.&lt;b&gt; &quot;어떤 로그가 어떤 사용자의 요청인지&quot; 구분할 수 없게 되는 문제&lt;/b&gt;가 발생합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 코루틴은 로그를 엉망으로 만드는거지?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문에 대한 답을 얻기 위해서는 &lt;b&gt;코루틴의 핵심인 중단(Suspend)과 재개(Resume), Dispatcher, ThreadLocal&lt;/b&gt;의 개념에 대해 이해해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;중단(Suspend)과 재개(Resume)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 함수는 호출되면 현재 스레드의 콜 스택에 쌓이고, 리턴될 때까지 그 스레드를 점유합니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;콜 스택(Call Stack)이란, 함수 호출을 추적하기 위해 메모리에 쌓아둔 자료구조입니다.&lt;br /&gt;예외가 터지면 찍히는 스택 트레이스가 바로 그 순간의 콜 스택입니다.&lt;br /&gt;java.lang.RuntimeException &lt;br /&gt;&amp;nbsp; &amp;nbsp; at Main.square(Main.kt:12) &lt;br /&gt;&amp;nbsp; &amp;nbsp; at Main.calculate(Main.kt:8) &lt;br /&gt;&amp;nbsp; &amp;nbsp; at Main.main(Main.kt:3)&lt;br /&gt;&lt;br /&gt;콜 스택은 두가지 중요한 속성을 갖는데 첫째, &lt;b&gt;콜 스택은 스레드에 묶여 있습니다.&lt;/b&gt; 각 스레드마다 자기만의 콜 스택이 있고, 이건 OS/JVM이 스레드를 만들 때 할당해줍니다. 스레드 A의 콜 스택을 스레드 B가 이어받을수 없습니다(구조적으로 불가능합니다.)&amp;nbsp;&lt;br /&gt;둘째, 콜 스택의 프레임은 해당 함수가 리턴할 때까지 그 자리에 남아있어야 합니다. 내가 square에 있는 동안, 아래에 깔린, calculate, main 프레임은 사라질 수 없습니다.(사라진다면 돌아갈 곳을 잃겠죠?) &lt;b&gt;즉 함수가 실행 중이면 그 스레드는 계속 그 콜 스택을 붙들고 있어야합니다.&amp;nbsp;&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;만약 일반 함수가 중간에 1초를 기다려야 한다면, 그 1초 동안 스택 프레임은 그대로 남아있고 스레드도 그걸 붙들고 있어야 합니다. 스레드가 놀 수 없다는것이 콜 스택구조의 핵심입니다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린의 suspend함수는 일반 함수와는 다릅니다. &lt;b&gt;&quot;중단&quot;은 블로킹이 아니라 현재 상태를 객체로 저장하고 스레드를 반환&lt;/b&gt;한다는 의미입니다. 그리고 &lt;b&gt;&quot;재개&quot;는 저장해둔 상태를 꺼내 이어서 실행&lt;/b&gt;한다는 의미입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 이해가 안되는것 같습니다..&lt;/p&gt;
&lt;pre id=&quot;code_1776641544499&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun fetchUser(id: String): User {
    val token = getToken()      // &amp;larr; 중단 지점 1
    val profile = getProfile(token, id)  // &amp;larr; 중단 지점 2
    return User(profile)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 suspend 키워드가 붙은 fetchUser를 보면 getToken(), getProfile() 함수를 내부에서 호출하고 있습니다. 중단 지점은 각각 두 군데라고 했을 때 컴파일러는 이걸 대략 이런 상태 기계(state machine)으로 바꿉니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;상태기계(State Machine)은 현재 어느 상태에 있는지 기억하는 기계로 입력에 따라 다음 상태로 전이하는 기계를 의미합니다. 코루틴이 상태기계로 바꾸는 이유는 중단됐다가 재개될 때 어디서부터 이어서 실행해야하는지를 알아야 하는데 일반함수라면 콜스택이 알려줄 수 있지만, suspend함수는 스레드를 반환해버려서 콜 스택이 없습니다.&amp;nbsp;&lt;br /&gt;그래서 함수 자체를 &quot;현재 어디까지 실행했는지를 기억하는 상태 기계&quot;로 바꿔버립니다. (아래 when 안에 0,1,2로 구분된 분기는 &quot;지금 이 합수의 어느 지점에 있는지&quot;를 나타내는 상태 번호인 label입니다.&amp;nbsp;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1776641615504&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun fetchUser(id: String, cont: Continuation&amp;lt;User&amp;gt;): Any {
    val sm = cont as FetchUserStateMachine
    when (sm.label) {
        0 -&amp;gt; {
            sm.label = 1
            val result = getToken(sm)  // 비동기 호출, 끝나면 sm으로 재개
            if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
            // 값이 바로 준비됐으면 아래로 흐름
        }
        1 -&amp;gt; {
            val token = sm.result as String
            sm.label = 2
            val result = getProfile(token, id, sm)
            if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
        }
        2 -&amp;gt; {
            val profile = sm.result as Profile
            return User(profile)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 Continuation 객체입니다. &lt;b&gt;중단 지점에서의 지역 변수, 어디까지 실행했는지(label), 어디로 돌아가야 하는지가 전부 이 객체에 담깁니다.&lt;/b&gt; 함수 호출 스택이 아니라 &lt;b&gt;힙(heap) 위에 저장&lt;/b&gt;되는 거죠. 그래서 어느 스레드에서든 이 Continuation만 있으면 함수 실행을 이어갈 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 그렇게 동작하는지 눈으로 봐야겠죠?&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776642026822&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun handleRequest(userId: String) {
    println(&quot;[$userId] 시작      - ${Thread.currentThread().name}&quot;)
    delay(100)  // 중단 지점
    println(&quot;[$userId] 중간      - ${Thread.currentThread().name}&quot;)
    delay(100)  // 중단 지점
    println(&quot;[$userId] 종료      - ${Thread.currentThread().name}&quot;)
}

fun main() = runBlocking(Dispatchers.Default) {
    launch { handleRequest(&quot;A&quot;) }
    launch { handleRequest(&quot;B&quot;) }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 대략 이렇게 나옵니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776642040277&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[A] 시작  - DefaultDispatcher-worker-1
[B] 시작  - DefaultDispatcher-worker-2
[A] 중간  - DefaultDispatcher-worker-3   &amp;larr; 스레드 바뀜!
[B] 중간  - DefaultDispatcher-worker-1   &amp;larr; 또 바뀜!
[A] 종료  - DefaultDispatcher-worker-2
[B] 종료  - DefaultDispatcher-worker-3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 A의 요청을 처리하는 하나의 논리적 흐름이 worker-1 &amp;rarr; worker-3 &amp;rarr; worker-2를 &lt;b&gt;돌아다닙니다&lt;/b&gt;. &quot;코루틴은 스레드에 묶이지 않는다&quot;는것을 확인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Dispatcher&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 &quot;어떤 스레드에서 재개할지&quot;는 누가 결정할까요? Dispatcher는 코루틴의 실행과 재개를 특정 스레드 풀에 배정하는 역할을 담당합니다. Dispatcher는 비유하자면 &quot;작업 반장&quot;으로 코루틴이라는 &quot;일감&quot;을 던져주면 작업 반장은 그 일감을 어떤 스레드(일꾼)에게 보낼지 결정하는 역할을 합니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-path-to-node=&quot;3&quot;&gt;Dispatchers.Default
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,0,0&quot;&gt;성격:&lt;/b&gt; 복잡한 수학 계산, 고화질 이미지 처리, JSON 데이터 파싱 등 &lt;b data-index-in-node=&quot;41&quot; data-path-to-node=&quot;4,0,0&quot;&gt;CPU가 쉴 새 없이 머리를 써야 하는 일&lt;/b&gt;을 담당합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,1,0&quot;&gt;일꾼 수:&lt;/b&gt; 내 컴퓨터의 &lt;b data-index-in-node=&quot;13&quot; data-path-to-node=&quot;4,1,0&quot;&gt;CPU 코어 수&lt;/b&gt;만큼만 일꾼을 뽑습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,2,0&quot;&gt;이유:&lt;/b&gt; 일꾼이 코어 수보다 많아봐야 어차피 동시에 머리 쓸 수 있는 능력은 정해져 있기 때문입니다. 일꾼들끼리 자리를 바꾸는(Context Switching) 시간조차 아까워서 딱 효율적인 수만 유지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-path-to-node=&quot;5&quot;&gt;2. Dispatchers.IO
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;성격:&lt;/b&gt; 외부 API 호출, DB 조회, 파일 읽기/쓰기처럼 **&quot;기다리는 게 일&quot;**인 작업을 담당합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;일꾼 수:&lt;/b&gt; 기본 &lt;b data-index-in-node=&quot;9&quot; data-path-to-node=&quot;6,1,0&quot;&gt;64명&lt;/b&gt; 정도이며, 일이 많아지면 더 늘리기도 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,2,0&quot;&gt;이유:&lt;/b&gt; 이 팀의 일꾼들은 일을 시켜놓고 응답이 올 때까지 가만히 서서 기다리는 시간이 많습니다. 일꾼 한 명이 기다리고 있어도 다른 일꾼이 옆에서 다른 일을 해야 하므로, 일꾼 수를 넉넉하게(Default보다 훨씬 많이) 잡아둡니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-path-to-node=&quot;7&quot;&gt;3. Dispatchers.Main
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,0&quot;&gt;성격:&lt;/b&gt; 사용자의 눈에 보이는 &lt;b data-index-in-node=&quot;16&quot; data-path-to-node=&quot;8,0,0&quot;&gt;화면을 그리거나 버튼 클릭을 처리&lt;/b&gt;하는 일만 담당합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0&quot;&gt;일꾼 수:&lt;/b&gt; 딱 &lt;b data-index-in-node=&quot;8&quot; data-path-to-node=&quot;8,1,0&quot;&gt;1명&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,2,0&quot;&gt;이유:&lt;/b&gt; 화면을 그리는 일은 여러 명에서 달려들면 꼬이기 쉽기 때문에, 한 명의 전담 일꾼이 순서대로 처리하게 합니다. (서버 환경에서는 보통 사용하지 않고 안드로이드 같은 UI 환경에서 필수입니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오해하기 쉬운건 해당 스레드들은 물리적으로 다른 하드웨어를 쓰는것이 아니라 OS위에서 실행되는 똑같은 스레드들입니다. 다만 Dispathcer라는 관리자가 &lt;b&gt;목적에 따라 스레드의 숫자를 조절하고 특정 용도로만 쓰도록 격리&lt;/b&gt;해 놓은 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴이 중단됐다가 재개될 때, Dispatcher가 &quot;이 Continuation을 내 풀의 스레드 중 아무 데나 올려서 실행해&quot;라고 지시합니다. 그래서 &lt;b&gt;같은 코루틴이라도 suspend 전후로 다른 스레드에서 실행될 수 있는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ThreadLocal&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadLocal은 이름 그대로 스레드별로 독립된 값을 저장하는 공간입니다. 스레드 A에서 threadLocal.set(&quot;A&quot;)를 해도 스레드 B에서 threadLocal.get()을 해도 보이지 않습니다. 즉, 각 스레드가 자기만의 저장소를 갖는 것을 의미합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로깅 프레임워크(Logback, Log4j)의 MDC(Mapped Diagnostic Context)가 바로 이 ThreadLocal을 기반&lt;/b&gt;으로 만들어져있습니다.&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;MDC(Mapped Diagnostic Context)는 로그 컨텍스트로 비유하자면 '로그에 붙이는 포스트잇'역할을 합니다. 보통 서버에는 초당 수백 개의 요청이 들어오는데 로그 저장소에 찍힌 수천줄의 로그 중에서 특정 사용자가 보낸 요청만을 골라내기 위해서 로그마다 꼬리표가 필요합니다. 하지만 모든 로그 코드에 logger.info(&quot;ID: $userId, 주문 시작&quot;)처럼 일일이 적는건 매우 귀찮기 때문에 MDC를 활용해 귀찮음을 해결할 수 있습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;MDC.put(&quot;traceId&quot;, &quot;abc-123&quot;) 는 지금부터 이 스레드가 찍는 모든 로그에는 abc-123이라는 포스트잇을 붙여!라고 명령하는것입니다. 그러면 뒤에 logger.info(...) 만 적어도 실제 출력은 [abc-123] ... 이라고 로그를 찍을 수 있게 됩니다.&amp;nbsp;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 코루틴은 로그를 엉망으로 만드는거지?&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MDC는 SLF4J가 ThreadLocal&amp;lt;Map&amp;gt;으로 구현합니다. 즉 각 스레드마다 서랍을 하나씩 갖고 있고, MDC.put(&quot;traceId&quot;, &quot;abc&quot;)를 호출하면 호출한 스레드의 서랍에만 값이 들어갑니다.&lt;/p&gt;
&lt;pre id=&quot;code_1776833940361&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Thread-1 서랍 &amp;rarr; { traceId: &quot;abc&quot; }
Thread-2 서랍 &amp;rarr; { traceId: &quot;xyz&quot; } 또는 비어있음
Thread-3 서랍 &amp;rarr; 비어있음&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 프레임워크가 %X{traceId}를 찍을 때도 지금 로그를 찍고 있는 스레드의 서랍만 들여다봅니다. 전통적인 Servlet 모델에서는 이 구조에서도 로그를 확인하는것이 용이합니다. 요청 1개 = 스레드 1개, 즉 톰캣 워커 스레드가 처음부터 끝까지 한 요청을 처리하기 때문이죠. 필터에서 MDC.put(&quot;traceId&quot;, ...)을 하면 그 스레드 서랍에 저장되고, 요청 내내 같은 스레드에서 실행되므로 모든 로그에 traceId가 찍힙니다. 요청 종료 시 MDC.clear()로 서랍을 비우면 끝나게 되기에 로그를 추적하기에 용이합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제는 코루틴이 이 전제(요청 1개 = 스레드 1개)를 완전히 부수기 때문에 발생합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dispatchers.IO는 기본 64개 스레드를 가진 풀입니다. async/launch를 호출할 때마다 Dispatcher가 그 중 유휴 스레드 아무거나 하나를 골라 할당합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1776834023767&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;MDC.put(&quot;traceId&quot;, &quot;abc&quot;)          // &amp;larr; 호출자 스레드(Tomcat-1)의 서랍에 저장

async(Dispatchers.IO) { api1() }   // Dispatcher가 IO-5를 골라 실행
async(Dispatchers.IO) { api2() }   // Dispatcher가 IO-12를 골라 실행
async(Dispatchers.IO) { api3() }   // Dispatcher가 IO-23을 골라 실행&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점의 서랍 상태를 보면:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1776834035010&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Tomcat-1 서랍 &amp;rarr; { traceId: &quot;abc&quot; }  &amp;larr; 여기에만 있음
IO-5 서랍    &amp;rarr; 비어있음 (또는 이전 요청 잔재)
IO-12 서랍   &amp;rarr; 비어있음
IO-23 서랍   &amp;rarr; 비어있음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api1() 안에서 로그를 찍으면 IO-5의 서랍을 보므로 traceId가 빈 로그가 나옵니다. Dispatcher는 &quot;어디에 찍혔든 신경 안 씀, 그냥 노는 스레드 골라줄 뿐&quot;이라는 태도라, 호출자의 서랍을 복사해주지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 고약한 문제가 있습니다. &lt;b&gt;하나의 코루틴 안에서도 스레드가 바뀝니다.(중단 재개&lt;/b&gt; 메커니즘에&lt;b&gt; 의해서 재개될 땐 다른 스레드일 수 있습니다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776834097854&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;withContext(Dispatchers.IO) {
    logger.info(&quot;A 시작&quot;)   // 예: IO-5에서 실행
    delay(100)             // &amp;larr; 중단: Continuation이 힙으로
    logger.info(&quot;A 끝&quot;)    // 예: IO-17에서 실행 (스레드 바뀜!)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 봤듯이 중단될 때 Continuation은 label과 로컬 변수만 힙에 저장합니다. 스레드의 서랍(ThreadLocal)은 스냅샷에 포함되지 않습니다. 재개 시 Dispatcher가 유휴 스레드를 아무거나 고르므로, 재개 스레드의 서랍은 비어 있거나 &amp;mdash; 더 나쁘게는 &amp;mdash; 과거에 그 스레드를 썼던 다른 요청의 잔재가 남아 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776834129555&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;T=0ms  Tomcat-1에 요청A 도착 &amp;rarr; MDC.put(traceId=A)
T=1ms  async(IO) 호출 &amp;rarr; IO-5 배정됨
           IO-5 서랍: 비어있음
           logger.info(&quot;호출 시작&quot;)
           &amp;rarr; 로그: [traceId=] 호출 시작   &amp;larr; 빈칸!

T=50ms 요청A의 코루틴 suspend (API 대기)
T=60ms Tomcat-1에 요청B 도착 &amp;rarr; MDC.put(traceId=B)  
T=61ms async(IO) 호출 &amp;rarr; 공교롭게도 IO-5 배정
           IO-5에서 요청B 작업 &amp;rarr; 이 스레드 서랍에 B의 traceId가 찍혀버림
           (MDCContext 없이 수동으로 put 했다고 가정)

T=200ms 요청A의 API 응답 도착, 재개 &amp;rarr; IO-5 배정
           IO-5 서랍: { traceId=B }  &amp;larr; B의 잔재!
           logger.info(&quot;호출 완료&quot;)
           &amp;rarr; 로그: [traceId=B] 호출 완료  &amp;larr; A의 로그가 B로 찍힘!!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 A를 디버깅하려고 로그를 뒤져도 &quot;호출 완료&quot; 로그가 B의 traceId로 찍혀 있으니 &lt;b&gt;찾을 수도 없고&lt;/b&gt;, B의 로그를 뒤지다 보면 &lt;b&gt;A에서 일어난 일이 B에서 일어난 것처럼 보입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-test-render-count=&quot;1&quot;&gt;
&lt;div&gt;
&lt;div data-is-streaming=&quot;false&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, &lt;b&gt;ThreadLocal은 스레드를 기준으로 값을 저장하는데, Dispatcher는 스레드를 무작위로 바꾸고, Suspend/Resume은 그 교체가 한 코루틴 안에서도 일어나게 만든다&lt;/b&gt; &amp;mdash; 이 세 가지가 맞물려서 로그와 요청 주인의 연결이 끊어지는 것이 핵심입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MDCContext는 어떻게 해결했는가?&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MDCContext()는 코루틴의 CoroutineContext에 &quot;현재 MDC 스냅샷&quot;을 끼워 넣습니다. 코루틴 라이브러리는 ThreadContextElement 메커니즘을 통해 이 스냅샷을 이용해:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코루틴이 어느 스레드에서 시작/재개되든 &amp;rarr; 그 스레드 서랍에 스냅샷을 심어줌 (updateThreadContext)&lt;/li&gt;
&lt;li&gt;중단되거나 종료될 때 &amp;rarr; 그 스레드 서랍을 원상복구함 (restoreThreadContext)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &quot;스레드에 묶인 ThreadLocal&quot;의 한계를, &quot;코루틴에 묶인 CoroutineContext&quot;로 덮어씌워 해결하는 것입니다. Continuation이 보관해주지 못하는 ThreadLocal을, MDCContext가 이삿짐센터처럼 매 중단&amp;ndash;재개마다 따라다니며 대신 옮겨주는 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Language/kotlin</category>
      <author>min.c00</author>
      <guid isPermaLink="true">https://jminc00.tistory.com/112</guid>
      <comments>https://jminc00.tistory.com/112#entry112comment</comments>
      <pubDate>Wed, 22 Apr 2026 14:03:31 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] OSIV(Open Session In View)</title>
      <link>https://jminc00.tistory.com/111</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Spring Boot OSIV의 개념에 대해서 학습한 내용을 공유해보려고 합니다. 다만 OSIV의 개념을 알기 위해서 부가적으로(부가적이라고 포함했지만, 사실은 매우 중요한 개념들) 알아야 하는 내용들도 중간중간 언급하고 갈 예정임으로 설명이 중구난방이라고 생각이 드실 수도 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OSIV(Open Session In View)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSIV는 'Open Session In View'의 약자 입니다. 그런데 'OSIV'에 대한 설명을 검색하면 다음과 같이 설명하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;OSIV는 HTTP 요청 전체 생명주기 동안 영속성 컨텍스트(Persistence Context)와 데이터베이스 커넥션을 열어두는 패턴입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저는 궁금했습니다. OSIV는 뷰 안에서 세션을 열어놓는다(뜻을 직역하면..ㅎㅎ)라고 하는데 검색 결과는 관련성이 없어 보이는데?라는 생각이 들었습니다. 그래서 이런 명칭이 붙은 이유에 대해서 찾아봤습니다. OSIV가 설계된 역사적 맥락을 보면 OSIV라는 이름이 붙은 이유를 쉽게 알 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2000년대 초반 자바 웹 개발 환경은 JSP + Servlet 시대였습니다. 여기서 view역할을 담당하는 녀석이 JSP입니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;여기서 Servlet은 클라이언트의 http 요청을 받아서 처리하고 응답을 돌려주는 자바 프로그램입니다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSP에서는 엔티티 객체를 직접 접근하는 것이 일반적이었습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776041313391&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;c:forEach items=&quot;${orders}&quot; var=&quot;order&quot;&amp;gt;
   ${order.member.name}      &amp;larr; JSP에서 Lazy Loading 발생
   ${order.delivery.address} &amp;larr; JSP에서 Lazy Loading 발생
&amp;lt;/c:forEach&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 위와 같이 order 엔티티에 대해서 직접 member, delivery같은 값에 직접 접근하는 것을 확인할 수 있습니다. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이 시대에는 View 레이어에서 엔티티를 직접 쓰는 게 자연스러운 패턴이었고, OSIV 없이는 JSP 렌더링 중 &lt;/span&gt;LazyInitializationException&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이 터지는 게 일상이었습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;LazyInitializationException는 영속성 컨텍스트가 닫힌 후에 Lazy Loading을 시도할 때 발생하는 예외를 의미합니다.(자세한 설명은 추후 언급)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;여기까지의 설명에 의해서 'Open Session In View' 중 Session을 제외하고는 어느 정도 이해가 된 것 같습니다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;뷰안에서 세션을 열겠다는데 여기서 말하는 세션은 아마 order.member, order.delivery 등 엔티티에 접근할 수 있도록(다른 의미로는 LazyInitializationException 예외가 발생하지 않도록) 세션을 열어둔다는 것 같은데 여기서 말하는 세션? 은 뭔지 궁금해졌습니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;영속성 컨텍스트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 영속성 + 컨텍스트의 합성어입니다. 영속성이란 '데이터를 영구 저장하는 성질', 컨텍스트는 '동작하기 위한 상태와 환경'이라는 의미를 갖습니다. 그래서 Spring Boot에서 영속성 컨텍스트란 '&lt;b&gt;엔티티(데이터)를 DB(영구 저장소)와 연결된 상태로 유지하고 관리하는 환경 전체&lt;/b&gt;'를 의미합니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;영속성 컨텍스트와 DB 커넥션은 혼동하기 쉽지만 엄연히 다른 개념입니다. 영속성 컨텍스트는 JPA가 엔티티를 관리하는 메모리 공간이라면, DB 커넥션은 실제 DB와의 네트워크 연결을 의미합니다. 둘의 관계는 &lt;b&gt;영속성 컨텍스트가 실제 DB 작업이 필요한 순간에 DB 커넥션을 빌려 씁니다.&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 쉽게 말하면 영속성 컨텍스트란 '&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;JPA가 엔티티 객체를 저장해 두는 1차 캐시&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;'입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776042290777&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 같은 트랜잭션 내에서
Member m1 = memberRepository.findById(1L); // DB 조회
Member m2 = memberRepository.findById(1L); // DB 조회 안 함! 영속성 컨텍스트에서 꺼냄

m1 == m2 // true &amp;rarr; 동일 객체 보장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;m1, m2과 객체를 새로 생성하는 것처럼 보이지만, 사실상 동일한 객체이며 그게 가능한 이유는 영속성 컨텍스트의 메커니즘 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776042454666&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;영속성 컨텍스트 (항상 열려있음)
       &amp;darr; SQL이 필요한 순간에만
   DB 커넥션 획득 &amp;rarr; SQL 실행 &amp;rarr; DB 커넥션 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;m1, m2 객체의 할당 과정을 도식화하면 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776042681913&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;findById(1L) 호출
        &amp;darr;
영속성 컨텍스트 HashMap 확인
EntityKey(id=1, Member.class) 있어?
        &amp;darr;
        ❌ 없음 (최초 조회)
        &amp;darr;
DB 커넥션 획득
        &amp;darr;
SELECT * FROM member WHERE id = 1 실행
        &amp;darr;
DB 커넥션 반환
        &amp;darr;
결과로 Member 객체 생성
{ id: 1, name: &quot;홍길동&quot; }
        &amp;darr;
┌─────────────────────────────────┐
│       영속성 컨텍스트 HashMap       │
│                                 │
│  EntityKey(1, Member) &amp;rarr; [m1]    │  &amp;larr; 저장
│                                 │
│  스냅샷도 함께 저장                  │
│  { id:1, name:&quot;홍길동&quot; }          │
└─────────────────────────────────┘
        &amp;darr;
m1 변수에 해당 객체 참조 반환&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1776042673831&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;findById(1L) 호출
        &amp;darr;
영속성 컨텍스트 HashMap 확인
EntityKey(id=1, Member.class) 있어?
        &amp;darr;
        ✅ 있음!
        &amp;darr;
┌─────────────────────────────────┐
│       영속성 컨텍스트 HashMap       │
│                                 │
│  EntityKey(1, Member) &amp;rarr; [m1]    │  &amp;larr; 그냥 꺼냄
│                                 │
└─────────────────────────────────┘
        &amp;darr;
DB 커넥션 획득 ❌ (DB 안 감)
        &amp;darr;
m2 변수에 똑같은 객체 참조 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 도식화를 보면, 처음 m1을 할당할 때는 EntityKey에 해당되는 객체가 없어 DB 커넥션을 획득하고 조회 쿼리를 실행한 뒤 Member 객체를 새로 생성하고 생성하고 m1에 할당하는 것을 확인할 수 있습니다. 그리고 조회한 Member 데이터는 새로운 EntityKey로 영속성 컨텍스트에 저장되는 것을 확인할 수 있습니다. 이후 m2는 동일한 EntityKey를 갖는 객체를 find 하려고 하기에 db에서 조회되지 않고 영속성 컨텍스트(힙 메모리)상에 있는 캐싱된 데이터를 할당받는 것을 확인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 통해 영속성 컨텍스트의 설계를 조금 더 자세히 이해해보려고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776042944514&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// JPA 스펙 (인터페이스)
public interface EntityManager {
    void persist(Object entity);
    &amp;lt;T&amp;gt; T find(Class&amp;lt;T&amp;gt; entityClass, Object primaryKey);
    void remove(Object entity);
    // ...
}

// Hibernate 구현체
public class SessionImpl implements EntityManager {

    // ✅ 1차 캐시의 실제 구현
    private final StatefulPersistenceContext persistenceContext;

    // ✅ DB 커넥션
    private JdbcCoordinator jdbcCoordinator;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1776043253047&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Hibernate - StatefulPersistenceContext.java
public class StatefulPersistenceContext implements PersistenceContext {

    // ✅ 바로 이게 1차 캐시의 실체 - 그냥 HashMap!
    private HashMap&amp;lt;EntityKey, Object&amp;gt; entitiesByKey;

    // ✅ 변경 감지를 위한 스냅샷 저장소
    private HashMap&amp;lt;EntityKey, Object[]&amp;gt; entitySnapshotsByKey;

    // 엔티티 저장
    public void addEntity(EntityKey key, Object entity) {
        entitiesByKey.put(key, entity);  // 단순히 Map에 put
    }

    // 엔티티 조회
    public Object getEntity(EntityKey key) {
        return entitiesByKey.get(key);   // 단순히 Map에서 get
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1776043266974&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EntityKey {
    private final Object identifier;      // PK 값 (예: 1L)
    private final EntityPersister persister; // 엔티티 타입 정보 (예: Member.class)

    // HashMap의 key로 쓰이므로 equals/hashCode 구현
    @Override
    public boolean equals(Object other) {
        EntityKey otherKey = (EntityKey) other;
        return identifier.equals(otherKey.identifier)
            &amp;amp;&amp;amp; persister.equals(otherKey.persister);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 JPA 스펙의&lt;span&gt;&amp;nbsp;&lt;/span&gt;EntityManager&lt;span&gt;&amp;nbsp;&lt;/span&gt;인터페이스이고, Hibernate가 이를 구현합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;JPA는 코드가 아니라 문서(명세)입니다. JPA (Jakarta Persistence API)의 뜻에서도 확인할 수 있듯이 JPA는 &quot;자바에서 ORM을 이렇게 구현하면 된다&quot;는 규칙 문서입니다. 실제로 jakarta.persistence 패키지에는 인터페이스와 어노테이션만 있는것을 확인할 수 있습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;Hibernate는 JPA 인터페이스를 실제로 구현한 클래스를 의미합니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;// 1. JPA Interface&lt;br /&gt;public&amp;nbsp;interface&amp;nbsp;EntityManager&amp;nbsp;{&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; void persist(Object entity);&amp;nbsp;&amp;nbsp;// &quot;이런 메서드 만들어라&quot;만 선언&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; ...&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;// 2. org.hibernate.internal.SessionImpl&lt;br /&gt;public&amp;nbsp;class&amp;nbsp;SessionImpl&amp;nbsp;implements&amp;nbsp;EntityManager&amp;nbsp;{&amp;nbsp;&amp;nbsp;// JPA 인터페이스 구현&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; @Override&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;public&amp;nbsp;void&amp;nbsp;persist(Object&amp;nbsp;entity)&amp;nbsp;{&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;// 실제로 영속성 컨텍스트(HashMap)에 넣는 코드&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;persistenceContext.addEntity(entity);&amp;nbsp;&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, findById(1L)을 두 번 호출하게 되면 다음과 같습니다. 결국 영속성 컨텍스트는 '&lt;b&gt;HashMap에 저장된 엔티티 객체의 집합을 의미합니다.&lt;/b&gt;'&lt;/p&gt;
&lt;pre id=&quot;code_1776043293167&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1번 호출 &amp;rarr; EntityKey(id=1, Member.class) &amp;rarr; Map에 없음 &amp;rarr; DB 조회 &amp;rarr; Map에 저장
2번 호출 &amp;rarr; EntityKey(id=1, Member.class) &amp;rarr; Map에 있음 &amp;rarr; 그냥 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;LazyInitializationException&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 영속성 컨텍스트까지 이해하면서 'Open Session In View'의 의미는 뷰안에서 세션(영속성 컨텍스트)을 열어둔다'로 이해할 수 있게 되었습니다. 그 목적은 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;OSIV 없이는 JSP 렌더링 중에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;LazyInitializationException 예외가 발생하기 때문이라고 위에서 잠깐 언급했었습니다. 그럼 &lt;b&gt;LazyInitializationException는 왜 발생하는 것일까요?&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 &lt;b&gt;LazyInitializationException&lt;/b&gt;이 발생하는 원인에 대해서 단계적으로 분류하면 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;findById() 호출 - DB조회&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; letter-spacing: 0px;&quot;&gt;Hibernate가 Order 객체 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;영속성 컨텍스트에 저장&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;@Transactional 메서드 종료&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;Lazy Loading 시도&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;&lt;b&gt;LazyInitializationException 예외 발생&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 예시를 통해 확인해 봅시다. 먼저 여기 Order와 Member 클래스가 있습니다. 두 엔티티 간 관계는 1:M 관계를 갖습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776043596342&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Order {
    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;
}

@Entity
public class Member {
    @Id
    private Long id;
    private String name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776043605697&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1단계: findById() 호출 - DB 조회
orderRepository.findById(1L) 호출
        &amp;darr;
영속성 컨텍스트 HashMap 확인
EntityKey(id=1, Order.class) 없음
        &amp;darr;
DB 커넥션 획득
        &amp;darr;
SELECT * FROM orders WHERE id = 1 실행
        &amp;darr;
결과: { id: 1, member_id: 100 }   &amp;larr; member_id(FK)만 있음
                                     member 테이블은 조회 안 함&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시나리오는 다음과 같습니다. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;orderRepository.findById(1L)를 호출하게 되면 영속성 컨텍스트에는 order에 대해서만 값을 갖고 하위 객체인 member에 대해서는 조회를 수행하지 않습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776043704270&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 2단계: Hibernate가 Order 객체 생성
DB 결과를 받아서 Order 객체 생성 시작
        &amp;darr;
id 필드       &amp;rarr; 1           정상 할당
member 필드   &amp;rarr; ???

LAZY 설정이므로 실제 Member 객체 대신
프록시 객체를 생성해서 넣음
        &amp;darr;
프록시 객체란?

class Member$HibernateProxy extends Member {

    private Long id = 100L;   // FK 값만 알고 있음
    private boolean loaded = false;  // 아직 DB 조회 안 함

    @Override
    public String getName() {
        if (!loaded) {
            // DB 조회 시도 &amp;larr; 나중에 문제가 터지는 곳
            loadFromPersistenceContext();
        }
        return super.getName();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776043917869&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 3단계: 영속성 컨텍스트에 저장
┌──────────────────────────────────────────────────┐
│              영속성 컨텍스트 HashMap               │
│                                                  │
│  EntityKey(1, Order) &amp;rarr;  Order {                  │
│                           id: 1                  │
│                           member: [프록시 ]      │
│                                    id=100        │
│                                    loaded=false  │
│                         }                       │
└──────────────────────────────────────────────────┘
        &amp;darr;
DB 커넥션 반환
        &amp;darr;
Order 객체 반환&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1776043933014&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 4단계: @Transactional 메서드 종료
@Transactional
public Order findOrder(Long id) {
    Order order = orderRepository.findById(id).get();
    return order;
}   // &amp;larr; 여기서 메서드 종료

메서드 종료
        &amp;darr;
트랜잭션 커밋
        &amp;darr;
flush() 실행 (변경사항 없으므로 SQL 없음)
        &amp;darr;
영속성 컨텍스트 소멸

┌──────────────────────────────────────────────────┐
│              영속성 컨텍스트 HashMap                 │
│                                                  │
│              (텅 빔)                              │
│                                                  │
└──────────────────────────────────────────────────┘

단, Order 객체 자체는 아직 JVM 힙 메모리에 살아있음
order 변수가 참조하고 있으므로 GC 안 됨

힙 메모리:
  Order {
    id: 1
    member: [프록시 ]  &amp;larr; 이 프록시는 여전히 존재
                          하지만 영속성 컨텍스트와
                          연결이 끊어진 상태
  }&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1776044042600&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 5단계: Controller에서 Lazy Loading 시도
@GetMapping(&quot;/order/{id}&quot;)
public String getOrder(@PathVariable Long id) {

    Order order = orderService.findOrder(id);
    //  영속성 컨텍스트 이미 없음

    String name = order.getMember().getName();
    // 프록시 객체의 order.getMember() 호출
            &amp;darr;
    힙 메모리에서 프록시 객체 반환
    (프록시는 살아있지만 영속성 컨텍스트와 단절된 상태)
            &amp;darr;
    .getName() 호출
            &amp;darr;
    프록시 내부 동작:

    if (!loaded) {
        // DB 조회하려면 영속성 컨텍스트 필요
        Session session = getCurrentSession();
        //                        &amp;uarr;
        //               ThreadLocal 뒤짐
        //               아무것도 없음 (이미 소멸)
        //                        &amp;darr;
        //               session == null
        //                        &amp;darr;
        //           LazyInitializationException !
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1776044060921&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 6단계: 예외 발생
org.hibernate.LazyInitializationException:
  failed to lazily initialize a collection or proxy
  of class com.example.Member
  - could not initialize proxy - no Session

          프록시 초기화 실패
                &amp;uarr;
         Session(영속성 컨텍스트)을
         찾을 수 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이 예외는 프록시 객체가 살아있는데 그것을 초기화할 수 있는 환경(영속성 컨텍스트)가 이미 사라진 상태에서 발생합니다. 예외 이름 자체도 &quot;지연 초기화(&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Lazy Initialization)에 실패했다는 것을 의미합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OSIV 주의사항&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;다시 처음으로 돌아와서 OSIV를 검색하면 나오는 간단 설명을 다시 언급하자면 '&lt;b&gt;&lt;span style=&quot;color: #666666; text-align: center;&quot;&gt;OSIV는 HTTP 요청 전체 생명주기 동안 영속성 컨텍스트(Persistence Context)와 데이터베이스 커넥션을 열어두는 패턴입니다.&lt;/span&gt;&lt;/b&gt;' 라고. 했습니다. 현대 개발 패턴은 View는 DTO만 알면 되기에 영속성 컨텍스트가 레이러를 벗어날 이유가 없음에도 아직 OSIV 기본 설정은 'true'입니다. 즉, &lt;b&gt;&lt;span style=&quot;color: #666666; text-align: center;&quot;&gt;HTTP 요청 전체 생명주기 동안 영속성 컨텍스트와 데이터베이스 커넥션을 열어두겠다는 것입니다!!&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #666666; text-align: center;&quot;&gt;그럼 왜 osiv는 여전히 기본값을 true값을 갖을까요?&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #666666; text-align: center;&quot;&gt;Spring Boot 팀의 입장은 &quot;하위 호환성을 깨지 않겠다!&quot;라고 표명했습니다. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;OSIV true를 믿고 짠 레거시 코드가 전 세계에 너무 많고,&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; 갑자기 false로 바꾸면 수많은 서비스가 LazyInitializationException으로 터질수 있기 때문에&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; 그래서 기본값은 유지하되, 경고 로그를 띄워서 인지하게 하도록 조치를 취했습니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. GitHub Issue #7107 (2016년, Spring Boot 1.x)&lt;br /&gt;2016년에 이미 커뮤니티에서&amp;nbsp;&quot;OSIV는 안티패턴으로 널리 여겨지므로 기본값을 opt-in 방식으로 바꿔야 한다&quot;는 이슈가 제기되었습니다. 제안이 받아들여지지 않는다면 최소한 문서에 이 동작을 명확히 기술해야 한다고 요청했습니다.&amp;nbsp;GitHub&lt;br /&gt;Spring Boot 팀은 기본값을 false로 바꾸는 대신,&amp;nbsp;경고 로그를 추가하는 것으로 타협했습니다. 이것이 우리가 보는 그 경고 로그의 탄생 배경입니다.&lt;br /&gt;&lt;br /&gt;2. GitHub Issue #47547 (2025년 10월, Spring Boot 4.0 논의)&lt;br /&gt;9년이 지난 2025년에도 같은 논의가 다시 제기됐습니다.&amp;nbsp;Spring Boot 4.0에서 기본값을 false로 바꾸자는 요청이었는데, Spring Boot 팀은 이번에도&amp;nbsp;status: declined&amp;nbsp;(현재 적용하지 않겠다)&amp;nbsp;로 닫아버렸습니다.&amp;nbsp;GitHub&lt;br /&gt;즉, &quot;하위 호환성을 깨지 않겠다&quot;는 입장이 9년째 유지되고 있는 것입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1776044518254&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Log a warning on startup when spring.jpa.open-in-view is enabled but user has not explicitly opted in &amp;middot; Issue #7107 &amp;middot; spring-p&quot; data-og-description=&quot;Considering OSIV/OEMIV is widely considered an anti-pattern, OpenEntityManagerInViewInterceptor should IMO not be enabled by default. Rather than that it should be opt-in. If this proposal isn't ac...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/spring-projects/spring-boot/issues/7107&quot; data-og-url=&quot;https://github.com/spring-projects/spring-boot/issues/7107&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Vxp11/dJMb8VNwEnM/u18GhvZORWhkTHnHcrkx40/img.png?width=1200&amp;amp;height=600&amp;amp;face=971_150_1023_208,https://scrap.kakaocdn.net/dn/bqeXxh/dJMb8SpJcNL/2PY1uKFSbDMSIotlk1lO01/img.png?width=1200&amp;amp;height=600&amp;amp;face=971_150_1023_208&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-boot/issues/7107&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/spring-projects/spring-boot/issues/7107&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Vxp11/dJMb8VNwEnM/u18GhvZORWhkTHnHcrkx40/img.png?width=1200&amp;amp;height=600&amp;amp;face=971_150_1023_208,https://scrap.kakaocdn.net/dn/bqeXxh/dJMb8SpJcNL/2PY1uKFSbDMSIotlk1lO01/img.png?width=1200&amp;amp;height=600&amp;amp;face=971_150_1023_208');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Log a warning on startup when spring.jpa.open-in-view is enabled but user has not explicitly opted in &amp;middot; Issue #7107 &amp;middot; spring-p&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Considering OSIV/OEMIV is widely considered an anti-pattern, OpenEntityManagerInViewInterceptor should IMO not be enabled by default. Rather than that it should be opt-in. If this proposal isn't ac...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Spring Boot는 OSIV가 켜져 있을 때 이런 경고를 냅니다:&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1776044432477&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WARN: spring.jpa.open-in-view is enabled by default.
Therefore, database queries may be performed during view rendering.
Explicitly configure spring.jpa.open-in-view to disable this warning.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;직역하면 &quot;이거 켜져 있는거 알고 있지? 명시적으로 너가 설정해!!&quot;라고 강한(?) 압박을 주는 경고 메시지를 주고 있습니다.&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마무리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 사용하면 빈번하게 만나는 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;LazyInitializationException로 인해서 시작한 학습은 꽤 성공적이었던것 같습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <author>min.c00</author>
      <guid isPermaLink="true">https://jminc00.tistory.com/111</guid>
      <comments>https://jminc00.tistory.com/111#entry111comment</comments>
      <pubDate>Mon, 13 Apr 2026 10:45:22 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] Transaction Proxy, Self-Invocation 문제</title>
      <link>https://jminc00.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번글은 지금껏 당연하게 써왔던 @Transactional 어노테이션이 동작하는지에 대해서 실습을 통해서 이해해보려고 합니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;결론부터 말하면 Spring에서 @Transactional 어노테이션을 선언하면 Spring은 해당 빈(Bean)을 직접 실행하는 대신 프록시 객체를 앞에 내세웁니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li style=&quot;color: #000000;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,0,0&quot;&gt;프록시 생성:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Spring 컨테이너는 트랜잭션이 적용된 클래스에 대해 프록시 객체를 생성하여 빈으로 등록합니다.&lt;/li&gt; 
 &lt;li style=&quot;color: #000000;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,1,0&quot;&gt;호출 가로채기 (Intercept):&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클라이언트가 메서드를 호출하면 실제 객체가 아닌&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;48&quot; data-path-to-node=&quot;4,1,0&quot;&gt;프록시의 메서드&lt;/b&gt;가 호출됩니다.&lt;/li&gt; 
 &lt;li style=&quot;color: #000000;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,2,0&quot;&gt;부가 기능 수행 (Transaction Advisor):&lt;/b&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;4,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;프록시는 실제 로직 실행 전에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;17&quot; data-path-to-node=&quot;4,2,1,0,0&quot;&gt;Transaction Manager&lt;/b&gt;를 통해 트랜잭션을 시작합니다(begin).&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li style=&quot;color: #000000;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,3,0&quot;&gt;타겟 메서드 실행:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프록시 내부에서 실제 비즈니스 로직(Target)을 호출합니다.&lt;/li&gt; 
 &lt;li style=&quot;color: #000000;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,4,0&quot;&gt;결과 처리:&lt;/b&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;4,4,1&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;성공 시: 트랜잭션을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;12&quot; data-path-to-node=&quot;4,4,1,0,0&quot;&gt;Commit&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;합니다.&lt;/li&gt; 
   &lt;li&gt;예외 발생 시: 트랜잭션을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;15&quot; data-path-to-node=&quot;4,4,1,1,0&quot;&gt;Rollback&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;합니다.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터 정리할 글의 요약본입니다. 다만, 직접 코드를 통해서 확인하기 전까지는 내용이 너무 추상적으로만 느껴지는 것 같습니다. 그래서 실제 코드 예시와 함께 원리를 좀 더 심층적으로 다뤄볼까 합니다.&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 원본 클래스
@Service
public class OrderService {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Transactional
	public void methodA() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;여기 OrderService라는 클래스가 있습니다. 어플리케이션이 시작되면 BeanPostProcesser는 @Transactional 어노테이션을 스캔하고 OrderService에 methodA에 해당 어노테이션이 있음을 감지합니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;그럼 OrderService를 빈으로 등록하지 않고 프록시 객체를 빈으로 등록합니다. 프록시 객체를 빈으로 등록한다는 말까지도 저한테는 너무 추상적인 것 같습니다. 구체적인 코드를 보면 다음과 같습니다.&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// Spring이 런타임에 생성하는 프록시 (개념적 표현)
public class OrderService$Proxy extends OrderService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private OrderService target; // 원본 객체를 내부에 보관

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void methodA() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;	// 트랜잭션 시작
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;	// 트랜잭션 종료
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;런타임에 생성하는 프록시는 다음과 같이 생성되며 프록시 객체내부에 원본 클래스인 OrderService를 객체 내부에 보관하고 methodA를 오버라이딩해서 트랜잭션의 시작과 종료 중간에 로직을 위치시켜서 Transaction Manager를 활용해서 트랜잭션을 관리합니다. (Transaction Manager는 본 글과 관련성은 있으나 글의 흐름을 위해서 구체적인 내용은 생략하겠습니다.)&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;정리하자면 다음과 같습니다. Controller에서는 OrderService가 아닌 OrderService의 프록시 객체를 주입받습니다. 그리고 methodA를 호출하면 프록시 객체 내에 오버라이딩된 methodA를 실행함으로써 트랜잭션 세션 내에서 원본 클래스의 methodA의 구현을 수행합니다.&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;Controller → proxy.methodA() 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ 트랜잭션 시작&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ← 프록시가 처리 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ target.methodA()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;← 원본 객체로 넘어감 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ 트랜잭션 커밋&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;여기까지가 사실 이 글의 핵심이지만, 실무에서 가장 중요한 Self-Invocation 문제를 알아야합니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;Self-Invocation은 Self = 자기자신, Invocation = 호출의 합성어입니다.&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 원본 클래스
public class OrderService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Transactional
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void methodA() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.methodB(); // 자기 자신(this)을 호출 = Self-invocation
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void methodB() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;	...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;여기 OrderService에 methodB가 추가되었고 methodB는 methodA에 의해서 호출됩니다. 여기서 this는 프록시가 아니라 '원본 객체 자신'을 가리키기 때문에, 프록시를 완전히 건너뛰게 됩니다. 그래서 AOP가 개입할 수 없는 이 상황을 Self-invocation 문제라고 부릅니다. FM대로 설명을 위한 문장을 작성했지만, 솔직히 잘 이해가 되지 않는 것 같습니다.&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// Spring이 런타임에 생성하는 프록시 (개념적 표현)
public class OrderService$Proxy extends OrderService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private OrderService target; // 원본 객체를 내부에 보관

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void methodA() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;	// 트랜잭션 시작
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;target.methodB();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;	// 트랜잭션 종료
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void methodB() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;	...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이전에 프록시 객체를 수정하면 다음과 같습니다. 여기서 methodA에서 호출하는 methodB는 target(원본객체)의 methodB를 호출하는 것입니다. 즉, 프록시 객체를 무시하고 원본 객체의 메서드를 호출하기 때문에 AOP가 개입할 수 없다는 문제가 있습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;그럼 왜 AOP가 개입할 수 없는게 문제가 될까요?&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;Spring의 트랜재겻ㄴ은 AOP 프록시 기반으로 동작합니다. 지금까지 설명한 프록시 객체는 전부 AOP 덕분에 가능한 메커니즘이었습니다. AOP가 개입할 수 없다는 것은 @Transactional&lt;span style=&quot;color: #000000;&quot;&gt;이나&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;@Async&lt;span style=&quot;color: #000000;&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;@Cacheable&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;같은 어노테이션을 통해 &quot;이 메서드에는 이런 부가 기능을 더해줘&quot;라고 명시적으로 명령 했음에도 불구하고 그 명령이 완벽하게 무시된다는 것을 의미합니다.&amp;nbsp;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;좀 더 설명을 덧붙히자면 스프링의 핵심 가치는 IoC(제어의 역전)입니다. 이 개념은 객체의 생명주기와 부가 기능 처리를 스프링 컨테이너가 제어하게 두는 것을 의미합니다.&amp;nbsp;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉,&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; 프록시의 개입 = 스프링의 관리&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 하에 있음을 의미합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만, this.method() 호출은 스프링이 아닌 JVM의 순수한 내부 호출로 돌아가는 것을 의미합니다. 스프링이 관리하는 빈으로서의 특권을 스스로 포기하고 일반 객체로 동작해 버리기 때문에 프로시가 끼어들 틈(Join Point) 자체가 사라져 버립니다.&amp;nbsp;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그럼 이게 왜 문제가 되는것이냐? 그건 트랜잭션이 의도대로&amp;nbsp;동작하지 않을 가능성이 있기 때문입니다. 코드로 이해해 봅시다&lt;/span&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
public class OrderService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final OrderRepository orderRepository;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public OrderService(OrderRepository orderRepository) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.orderRepository = orderRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Transactional
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void methodA() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// this.methodB()는 프록시를 거치지 않고 직접 호출됨
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 따라서 methodB의 @Transactional이 무시됨
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.methodB();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;throw new RuntimeException(&quot;methodA 실패&quot;); // methodA 트랜잭션 롤백
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 다른 트랜잭션에서 methodB를 호출할 때 확인용
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Transactional(propagation = Propagation.REQUIRES_NEW)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void methodB() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order order = new Order(&quot;ORDER-NEW-&quot; + System.currentTimeMillis());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderRepository.save(order);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Transaction Manager에 대해서 잠깐 언급하고 가자면 트랜잭션을 처리할 때 참고하는 4가지 핵심 설정 값이 있습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;Propagation (전파):&lt;/b&gt;&amp;nbsp;기존 트랜잭션이 있을 때 새로 시작할지, 참여할지 결정 (예:&amp;nbsp;REQUIRED,&amp;nbsp;REQUIRES_NEW)&lt;/li&gt;&lt;li&gt;&lt;b&gt;Isolation (격리 수준):&lt;/b&gt;&amp;nbsp;트랜잭션 간의 데이터 간섭 정도를 설정 (예:&amp;nbsp;READ_COMMITTED)&lt;/li&gt;&lt;li&gt;&lt;b&gt;Timeout (시간제한):&lt;/b&gt;&amp;nbsp;특정 시간 내에 완료되지 않으면 롤백.&lt;/li&gt;&lt;li&gt;&lt;b&gt;ReadOnly (읽기 전용):&lt;/b&gt;&amp;nbsp;true로 설정 시 성능 최적화(Flush 생략 등)를 꾀함.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그중에서 전파는 위의 설명처럼 &quot;이미 트랜잭션이 진행 중일 때, 추가로 트랜잭션을 수행해야 할 경우 어떻게 동작할지를 결정하는 설정&quot;입니다. 전파는 7가지가 있지만 2가지에 대해서만 언급하자면&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;REQUIRED(default): 기존 트랜잭션이 있으면 참여하고, 없으면 새로 생성합니다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;REQUIRED_NEW: 기존 트랜잭션 유무와 상관없이 항상 새로운 트랜잭션을 시작합니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위의 코드 예시를 보면 methodB는 전파 옵션이 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;REQUIRED_NEW로 되어있기에 methodA의 트랜잭션과 별개로 항상 새로운 트랜잭션을 시작합니다. 그래서 methodA가 작업을 실패하더라도 order 객체가 생성되어 DB에 저장될 것입니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;과연 그럴까요?&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞서 설명한 것처럼 현재의 코드는 Sefl-Invocation 문제가 있습니다. 즉, this.methodB로 실행될 때 원본 객체를 참조하기에 AOP가 개입할 수 없는 문제가 있습니다. 때문에 @Transactional 어노테이션의 설정을 무시하고 methodA가 실패하면 전체 롤백되는 의도와는 다른 동작을 할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그럼 이 Self-Invocation 문제를 어떻게 해결할 수 있을까요?&amp;nbsp;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;답은 프록시 객체를 이용한다 입니다. 프록시가 개입하기 위해서는 반드시 프록시를 통해 호출되어야 합니다. Order2Service또한 프록시 객체를 생성할 것이기 때문에 AOP 관리 대상이고 이제는 트랜잭션이 의도대로 동작할 수 있게 되었습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
public class Order2Service {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final OrderRepository orderRepository;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public Order2Service(OrderRepository orderRepository) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.orderRepository = orderRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 다른 트랜잭션에서 methodB를 호출할 때 확인용
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Transactional(propagation = Propagation.REQUIRES_NEW)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void methodB() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order order = new Order(&quot;ORDER-NEW-&quot; + System.currentTimeMillis());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderRepository.save(order);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
public class OrderService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Autowired
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Order2Service order2Service;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Transactional
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void methodA() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;order2Service.methodB();&amp;nbsp;&amp;nbsp;// methodB 트랜잭션은 별도의 트랜잭션을 선언되어 롤백되지 않음.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;throw new RuntimeException(&quot;methodA 실패&quot;); // methodA 트랜잭션 롤백
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;proxy.methodA() 호출
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ 트랜잭션A 시작 (커넥션 1)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ target.methodA() 실행
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ proxy.methodB() 호출&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ← 프록시 거침
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ REQUIRES_NEW 확인
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ 트랜잭션A 중단
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ 트랜잭션B 시작 (커넥션 2)&amp;nbsp;&amp;nbsp;← 물리적으로 별도 커넥션
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ methodB 작업 실행
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ 트랜잭션B 커밋
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ 트랜잭션A 재개
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ RuntimeException 발생
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ 트랜잭션A 롤백 (methodB는 이미 커밋됐으므로 영향 없음)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <author>min.c00</author>
      <guid isPermaLink="true">https://jminc00.tistory.com/110</guid>
      <comments>https://jminc00.tistory.com/110#entry110comment</comments>
      <pubDate>Thu, 9 Apr 2026 10:23:20 +0900</pubDate>
    </item>
    <item>
      <title>Choreography 전략 - OpenTelemetry, Jaeger를 활용한 분산 추적</title>
      <link>https://jminc00.tistory.com/109</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로서비스 아키텍처(MSA)는 기존의 모놀리식 구조가 가진 비즈니스 확장성의 한계를 극복하기 위해서 현대의 백엔드 생태계에서는 MSA가 선택이 아닌 필수가 되어가고 있습니다. 하지만 서비스를 분리하는 것보다 어려운것은 '분리된 서비스들을 어떻게 유기적으로 연결하고 관리할 것인가' 인것 같습니다. 서비스가 1~2개 수준이아니라 더 많은 수의 서비스로 분리되는데 적절한 관리 체계가 구축되어있지 않다면 오히려 유지보수 비용이 증가할것 이라고 생각합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 글에서는 MSA에서 서비스간 통신 모델과 분산 추적 실습에 대해서 학습한 내용을 공유해보려고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Orchestration vs &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Choreography&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;MSA 설계에서 중요한건 서비스 간의 통신 모델을 결정하는 것입니다. 이 선택은 시스템의 복잡도와 유지보수성에 영향을 줄 수 있는 중요한 부분이라고 생각합니다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 도메인 내부의 질서 Orchestration&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 도메인(예: 결제, 주문 등) 내부에서는 비즈니스 규칙이 복잡하고 단계별 원자성(Atomicity)이 중요합니다. 이때는 중앙에서 흐름을 제어하는 '지휘자'가 필요합니다. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;도메인 내부에서 원자성을 지키기 위해 가장 많이 사용되는 방식은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b data-index-in-node=&quot;36&quot; data-path-to-node=&quot;3&quot;&gt;Orchestration 기반의 Saga 패턴&lt;/b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;입니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이를 위해 중앙의 'Saga 매니저'가 각 서비스의 실행 순서를 제어하고, 오류 발생 시 역순으로 '취소' 명령을 내리는 과정의 간단한 예시를 통해 이해해보겠습니다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;구현 예시: Temporal 워크플로우 (Java/Kotlin)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;비즈니스 시나리오: 결제 승인 프로세스&amp;nbsp;&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잔액 차감 (Bank Service)&lt;/li&gt;
&lt;li&gt;포인트 적립 (Point Service)&lt;/li&gt;
&lt;li&gt;결제 상태 완료 변경 (Payment DB)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1775103778837&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 결제 도메인 내부의 지휘자(Orchestrator) 역할
@WorkflowInterface
interface PaymentWorkflow {
    @WorkflowMethod
    fun processPayment(orderId: String, amount: Long)
}

class PaymentWorkflowImpl : PaymentWorkflow {
    // 하위 서비스들(Activities) 정의
    private val activities = Workflow.newActivityStub(PaymentActivities::class.java)

    override fun processPayment(orderId: String, amount: Long) {
        val saga = Saga(Saga.Options.Builder().setContinueWithError(false).build())
        
        try {
            // 1. 잔액 차감 시도
            activities.deductBalance(amount)
            // 실패 시 실행할 보상 트랜잭션 예약 (돈 다시 입금)
            saga.addCompensation { activities.refundBalance(amount) }

            // 2. 포인트 적립 시도
            activities.addPoints(amount / 100)
            // 실패 시 실행할 보상 트랜잭션 예약 (포인트 회수)
            saga.addCompensation { activities.removePoints(amount / 100) }

            // 3. 결제 상태 업데이트
            activities.updatePaymentStatus(orderId, &quot;COMPLETED&quot;)
            
        } catch (e: Exception) {
            // 어느 단계에서든 에러 발생 시, 예약된 보상 트랜잭션들을 역순으로 실행!
            saga.compensate()
            throw e
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식이 세 가지 이유로 인해 도메인 내부에서 원자성을 확보할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;color: #000000;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;지휘자의 존재:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;위 코드의&lt;span&gt;&amp;nbsp;&lt;/span&gt;PaymentWorkflow가 바로 지휘자입니다. 각 서비스(Bank, Point)는 자신이 전체 흐름 중 어디에 있는지 몰라도 됩니다. 오직 지휘자의 명령에만 응답합니다.&lt;/li&gt;
&lt;li style=&quot;color: #000000;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0&quot;&gt;상태 보존:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;만약 2번(포인트 적립)까지 성공하고 3번에서 서버가 다운되더라도, Orchestration 엔진은 어느 단계까지 성공했는지 기억하고 있다가 서버가 복구되면 남은 작업을 재개하거나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;108&quot; data-path-to-node=&quot;11,1,0&quot;&gt;전체 보상 트랜잭션&lt;/b&gt;을 돌립니다.&lt;/li&gt;
&lt;li style=&quot;color: #000000;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,2,0&quot;&gt;가시성:&lt;/b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;관리 도구(예: Temporal UI)를 열면 현재 결제가 어느 단계에서 멈춰 있는지, 왜 실패했는지 시각적으로 즉시 확인이 가능합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;2. 도메인 간의 유연함:&amp;nbsp;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Choreography&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 도메인(예: 주문 도메인과 배송 도메인) 사이에서는 독립성이 최우선입니다. 중앙 지휘자가 모든 도메인을 알게 되면 서비스 간 결합도가 높아져 '거대한 분산 모놀리스'가 될 위험이 있습니다. 그래서 일반적으로 'Event'를 통해 서비스간 결합도를 낮추는것이 정석입니다. 예를 들어 주문 서비스는 '주문됨(OrderCompleted)'라는 이벤트만 발행하고 사라지면 이후의 처리는 해당 이벤트를 소비하는 서비스가 자율적으로 결정해서 비즈니스 로직을 수행하도록 설계합니다. 자세한 예시는 이후의 설명 하려고합니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Choreography 전략의 두 가지 치명적 문제점&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;서비스간의 결합도를 낮추기 위해서 선택한 &lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Choreography(이벤트 기반)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; 전략은 강력하지만, 두 가지 치명적인 숙제를 던집니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;1. 데이터 신뢰성 문제 (Atomic Write &amp;amp; Publish)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;DB에 주문 정보를 저장하는 것과 Kafka에 이벤트를 발행하는 것은 서로 다른 작업입니다. DB 저장은 성공했는데 네트워크 오류로 이벤트 발행에 실패한다면? 주문은 들어왔지만 결제와 배송은 영원히 시작되지 않는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b data-index-in-node=&quot;119&quot; data-path-to-node=&quot;17&quot;&gt;데이터 불일치&lt;/b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;상태가 됩니다. 분산 환경에서는 2PC(2-Phase Commit) 사용이 어렵기 때문에 이를 해결할 특수한 패턴이 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;2. 디버깅과 관측의 어려움&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 기반 시스템에서는 &quot;A가 B를 호출했다&quot;는 명확한 선후 관계가 사라집니다. 메시지 브로커라는 완충지대 때문에, 주문 버튼을 눌렀는데 왜 배송 준비가 안 되는지 추적하려면 수많은 서비스의 로그를 일일이 대조해야 합니다. 서비스가 50개, 100개로 늘어나면 사실상&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;151&quot; data-path-to-node=&quot;19&quot;&gt;블랙박스&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;시스템이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 숙제는 위에서 언급한 SAGA 패턴(보상 트랜잭션 설계)를 통해 해결할 수 있습니다. 분산 환경에서는 앞서 언급한것처럼 서비스가 늘어남에 따라 디버깅이 어렵다는 치명적 단점이 있습니다. 적절한 모니터링 툴이 부재하다면 이벤트의 발행/소비 출처를 소스코드를 뒤져가며 판단하고 디버깅하는 것은 시간과 비용을 늘릴 수 있다고 생각합니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;OpenTelemetry, Jaeger를 활용한 분산 추적 설계 실습&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급한 분산 환경에서의 디버깅 문제를 해결하기 위해 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;OpenTelemetry, Jaeger 같은 오픈소스가 등장했습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;1. OpenTelemetry (OTel): 관측의 표준 규격&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenTelemetry는 로그, 메트릭, 트레이스를 수집하기 위한 오픈소스 프레임워크입니다. 특정 솔루션(Datadog, New Relic 등)에 종속되지 않고 시스템의 내부 상태를 외부로 노출할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Jaeger: 분산 추적 시각화 툴&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;OTel이 수집한 방대한 데이터를 사람이 이해하기 쉬운 타임라인 형태로 보여주는 도구입니다. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;어떤 지점에서 병목(Latency)이 발생하는지, 어느 서비스에서 에러가 터졌는지 직관적으로 파악할 수 있게 해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;이론은 여기까지 하고 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;분산 추적을 위한 &lt;/span&gt;실제 환경을 구축하는 실습을 진행해보겠습니다. 해당 실습에는 별도의 DB 저장없이 서비스간 이벤트 발행/소비 흐름을 이해하고 &lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;OpenTelemetry, Jaeger&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;를 활용해서 어떻게 모니터링할 수 있는지 확인하는 과정만을 포함할 것입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;기술 스택은 다음과 같습니다. (해당 실습에서 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;OpenTelemetry, Jaeger&lt;/span&gt;을 제외하고는 실습의 편의를 위해서 선택했습니다.)&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;소스코드는 위 레포에서 확인할 수 있습니다. (&lt;a href=&quot;https://github.com/MinChangJeong/sandbox-go-kafka-otel&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/MinChangJeong/sandbox-go-kafka-otel&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Language&lt;/b&gt;: Go 1.22&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Framework&lt;/b&gt;: Gin&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Messaging&lt;/b&gt;: Apache Kafka (Confluent)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Tracing&lt;/b&gt;: OpenTelemetry SDK + OTLP Exporter&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Visualization&lt;/b&gt;: Jaeger&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Container&lt;/b&gt;: Docker Compose&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_8jw99b8jw99b8jw9.png&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xqplf/dJMcabKuX9P/8GnLoq4PkKYrLv6gxOJ3y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xqplf/dJMcabKuX9P/8GnLoq4PkKYrLv6gxOJ3y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xqplf/dJMcabKuX9P/8GnLoq4PkKYrLv6gxOJ3y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxqplf%2FdJMcabKuX9P%2F8GnLoq4PkKYrLv6gxOJ3y0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1408&quot; height=&quot;768&quot; data-filename=&quot;Gemini_Generated_Image_8jw99b8jw99b8jw9.png&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;1단계: 외부 요청 수신 및 고유 식별자(Trace ID) 생성&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-path-to-node=&quot;5&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;수신:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;외부 사용자가&lt;span&gt;&amp;nbsp;&lt;/span&gt;Order Service의 특정 엔드포인트(POST /orders)로 HTTP 요청을 보냅니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;생성:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;요청을 받은 서비스는 내부적으로 이 작업을 관리할 최상위 객체(Root Span)를 생성합니다. 이때 전역적으로 고유한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;75&quot; data-path-to-node=&quot;5,1,0&quot;&gt;Trace ID&lt;/b&gt;가 부여되며, 이 ID는 전체 여정이 끝날 때까지 바뀌지 않는 고정값이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;2단계: 서비스 경계 초월 및 맥락(Context) 주입&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,0&quot;&gt;주입(Injection):&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Order Service는 비즈니스 로직 처리를 마친 후, 다음 단계인&lt;span&gt;&amp;nbsp;&lt;/span&gt;Payment Service로 신호를 보내기 위해 메시지 브로커(Kafka)를 이용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0&quot;&gt;헤더 활용:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이때 Kafka 메시지의 페이로드(내용물)가 아닌,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;36&quot; data-path-to-node=&quot;8,1,0&quot;&gt;메시지 헤더(Metadata)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;영역에 생성된 Trace ID와 현재 스팬 정보를 담습니다(W3C TraceContext 표준). 이는 서비스 간의 직접적인 호출 없이도 **'실행 맥락'**을 전달하는 핵심 수단이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;3단계: 메시지 소비 및 맥락 추출(Extraction) - 비동기적으로 동작하는 다음 서비스가 이전의 상태를 이어받는 단계입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-path-to-node=&quot;11&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;추출:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Payment Service는 Kafka 토픽에서 메시지를 가져옵니다. 이때 헤더에 포함된 Trace ID를 읽어 들여 자신의 실행 환경으로 복원합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0&quot;&gt;연결:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;추출된 ID를 기반으로 새로운 하위 작업(Child Span)을 생성합니다. 시스템적으로는 별도의 프로세스이지만, 관리 측면에서는 이전 작업의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;84&quot; data-path-to-node=&quot;11,1,0&quot;&gt;연속성&lt;/b&gt;을 확보하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;4단계: 지연 발생 및 작업 완료 - 시스템의 성능 특성을 데이터로 남기는 과정입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-path-to-node=&quot;14&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,0,0&quot;&gt;지연 기록:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Payment Service는 의도적으로 2초간 실행을 멈춥니다. 이 시간 동안 해당 스팬의 '시작 시각'과 '종료 시각'의 차이가 기록됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0&quot;&gt;최종 전파:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;결제 처리가 완료되면 다시 동일한 Trace ID를 Kafka 헤더에 실어&lt;span&gt;&amp;nbsp;&lt;/span&gt;Delivery Service로 전달하며, 마지막 서비스 역시 동일한 방식으로 자신의 작업 기록을 생성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;5단계: 추적 데이터(Telemetry)의 비동기 수집 - 각 서비스가 생성한 기록들이 중앙으로 모이는 과정입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-path-to-node=&quot;17&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,0,0&quot;&gt;개별 발송:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;각 마이크로서비스는 자신의 작업(Span)이 종료될 때마다, 그 기록을 즉시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;50&quot; data-path-to-node=&quot;17,0,0&quot;&gt;OTel Collector&lt;/b&gt;라는 중간 수집 서버로 전송합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,1,0&quot;&gt;비동기 처리:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 전송 과정은 실제 비즈니스 로직(주문, 결제)의 성능에 영향을 주지 않도록 별도의 백그라운드 스레드에서 비동기적으로 처리됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;6단계: 데이터 병합 및 시각화(Visualization) - 파편화된 조각들을 하나의 타임라인으로 재구성하는 최종 단계입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-path-to-node=&quot;20&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20,0,0&quot;&gt;병합:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;4&quot; data-path-to-node=&quot;20,0,0&quot;&gt;OTel Collector&lt;/b&gt;는 여러 서비스에서 산발적으로 들어오는 스팬 데이터들을 동일한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;53&quot; data-path-to-node=&quot;20,0,0&quot;&gt;Trace ID&lt;/b&gt;를 기준으로 그룹화합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20,1,0&quot;&gt;저장 및 노출:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;병합된 데이터를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;18&quot; data-path-to-node=&quot;20,1,0&quot;&gt;Jaeger&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;저장소에 기록합니다. 사용자가 Jaeger UI에 접속하여 해당 Trace ID를 조회하면, 각 서비스의 시작/종료 시점과 순서가 정렬된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;102&quot; data-path-to-node=&quot;20,1,0&quot;&gt;간트 차트(Gantt Chart)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태의 시각적 리포트를 얻게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1484&quot; data-origin-height=&quot;860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dT0S3f/dJMcahRq1kL/Ip3MT9iRVE1vUjPW5rWw00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dT0S3f/dJMcahRq1kL/Ip3MT9iRVE1vUjPW5rWw00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dT0S3f/dJMcahRq1kL/Ip3MT9iRVE1vUjPW5rWw00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdT0S3f%2FdJMcahRq1kL%2FIp3MT9iRVE1vUjPW5rWw00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1484&quot; height=&quot;860&quot; data-origin-width=&quot;1484&quot; data-origin-height=&quot;860&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA는 단순한 유행이나 기술적 선택이 아닌것 같습니다. 서비스가 쪼개질수록 우리가 관리해야 할 '복합도'는 기하급수적으로 늘어납니다. 이때 우리가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;83&quot; data-path-to-node=&quot;2&quot;&gt;Orchestration&lt;/b&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;98&quot; data-path-to-node=&quot;2&quot;&gt;Choreography&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사이에서 균형을 잡고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;124&quot; data-path-to-node=&quot;2&quot;&gt;OpenTelemetry&lt;/b&gt;라는 강력한 도구를 손에 쥐어야 하는 이유는 명확합니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;3&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-path-to-node=&quot;3,0&quot; data-ke-size=&quot;size16&quot;&gt;&quot;보이지 않는 것은 관리할 수 없고, 관리할 수 없는 것은 개선할 수 없기 때문입니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <author>min.c00</author>
      <guid isPermaLink="true">https://jminc00.tistory.com/109</guid>
      <comments>https://jminc00.tistory.com/109#entry109comment</comments>
      <pubDate>Thu, 2 Apr 2026 13:53:31 +0900</pubDate>
    </item>
    <item>
      <title>PostgreSQL MVCC: 덮어쓰지 않는 데이터베이스가 동시성을 제어하는 법</title>
      <link>https://jminc00.tistory.com/108</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번 글에서는 사내에서 높은 신뢰성과 확장성, 그리고 강력한 데이터 무결성을 위해 널리 채택하고 있는&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;PostgreSQL&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에 대해 학습한 내용을 공유하고자 합니다. 특히 PostgreSQL의 핵심 메커니즘 중 하나인&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;MVCC(Multi-Version Concurrency Control)의 동작 원리&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 깊이 있게 파악하는 데 초점을 맞추었습니다&lt;/span&gt;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. PostgreSQL의 특징&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL는 다른 RDBMS와 비교했을 때 가장 큰 장점은 '타협하지 않는 엄격함'입니다. 이와 관련된 여러가지 특징이 있습니다.&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;향상된 체크 제약 조건(Check Constraints): not null 뿐만아니라 아주 복잡한 조건을 DB 레벨에서 강제할 수 있습니다.&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &quot;재고 수량은 반드시 0보다 커야 하며, 입고일은 출고일보다 빨라야 한다&quot;는 조건을 설정하면, 어떤 애플리케이션 버그로 인해 잘못된 값이 들어와도 DB가 잘못된 값이 들어오지 못하도록 막습니다.&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;원자성 보장(Write-Ahead Logging): 데이터를 디스크에 실제로 쓰기 전에, 변경 이력을 로그에 먼저 기록합니다.&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;만약 데이터를 저장하는 도중에 갑자기 서버 전원이 나가더라도, 로그를 보고 어디까지 저장됐는지를 복구할 수 있어 데이터 유실 가능성을 낮출 수 있습니다.&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;동시성 제어 (MVCC)의 정교함&lt;/b&gt;: PostgreSQL은 데이터를 수정할 때 원본을 바로 덮어쓰지 않고 새로운 버전을 만듭니다.&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 읽기 작업이 진행중일 때 쓰기 작업이 들어와도 서로를 기다리지 않고&amp;nbsp;&amp;nbsp;자신이 작업을 시작한 시점에 일관된 데이터를 볼 수 있습니다.&amp;nbsp;&lt;br&gt;&lt;br&gt;이 글에서는 3가지 큰 특징 중에서도 MVCC에 대해서 심층도 있게 학습한 내용을 공유 해보려고 합니다.&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. PostgreSQL의 MVCC&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL의 MVCC 핵심 메커니즘을 이해하기 위해서는 3가지 요소에 대해서 이해해야합니다.&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;스냅샵(SnapShot)과 튜플(Tuple)의 구조&lt;/li&gt;&lt;li&gt;수정(update)시 발생하는 과정: &quot;insert +delete&quot;&lt;/li&gt;&lt;li&gt;청소부 역할: VACCUM&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 스냅샷과 튜플의 구조에 대해서 이해해보겠습니다. PostgreSQL에서 테이블의 한 행을 저장하는 단위를 튜플이라고 부릅니다. 각 튶플에는 눈에 보이지 않는 두 가지 중요한 타임스탬프 정보가 숨어있습니다.&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;xmin (insert Id): 이 데이터를 생성한 트랜잭션의 ID&lt;/li&gt;&lt;li&gt;xmax (delete Id): 이 데이터를 삭제하거나 수정한 트랜잭션의 ID (수정하지 않았다면 0)&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;xmin, xmax는 PostgreSQL의 오픈소스에서 확인해볼수 도 있는데 구조체 내의 멤버변수를 의미합니다. (네이밍에 대해서 궁금해서 찾아봤습니다. )&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;두번째는 수정시 발생하는 과정(VACCUM 포함)에 대해서 심층도 있게 보려고합니다. PostgreSQL는 데이터를 수정할 때 기존 데이터를 직접 고치지 않고 다음 4단계를 거칩니다.&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;1단계: 초기 상태 (데이터 삽입 직후)&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 데이터가 처음 저장된 상태입니다. 모든 데이터 행(Tuple)은 눈에 보이지 않는&amp;nbsp;xmin(생성 트랜잭션 ID)과&amp;nbsp;xmax(삭제/수정 트랜잭션 ID) 정보를 가지고 있습니다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;xmin: 100&lt;/b&gt;&amp;nbsp;(100번 트랜잭션이 만들었음)&lt;/li&gt;&lt;li&gt;&lt;b&gt;xmax: 0&lt;/b&gt;&amp;nbsp;(아직 삭제되거나 수정되지 않은 유효한 데이터)&lt;/li&gt;&lt;/ul&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brUy1F/dJMcagSwyXp/SMvk73mMTxfAshaUkGwbj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brUy1F/dJMcagSwyXp/SMvk73mMTxfAshaUkGwbj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brUy1F/dJMcagSwyXp/SMvk73mMTxfAshaUkGwbj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrUy1F%2FdJMcagSwyXp%2FSMvk73mMTxfAshaUkGwbj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;242&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;2단계: 데이터 수정 (UPDATE) 발생&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이제 101번 트랜잭션이&amp;nbsp;&lt;b&gt;Alice의 잔액을 200으로 수정&lt;/b&gt;하는&amp;nbsp;UPDATE&amp;nbsp;명령을 내립니다.&lt;br&gt;&lt;b&gt;그림을 보면 원본(Tuple A)은 그대로 두고, 옆에 새 데이터(Tuple B)를 추가합니다.&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;Tuple A (원본):&lt;/b&gt;&amp;nbsp;xmax가&amp;nbsp;&lt;b&gt;101&lt;/b&gt;로 바뀝니다. &quot;101번 트랜잭션에 의해 삭제 예정&quot;이라는 뜻입니다.&lt;/li&gt;&lt;li&gt;&lt;b&gt;Tuple B (신규):&lt;/b&gt;&amp;nbsp;xmin이&amp;nbsp;&lt;b&gt;101&lt;/b&gt;인 새로운 행이 생깁니다. 잔액은 200입니다.&lt;/li&gt;&lt;/ul&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kTVSC/dJMb99TnIuL/yOQbUVQI90c98x6bkhPvPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kTVSC/dJMb99TnIuL/yOQbUVQI90c98x6bkhPvPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kTVSC/dJMb99TnIuL/yOQbUVQI90c98x6bkhPvPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkTVSC%2FdJMb99TnIuL%2FyOQbUVQI90c98x6bkhPvPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;250&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;3단계: 커밋 후 가시성 (조회 시점의 차이)&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;101번 트랜잭션이 성공적으로 완료(Commit)되었습니다. 이제 디스크에는 두 개의 버전(A, B)이 공존합니다.&lt;br&gt;&lt;b&gt;이 부분이 MVCC의 핵심입니다. 조회하는 사람이 언제 작업을 시작했느냐에 따라 보는 데이터가 다릅니다.&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;사용자 A (102번):&lt;/b&gt;&amp;nbsp;데이터 수정 전에 이미 조회를 시작했으므로, 구버전인&amp;nbsp;&lt;b&gt;Tuple A&lt;/b&gt;를 봅니다.&lt;/li&gt;&lt;li&gt;&lt;b&gt;사용자 B (103번):&lt;/b&gt;&amp;nbsp;수정이 끝난 후 조회를 시작했으므로, 신버전인&amp;nbsp;&lt;b&gt;Tuple B&lt;/b&gt;를 봅니다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과적으로 수정 중에도 읽기 작업이 멈추지 않고 대기 없이 바로 데이터를 볼 수 있습니다.&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cK0m1b/dJMcaaLxmTF/EDUK8bmekgs5NGXVMjscIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cK0m1b/dJMcaaLxmTF/EDUK8bmekgs5NGXVMjscIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cK0m1b/dJMcaaLxmTF/EDUK8bmekgs5NGXVMjscIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcK0m1b%2FdJMcaaLxmTF%2FEDUK8bmekgs5NGXVMjscIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;468&quot; height=&quot;255&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;4단계: 사후 정리 (VACUUM 실행)&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 지나서 더 이상 구버전(Tuple A)을 참조하는 트랜잭션이 하나도 없게 되었습니다. 이때&amp;nbsp;&lt;b&gt;VACUUM&lt;/b&gt;&amp;nbsp;프로세스가 등장합니다.&lt;br&gt;&lt;b&gt;그림처럼 VACUUM은 쓸모없어진 구버전을 지우고, 그 자리를 나중에 다시 사용할 수 있도록 만듭니다.&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;Tuple A (삭제):&lt;/b&gt;&amp;nbsp;물리적으로 지워집니다.&lt;/li&gt;&lt;li&gt;&lt;b&gt;빈 공간 (Reusable Space):&lt;/b&gt;&amp;nbsp;새로운 데이터가 들어올 수 있는 빈자리로 변합니다.&lt;/li&gt;&lt;/ul&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9hHzn/dJMcahcOmCB/YXQPSiblyRRkXQoiIxN0PK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9hHzn/dJMcahcOmCB/YXQPSiblyRRkXQoiIxN0PK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9hHzn/dJMcahcOmCB/YXQPSiblyRRkXQoiIxN0PK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9hHzn%2FdJMcahcOmCB%2FYXQPSiblyRRkXQoiIxN0PK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;473&quot; height=&quot;258&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL는 MVCC 덕분에 대규모 트래픽에서도 데이터의 일관성을 유지할 수 있도록 지원해 많은 개발자로부터 선택되어 사용되고 있습니다. 이번에 처음으로 PostgreSQL를 사용하게 됬지만, &quot;왜 PostgreSQL이어야 하지?&quot;라는 개인적인 물음에 답을 줄수 있는 공부였던것 같습니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>CS/DB</category>
      <author>min.c00</author>
      <guid isPermaLink="true">https://jminc00.tistory.com/108</guid>
      <comments>https://jminc00.tistory.com/108#entry108comment</comments>
      <pubDate>Tue, 31 Mar 2026 17:19:20 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 비동기 프로그래밍: Future에서 Virtual Threads까지</title>
      <link>https://jminc00.tistory.com/107</link>
      <description>&lt;h4 style=&quot;color: #141413; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Future (Java 5) &amp;mdash; 비동기의 시작, 블로킹의 모순&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서는 new Thread(task).start()를 통해 비동기 실행을 할 수 있습니다. 다만, 요청마다 스레드를 새로 만들게 되면 스레드의 생성/소멸 비용이 매번 발생하고, 스레드가 무한정 늘어나서 결국에는 서버가 터질 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 자바에서는 Executor 클래스를 통해 스레드의 수를 제한하고 재사용함으로써 이문제를 해결합니다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;ExecutorService executor = Executors.newFixedThreadPool(10);
// 스레드 10개짜리 풀 생성 &amp;mdash; 이 10개를 돌려쓰겠다는 선언

Future&amp;lt;String&amp;gt; future = executor.submit(() -&amp;gt; {
    Thread.sleep(2000);
    return &quot;Data&quot;;
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;submit()은 이 스레드 풀에 작업을 맡기는 행위로 스레드 풀에서 놀고 잇는 스레드가 잇으면 즉시 그 스레드를 작업에 할당합니다. 호출한 스레드에게는 Future 객체를 즉시 반환하고 작업 완료를 기다리지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;executor.submit()을 호출하는 순간 작업은 별도 스레드에서 실행되기 시작합니다. 앞서 말한것처럼 Future 객체를 즉시반환하고 다음 코드를 실행하는 메커니즘은 비동기로 동작합니다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;Future&amp;lt;String&amp;gt; future = executor.submit(() -&amp;gt; heavyWork()); // 즉시 반환
doSomethingElse(); // 작업이 돌아가는 동안 여기도 실행 가능
String result = future.get(); // 여기서 블로킹&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 작업의 사작은 비동기로 호출자와 작업이 동시에 실행되게 됩니다. 다만 실제로 비동기 작업의 결과를 쓰려면 future.get() 메서드를 호출해야하는데 그 순간 스레드는 멈춰버립니다. 비동기라고 이름을 붙인 API가 사실상 동작처럼 동작하는 아이러니한 상황이 생겼습니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;왜 블로킹이 발생하는가?&amp;nbsp;&lt;/b&gt;&lt;br /&gt;future.get() 은 결과 값을 호출 스레드의 스택 프레임으로 복사해야 합니다. 값이 아직 메모리에 없으면, OS는 해당 스레드의 Program Counter를 멈추고 상태를 WAITING으로 전환합니다. &lt;b&gt;스레드는 아무 일도 하지 않으면서 약 1MB의 스택 메모리를 계속 점유합니다. 동시에 1,000개의 요청이 대기 중이라면 결과도 없이 1GB가 낭비&lt;/b&gt;되는 셈입니다.&lt;/blockquote&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #141413; text-align: start;&quot;&gt;&lt;code&gt;ExecutorService executor = Executors.newFixedThreadPool(10);
Future&amp;lt;String&amp;gt; future = executor.submit(() -&amp;gt; {
    Thread.sleep(2000); // I/O 대기 시뮬레이션
    return &quot;Data&quot;;
});

// 이 시점에 호출 스레드가 WAITING으로 전환 &amp;mdash; 자원 낭비 시작
String result = future.get(); &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Future의 근본적인 문제는 결과를 Pull 하는 구조이기 때문에 결과가 언제 올지 모르니 기다릴수 밖에 없고, 그 결과 스레드 점유가 필요한것 입니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #141413; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;CompletableFuture (Java 8) &amp;mdash; Push 모델과 제어의 역전&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 8의 CompletableFuture는 Future와 달리 결과를 기다리는 대신 &quot;결과가 오면 이걸 실행해줘&quot; 라고 미리 선로를 깔아두는 방식입니다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;xl&quot; style=&quot;color: #141413; text-align: start;&quot;&gt;&lt;code&gt;CompletableFuture
    .supplyAsync(() -&amp;gt; fetchUser(userId))          // ForkJoinPool에서 실행
    .thenApply(user -&amp;gt; enrichWithOrders(user))   // 완료 이벤트 시 즉시 실행
    .thenCompose(user -&amp;gt;
        CompletableFuture.supplyAsync(           // 중첩 비동기도 평탄화 가능
            () -&amp;gt; callExternalApi(user)))
    .exceptionally(ex -&amp;gt; fallbackUser())         // 예외도 파이프라인 안에서 처리
    .thenAccept(result -&amp;gt;
        log.info(&quot;done: {}&quot;, result));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CompletableFuture는 내부에 두 가지를 들고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;// 단순화한 내부 구조 (실제 구현은 더 복잡)
class CompletableFuture&amp;lt;T&amp;gt; {
    volatile Object result;      // 아직 없으면 null
    volatile Completion stack;   // 등록된 콜백들의 연결 리스트
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;thenApply(fn)를 호출하면 fn을 즉시 실행하는 게 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;stack에 콜백을 등록합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;complete(value)가 호출되어&lt;span&gt;&amp;nbsp;&lt;/span&gt;result에 값이 써지는 순간, stack에 쌓인 콜백들이 연쇄 실행됩니다.&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;String&amp;gt; cf = new CompletableFuture&amp;lt;&amp;gt;();

cf.thenApply(s -&amp;gt; s + &quot;!&quot;)      // 콜백 등록 (아직 실행 안 됨)
  .thenApply(s -&amp;gt; s + &quot;!&quot;)      // 콜백 등록 (아직 실행 안 됨)
  .thenAccept(System.out::println);

cf.complete(&quot;hello&quot;);  // 이 순간 &quot;hello!!&quot; 출력 &amp;mdash; 콜백 연쇄 실행&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. thenApply vs thenCompose &amp;mdash; 왜 헷갈리는가&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 둘의 차이는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;콜백이 반환하는 것이 값이냐, 또 다른 Future냐&lt;/b&gt;의 차이입니다. thenApply는 동기 변환입니다. 값을 받아서 값을 반환합니다.&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;// String -&amp;gt; String 변환 (동기)
CompletableFuture&amp;lt;String&amp;gt; result =
    CompletableFuture.supplyAsync(() -&amp;gt; &quot;hello&quot;)
                     .thenApply(s -&amp;gt; s.toUpperCase()); // &quot;HELLO&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;thenCompose는 비동기 변환입니다. 값을 받아서 Future를 반환하고, 그 Future를 평탄화합니다.&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;// String -&amp;gt; CompletableFuture&amp;lt;String&amp;gt; 반환 후 평탄화
CompletableFuture&amp;lt;String&amp;gt; result =
    CompletableFuture.supplyAsync(() -&amp;gt; &quot;userId-123&quot;)
                     .thenCompose(id -&amp;gt; fetchUserAsync(id)); // Future&amp;lt;User&amp;gt;를 평탄화&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;thenApply에 Future를 반환하는 함수를 넘기면,&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;// 실수 &amp;mdash; 중첩 Future 생성
CompletableFuture&amp;lt;CompletableFuture&amp;lt;User&amp;gt;&amp;gt; wrong =
    CompletableFuture.supplyAsync(() -&amp;gt; &quot;userId-123&quot;)
                     .thenApply(id -&amp;gt; fetchUserAsync(id)); // 래핑이 한 겹 더 생김

// 이걸 꺼내 쓰려면 이중으로 get()해야 함 &amp;mdash; 사실상 쓸 수 없는 타입
wrong.get().get(); // 이렇게 써야 한다면 설계가 잘못된 것&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Stream의&lt;span&gt;&amp;nbsp;&lt;/span&gt;map&lt;span&gt;&amp;nbsp;&lt;/span&gt;vs&lt;span&gt;&amp;nbsp;&lt;/span&gt;flatMap과 완전히 같은 구조입니다. 내부에서 비동기 작업이 나온다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;thenCompose(flatMap)을 써야 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;왜 체이닝이 가능해졌는가?&lt;/b&gt;&lt;br /&gt;내부적으로 완료 상태가 true로 바뀌는 순간, 연결된 CompletionStage들이 연쇄 실행되는 이벤트 리스너 구조입니다. 각 콜백은 대부분 ForkJoinPool의 워커 스레드에서 실행되므로, 호출 스레드는 선로만 깔고 자리를 떠날 수 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;ForkJoinPool &amp;mdash; Work-Stealing이 빠른 진짜 이유&lt;/h4&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CompletableFuture&lt;/span&gt;의 기본 실행 엔진은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ForkJoinPool.commonPool()&lt;/span&gt;입니다. 일반 스레드 풀과 다르게 동작하는데, 그 핵심이 Work-Stealing입니다.&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;WORK-STEALING의 논리&lt;/b&gt;&lt;br /&gt;일반 스레드 풀은 단일 공유 큐에서 작업을 꺼냅니다. 경합(Contention)이 발생할 수밖에 없습니다. ForkJoinPool은 각 워커 스레드가 개인 덱(Deque) 을 가집니다. 자신의 큐가 비면 다른 스레드 덱의 꼬리(Tail)에서 일을 훔쳐와 처리합니다. 본인은 앞(Head)에서 꺼내고, 도둑질은 뒤(Tail)에서 일어나므로 충돌이 최소화됩니다. 결과적으로 컨텍스트 스위칭이 줄고, 같은 스레드가 연관 작업을 이어 처리하므로 CPU 캐시 히트율도 높아집니다.&lt;/blockquote&gt;
&lt;div&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;COMMONPOOL을 함부로 쓰면 안 되는 이유&lt;/b&gt;&lt;br /&gt;commonPool은 JVM 전역 공유 자원입니다.&amp;nbsp;supplyAsync()에 executor를 넘기지 않으면 여기서 실행됩니다. I/O 블로킹 작업이 commonPool을 점령하면&amp;nbsp;Parallel Stream을 비롯한 다른 곳에도 영향이 퍼집니다. 프로덕션에서는 반드시 별도 executor를 분리하세요.&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;&lt;span style=&quot;font-size: 1.25em; letter-spacing: -1px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #141413; font-size: 1.25em; letter-spacing: -1px;&quot;&gt;Virtual Threads (Java 21) &amp;mdash; M:N 매핑의 혁명&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #141413; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #141413; text-align: start;&quot;&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;기존 OS 스레드(PLATFORM THREAD)가 무거운 이유&lt;/b&gt;&lt;br /&gt;생성할 때마다 OS 커널 시스템 콜을 호출하고, 최소 1MB의 스택이 할당됩니다. 전환 시엔 커널 모드로 진입하는 컨텍스트 스위칭이 발생합니다. 보통 서버 한 대에 수천 개 이상 만들기가 현실적으로 어렵습니다.&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div style=&quot;color: #141413; text-align: start;&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;80&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/45OJr/dJMcai3Tvuw/66kHHn3Cazx6zDDw8Lv091/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/45OJr/dJMcai3Tvuw/66kHHn3Cazx6zDDw8Lv091/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/45OJr/dJMcai3Tvuw/66kHHn3Cazx6zDDw8Lv091/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F45OJr%2FdJMcai3Tvuw%2F66kHHn3Cazx6zDDw8Lv091%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;708&quot; height=&quot;80&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;80&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;Virtual Thread는 OS 자원이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JVM 힙에 생성되는 일반 객체&lt;/b&gt;입니다. I/O를 만나면 JVM은 해당 스레드의 스택 프레임을 힙에 저장하고, 실제 OS 스레드(Carrier Thread)에서 내려버립니다(Unmount). I/O가 완료되면 다시 적절한 Carrier Thread에 올라탑니다(Mount). OS 입장에선 Carrier Thread가 쉬지 않고 다른 객체를 처리하는 것처럼 보이므로, 커널 스위칭이 전혀 발생하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #141413; text-align: start;&quot;&gt;&lt;code&gt;// 20만 개를 생성해도 실제 OS 스레드는 소수만 사용됨
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 200_000).forEach(i -&amp;gt; {
        executor.submit(() -&amp;gt; {
            Thread.sleep(100); // 겉으론 블로킹, 실제론 Unmount
            return i;
        });
    });
} // try-with-resources로 자동 종료&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;color: #141413; text-align: start;&quot;&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;PINNING &amp;mdash; VIRTUAL THREAD의 치명적 함정&lt;/b&gt;&lt;br /&gt;synchronized 블록이나 네이티브 메서드 내에서 I/O가 발생하면, Virtual Thread가 Carrier Thread에 고정(Pinned)되어 Unmount가 되지 않습니다. 이 경우 결국 OS 스레드를 점유하게 되어 Virtual Thread의 장점이 사라집니다.&lt;br /&gt;ReentrantLock 으로 교체하거나, JVM 옵션 -Djdk.tracePinnedThreads=full 으로 문제 지점을 먼저 확인하세요.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;결국 비동기의 역사는 &quot;한정된 스레드로 어떻게 더 많은 요청을 처리할까&quot;의 기록입니다. Future는 도구를 줬고, CompletableFuture는 조합을 줬고, Virtual Thread는 스레드 자체의 비용을 바꿨습니다. 각 기술의 탄생 이유를 이해하면, 어떤 상황에서 무엇을 선택해야 할지 자연스럽게 보입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Language/Java</category>
      <author>min.c00</author>
      <guid isPermaLink="true">https://jminc00.tistory.com/107</guid>
      <comments>https://jminc00.tistory.com/107#entry107comment</comments>
      <pubDate>Wed, 25 Mar 2026 14:53:03 +0900</pubDate>
    </item>
    <item>
      <title>CJ 대한통운 미래기술챌린지 전형 입사 후기</title>
      <link>https://jminc00.tistory.com/106</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 지난 4년 동안 CJ대한통운 최종 임원 면접을 보기까지의 여정을 정리해보고 싶어 이렇게 글을 쓰게 되었습니다&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;1238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6vSRz/dJMcaaX83nv/p2UKRTkyRIK4wJhJmaxr8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6vSRz/dJMcaaX83nv/p2UKRTkyRIK4wJhJmaxr8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6vSRz/dJMcaaX83nv/p2UKRTkyRIK4wJhJmaxr8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6vSRz%2FdJMcaaX83nv%2Fp2UKRTkyRIK4wJhJmaxr8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;349&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;1238&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;저는 2021년 대한통운 미래기술챌린지에 참가했고 당시 선택할 수 있는 4가지 주제 중에서 저희 팀은 'Dynamic Pricing 모델 개발'이라는 주제로 참가했고, CJ대한통운에서 전달받은 데이터를 기반으로 유동적인 배송 비용을 고객이 직접 결정할 수 있는 솔루션을 만들었습니다. 당시에는 지금처럼 AI에 대한 관심이 높지 않을 때라 모델을 직접 학습시키고 유의미한 결과를 내는 것이 힘들었지만, 좋은 결과를 얻을 수 있었습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lGtLq/dJMcaacLJaM/K0tK6TqAEjQzdkka7qimH1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lGtLq/dJMcaacLJaM/K0tK6TqAEjQzdkka7qimH1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lGtLq/dJMcaacLJaM/K0tK6TqAEjQzdkka7qimH1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlGtLq%2FdJMcaacLJaM%2FK0tK6TqAEjQzdkka7qimH1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;576&quot; height=&quot;432&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;첫 대회에서 조금 아쉬웠던 점은 출제자의 의도를 좀 더 파악했더라면 더 높은 성적을 받을 수 있지 않았을까 하는 생각이었습니다. 그래서 2022년 제2회 CJ대한통운 미래기술 챌린지에 참가하기로 결심했습니다. 당시 손발이 잘 맞던 팀원 한 명과 같은 학과 동기 두 명을 포함해 총 4명으로 팀을 꾸렸고, 그 팀의 팀장을 맡아 프로젝트를 수행했습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LHxNu/dJMcaiWcMdG/dEiTej8ipoXC0yOp1usMS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LHxNu/dJMcaiWcMdG/dEiTej8ipoXC0yOp1usMS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LHxNu/dJMcaiWcMdG/dEiTej8ipoXC0yOp1usMS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLHxNu%2FdJMcaiWcMdG%2FdEiTej8ipoXC0yOp1usMS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;436&quot; height=&quot;319&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로 참가한 대회에서는 5가지 과제 중 크라우드 소싱 모바일 배송 애플리케이션을 구현하는 과제를 선택했습니다. 해당 과제의 핵심은 일반인이 배송 프로세스에 참여하는 것이었습니다. 배송 물품 픽업, 배송, 배송 완료 등의 기능을 구현하면서도 앱의 편리성과 완성도를 중점적으로 평가하는 과제였습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;저희 팀은 일반인의 배송에서 가장 중요하게 고려해야 할 요소는 '신뢰성'이라고 생각했습니다. 정식 고용된 배송 기사가 아닌 부업을 위해 참여하는 일반인에게 고객의 물품을 맡길 수 있는 강한 신뢰성이 필요하다고 판단했습니다. 그래서 앱의 신뢰도를 최우선으로 고려하면서도 전체적인 완성도를 높이기 위해 고민하고 개발했던 것 같습니다. 무엇보다 첫 번째 대회에서의 경험과 나름의 노하우 덕분에 두 번째는 좀 더 순조롭게 준비할 수 있었습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhyuN9/dJMcagRAsug/Yj9UFPLx8f62D7OpfpmHP1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhyuN9/dJMcagRAsug/Yj9UFPLx8f62D7OpfpmHP1/img.jpg&quot; data-alt=&quot;https://www.cjlogistics.com/ko/newsroom/news/NR_00000979&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhyuN9/dJMcagRAsug/Yj9UFPLx8f62D7OpfpmHP1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhyuN9%2FdJMcagRAsug%2FYj9UFPLx8f62D7OpfpmHP1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;459&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.cjlogistics.com/ko/newsroom/news/NR_00000979&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;정말 감사하게도 해당 대회에서 대상을 수상할 수 있었습니다. 밤을 새가며 애플리케이션을 소개할 발표 자료를 준비하고, 심사위원을 설득할 수 있는 발표 대본을 만드는 과정이 어쩌면 애플리케이션을 개발하는 것보다 더 힘들게 느껴졌던 것 같습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w02J0/dJMcaihD08W/3VZyeqCDt37T3QrPU3DXe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w02J0/dJMcaihD08W/3VZyeqCDt37T3QrPU3DXe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w02J0/dJMcaihD08W/3VZyeqCDt37T3QrPU3DXe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw02J0%2FdJMcaihD08W%2F3VZyeqCDt37T3QrPU3DXe1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;501&quot; height=&quot;119&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그리고 저는 2023년 1월 16일부터 2월 17일까지 CJ대한통운 TES 물류연구소에서 인턴을 수행할 수 있는 기회를 받았습니다. 한 달이라는 기간 동안 '원자재 비용 정보 수집 자동화 및 대시보드 개발' 업무를 수행했습니다. PoC로 진행하는 업무였지만, 나름의 성과를 인정받아 당시 프로젝트 팀장님께 좋은 인상을 남겼던 경험이었습니다. 한 달은 매우 짧은 시간이지만, 그 귀중한 시간을 정말 소중하게 쓰기 위해 주어진 일에 책임감을 갖고 임했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&quot;그리고 저는 2024년 2월 한국공학대학교를 졸업했습니다. 당시 CJ대한통운 미래기술 챌린지에 함께 참가했던 팀원 중 몇몇은 이미 CJ대한통운에 입사할 수 있었지만, 저는 병역 문제로 그럴 수 없었습니다. 병역특례 산업기능요원을 준비하고 있던 차라 2년간의 병역이 끝난 이후 CJ대한통운에 입사하기 위해 잠시 재정비하는 시간을 가졌습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고 운 좋게 2023년 11월 B2B 이커머스 SaaS를 제공하는 셀메이트라는 회사에 입사해 백엔드 개발자로 경력을 쌓을 기회를 얻었습니다. 저는 셀메이트에서 2년 2개월 동안 근무했으며, 그중 1년간 WMS팀에 소속되어 WMS 신규 기능 개발 및 서비스 출시를 담당했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1868&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pf3Ev/dJMcadghUEd/NAn42ImFSO9vDo9o8kkBGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pf3Ev/dJMcadghUEd/NAn42ImFSO9vDo9o8kkBGk/img.png&quot; data-alt=&quot;https://sellmate.io/sellmate_wms&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pf3Ev/dJMcadghUEd/NAn42ImFSO9vDo9o8kkBGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpf3Ev%2FdJMcadghUEd%2FNAn42ImFSO9vDo9o8kkBGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;473&quot; height=&quot;191&quot; data-origin-width=&quot;1868&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://sellmate.io/sellmate_wms&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그 과정에서 정말 많은 개발적 이슈와 문제를 겪었고, 해당 문제를 하나하나 해결하면서 개인적으로 성장하고 있다고 느꼈던 것 같습니다. 뿐만 아니라 사내 컨퍼런스에서 WMS팀에서 겪었던 문제를 해결했던 경험을 발표하면서 작은 버킷리스트도 이룰 수 있었습니다.&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;2025 상반기 사내 컨퍼런스 - 도메인을 지키는 설계&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;이번 블로그에서는 상반기 동안 진행한 WMS 프로젝트에서 마주했던 문제들과 그 해결 방법을 공유하고자 합니다. 제가 정의하고 해결한 문제는 총 4가지인데, 4가지의 특징을 한데 모아보니 제가&quot; data-og-host=&quot;jminc00.tistory.com&quot; data-og-source-url=&quot;https://jminc00.tistory.com/101&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/fzqmS/hyZQGfvLHm/AAAAAAAAAAAAAAAAAAAAAIMQXECLMpozr3SmP2_miyo5EcdPgf79A5IuoxVbaLXL/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1767193199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=dVpj5dl6X8VzjSx58xZOVB1A6ks%3D&quot; data-og-url=&quot;https://jminc00.tistory.com/101&quot;&gt;&lt;a href=&quot;https://jminc00.tistory.com/101&quot; target=&quot;_blank&quot; data-source-url=&quot;https://jminc00.tistory.com/101&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/fzqmS/hyZQGfvLHm/AAAAAAAAAAAAAAAAAAAAAIMQXECLMpozr3SmP2_miyo5EcdPgf79A5IuoxVbaLXL/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1767193199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=dVpj5dl6X8VzjSx58xZOVB1A6ks%3D')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;2025 상반기 사내 컨퍼런스 - 도메인을 지키는 설계&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;이번 블로그에서는 상반기 동안 진행한 WMS 프로젝트에서 마주했던 문제들과 그 해결 방법을 공유하고자 합니다. 제가 정의하고 해결한 문제는 총 4가지인데, 4가지의 특징을 한데 모아보니 제가&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;jminc00.tistory.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;2년간의 군 복무를 마치고 2025년 12월 4일 기준으로 퇴사를 결정하게 되었습니다. 그리고 2025년 12월 9일, 4년간 기다렸던 CJ대한통운 2차 면접을 보게 되었습니다. CJ대한통운은 미래기술 챌린지 입상자를 대상으로 별도의 채용 전형을 제공하고 있습니다. 제가 지원한 직무는 IT 개발 직무로, 해당 직무는 2025년도 기준 신입 공채로는 지원할 수 없는 전형이었습니다. (로봇/자동화, AI 빅데이터/최적화 전형의 경우는 신입으로도 지원할 수 있더라고요)&lt;/p&gt;&lt;figure data-ke-type=&quot;image&quot; data-ke-style=&quot;alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-link=&quot;https://www.cjlogistics.com/ko/newsroom/news/NR_00000979&quot; data-link-islinknewwindow=&quot;true&quot;&gt; 
 &lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt; 
&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwU5kT/dJMcafrGACf/I9r2NyY08pzXaFP1km5Qnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwU5kT/dJMcafrGACf/I9r2NyY08pzXaFP1km5Qnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwU5kT/dJMcafrGACf/I9r2NyY08pzXaFP1km5Qnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwU5kT%2FdJMcafrGACf%2FI9r2NyY08pzXaFP1km5Qnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;560&quot; height=&quot;247&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;해당 직무의 경우 클라우드 기반 WMS 플랫폼을 개발할 수 있다는 점에서 제 경험이 굉장히 큰 강점이 될 수 있다고 생각했습니다. 물론 저 역시 해당 업무에 큰 매력을 느껴 지원하게 되었습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;2차 면접에서는 간단한 자기소개와 서류에 포함된 저에 관련된 기술 질문 몇 가지를 받았습니다. 제 경험을 기반으로 답변했고, 크게 무리가 있지는 않았던 것 같습니다. 제가 가장 떨렸던 건 CJ대한통운 면접을 보기 위해 안랩의 입사를 포기했다는 점이었습니다. 안랩이라는 회사도 물론 저에게는 매우 과분한 회사라고 생각하지만, CJ대한통운은 4년 전 물류 산업에 처음 관심을 갖게 해줬던 회사인 만큼 개인적인 의미가 더 컸습니다. 그래서 면접 기회를 포기할 수 없었던 저는 CJ대한통운 면접을 선택했고, 안랩에 입사 포기 의사를 전달했습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNihR6/dJMcabQlGVv/1v79dvA9WpUCkbOOAhboAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNihR6/dJMcabQlGVv/1v79dvA9WpUCkbOOAhboAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNihR6/dJMcabQlGVv/1v79dvA9WpUCkbOOAhboAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNihR6%2FdJMcabQlGVv%2F1v79dvA9WpUCkbOOAhboAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;605&quot; height=&quot;244&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;2차 면접의 결과는 2025년 12월 24일 크리스마스이브에 도착했습니다. (그전까지는 면접 결과를 기다리느라 밤에 잠도 잘 못 잤던 것 같습니다.) 저에게는 크리스마스의 기적과도 같은 합격 소식이었고, 그날 저를 정말 기쁘게 만들었던 것 같습니다. 4년간의 노력이 결실을 맺었다는 것에 대한 대견함과 불확실성에 대한 투자의 성공이라는 짜릿함 등 여러 감정이 교차했던 것 같습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;글을 마무리하며&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;돌이켜보면 2021년 처음 CJ대한통운 미래기술 챌린지에 도전했을 때만 해도, 4년 후 이렇게 합격의 기쁨을 누릴 거라고는 상상하지 못했습니다. 그 사이 우수상과 대상 수상, 인턴 경험, 군 복무, 그리고 현장에서의 개발 경력까지, 모든 순간이 지금의 저를 만든 소중한 과정이었던 것 같습니다. 때로는 멀게만 느껴졌던 목표였지만, 포기하지 않고 한 걸음씩 나아간 덕분에 결국 이 자리에 설 수 있었습니다. 이제 새로운 시작점에 서서, 이제 CJ대한통운에서 그동안의 경험을 바탕으로 일할 수 있게 되었습니다. 4년간의 여정은 여기서 끝이 아니라 새로운 시작이라고 생각합니다. 지금까지 긴 글 읽어 주셔서 감사합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>min.c00</author>
      <guid isPermaLink="true">https://jminc00.tistory.com/106</guid>
      <comments>https://jminc00.tistory.com/106#entry106comment</comments>
      <pubDate>Fri, 26 Dec 2025 21:48:51 +0900</pubDate>
    </item>
    <item>
      <title>대한항공 전문인력 채용 면접 후기</title>
      <link>https://jminc00.tistory.com/105</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 이번 글에서는 제 첫 이직 여정 중 하나였던 &lt;b&gt;대한항공 채용 절차 후기&lt;/b&gt;를 작성해보려고 합니다. 이전에는 안랩 채용 후기를 공유했는데, 이렇게 대한항공에 대한 글까지 쓰게 되어 개인적으로 매우 기쁘고 감회가 새롭습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 2026년 대한통운 전문인력(신입/경력) 모집 중 &quot;IT개발&quot; 직무에 지원했습니다. 저의 2년간의 백엔드 개발 경험을 살릴 수 있는 좋은 포지션이라고 생각했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/prT3D/dJMcacVTs5B/rkyZh3Ax3MuHwecdUxh8J1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/prT3D/dJMcacVTs5B/rkyZh3Ax3MuHwecdUxh8J1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/prT3D/dJMcacVTs5B/rkyZh3Ax3MuHwecdUxh8J1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FprT3D%2FdJMcacVTs5B%2FrkyZh3Ax3MuHwecdUxh8J1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;411&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 우대사항에 적혀있는 클라우드 네이티브 환경에 대한 개발 경험(개인적인 공부를 제외하고는 현업에서 사용한 경험은 없습니다.), AWS 자격증은 없는 상황이었고 나머지 항목에 대한 경험은 아주! 조금...은 관련성이 있다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1708&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcc6C0/dJMcafZnYqm/Mr586qIamfDREvHce8B9x1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcc6C0/dJMcafZnYqm/Mr586qIamfDREvHce8B9x1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcc6C0/dJMcafZnYqm/Mr586qIamfDREvHce8B9x1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbcc6C0%2FdJMcafZnYqm%2FMr586qIamfDREvHce8B9x1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;692&quot; height=&quot;206&quot; data-origin-width=&quot;1708&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대한항공 전문인력 전형 절차는 보시는 것처럼 단계가 굉장히 많았다고 생각합니다. 특히 2차 면접의 경우 총 3개의 절차로 이루어져있는데 코딩테스트, PT 면접, 영어 인터뷰 면접을 하루에 모두 진행하게 됩니다. 제가 경험했던 모든 채용 절차중에 가장 길었던것 같고 그만큼 많은 노력을 들였던것 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9fBcd/dJMcahbPv4R/A6xPW7PHeTldfYDHQQk5w1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9fBcd/dJMcahbPv4R/A6xPW7PHeTldfYDHQQk5w1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9fBcd/dJMcahbPv4R/A6xPW7PHeTldfYDHQQk5w1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9fBcd%2FdJMcahbPv4R%2FA6xPW7PHeTldfYDHQQk5w1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;642&quot; height=&quot;312&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 이 서류 합격 소식을 들었을 때 얼마나 놀랐는지 아직도 생생하게 기억이 나는것 같습니다. 대기업 서류 합격이 대한항공이 처음이었기 때문에 더 특별하게 느껴졌습니다. 게다가 대한항공은 반드시 영어 성적을 제출해야 하는데, 당시 제가 제출할 수 있는 유일한 성적이 TOEIC Speaking이었습니다. 그런데 그 점수의 유효기간이 거의 끝나서 등록이 불가능한 상황이었고, 결국 급하게 시험을 다시 치뤄, 입사 원서 접수 하루 전에 간신히 결과를 받았습니다. 그것도 정말 아슬아슬하게 최소 점수를 맞추면서요&amp;hellip;&lt;/p&gt;
&lt;p data-end=&quot;462&quot; data-start=&quot;366&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 극적으로 서류를 제출했는데 합격 통보를 받아서 정말 기뻤습니다. 이 경험 덕분에 &lt;b&gt;끝까지 포기하지 말자&lt;/b&gt;는 다짐을 다시 한 번 굳건히 할 수 있었던 것 같습니다.&lt;/p&gt;
&lt;p data-end=&quot;462&quot; data-start=&quot;366&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bit7Qw/dJMcahXdodS/QbtK2y27uyBjncOrqaHNQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bit7Qw/dJMcahXdodS/QbtK2y27uyBjncOrqaHNQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bit7Qw/dJMcahXdodS/QbtK2y27uyBjncOrqaHNQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbit7Qw%2FdJMcahXdodS%2FQbtK2y27uyBjncOrqaHNQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;847&quot; height=&quot;261&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 절차인 1차 면접은 화상 기반의 온라인 면접으로 진행되었습니다. 정해진 시간 안에 간단한 자기소개를 하고, 이어서 세 가지 질문에 대해 답변하는 형식이었습니다. 협업 경험이 없더라도, 여러 명과 프로젝트를 진행해본 경험이 있다면 충분히 답할 수 있는 수준의 질문들이었습니다. 다행히 큰 어려움 없이 면접을 마칠 수 있었고, 이후 1차 면접 합격 통보를 받을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n3sC9/dJMcabJsOSY/gf7N8pJIUDMzb7AkewkYCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n3sC9/dJMcabJsOSY/gf7N8pJIUDMzb7AkewkYCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n3sC9/dJMcabJsOSY/gf7N8pJIUDMzb7AkewkYCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn3sC9%2FdJMcabJsOSY%2Fgf7N8pJIUDMzb7AkewkYCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;827&quot; height=&quot;349&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차면접은 대한항공 인재개발원에서 진행되며 앞써 언급했던것 처럼 코딩테스트, PT 면접, 영어 면접을 진행합니다. 코딩테스트는 사실상 제 실력이 맡겼습니다.(저는 최근 몇년간 코딩테스트 문제를 풀어본적이 없어요...그 시간에 다른걸 한다는 주의라...) PT면접은 질문을 알 수 없으니 사실상 준비를 하기 어려웠습니다. 그래서 그냥 제 경험에 기반한 문제가 나오길 하느님께 빌었어요. 그래서 모든 시간을 저는 영어 면접에 투자했습니다. 1년간 듀오링고로 다져온 영어실력은 안랩 면접 당시 형편없었다는 걸 느꼈기에 정말 여러 질문에 대한 답을 준비해서 반복 연습했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 1시간 동안 3문제를 풀어야하는 코딩테스트를 진행했습니다. 저는 1.5개를 풀었습니다. 0.5는 3번문제의 모든 테스트 케이스를 만족하지 못해서 0.5라고 하겠습니다.. ㅜㅜ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음으로는 그룹으로 진행되는 PT 면접을 진행합니다. PT 면접은 주어진 질문에 대해서 30분간 준비하고 준비한 내용을 차례대로 3분간 발표하는 식으로 진행됩니다. 다대다로 진행되며, 모든 발표가 끝나면 면접관분들께서 차례대로 질문을 하시고 답하는 식으로 진행됬습니다. 발표한 내용 뿐만아니라 입사원서에서 기재된 내용들도 같이 물어보는 식으로 진행됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막은 대망의 영어 면접으로 제가 가장 떨렸지만 잘 마무리했다고 생각합니다. 제 체감상 10분 동안했던것 같은데 준비했던 질문도 나왔고 준비하지 않았던 질문도 나왔던것 같은데 기세로 밀어붙였던것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 많이 떨렸고 가장 많은 사람이 이 단계에서 떨어진다고 들었기 때문에 큰 기대를 하지 않았지만, 2차 면접에 합격소식을 들어 굉장히 기뻤던것 같습니다.ㅜㅜ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W98L1/dJMcafkUNR6/OSIel2ZBZghQrCCEZb0EJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W98L1/dJMcafkUNR6/OSIel2ZBZghQrCCEZb0EJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W98L1/dJMcafkUNR6/OSIel2ZBZghQrCCEZb0EJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW98L1%2FdJMcafkUNR6%2FOSIel2ZBZghQrCCEZb0EJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;765&quot; height=&quot;481&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 최종 면접에서 아쉽게 탈락하며, 비록 원하는 결과를 얻지는 못했지만, 그 과정에서 쌓아온 경험과 성장은 제게 소중한 자산이 되었습니다. 이번 도전이 끝은 아니라고 생각합니다. 언젠가 다시 기회가 온다면, 그때는 더 성장한 모습으로 도전할 수 있을 거라 믿습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 긴 글을 읽어 주셔서 감사합니다.&lt;/p&gt;</description>
      <author>min.c00</author>
      <guid isPermaLink="true">https://jminc00.tistory.com/105</guid>
      <comments>https://jminc00.tistory.com/105#entry105comment</comments>
      <pubDate>Thu, 4 Dec 2025 23:55:37 +0900</pubDate>
    </item>
    <item>
      <title>안랩 경력 채용 후기</title>
      <link>https://jminc00.tistory.com/104</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 오랜만에 블로그를 작성하는 것 같습니다. 2025년은 저에게 있어서 굉장히 바쁘고 힘든 해였던 것 같아요. 그중에서도 가장 큰 이벤트는 2025년 12월 4일은 병역특례로 입사한 현재 회사에서의 복무만료가 예정되어 있었습니다. 현재 재직 중인 회사는 WMS 팀에 소속되어 팀장님과 팀원들과 시너지를 발휘하며 재밌게 일할 수 있었고 어느 정도의 성과와 능력을 인정받으며 안정적인 회사생활을 이어나갈 수 있는 상황이었지만, 새로운 곳에서의 도전과 더 다양한 경험에 대한 욕심 때문에 이직을 결정하게 되었습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5eu7A/dJMcacO5g9J/f4gvo0JrwSvqqb95rLFMTK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5eu7A/dJMcacO5g9J/f4gvo0JrwSvqqb95rLFMTK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5eu7A/dJMcacO5g9J/f4gvo0JrwSvqqb95rLFMTK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5eu7A%2FdJMcacO5g9J%2Ff4gvo0JrwSvqqb95rLFMTK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;540&quot; height=&quot;374&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;국내에서 알아주는 IT 기업 약 40곳에 입사원서를 지원했던것 같습니다. 그중에서 정말 운이 좋게 '안랩'이라는 회사에 서류 합격 통보를 받았습니다.&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;1368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GR6D3/dJMcac9lwy7/axKOj4Z8vbZfJkcx4YFBk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GR6D3/dJMcac9lwy7/axKOj4Z8vbZfJkcx4YFBk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GR6D3/dJMcac9lwy7/axKOj4Z8vbZfJkcx4YFBk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGR6D3%2FdJMcac9lwy7%2FaxKOj4Z8vbZfJkcx4YFBk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;468&quot; height=&quot;923&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;1368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;저는 경력직으로 'Web 개발 보안관제 플랫폼' 직무에 지원했습니다. 보안관제 업무가 사실 끌리지는 않았던 것 같습니다(악명이 높은 업무라고 주변에서 들었던...) 다만, 막연하게 보안 지식과 경험이 많이 부족한 저에게 새롭고 좋은 기회라는 생각에 지원했던 것 같습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cikUbl/dJMcabCDUKb/gUJJeuLGj1TsJWICkJ7KfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cikUbl/dJMcabCDUKb/gUJJeuLGj1TsJWICkJ7KfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cikUbl/dJMcabCDUKb/gUJJeuLGj1TsJWICkJ7KfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcikUbl%2FdJMcabCDUKb%2FgUJJeuLGj1TsJWICkJ7KfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;281&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;정말 운이 좋게 서류 합격 통보를 받았습니다ㅜㅜ 당시에 수많은 회사로부터 서류탈락 통보를 받고 있는 상황에서 가뭄의 단비같은 소식이었던 것 같아요. 추후 1차 면접을 진행하면서 저의 서류 합격에 대한 이유를 감히 추측하자면 두 가지라고 생각이 들었습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;첫 번째는, 대학시절 일과 학습을 병행했던 경험(대략 8개월)인데 당시에 막연하게 실무를 경험하고 싶은 강한 욕구로 교내에 있는 SI 업체에서 일을 배울 수 있는 기회를 얻게 되어 풀스택 개발자로 일을 했었습니다. (면접분들께서 그 열정을 좋게 봐주신 것 같아요.)&lt;br&gt;&amp;nbsp;&lt;br&gt;두 번째는, 현재 직장에서 문제 해결 경험이라고 생각하는데 현 WMS 팀에 소속되어 제가 경험한 문제와 해결과정들을 잘 정리하고 더 나아가 팀 스터디와 사내 컨퍼런스, 사내 블로그까지 이어지는 경험을 좋게 봐주신 것 같습니다. (경력직이라도 신입이라 다를 바가 없어 아마 성장하고자 하는 태도를 강하게 보신 듯합니다...!)&lt;br&gt;&amp;nbsp;&lt;br&gt;서류 합격 그다음 단계는 1차 면접을 3:1의 기술 면접을 진행했습니다. 면접은 결국 저한테 유리한 질문들을 물어보게끔 포폴이나 이력서를 만드는 게 좋다고 알고있었지만 그럼에도 불구하고 저는 기출(?)을 참고했습니다.&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2662&quot; data-origin-height=&quot;1396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/enxHl5/dJMcafLOnae/JSzqw5ctbUnkLWjxqpKTyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/enxHl5/dJMcafLOnae/JSzqw5ctbUnkLWjxqpKTyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/enxHl5/dJMcafLOnae/JSzqw5ctbUnkLWjxqpKTyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FenxHl5%2FdJMcafLOnae%2FJSzqw5ctbUnkLWjxqpKTyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;782&quot; height=&quot;410&quot; data-origin-width=&quot;2662&quot; data-origin-height=&quot;1396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;잡플래닛에서는 면접 후기를 확인할 수 있는데 이전 기술 면접에서 어떤 질문들을 받았는지 확인할 수 있습니다. 일반적인 기술 질문들 예를 들면 MVC 패턴, 기본적인 알고리즘 등과 관련된 질문을 많지는 않지만 받았던 것 같습니다. 특히 인상 깊었던 건 가장 자신 있는 알고리즘을 설명하는 질문이었는데 무슨 자신감인지 모르겠지만, DFS, BFS에 대해서 설명했고 당시에 화이트보드를 활용해서 설명하다가 절었... 암튼 잘 설명했습니다....&lt;br&gt;&amp;nbsp;&lt;br&gt;1차 면접에서 가장 중요한 건 참여했던 프로젝트에서 문제를 해결했던 경험이지 않을까 싶습니다. 저의 기술 면접 대부분이 그와 관련된 질문이었고 저의 포트폴리오에도 문제 해결과 관련된 경험을 작성했습니다. 그중에서도 아래 블로그 글과 관련된 문제 해결경험을 자신 있게 답변했고 그 부분에서 좋은 인상과 점수를 받았다고 생각합니다.&amp;nbsp;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;2025 상반기 사내 컨퍼런스 - 도메인을 지키는 설계&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;이번 블로그에서는 상반기 동안 진행한 WMS 프로젝트에서 마주했던 문제들과 그 해결 방법을 공유하고자 합니다. 제가 정의하고 해결한 문제는 총 4가지인데, 4가지의 특징을 한데 모아보니 제가&quot; data-og-host=&quot;jminc00.tistory.com&quot; data-og-source-url=&quot;https://jminc00.tistory.com/101&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/bwkuuv/hyZOmIQOvN/AAAAAAAAAAAAAAAAAAAAAMJgKN5tXT-2_mcsL0qGrU14nFn6THI9cWdoE1o8rtDf/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1764514799&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Yle6oDDYyWZgYk6QwlyCnVeXfGg%3D&quot; data-og-url=&quot;https://jminc00.tistory.com/101&quot;&gt;&lt;a href=&quot;https://jminc00.tistory.com/101&quot; target=&quot;_blank&quot; data-source-url=&quot;https://jminc00.tistory.com/101&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/bwkuuv/hyZOmIQOvN/AAAAAAAAAAAAAAAAAAAAAMJgKN5tXT-2_mcsL0qGrU14nFn6THI9cWdoE1o8rtDf/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1764514799&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Yle6oDDYyWZgYk6QwlyCnVeXfGg%3D')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;2025 상반기 사내 컨퍼런스 - 도메인을 지키는 설계&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;이번 블로그에서는 상반기 동안 진행한 WMS 프로젝트에서 마주했던 문제들과 그 해결 방법을 공유하고자 합니다. 제가 정의하고 해결한 문제는 총 4가지인데, 4가지의 특징을 한데 모아보니 제가&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;jminc00.tistory.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;1차 기술 면접은 분명 기초(40%) + 프로젝트에서 문제 해결 경험(60%)으로 준비하시면 좋을 것 같습니다. 추가로 자기소개! 간단한 자기소개를 준비해 주세요.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9jeSV/dJMcafyheev/99HwNLkwojkpecWk6qK9j0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9jeSV/dJMcafyheev/99HwNLkwojkpecWk6qK9j0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9jeSV/dJMcafyheev/99HwNLkwojkpecWk6qK9j0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9jeSV%2FdJMcafyheev%2F99HwNLkwojkpecWk6qK9j0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;674&quot; height=&quot;322&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이번에도 운이 좋게 1차 면접을 합격할 수 있었고 마지막 과정은 2차 면접이었습니다. 2차 면접은 4:1로 진행하는 컬처핏, 그리고 인성 면접을 진행했습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;2차 면접은 &quot;네가 어떤 사람인가?&quot;, &quot;우리 회사랑 너가 잘 맞을까?&quot;, &quot;우리가 널 왜 뽑아야 하는데?&quot; 등과 같은 뉘앙스의 질문들을 많이 받았습니다. 가장 기억에 남는 질문은 &quot;우리가 너를 뽑아야 하는 이유가 뭘까요?&quot;라는 질문이었던 것 같아요. 뭐 능글맞게 대답했던 것 같은데 암튼 면접장 분위기가 나쁘지 않았습니다. (제 가장 큰 장점이 능글맞음이 아닐까 합니다.)&lt;br&gt;&amp;nbsp;&lt;br&gt;다만, 좋았던 면접장 분위기를 바로 바꿨던 질문이 있는데 바로 일부 영어 면접(?)이 있었습니다. 듀오링고로 다져온 저의 영어 실력이 드러나는 순간 굉장히 창피했던 기억이 아직도 강렬합니다....ㅎㅎ&lt;br&gt;&amp;nbsp;&lt;br&gt;다만, 면접은 기세! 못하지만 아는 단어 여러 개 조합해서 잘(?) 대답하고 2차 면접을 무사히 마칠 수 있었습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mjO6a/dJMcagDXSnG/uW0zdEIBLTPy04Me4yNaWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mjO6a/dJMcagDXSnG/uW0zdEIBLTPy04Me4yNaWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mjO6a/dJMcagDXSnG/uW0zdEIBLTPy04Me4yNaWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmjO6a%2FdJMcagDXSnG%2FuW0zdEIBLTPy04Me4yNaWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;722&quot; height=&quot;168&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;며칠 뒤 2차 면접 최종 합격 소식을 듣고 정말 많이 기뻤던 것 같습니다. 저 같은 감자도 이직할 수 있구나 하는 신기함고 합격을 시켜주신 안랩에 감사함을 느꼈습니다. 이직 기간 동안 정말 많은 스트레스를 받았는데 저 날은 기분이 좋아서 한잔 한 것 같아요!&lt;br&gt;&amp;nbsp;&lt;br&gt;마지막으로 저도 이런 면접 후기를 오래전부터 작성해보고 싶었는데 이번에 좋은 결과가 있어서 작은 바람을 이뤘던 것 같습니다. 혹시 이 글을 읽는 취준, 이직생 여러분들 저 같은 감자도 해냈어요..! 여러분은 더 잘하실 거라고 생각합니다. 긴 글 읽어주셔서 감사합니다&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8DHCl/dJMcahXaGcs/IFkzodp7MKiHpeQmtrh5V0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8DHCl/dJMcahXaGcs/IFkzodp7MKiHpeQmtrh5V0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8DHCl/dJMcahXaGcs/IFkzodp7MKiHpeQmtrh5V0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8DHCl%2FdJMcahXaGcs%2FIFkzodp7MKiHpeQmtrh5V0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;176&quot; height=&quot;155&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>min.c00</author>
      <guid isPermaLink="true">https://jminc00.tistory.com/104</guid>
      <comments>https://jminc00.tistory.com/104#entry104comment</comments>
      <pubDate>Thu, 27 Nov 2025 23:36:57 +0900</pubDate>
    </item>
    <item>
      <title>AWS Certified Solutions Architect - IAM(Identity and Access Management)</title>
      <link>https://jminc00.tistory.com/103</link>
      <description>&lt;h3 data-end=&quot;94&quot; data-start=&quot;43&quot; data-ke-size=&quot;size23&quot;&gt;1. &lt;b&gt;AWS IAM 서비스의 주요 역할에 대한 설명으로 옳은 것은 무엇일까요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;152&quot; data-start=&quot;96&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답: B. AWS 계정 내 리소스에 대한 액세스 권한을 안전하게 관리하는 글로벌 서비스입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;396&quot; data-start=&quot;154&quot; data-ke-size=&quot;size16&quot;&gt;AWS &lt;b&gt;IAM&lt;/b&gt;(Identity and Access Management)은 &lt;b&gt;AWS 리소스에 대한 액세스를 안전하게 관리&lt;/b&gt;하는 서비스입니다. 이 서비스를 통해 사용자, 그룹, 역할을 만들고, 그들에게 AWS 리소스에 대한 권한을 부여하거나 거부할 수 있습니다. &lt;b&gt;IAM&lt;/b&gt;을 사용하면 리소스에 대한 세밀한 액세스 제어를 통해 &lt;b&gt;보안을 강화&lt;/b&gt;할 수 있으며, &lt;b&gt;AWS 계정에 대한 중앙집중적인 보안 관리를&lt;/b&gt; 제공합니다.&lt;/p&gt;
&lt;p data-end=&quot;412&quot; data-start=&quot;398&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기타 선택지 분석:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;543&quot; data-start=&quot;413&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;455&quot; data-start=&quot;413&quot;&gt;&lt;b&gt;A&lt;/b&gt;: IAM은 비용 절감 서비스가 아니라 보안 관리 서비스입니다.&lt;/li&gt;
&lt;li data-end=&quot;496&quot; data-start=&quot;456&quot;&gt;&lt;b&gt;C&lt;/b&gt;: IAM은 서버의 운영 체제를 관리하는 서비스가 아닙니다.&lt;/li&gt;
&lt;li data-end=&quot;543&quot; data-start=&quot;497&quot;&gt;&lt;b&gt;D&lt;/b&gt;: IAM은 사용자 간의 파일 공유를 위한 스토리지 서비스가 아닙니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;548&quot; data-start=&quot;545&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;604&quot; data-start=&quot;550&quot; data-ke-size=&quot;size23&quot;&gt;2. &lt;b&gt;IAM 사용자(User)와 역할(Role)의 가장 중요한 차이점은 무엇일까요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;668&quot; data-start=&quot;606&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답: B. 사용자는 개별 사용자를 위한 영구 자격 증명, 역할은 임시 권한 위임을 위한 자격 증명입니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;999&quot; data-start=&quot;670&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;801&quot; data-start=&quot;670&quot;&gt;&lt;b&gt;IAM 사용자(User)&lt;/b&gt;: 각 사용자는 &lt;b&gt;영구적인 자격 증명&lt;/b&gt;(예: 액세스 키, 비밀번호)을 가지며, 해당 사용자가 AWS 서비스에 접근할 수 있는 권한을 가집니다. 각 사용자는 고유한 자격 증명 정보를 가지고 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;999&quot; data-start=&quot;805&quot;&gt;&lt;b&gt;IAM 역할(Role)&lt;/b&gt;: 역할은 특정 사용자가 아닌, &lt;b&gt;임시 자격 증명&lt;/b&gt;을 통해 권한을 위임하는 객체입니다. 역할은 &lt;b&gt;사용자에게 권한을 부여&lt;/b&gt;하는 것이 아니라, 다른 AWS 리소스나 &lt;b&gt;서비스에 임시로 권한을 부여&lt;/b&gt;합니다. 예를 들어 EC2 인스턴스가 다른 AWS 리소스(S3 버킷 등)에 접근할 수 있도록 하는 데 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1004&quot; data-start=&quot;1001&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1046&quot; data-start=&quot;1006&quot; data-ke-size=&quot;size23&quot;&gt;3. &lt;b&gt;IAM 정책(Policy)의 핵심 역할은 무엇일까요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1124&quot; data-start=&quot;1048&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답: B. 어떤 보안 주체(사용자/그룹/역할)가 어떤 AWS 리소스에 대해 어떤 작업(허용/거부)을 수행할 수 있는지 정의합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1314&quot; data-start=&quot;1126&quot; data-ke-size=&quot;size16&quot;&gt;IAM 정책(Policy)는 &lt;b&gt;사용자, 그룹, 역할&lt;/b&gt;과 같은 보안 주체에게 특정 작업을 허용하거나 거부하는 규칙을 정의합니다. 각 정책은 &lt;b&gt;허용(Allow)&lt;/b&gt; 또는 거부(Deny)를 기준으로 특정 작업(예: s3:ListBucket 또는 ec2:StartInstances)에 대한 권한을 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;1429&quot; data-start=&quot;1316&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정책은 JSON 형식으로 정의되며,&lt;/b&gt; 이를 통해 AWS 서비스 및 리소스에 대해 세밀한 제어를 할 수 있습니다. 또한 IAM 정책은 &lt;b&gt;AWS 리소스의 보안 관리에서 핵심적인 역할&lt;/b&gt;을 담당합니다.&lt;/p&gt;
&lt;hr data-end=&quot;1434&quot; data-start=&quot;1431&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1521&quot; data-start=&quot;1436&quot; data-ke-size=&quot;size23&quot;&gt;4. &lt;b&gt;AWS 서비스(예: EC2)가 다른 AWS 서비스(예: S3)에 안전하게 액세스해야 할 때, 권한을 부여하는 권장 방법은 무엇일까요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1575&quot; data-start=&quot;1523&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답: B. EC2 인스턴스에 적절한 권한이 부여된 IAM 역할(Role)을 연결합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1805&quot; data-start=&quot;1577&quot; data-ke-size=&quot;size16&quot;&gt;EC2 인스턴스와 같은 &lt;b&gt;AWS 서비스&lt;/b&gt;가 다른 서비스에 접근할 때는 IAM 역할(Role)을 사용하는 것이 &lt;b&gt;가장 안전한 방법&lt;/b&gt;입니다. 이를 통해 &lt;b&gt;임시 자격 증명&lt;/b&gt;을 사용하여 다른 리소스에 접근할 수 있게 됩니다. &lt;b&gt;IAM 역할&lt;/b&gt;은 EC2 인스턴스에 연결되어 &lt;b&gt;액세스 키나 비밀번호 없이도 필요한 권한을 부여&lt;/b&gt;합니다. 이렇게 하면 &lt;b&gt;자격 증명 유출 위험&lt;/b&gt;을 줄일 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;1821&quot; data-start=&quot;1807&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기타 선택지 분석:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2000&quot; data-start=&quot;1822&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1869&quot; data-start=&quot;1822&quot;&gt;&lt;b&gt;A&lt;/b&gt;: EC2 인스턴스 내부에 자격 증명을 저장하는 것은 보안상 위험합니다.&lt;/li&gt;
&lt;li data-end=&quot;1932&quot; data-start=&quot;1870&quot;&gt;&lt;b&gt;C&lt;/b&gt;: IAM 사용자의 액세스 키를 EC2 인스턴스에 저장하는 것은 안전하지 않으며 권장되지 않습니다.&lt;/li&gt;
&lt;li data-end=&quot;2000&quot; data-start=&quot;1933&quot;&gt;&lt;b&gt;D&lt;/b&gt;: 보안 그룹은 네트워크 트래픽을 제어하는 서비스이며, 리소스 액세스 권한을 제어하는 데 사용되지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2005&quot; data-start=&quot;2002&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2109&quot; data-start=&quot;2007&quot; data-ke-size=&quot;size23&quot;&gt;5. &lt;b&gt;AWS 계정 내 모든 IAM 사용자의 암호 사용 여부, 마지막 암호 변경일, MFA 활성화 상태 등 자격 증명 정보를 감사하기 위한 IAM 보안 도구는 무엇일까요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2151&quot; data-start=&quot;2111&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답: C. 자격 증명 보고서 (Credentials Report)&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2370&quot; data-start=&quot;2153&quot; data-ke-size=&quot;size16&quot;&gt;자격 증명 보고서(Credentials Report)는 AWS 계정 내 &lt;b&gt;모든 IAM 사용자에 대한 자격 증명 상태&lt;/b&gt;를 포괄적으로 확인할 수 있는 도구입니다. 이 보고서는 사용자가 설정한 &lt;b&gt;암호 사용 여부&lt;/b&gt;, &lt;b&gt;암호 변경 일자&lt;/b&gt;, &lt;b&gt;MFA 활성화 상태&lt;/b&gt; 등을 포함한 상세 정보를 제공합니다. 이를 통해 보안 감사를 수행하고 자격 증명 관리 상태를 점검할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;2386&quot; data-start=&quot;2372&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기타 선택지 분석:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2585&quot; data-start=&quot;2387&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2444&quot; data-start=&quot;2387&quot;&gt;&lt;b&gt;A&lt;/b&gt;: IAM 정책 시뮬레이터는 정책이 특정 조건에서 어떻게 작동하는지 테스트하는 도구입니다.&lt;/li&gt;
&lt;li data-end=&quot;2518&quot; data-start=&quot;2445&quot;&gt;&lt;b&gt;B&lt;/b&gt;: 액세스 관리자는 사용자가 액세스 할 수 있는 리소스를 보여주지만, 자격 증명 정보와 관련된 정보를 제공하지 않습니다.&lt;/li&gt;
&lt;li data-end=&quot;2585&quot; data-start=&quot;2519&quot;&gt;&lt;b&gt;D&lt;/b&gt;: IAM 역할은 리소스 액세스 권한을 부여하는 데 사용되며, 자격 증명 정보를 감사하는 도구는 아닙니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2590&quot; data-start=&quot;2587&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2636&quot; data-start=&quot;2592&quot; data-ke-size=&quot;size23&quot;&gt;6. &lt;b&gt;IAM 그룹(Group)을 사용하는 주요 이유는 무엇일까요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2692&quot; data-start=&quot;2638&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답: B. 여러 IAM 사용자에게 동일한 권한을 쉽게 부여하고 관리할 수 있기 때문입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2876&quot; data-start=&quot;2694&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IAM 그룹&lt;/b&gt;은 여러 사용자가 &lt;b&gt;공통의 권한을 가질 수 있도록&lt;/b&gt; 그룹화하는 데 유용한 도구입니다. 그룹에 정책을 연결하면, 해당 그룹에 속한 모든 사용자에게 &lt;b&gt;동일한 권한&lt;/b&gt;을 부여할 수 있습니다. 이를 통해 권한을 효율적으로 관리할 수 있으며, &lt;b&gt;사용자별로 개별적으로 권한을 관리&lt;/b&gt;하는 것보다 더 효율적입니다.&lt;/p&gt;
&lt;hr data-end=&quot;2881&quot; data-start=&quot;2878&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2923&quot; data-start=&quot;2883&quot; data-ke-size=&quot;size23&quot;&gt;7. &lt;b&gt;IAM에서 정책을 첨부할 수 있는 대상은 무엇인가요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2946&quot; data-start=&quot;2925&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답: A. 사용자, 그룹, 역할&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3120&quot; data-start=&quot;2948&quot; data-ke-size=&quot;size16&quot;&gt;IAM 정책은 &lt;b&gt;사용자, 그룹, 역할&lt;/b&gt;에 첨부하여 해당 대상이 접근할 수 있는 리소스와 작업을 정의할 수 있습니다. 예를 들어, 특정 &lt;b&gt;IAM 사용자&lt;/b&gt;가 S3 버킷에 접근할 수 있도록 정책을 첨부하거나, &lt;b&gt;IAM 그룹&lt;/b&gt;에 정책을 첨부하여 그룹의 모든 사용자가 해당 권한을 가지도록 할 수 있습니다.&lt;/p&gt;
&lt;hr data-end=&quot;3125&quot; data-start=&quot;3122&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;3173&quot; data-start=&quot;3127&quot; data-ke-size=&quot;size23&quot;&gt;8. &lt;b&gt;IAM 역할(Role)을 사용해야 하는 주된 이유는 무엇인가요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;3217&quot; data-start=&quot;3175&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답: C. 임시 보안 자격 증명으로 AWS 리소스를 액세스 하기 위해서&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3437&quot; data-start=&quot;3219&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IAM 역할&lt;/b&gt;은 &lt;b&gt;임시 자격 증명&lt;/b&gt;을 제공하여 AWS 리소스에 접근할 수 있도록 합니다. 이를 통해 &lt;b&gt;서비스나 애플리케이션에 임시로 권한을 부여&lt;/b&gt;하고, 사용자가 권한을 직접적으로 보유하지 않게 할 수 있습니다. 예를 들어, EC2 인스턴스가 S3 버킷에 접근하려면 EC2에 적절한 IAM 역할을 연결하고 &lt;b&gt;임시 보안 자격 증명&lt;/b&gt;을 사용하여 S3에 접근하도록 할 수 있습니다.&lt;/p&gt;
&lt;hr data-end=&quot;3442&quot; data-start=&quot;3439&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;3485&quot; data-start=&quot;3444&quot; data-ke-size=&quot;size23&quot;&gt;9. &lt;b&gt;IAM에서 액세스 키와 비밀번호의 차이점은 무엇인가요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;3575&quot; data-start=&quot;3487&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답: B. 액세스 키는 프로그램적 접근을 위한 자격 증명이고, 비밀번호는 AWS Management Console에 대한 접근을 위한 자격 증명입니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3786&quot; data-start=&quot;3577&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3685&quot; data-start=&quot;3577&quot;&gt;&lt;b&gt;액세스 키&lt;/b&gt;는 &lt;b&gt;AWS CLI&lt;/b&gt;, &lt;b&gt;API&lt;/b&gt;, &lt;b&gt;SDK&lt;/b&gt; 등 &lt;b&gt;프로그래밍적 접근&lt;/b&gt;을 위한 자격 증명입니다. 이 키를 사용하여 코드에서 AWS 리소스를 제어할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;3786&quot; data-start=&quot;3686&quot;&gt;&lt;b&gt;비밀번호&lt;/b&gt;는 &lt;b&gt;AWS Management Console&lt;/b&gt;에 로그인하기 위한 자격 증명입니다. 이를 통해 사용자 인터페이스(UI)를 사용해 리소스를 관리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>min.c00</author>
      <guid isPermaLink="true">https://jminc00.tistory.com/103</guid>
      <comments>https://jminc00.tistory.com/103#entry103comment</comments>
      <pubDate>Sun, 12 Oct 2025 16:31:58 +0900</pubDate>
    </item>
  </channel>
</rss>