<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>팝콘도팝이다</title>
    <link>https://ohksj77.tistory.com/</link>
    <description> &amp;nbsp;is&amp;nbsp;also&amp;nbsp;pop  </description>
    <language>ko</language>
    <pubDate>Mon, 25 May 2026 19:49:50 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ohksj77</managingEditor>
    <image>
      <title>팝콘도팝이다</title>
      <url>https://tistory1.daumcdn.net/tistory/4905967/attach/77daf44253ca4ea8bd26f5b6330d8fcd</url>
      <link>https://ohksj77.tistory.com</link>
    </image>
    <item>
      <title>Kanal을 만들며: JVM 서버에서 실시간 기능을 조금 더 편하게 다루기</title>
      <link>https://ohksj77.tistory.com/285</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket은 연결을 열어주지만, 채팅방과 접속자 목록까지 대신 만들어주지는 않는다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&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;채팅, 알림, 실시간 대시보드, 협업 문서, 접속자 목록, 타이핑 표시 같은 기능이 대표적이다. Spring 서버에서도 WebSocket을 열 수 있고, STOMP를 사용할 수도 있다. 그런데 실제로 기능을 만들다 보면 WebSocket 연결을 여는 것만으로는 부족하다는 걸 금방 느끼게 된다.&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;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 사용자가 어떤 방에 들어와 있는가?&lt;/li&gt;
&lt;li&gt;같은 사용자가 브라우저 탭을 여러 개 열면 어떻게 볼 것인가?&lt;/li&gt;
&lt;li&gt;사용자가 방에 들어올 때와 나갈 때 어떤 처리를 할 것인가?&lt;/li&gt;
&lt;li&gt;방에 메시지를 보내면 누구에게 전달할 것인가?&lt;/li&gt;
&lt;li&gt;느린 클라이언트 때문에 메시지가 쌓이면 어떻게 할 것인가?&lt;/li&gt;
&lt;li&gt;접속자 목록은 어디에 저장하고 언제 지울 것인가?&lt;/li&gt;
&lt;li&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;Kanal은 이 반복되는 작업을 조금 더 높은 수준의 모델로 다루기 위해 시작한 프로젝트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 말하면, Kanal은 JVM 서버 개발자가 실시간 기능을 더 애플리케이션 코드답게 작성할 수 있게 만들려는 시도다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WebSocket만으로는 무엇이 부족할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket은 클라이언트와 서버 사이에 양방향 연결을 열어준다. 이것은 매우 중요한 기능이지만, 제품 기능을 만들기에는 낮은 수준의 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket handler 안에서 직접 이런 코드를 만들 수는 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;roomId&lt;/code&gt;별 session 목록&lt;/li&gt;
&lt;li&gt;user id와 session id 매핑&lt;/li&gt;
&lt;li&gt;join, leave 처리&lt;/li&gt;
&lt;li&gt;broadcast loop&lt;/li&gt;
&lt;li&gt;disconnect cleanup&lt;/li&gt;
&lt;li&gt;접속자 목록 저장&lt;/li&gt;
&lt;li&gt;느린 client 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 빠르게 만들 수 있다. 하지만 기능이 커지면 코드가 여러 handler와 service에 흩어지기 쉽다. 특히 접속자 목록, 방 참여 상태, 메시지 전달 대상이 서로 섞이면 나중에 고치기 어려워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kanal은 이 부분을 &lt;code&gt;channel&lt;/code&gt;이라는 단위로 묶으려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 식이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;import io.github.kimseungjin.kanal.core.dsl.channel
import io.github.kimseungjin.kanal.core.dsl.realtime

val app =
    realtime {
        channel&amp;lt;ChatMessage&amp;gt;(&quot;chat/{roomId}&quot;) {
            description(&quot;Realtime chat channel&quot;)

            onJoin {
                presence.track(
                    key = session.userId ?: session.id,
                    metadata = mapOf(&quot;roomId&quot; to address.parameters.getValue(&quot;roomId&quot;)),
                )
            }

            onMessage { message -&amp;gt;
                broadcast(message)
            }
        }
    }&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;여기서 개발자는 socket 자체보다 애플리케이션의 의미에 집중한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;chat/{roomId}&lt;/code&gt;라는 채널이 있다.&lt;/li&gt;
&lt;li&gt;사용자가 들어오면 &lt;code&gt;onJoin&lt;/code&gt;이 실행된다.&lt;/li&gt;
&lt;li&gt;접속자 목록은 &lt;code&gt;presence&lt;/code&gt;로 관리한다.&lt;/li&gt;
&lt;li&gt;메시지가 오면 &lt;code&gt;onMessage&lt;/code&gt;가 실행된다.&lt;/li&gt;
&lt;li&gt;같은 방에 있는 사람들에게 &lt;code&gt;broadcast&lt;/code&gt;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 코드가 Kanal이 원하는 방향이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 프로젝트 상태&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 Kanal은 완성된 실시간 서버 프레임워크가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점을 먼저 말하면, 지금은 WebSocket 연결을 실제로 받고 메시지를 주고받는 runtime이 없다. 그러니까 지금 당장 &lt;code&gt;/realtime&lt;/code&gt; 같은 endpoint에 브라우저를 연결해서 채팅할 수 있는 상태는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 그 runtime을 만들기 위한 기초 구조를 먼저 만들었다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;kanal-core&lt;/code&gt;&lt;br /&gt;실시간 기능을 표현하는 핵심 모델을 담는다. channel, session, presence, channel pattern, resolver가 여기에 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kanal-runtime&lt;/code&gt;&lt;br /&gt;실제 runtime에서 필요할 성능 관련 구조를 담는다. 방 참여 목록, bounded queue, metrics 같은 것들이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kanal-spring-boot-starter&lt;/code&gt;&lt;br /&gt;Spring Boot에서 자동 설정으로 붙일 수 있는 시작점이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kanal-samples:chat-presence&lt;/code&gt;&lt;br /&gt;채팅과 접속자 목록을 어떻게 모델링할지 보여주는 예제다.&lt;/li&gt;
&lt;/ul&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;h2 data-ke-size=&quot;size26&quot;&gt;1. 채널 주소를 해석하는 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅방 주소를 보통 이렇게 표현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;chat/general
chat/random
chat/spring&lt;/code&gt;&lt;/pre&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;pre class=&quot;dust&quot;&gt;&lt;code&gt;chat/{roomId}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;chat/general&lt;/code&gt;이 들어오면 &lt;code&gt;roomId = general&lt;/code&gt;로 해석되는 식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kanal에서는 이것을 &lt;code&gt;ChannelPattern&lt;/code&gt;으로 표현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;ChannelPattern(&quot;chat/{roomId}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 이걸 단순 문자열로만 들고 있었다. 하지만 runtime이 생기면 이 작업은 매우 자주 일어난다. 클라이언트가 메시지를 보낼 때마다 서버는 이 메시지가 어떤 channel에 해당하는지 찾아야 한다.&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;code&gt;ChannelPattern&lt;/code&gt;을 만들 때 미리 검증하고 쪼개둔다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;chat/{roomId}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패턴은 내부적으로 대략 이렇게 나뉜다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;고정된 부분: chat
변수 부분: roomId&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해두면 나중에 매번 문자열을 처음부터 분석하지 않아도 된다.&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;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;/chat/{roomId}
chat/{roomId}/
chat//{roomId}
chat/{room-id}
chat/{roomId}/{roomId}&lt;/code&gt;&lt;/pre&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;h2 data-ke-size=&quot;size26&quot;&gt;2. ChannelResolver&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ChannelResolver&lt;/code&gt;는 실제 주소를 channel 정의와 연결해주는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 서버에 이런 channel이 등록되어 있다고 하자.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;realtime {
    channel&amp;lt;Message&amp;gt;(&quot;chat/{roomId}&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;클라이언트가 &lt;code&gt;chat/general&lt;/code&gt;로 들어오면 resolver는 이렇게 해석한다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;pattern: chat/{roomId}
parameters: roomId = general&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;Kanal에서는 &lt;code&gt;RealtimeApplication.resolve(path)&lt;/code&gt;로 이 기능을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;val resolution = app.resolve(&quot;chat/general&quot;)&lt;/code&gt;&lt;/pre&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;예를 들어 다음 두 channel이 함께 있다고 하자.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;realtime {
    channel&amp;lt;Message&amp;gt;(&quot;chat/{roomId}&quot;) {}
    channel&amp;lt;Message&amp;gt;(&quot;chat/system&quot;) {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;chat/system&lt;/code&gt;은 &lt;code&gt;chat/{roomId}&lt;/code&gt;에도 들어맞을 수 있다. 이때 Kanal은 &lt;code&gt;chat/system&lt;/code&gt;이라는 더 구체적인 channel을 먼저 선택한다.&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;또한 이런 등록은 막는다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;channel&amp;lt;Message&amp;gt;(&quot;chat/{roomId}&quot;) {}
channel&amp;lt;Message&amp;gt;(&quot;chat/{id}&quot;) {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 패턴은 이름만 다를 뿐 실제 요청 경로 기준으로는 구분할 수 없다. 이런 애매한 등록은 시작할 때 실패하게 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Membership과 Presence를 나누기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 기능을 만들 때 자주 헷갈리는 개념이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 membership과 presence다.&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;둘 다 &quot;누가 방에 있나?&quot;와 관련 있어 보인다. 하지만 서버 내부에서는 역할이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Membership은 runtime이 메시지를 보내기 위해 필요한 정보다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 채팅방에 연결된 session id들은 무엇인가?&lt;/li&gt;
&lt;li&gt;이 session은 어떤 방들에 들어가 있는가?&lt;/li&gt;
&lt;li&gt;connection이 끊기면 어떤 방에서 제거해야 하는가?&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;Presence는 사용자에게 보여줄 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/li&gt;
&lt;li&gt;display name은 무엇인가?&lt;/li&gt;
&lt;li&gt;device 정보는 무엇인가?&lt;/li&gt;
&lt;li&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;Kanal은 이 둘을 섞지 않으려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;kanal-runtime&lt;/code&gt;에 &lt;code&gt;LocalMembershipIndex&lt;/code&gt;를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 index는 두 방향으로 정보를 들고 있다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;ChannelAddress -&amp;gt; SessionId 목록
SessionId -&amp;gt; ChannelAddress 목록&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 broadcast할 때 필요하다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;chat/general 방에 있는 session들을 찾아서 메시지를 보낸다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 연결이 끊겼을 때 필요하다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;s1 session이 들어가 있던 모든 방을 찾아서 정리한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 미리 잡아두면 나중에 WebSocket runtime을 만들 때 메시지 전달과 cleanup이 훨씬 명확해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 느린 클라이언트를 어떻게 다룰 것인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 서버에서 어려운 문제 중 하나는 느린 클라이언트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 빠르게 메시지를 만들고 있는데, 어떤 클라이언트가 네트워크 문제나 브라우저 상태 때문에 천천히 받는다고 해보자. 그러면 그 클라이언트에게 보낼 메시지가 queue에 쌓인다.&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;이 queue가 무한히 커지면 언젠가 메모리 문제가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Kanal은 outbound queue를 기본적으로 크기가 정해진 queue로 보려고 한다. 이것을 &lt;code&gt;BoundedOutboundQueue&lt;/code&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;queue가 가득 찼을 때는 정책이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 네 가지 정책을 둔다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;enum class BackpressurePolicy {
    SUSPEND,
    DROP_OLDEST,
    DROP_LATEST,
    DISCONNECT,
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 의미는 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SUSPEND&lt;/code&gt;&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;code&gt;DROP_OLDEST&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 오래된 메시지를 버리고 새 메시지를 넣는다. 최신 상태가 중요한 기능에 어울린다. 예를 들어 실시간 대시보드 업데이트나 presence 상태처럼 &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;code&gt;DROP_LATEST&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 들어온 메시지를 버린다. 이미 queue에 있는 메시지를 보존하는 쪽이다. typing indicator처럼 중요도가 낮고 자주 발생하는 이벤트에 쓸 수 있다.&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;code&gt;DISCONNECT&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 느린 클라이언트는 연결을 끊는 방식이다. 조금 거칠지만, 어떤 서비스에서는 이게 가장 정직한 선택일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제에서는 chat message와 typing signal에 서로 다른 정책을 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;channel&amp;lt;ChatMessage&amp;gt;(&quot;chat/{roomId}&quot;) {
    backpressure(BackpressurePolicy.DROP_OLDEST)
}

channel&amp;lt;TypingSignal&amp;gt;(&quot;chat/{roomId}/typing&quot;) {
    backpressure(BackpressurePolicy.DROP_LATEST)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chat message와 typing signal은 둘 다 실시간 이벤트지만 중요도가 다르다. Kanal은 이런 차이를 코드에 드러내고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Metrics를 처음부터 생각하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 중인 실시간 서버에서 문제가 생기면 이런 질문을 하게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 연결된 session은 몇 개인가?&lt;/li&gt;
&lt;li&gt;방 참여 수는 얼마나 되는가?&lt;/li&gt;
&lt;li&gt;메시지가 얼마나 들어오고 나가는가?&lt;/li&gt;
&lt;li&gt;queue가 계속 쌓이는가?&lt;/li&gt;
&lt;li&gt;메시지가 drop되고 있는가?&lt;/li&gt;
&lt;li&gt;어떤 정책 때문에 disconnect가 발생했는가?&lt;/li&gt;
&lt;li&gt;channel resolve가 느려지고 있는가?&lt;/li&gt;
&lt;li&gt;handler 실행 시간이 튀고 있는가?&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;kanal-runtime&lt;/code&gt;에는 &lt;code&gt;RuntimeMetrics&lt;/code&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;현재는 Micrometer나 Spring Actuator에 바로 연결된 상태는 아니다. 하지만 runtime 내부에서 어떤 값을 측정해야 하는지 먼저 정해두었다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;active sessions&lt;/li&gt;
&lt;li&gt;active memberships&lt;/li&gt;
&lt;li&gt;inbound frames&lt;/li&gt;
&lt;li&gt;outbound frames&lt;/li&gt;
&lt;li&gt;dropped outbound messages&lt;/li&gt;
&lt;li&gt;disconnects by policy&lt;/li&gt;
&lt;li&gt;heartbeat timeouts&lt;/li&gt;
&lt;li&gt;channel resolution latency&lt;/li&gt;
&lt;li&gt;handler latency&lt;/li&gt;
&lt;li&gt;max outbound queue depth&lt;/li&gt;
&lt;li&gt;max broadcast fan-out&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 Spring Boot integration을 더 만들면 이 값들을 Micrometer metric으로 노출할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Chat Presence 예제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상적인 설명만 있으면 감이 잘 오지 않는다. 그래서 &lt;code&gt;kanal-samples:chat-presence&lt;/code&gt; 예제를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제는 아직 실제 WebSocket 서버를 열지는 않는다. 대신 Spring Boot application 안에서 Kanal DSL로 channel을 정의한다.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 &lt;code&gt;chat/{roomId}&lt;/code&gt; channel에 들어온다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onJoin&lt;/code&gt;에서 presence를 기록한다.&lt;/li&gt;
&lt;li&gt;들어온 사용자에게 welcome message를 보낸다.&lt;/li&gt;
&lt;li&gt;방 전체에 presence 변경 이벤트를 broadcast한다.&lt;/li&gt;
&lt;li&gt;message가 오면 같은 방에 broadcast한다.&lt;/li&gt;
&lt;li&gt;typing signal은 별도 channel에서 더 가벼운 이벤트로 다룬다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 일부는 이렇다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;channel&amp;lt;ChatMessage&amp;gt;(&quot;chat/{roomId}&quot;) {
    description(&quot;Room chat messages with presence-aware join and leave hooks&quot;)
    backpressure(BackpressurePolicy.DROP_OLDEST)

    onJoin {
        val roomId = address.parameters.getValue(&quot;roomId&quot;)
        val memberKey = session.userId ?: session.id

        presence.track(
            key = memberKey,
            metadata = session.presenceMetadata(roomId),
        )

        send(
            ChatSystemNotice(
                roomId = roomId,
                message = &quot;Welcome to $roomId.&quot;,
            ),
        )
    }

    onMessage { message -&amp;gt;
        val roomId = address.parameters.getValue(&quot;roomId&quot;)

        broadcast(
            message.copy(
                roomId = roomId,
                authorId = message.authorId.ifBlank { session.userId ?: session.id },
            ),
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 runtime이 없으니 이 코드는 실제 client와 통신하지는 않는다. 하지만 앞으로 runtime이 실행해야 할 application code의 기준점이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금 하지 않기로 한 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kanal이 실시간 프레임워크라고 해서 처음부터 모든 것을 하려는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/li&gt;
&lt;li&gt;전체 클러스터에서 강한 일관성 보장&lt;/li&gt;
&lt;li&gt;연결 자동 이동&lt;/li&gt;
&lt;li&gt;actor runtime 만들기&lt;/li&gt;
&lt;li&gt;Kafka나 Redis 같은 message broker 대체하기&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;/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;지금 가장 중요한 목표는 작지만 제대로 동작하는 single-node runtime이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 한 서버 안에서 다음이 잘 되어야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebSocket 연결 받기&lt;/li&gt;
&lt;li&gt;channel path 해석하기&lt;/li&gt;
&lt;li&gt;join, leave, message 처리하기&lt;/li&gt;
&lt;li&gt;presence 관리하기&lt;/li&gt;
&lt;li&gt;bounded queue로 느린 client 다루기&lt;/li&gt;
&lt;li&gt;metrics로 현재 상태 설명하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 잘 되어야 그 다음에 cluster 이야기를 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 단계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기초 모델은 어느 정도 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 실제 WebSocket runtime을 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 일은 대략 이런 순서다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;WebSocket endpoint 만들기&lt;/li&gt;
&lt;li&gt;client와 주고받을 JSON frame 형식 정하기&lt;/li&gt;
&lt;li&gt;join, leave, message frame 처리하기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RealtimeApplication.resolve(path)&lt;/code&gt;로 channel 찾기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ChannelContext&lt;/code&gt;를 만들어 handler 실행하기&lt;/li&gt;
&lt;li&gt;session별 outbound queue 붙이기&lt;/li&gt;
&lt;li&gt;heartbeat 처리하기&lt;/li&gt;
&lt;li&gt;graceful shutdown 처리하기&lt;/li&gt;
&lt;li&gt;metrics를 Spring Boot에서 볼 수 있게 연결하기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계가 끝나면 README에 있는 DSL 예제가 실제 client와 통신할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때부터 Kanal은 &quot;아이디어가 담긴 skeleton&quot;에서 &quot;실제로 써볼 수 있는 local realtime framework&quot;로 넘어간다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kanal은 아직 초기 프로젝트다.&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;JVM 서버 개발자가 실시간 기능을 만들 때 매번 WebSocket handler 주변에 session map, room map, presence map, broadcast loop를 직접 조립하지 않아도 되는 것. 실시간 기능을 socket 코드가 아니라 application model로 다룰 수 있게 하는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 시작점으로 &lt;code&gt;channel&lt;/code&gt;, &lt;code&gt;presence&lt;/code&gt;, &lt;code&gt;membership&lt;/code&gt;, &lt;code&gt;backpressure&lt;/code&gt;, &lt;code&gt;metrics&lt;/code&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;좋은 프레임워크는 보통 멋진 transport 코드보다 좋은 모델에서 시작한다고 생각한다. Kanal은 지금 그 모델을 다지는 중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/not-a-platform-bug/kanal&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/not-a-platform-bug/kanal&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778993872098&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;GitHub - not-a-platform-bug/kanal: Kanal is a Jvm realtime framework (alike Phoenix Channels's JVM/Kotlin Version)&quot; data-og-description=&quot;Kanal is a Jvm realtime framework (alike Phoenix Channels's JVM/Kotlin Version) - not-a-platform-bug/kanal&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/not-a-platform-bug/kanal&quot; data-og-url=&quot;https://github.com/not-a-platform-bug/kanal&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lOk1P/dJMb9iaWo2l/8dKYXdRK6vk3kOc71GzZJk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cvUhV3/dJMb9hC6usV/KzDxvLkxnTfmhxS4uE3oAk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/not-a-platform-bug/kanal&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/not-a-platform-bug/kanal&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lOk1P/dJMb9iaWo2l/8dKYXdRK6vk3kOc71GzZJk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cvUhV3/dJMb9hC6usV/KzDxvLkxnTfmhxS4uE3oAk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - not-a-platform-bug/kanal: Kanal is a Jvm realtime framework (alike Phoenix Channels's JVM/Kotlin Version)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Kanal is a Jvm realtime framework (alike Phoenix Channels's JVM/Kotlin Version) - not-a-platform-bug/kanal&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;</description>
      <author>ohksj77</author>
      <guid isPermaLink="true">https://ohksj77.tistory.com/285</guid>
      <comments>https://ohksj77.tistory.com/285#entry285comment</comments>
      <pubDate>Sun, 17 May 2026 13:29:33 +0900</pubDate>
    </item>
    <item>
      <title>JVM/Spring 서버에서 JSON 직렬화를 profile-guided fast path로 최적화해보기</title>
      <link>https://ohksj77.tistory.com/284</link>
      <description>&lt;h1&gt;Spring 서버에서 JSON 처리 속도를 높일 수 있을까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring으로 API 서버를 만들다 보면 대부분의 요청과 응답은 JSON이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 서버 성능을 이야기하면 DB 쿼리, Redis, 외부 API 호출, 네트워크를 먼저 본다. 맞다. 실제로 많은 서비스에서 병목은 JSON이 아니라 DB 쪽이다.&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;예를 들어 이런 서버를 생각해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB는 이미 빠르다.&lt;/li&gt;
&lt;li&gt;캐시 hit 비율이 높다.&lt;/li&gt;
&lt;li&gt;BFF 서버처럼 여러 응답을 조합해서 JSON으로 내려준다.&lt;/li&gt;
&lt;li&gt;목록 API 응답이 크다.&lt;/li&gt;
&lt;li&gt;gateway처럼 요청/응답 JSON을 많이 만진다.&lt;/li&gt;
&lt;li&gt;QPS가 높다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에는 JSON을 읽고 쓰는 비용도 꽤 커질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 &amp;ldquo;Spring 서버에서 JSON 처리를 더 빠르게 할 수 있을까?&amp;rdquo;라는 질문에서 시작한 실험 기록이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보통 Spring은 어떻게 JSON을 처리할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 보통 REST API를 만들면 이런 코드를 쓴다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@PostMapping(&quot;/orders&quot;)
public OrderResponse createOrder(@RequestBody OrderRequest request) {
    return orderService.create(request);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자는 &lt;code&gt;@RequestBody&lt;/code&gt;와 return DTO만 신경 쓰면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 내부에서는 대략 이런 일이 일어난다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;요청 JSON bytes
-&amp;gt; Jackson이 읽음
-&amp;gt; OrderRequest 객체 생성

OrderResponse 객체
-&amp;gt; Jackson이 JSON으로 씀
-&amp;gt; HTTP 응답으로 전송&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;Spring Boot는 기본적으로 Jackson을 잘 통합해준다. Jackson은 매우 강력하고 안정적인 JSON 라이브러리다. 대부분의 서비스에서는 이 기본값으로 충분하다.&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;하지만 Jackson은 범용 라이브러리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 어떤 JSON이 들어올지 모르는 상황을 항상 처리할 준비를 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;field 순서가 달라도 처리해야 한다.&lt;/li&gt;
&lt;li&gt;모르는 field가 있어도 처리해야 한다.&lt;/li&gt;
&lt;li&gt;다양한 타입을 처리해야 한다.&lt;/li&gt;
&lt;li&gt;reflection, annotation, 설정 등을 고려해야 한다.&lt;/li&gt;
&lt;li&gt;복잡한 객체 구조도 처리해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 범용성이 Jackson의 장점이다. 하지만 hot path에서는 비용이 될 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 API JSON은 생각보다 단순하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실서비스의 API payload를 보면, 많은 경우 JSON 구조가 꽤 안정적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;/checkout&lt;/code&gt; 요청이 항상 이런 모양이라고 해보자.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;userId&quot;: 492001,
  &quot;items&quot;: [
    {
      &quot;sku&quot;: &quot;SKU-COFFEE-1KG&quot;,
      &quot;quantity&quot;: 2,
      &quot;unitPriceCents&quot;: 18900
    }
  ],
  &quot;shippingAddress&quot;: {
    &quot;country&quot;: &quot;KR&quot;,
    &quot;city&quot;: &quot;Seoul&quot;,
    &quot;line1&quot;: &quot;Teheran-ro 427&quot;,
    &quot;line2&quot;: &quot;15F&quot;,
    &quot;postalCode&quot;: &quot;06159&quot;
  },
  &quot;couponCode&quot;: &quot;SPRING-ORDER-10&quot;,
  &quot;gift&quot;: false,
  &quot;clientTraceId&quot;: &quot;ios-...&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 API는 대부분 비슷한 field를 비슷한 순서로 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 매번 범용 JSON 처리 비용을 모두 지불할 필요가 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실험의 아이디어는 여기서 시작한다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;자주 들어오는 JSON 모양을 관찰한다.
그 모양이 충분히 안정적이면 빠른 전용 처리 코드를 쓴다.
예상과 다른 JSON이면 기존 Jackson으로 fallback한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 이름: json-fastlane&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실험 프로젝트의 이름은 &lt;code&gt;json-fastlane&lt;/code&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 Jackson을 대체하려는 목적은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표는 이쪽에 가깝다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;일반적인 경우: 빠른 전용 경로 사용
예외적인 경우: Jackson으로 안전하게 fallback&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Jackson을 버리는 게 아니라 Jackson 옆에 &amp;ldquo;빠른 길&amp;rdquo;을 하나 더 만드는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;첫 번째: JSON 모양을 관찰하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 API별 JSON 모양을 기록하는 작은 profiler를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 정보를 기록한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;endpoint&lt;/li&gt;
&lt;li&gt;JSON 크기&lt;/li&gt;
&lt;li&gt;최상위 field 목록&lt;/li&gt;
&lt;li&gt;field 순서&lt;/li&gt;
&lt;li&gt;field 타입&lt;/li&gt;
&lt;li&gt;sample 수&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;code&gt;/checkout&lt;/code&gt; 요청을 많이 받으면 이런 식으로 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;/checkout samples=42000 avgBytes=479

commonOrder:
userId,items,shippingAddress,couponCode,gift,clientTraceId

field=userId kinds={NUMBER=42000}
field=items kinds={ARRAY=42000}
field=shippingAddress kinds={OBJECT=42000}
field=couponCode kinds={STRING=42000}
field=gift kinds={BOOLEAN=42000}
field=clientTraceId kinds={STRING=42000}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 질문은 하나다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 API의 JSON 모양이 충분히 안정적인가?&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&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;h2 data-ke-size=&quot;size26&quot;&gt;두 번째: 응답 JSON을 더 빠르게 쓰기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 응답은 이렇게 처리된다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;OrderResponse 객체
-&amp;gt; Jackson
-&amp;gt; JSON byte[]
-&amp;gt; HTTP 응답&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 실험용으로 이런 전용 writer를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;OrderResponse 객체
-&amp;gt; 직접 UTF-8 JSON으로 쓰기
-&amp;gt; HTTP 응답&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 field 이름은 매번 문자열로 처리하지 않고 미리 byte로 들고 있는다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;private static final byte[] ORDER_ID = &quot;{\&quot;orderId\&quot;:&quot;.getBytes(US_ASCII);
private static final byte[] STATUS = &quot;,\&quot;status\&quot;:&quot;.getBytes(US_ASCII);&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;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;out.writeRaw(ORDER_ID).writeLong(value.orderId());
out.writeRaw(STATUS).writeString(value.status());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson처럼 annotation, reflection, 설정을 매번 확인하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 이 DTO의 구조를 알고 있다고 가정하고 바로 JSON을 쓰는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세 번째: 매번 byte[]를 만들지 않기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 writer가 &lt;code&gt;byte[]&lt;/code&gt;를 반환했다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;byte[] json = writer.write(response);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 서버에서는 꼭 &lt;code&gt;byte[]&lt;/code&gt;를 새로 만들 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 좋은 방향은 reusable buffer를 쓰는 것이다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;DTO
-&amp;gt; 재사용 가능한 buffer에 JSON 쓰기
-&amp;gt; response OutputStream으로 전송&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이런 인터페이스를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface FastJsonBufferWriter&amp;lt;T&amp;gt; {
    void write(T value, Utf8JsonBuffer out);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 매 요청마다 큰 byte array를 새로 만드는 비용을 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;네 번째: Spring에 붙이기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC에서는 JSON 변환을 &lt;code&gt;HttpMessageConverter&lt;/code&gt;가 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 generated writer를 Spring converter에 연결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조는 이렇다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;Spring Controller return DTO
-&amp;gt; FastJsonHttpMessageConverter
-&amp;gt; 등록된 writer가 있는지 확인
-&amp;gt; 있으면 빠른 writer 사용
-&amp;gt; 없으면 기존 Jackson 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 식으로 writer를 등록한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;FastJsonWriterRegistry registry = new FastJsonWriterRegistry();

registry.register(
    OrderSummaryResponse.class,
    new OrderSummaryResponseWriter()
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 fallback이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록된 writer가 있는 타입만 빠른 경로를 타고, 나머지는 기존 Jackson을 그대로 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 위험하게 전체 JSON 처리를 한 번에 바꾸지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다섯 번째: 요청 JSON도 빠르게 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답뿐 아니라 요청도 실험했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 요청 JSON은 Jackson이 읽어서 DTO를 만든다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;JSON bytes -&amp;gt; Jackson -&amp;gt; Request DTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 자주 들어오는 모양에 대해서는 전용 reader를 쓸 수 있다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;JSON bytes -&amp;gt; 전용 reader -&amp;gt; Request DTO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전용 reader는 field 이름을 직접 비교한다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;&quot;{\&quot;userId\&quot;:&quot; 맞는지 확인
숫자 읽기
&quot;,\&quot;items\&quot;:[&quot; 맞는지 확인
배열 읽기
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 범용 JSON parser라기보다, 특정 API 요청에 맞춘 작은 reader다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그런데 JSON 순서가 바뀌면?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 JSON field 순서는 바뀔 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 원래는 이런 순서였는데:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;userId&quot;: 1,
  &quot;items&quot;: [],
  &quot;shippingAddress&quot;: {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 클라이언트가 이렇게 보낼 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;shippingAddress&quot;: {},
  &quot;items&quot;: [],
  &quot;userId&quot;: 1
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 JSON은 잘못된 JSON이 아니다. 단지 우리가 예상한 빠른 경로와 순서가 다를 뿐이다.&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;처음에는 이런 경우 fast reader가 실패하고 exception을 던진 뒤 Jackson으로 fallback했다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;fast reader 시도
실패
exception 발생
Jackson fallback&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이건 좋지 않다. 정상적인 JSON인데 exception을 쓰는 것은 비싸다.&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 class=&quot;routeros&quot;&gt;&lt;code&gt;public interface TryFastJsonReader&amp;lt;T&amp;gt; {
    T tryRead(byte[] json);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 reader는 처리할 수 있으면 DTO를 반환하고, 처리할 수 없으면 &lt;code&gt;null&lt;/code&gt;을 반환한다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;tryRead(json) -&amp;gt; 성공하면 DTO
tryRead(json) -&amp;gt; 처리할 수 없으면 null
null이면 Jackson fallback&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 field 순서가 다른 JSON도 exception 없이 자연스럽게 fallback할 수 있다.&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 class=&quot;angelscript&quot;&gt;&lt;code&gt;exception fallback 방식        904,318 ops/s
tryRead fallback 방식        2,039,685 ops/s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 fallback은 100% 발생했지만, exception을 없앤 것만으로 2배 이상 빨라졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능은 얼마나 나왔나?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 부하 시뮬레이션을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교 대상은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring 기본 Jackson converter&lt;/li&gt;
&lt;li&gt;직접 만든 generated reader&lt;/li&gt;
&lt;li&gt;직접 만든 generated writer&lt;/li&gt;
&lt;li&gt;Spring에 붙인 fast converter&lt;/li&gt;
&lt;li&gt;reusable buffer writer&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 실행에서 나온 결과는 대략 이렇다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요청 읽기&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Spring 기본 read       1,382,506 ops/s
Fast generated read    2,114,481 ops/s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 &lt;b&gt;1.53배&lt;/b&gt; 빨랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;응답 쓰기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 기본 writer와 실제 Spring converter 경로를 비교하면:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Spring 기본 write              1,659,146 ops/s
Fast dedicated converter       2,022,223 ops/s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 &lt;b&gt;1.22배&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;그런데 Spring converter 경로를 제외하고, writer 자체만 보면 차이는 더 크다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Jackson writeValueAsBytes      1,609,938 ops/s
Fast generated writer          3,815,398 ops/s
Reusable buffer writer         4,097,266 ops/s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 writer 자체는 &lt;b&gt;2배 이상 빠르지만&lt;/b&gt;, 실제 Spring 경로에 붙이면 Spring abstraction 비용도 같이 들어가서 이득이 줄어든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 중요하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리 내부 benchmark와 실제 서버 integration benchmark는 다르다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&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;h2 data-ke-size=&quot;size26&quot;&gt;JMH도 추가했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부하 시뮬레이션은 현실적인 비교에는 좋지만, JVM 최적화나 스케줄링에 따라 숫자가 흔들릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 JMH benchmark도 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 JMH 결과는 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Jackson byte[] writer         5,907,167 ops/s
Fast byte[] writer            9,549,247 ops/s
Fast reusable buffer writer  17,131,683 ops/s
Netty ByteBuf writer         15,717,725 ops/s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서도 reusable buffer 방향이 꽤 좋게 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Netty ByteBuf도 실험했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC의 &lt;code&gt;OutputStream&lt;/code&gt;보다 더 zero-copy에 가까운 방향은 Netty 쪽이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Netty &lt;code&gt;ByteBuf&lt;/code&gt;에 직접 JSON을 쓰는 실험도 추가했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface FastJsonByteBufWriter&amp;lt;T&amp;gt; {
    void write(T value, ByteBuf out);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 WebFlux codec까지 만든 것은 아니지만, pooled buffer로 가기 위한 첫 구조는 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트는 이제 멀티모듈로 나누었다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;json-fastlane-core
  JSON profiler, reader/writer contract, buffer

json-fastlane-spring
  Spring MVC converter, Jackson fallback

json-fastlane-netty
  Netty ByteBuf writer

json-fastlane-benchmarks
  부하 테스트, JFR, JMH&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 나눈 이유는 간단하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;core&lt;/code&gt;는 Spring, Jackson, Netty를 몰라야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래야 나중에 Spring 없이도 쓸 수 있고, Netty 없이도 쓸 수 있고, 필요한 모듈만 가져다 쓸 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금까지 배운 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실험에서 얻은 가장 큰 교훈은 이렇다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Jackson은 느린 라이브러리가 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson은 이미 충분히 빠르고 안정적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 모든 상황을 처리하는 범용 라이브러리이기 때문에, 특정 API의 hot path에서는 전용 코드가 더 빠를 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 전체를 바꾸면 위험하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 라이브러리를 통째로 교체하는 것은 위험하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 특정 DTO, 특정 endpoint에만 fast path를 적용하고 fallback을 두는 방식이 현실적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 응답 쓰기는 최적화 여지가 크다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 response JSON은 서버가 구조를 알고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 generated writer가 잘 맞는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 요청 읽기는 fallback 설계가 중요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;field 순서가 바뀌어도 정상 JSON이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 exception으로 fallback하지 말고, &lt;code&gt;tryRead -&amp;gt; null -&amp;gt; fallback&lt;/code&gt; 구조가 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. byte[]보다 reusable buffer가 좋다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 새 &lt;code&gt;byte[]&lt;/code&gt;를 만드는 것보다, 재사용 가능한 buffer나 Netty &lt;code&gt;ByteBuf&lt;/code&gt;에 직접 쓰는 방향이 더 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Spring에 붙이면 이득이 줄어든다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순수 writer는 2배 이상 빨라도, Spring converter 경로에 붙이면 Spring 자체의 비용이 섞인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &amp;ldquo;라이브러리 benchmark&amp;rdquo;와 &amp;ldquo;Spring integration benchmark&amp;rdquo;를 둘 다 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앞으로 할 일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 제일 중요한 것은 code generation이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 reader/writer를 손으로 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 라이브러리라면 이런 흐름이어야 한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;API payload shape 관찰
DTO 구조 확인
reader/writer 코드 생성
Spring registry에 자동 등록
fallback rate 측정
성능 리포트 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음은 Spring Boot starter다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;implementation(&quot;io.jsonfastlane:json-fastlane-spring-boot-starter&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 붙이면 자동으로 profiler와 converter가 등록되는 형태가 되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 뒤에는 WebFlux codec을 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netty &lt;code&gt;ByteBuf&lt;/code&gt; writer가 이미 있으니, 다음에는 WebFlux에서 pooled buffer에 직접 JSON을 쓰는 쪽으로 갈 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실험은 &amp;ldquo;Jackson을 버리자&amp;rdquo;는 이야기가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 반대에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson은 계속 안전한 fallback으로 두고, 자주 호출되는 안정적인 API에 대해서만 빠른 길을 추가하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;일반 요청 -&amp;gt; Jackson
자주 나오는 안정적인 요청/응답 -&amp;gt; generated fast path
예상과 다른 JSON -&amp;gt; Jackson fallback&lt;/code&gt;&lt;/pre&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;Spring 서버에서 JSON 성능이 실제로 문제가 되는 상황이라면, 전체 JSON 라이브러리를 갈아엎기보다 이런 식의 &lt;b&gt;부분적이고 관측 기반인 fast path&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;a href=&quot;https://github.com/not-a-platform-bug/json-fastlane&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/not-a-platform-bug/json-fastlane&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778993891448&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;GitHub - not-a-platform-bug/json-fastlane: json-fastlane is a JVM experiment for profile-guided JSON serialization&quot; data-og-description=&quot;json-fastlane is a JVM experiment for profile-guided JSON serialization - not-a-platform-bug/json-fastlane&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/not-a-platform-bug/json-fastlane&quot; data-og-url=&quot;https://github.com/not-a-platform-bug/json-fastlane&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lONpA/dJMb9g5gmQd/Y1qY79zn4M3KPKN0Orx4V1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bTyjkp/dJMb9fZAANj/buZcbaKAGx0BxQK5l3xz20/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/not-a-platform-bug/json-fastlane&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/not-a-platform-bug/json-fastlane&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lONpA/dJMb9g5gmQd/Y1qY79zn4M3KPKN0Orx4V1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bTyjkp/dJMb9fZAANj/buZcbaKAGx0BxQK5l3xz20/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - not-a-platform-bug/json-fastlane: json-fastlane is a JVM experiment for profile-guided JSON serialization&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;json-fastlane is a JVM experiment for profile-guided JSON serialization - not-a-platform-bug/json-fastlane&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;</description>
      <author>ohksj77</author>
      <guid isPermaLink="true">https://ohksj77.tistory.com/284</guid>
      <comments>https://ohksj77.tistory.com/284#entry284comment</comments>
      <pubDate>Sun, 17 May 2026 13:26:29 +0900</pubDate>
    </item>
    <item>
      <title>DB 프로시저 로직 서버로 이전과 비즈니스 기여를 위한 고민</title>
      <link>https://ohksj77.tistory.com/283</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;정산 서버의 DB 프로시저 전환 작업이 진행 중이었다. 새로운 계기로 비즈니스 기여에 대해 더욱 고민해보기로 결정하였다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;이 과정에서의 노력들이 개인적으로 의미가 깊었기에 기록해두고자 글을 작성한다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 당시 업무 상황과 고민한 방향성&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;깊은 고민의 엔지니어링에서 희열을 느끼던 나로서는 개인적으로 새로운 변화와 챌린지였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최대 1000줄 가량의 총 8개의 DB 프로시저 전환 과제 중, 비즈니스 영향도가 가장 높은 3개 프로시저를 우선 대상으로 삼아 작업이 진행되고 있었고 그 중 2개는 구현이 거의 완료되어 테스트 단계, 나머지 1개는 아직 착수하지 않은 상태였다. 이 3개 프로시저 마무리까지를 내 개인적 목표를 갖고 비즈니스적 고민을 해보고자 했다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;남은 1개 프로시저를 새로 설계&amp;middot;구현하고, 기존 2개를 포함해 우선 목표로 설정된 3개 프로시저 전환 작업을 라이브 서버까지 반영하는 역할을 해야했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단순히 진행 중이던 작업을 마무리하는 것이 아니라, 왜 이 구조로 전환하고 있는지, 그리고 이 선택이 정산 마감과 운영 상황에서도 유효한지를 다시 점검하는 데 집중하고자 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 문제 정의와 구체 목표 설정&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 정산 로직은 최대 약 800줄 규모의 프로시저 3개로 구성되어 있었고, 각 프로시저 내부에서 최대 20회 가량 서로 다른 DBMS 간 DB to DB 통신이 이루어지고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 구조의 가장 큰 문제는 복잡성 자체보다 실패가 발생했을 때 대응 가능한 선택지가 거의 없다는 점이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에러가 자주 발생하는 구조는 아니었지만, 한 번 문제가 발생하면 재시도를 위해 평균 3시간 30분 이상을 기다려야 했고 사실상 재실행 외에는 뚜렷한 대응 방법이 없었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&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 style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 어떤 단계에서 실패했는지&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다시 시도할 수 있는 시점이 언제인지&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정산 마감 일정에 영향을 주는지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이는 단순한 구현상의 불편함이 아니라, 업무 전반의 신뢰도를 떨어뜨릴 수 있는 운영 리스크라고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. 설계 방향성 재점검&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구조를 그대로 옮기는 방식은 문제를 해결하지 못한다고 판단했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&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 style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로직을 서버 코드로 이전하여 테스트와 디버깅이 가능한 구조로 전환&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쿼리를 기능 단위로 분리&amp;middot;통합하여 책임을 명확히 함&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;장애 상황 시 신속히 원인 파악 가능하도록 재구성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특히 아직 착수하지 않았던 1개 프로시저는 기존 문제를 반영해 로직 흐름과 책임을 처음부터 다시 설계했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 통해 정산 로직이 한 번에 성공해야 하는 블랙박스가 아니라 단계별로 상태를 파악할 수 있는 구조가 되도록 하는 것을 목표로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. DB to DB 통신 제거와 분산 트랜잭션 판단&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 DB to DB 통신을 서버 간 API 호출로 전환하면서 다음과 같은 흐름을 반복하는 구조가 되었다.&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 style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;begin transaction &amp;rarr; DB 작업 &amp;rarr; 데이터 저장 API 호출 &amp;rarr; DB에 처리 상태 갱신 &amp;rarr; commit&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;API로 인해 서로 다른 트랜잭션에서 수행되는 DB 작업 때문에 분산 트랜잭션 이슈를 어떻게 다룰지에 대한 판단이 필요했다. 이 과정에서 다음과 같은 선택지를 검토했으나 선택하지 않았다.&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 style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아웃박스 패턴&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처리 상태 갱신 로직을 별도 API로 분리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; 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;try-catch 기반으로 실패 시 롤백 API를 호출&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하는 방식을 선택했고, 장애 발생 시 즉시 인지할 수 있도록 에러 메일을 추가했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;완벽한 해법이라기보다, 정산 배치 특성과 팀의 운영 맥락에서 가장 단순하고 통제 가능한 선택이라고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. 구현과 성능 검증&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; 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;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3개 프로시저 전환 작업을 모두 완료한 뒤, 라이브 서버에 반영된 상태에서 요구 처리량을 기준으로 성능을 측정했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그 결과 기존 3시간 32분 가량 소요되던 로직이 평균 1분 13초에 처리가 가능했고, 기존 프로시저 기반 구조 대비 약 174배의 성능 개선을 확인했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트 환경이 아닌 실제 운영 환경에서 측정된 결과였기 때문에, 정산 배치 운영 시 처리 시간에 대한 예측 가능성을 높이는 데 의미가 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;성능 개선의 핵심은 복잡한 프로시저 내부 흐름을 단순화하고 일부 쿼리 개선과 불필요한 DB 간 통신을 제거한 구조적 변화였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. 운영 관점 확장과 가시성 개선&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구현 이후에는 &amp;ldquo;배포된 뒤, 개발자와 정산 담당자가 어떤 정보를 기준으로 상황을 판단할 수 있을까&amp;rdquo;를 기준으로 추가 작업을 진행했다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;API Latency 가시화&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 API 로그에는 소요 시간이 기록되지 않아 병목 지점을 파악하기 어려웠다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 API에 duration(ms) 로그를 추가하고, Kibana에서 직접 Latency 대시보드를 구성했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설정 방법과 활용 방식을 매뉴얼로 정리해 공유했고, 평균 10초 이상 소요되는 API 목록을 팀 내에서 확인할 수 있는 환경을 만들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;API Latency 확인이 가능해졌기에 앞으로 서버 병목을 파악하고 개선 대상을 파악하는데 도움이 될 것이라 기대한다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에러 메일 재분류&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전체 에러 메일 중 약 25.3%를 차지하지만 실질적으로 대응이 필요하지 않은 예외들이 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 케이스를 재분류하여 불필요한 메일 발송을 줄였고, 정산 운영 중 중요한 신호가 묻히지 않도록 개선했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스케줄러 및 배포 안정성&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&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 style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스프링 스케줄러 기본 스레드풀 설정으로 인해 작업이 밀릴 수 있음을 파악했다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스케줄러 스레드풀 설정법과 비동기 방식 등 여러 가능한 해결 방법과 함께 정리해 팀 내에 공유했다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버 재배포 시 실행 중이던 스케줄러&amp;middot;API 스레드가 강제 종료되는 문제를 확인하였다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단순히 스프링부트에 Graceful Shutdown 설정만 하지 않고 배포 스크립트의 systemctl restart 로 인한 SIGTERM 타임아웃 설정 값을 확인해 문제 없음을 검증한 후 설정해 장애 반복을 방지했다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. 공유와 커뮤니케이션&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;작업 결과는 컨플루언스 문서로 정리해 공유했고, 팀 주간 회의에서 데이터 기반으로 설명하고자 노력했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단순히 결과만 전달하기보다, 정산 운영 관점에서 왜 이런 선택을 했는지에 대한 문맥을 남기는 것을 중요하게 생각했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. 팀 문화에 대한 작은 시도&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;팀의 코드 리뷰 문화가 더 건설적으로 발전할 수 있는 여지가 있다고 느꼈다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;점심 시간에 남은 팀 예산에 대한 이야기가 나오던 중, 코드 리뷰에 대한 공통된 기준을 맞추는 데 도움이 될 것이라 생각해 &amp;ldquo;Looks Good To Me&amp;rdquo; 라는 코드 리뷰 관련 서적을 구매하자는 의견을 제안했고, 팀에서 이를 반영해주었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;강요가 아닌 팀 맥락에 맞는 제안을 통해, 작은 개선을 시도하고자 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;9. 회고&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;한 달간의 작업을 통해 이전된 정산 로직은 &amp;ldquo;한 번에 성공해야 하는 작업&amp;rdquo;이 아니라, 상황을 설명하고 판단할 수 있는 구조로 한 단계 개선되었다고 생각한다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞으로도 기능 구현마다 실패했을 때 어떤 정보를 제공할 수 있는지, 운영 중 어떤 기준으로 의사결정을 도울 수 있는지를 함께 고민하는 개발자가 되고자 한다.&lt;/span&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; 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;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff;&quot; data-ke-size=&quot;size16&quot;&gt;개발만 잘하는 개발자가 아니라 일을 잘하는 개발자가 되는 방향에 대한 조그마한 힌트를 얻은 기분이다.&lt;/p&gt;</description>
      <category>프로젝트-탐구/DB 프로시저 로직 서버로 이전</category>
      <author>ohksj77</author>
      <guid isPermaLink="true">https://ohksj77.tistory.com/283</guid>
      <comments>https://ohksj77.tistory.com/283#entry283comment</comments>
      <pubDate>Fri, 23 Jan 2026 22:30:34 +0900</pubDate>
    </item>
    <item>
      <title>Java 가상 스레드 (Virtual Thread) 에 관한 오해 짚고 넘어가기</title>
      <link>https://ohksj77.tistory.com/282</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왜&amp;nbsp;virtual&amp;nbsp;thread를&amp;nbsp;many-to-many&amp;nbsp;모델이라고&amp;nbsp;부르는가?&lt;/li&gt;
&lt;li&gt;ForkJoinPool이&amp;nbsp;내부적으로&amp;nbsp;어떻게&amp;nbsp;활용되는가?&lt;/li&gt;
&lt;li&gt;왜&amp;nbsp;virtual&amp;nbsp;thread는&amp;nbsp;기존&amp;nbsp;플랫폼&amp;nbsp;스레드보다&amp;nbsp;메모리&amp;nbsp;덜&amp;nbsp;차지하는가?&lt;/li&gt;
&lt;li&gt;가상스레드를&amp;nbsp;어느정도로&amp;nbsp;생성했을&amp;nbsp;때&amp;nbsp;위험하며,&amp;nbsp;어떻게&amp;nbsp;방지할&amp;nbsp;수&amp;nbsp;있는가?&lt;/li&gt;
&lt;li&gt;virtual&amp;nbsp;thread&amp;nbsp;pool을&amp;nbsp;설정하지&amp;nbsp;않는&amp;nbsp;게&amp;nbsp;좋은&amp;nbsp;이유?&lt;/li&gt;
&lt;li&gt;가상스레드 활용 시 carrier thread(platform thread) 설정은 어떻게 할 것인가?&lt;/li&gt;
&lt;li&gt;기존&amp;nbsp;platform&amp;nbsp;thread는&amp;nbsp;왜&amp;nbsp;pool로&amp;nbsp;설정하였는가?&lt;/li&gt;
&lt;li&gt;virtual&amp;nbsp;thread가&amp;nbsp;생성&amp;nbsp;되어지는&amp;nbsp;위치?&lt;/li&gt;
&lt;li&gt;virtual&amp;nbsp;thread가&amp;nbsp;실행&amp;nbsp;되어지며&amp;nbsp;갖는&amp;nbsp;transaction,&amp;nbsp;context는&amp;nbsp;어떻게&amp;nbsp;저장&amp;nbsp;되어져서&amp;nbsp;다른&amp;nbsp;carrier&amp;nbsp;thread에서&amp;nbsp;실행&amp;nbsp;되어져도&amp;nbsp;데이터&amp;nbsp;일관성을&amp;nbsp;지킬&amp;nbsp;수&amp;nbsp;있는가?&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;virtual thread 와 spring 과의 연동 되어지는 내부 과정?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;실제로&amp;nbsp;spring&amp;nbsp;프레임워크와&amp;nbsp;연동&amp;nbsp;하였을&amp;nbsp;때&amp;nbsp;이슈가&amp;nbsp;없는가?&lt;/li&gt;
&lt;li&gt;기존&amp;nbsp;thread와&amp;nbsp;구조가&amp;nbsp;달라졌는데&amp;nbsp;Spring도&amp;nbsp;업데이트되었는지&amp;nbsp;혹은&amp;nbsp;반영을&amp;nbsp;위한&amp;nbsp;업데이트가&amp;nbsp;필요&amp;nbsp;없었는지?&lt;/li&gt;
&lt;li&gt;virtual&amp;nbsp;thread&amp;nbsp;사용시에&amp;nbsp;synchronized를&amp;nbsp;사용하면&amp;nbsp;이슈가&amp;nbsp;발생&amp;nbsp;하는&amp;nbsp;이유?&lt;/li&gt;
&lt;li&gt;다른&amp;nbsp;주요&amp;nbsp;라이브러리&amp;nbsp;및&amp;nbsp;프레임워크에서&amp;nbsp;이슈가&amp;nbsp;발생할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;예시는&amp;nbsp;어떤게&amp;nbsp;있나?&lt;/li&gt;
&lt;li&gt;virtual&amp;nbsp;thread&amp;nbsp;가&amp;nbsp;성능을&amp;nbsp;높이기&amp;nbsp;위한&amp;nbsp;기술인가?&lt;/li&gt;
&lt;li&gt;continuation이란&amp;nbsp;무엇이며&amp;nbsp;기존&amp;nbsp;플랫폼&amp;nbsp;스레드의&amp;nbsp;스택&amp;nbsp;구조와의&amp;nbsp;차이는&amp;nbsp;어떠한가?&lt;/li&gt;
&lt;li&gt;jdk24&amp;nbsp;부터는&amp;nbsp;pin&amp;nbsp;이슈가&amp;nbsp;어떻게&amp;nbsp;해결되었으며&amp;nbsp;완벽히&amp;nbsp;pin&amp;nbsp;이슈가&amp;nbsp;해결된&amp;nbsp;것인가?&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 왜 virtual thread를 many-to-many 모델이라고 부르는가?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수많은 virtual thread(사용자 수준)는 한정된 개수의 carrier thread(커널 수준)에 매핑된다. virtual&amp;nbsp;thread는&amp;nbsp;blocking&amp;nbsp;등으로&amp;nbsp;인해&amp;nbsp;멈추면,&amp;nbsp;해당&amp;nbsp;carrier&amp;nbsp;thread를&amp;nbsp;풀어서&amp;nbsp;다른&amp;nbsp;virtual&amp;nbsp;thread가&amp;nbsp;재빨리&amp;nbsp;실행될&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;이 과정에서 virtual thread가 필요에 따라 여러 carrier thread 위에 실행될 수 있다는 점, 그리고 각 carrier thread는 여러 virtual thread를 번갈아 mounting/unmounting하면서 실행한다는 점에서 many-to-many 모델로 분류된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. ForkJoinPool이 내부적으로 어떻게 활용되는가?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;핵심 역할&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: JVM은 가상스레드의 실행을 &lt;/span&gt;&lt;b&gt;스케줄링&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하기 위해 내부적으로 ForkJoinPool(일종의 work-stealing pool)을 사용한다. &lt;/span&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;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 풀은 carrier(플랫폼) 스레드의 집합을 관리하며, 각 carrier 스레드는 자신의 work-queue를 갖고 있고 virtual-thread의 runContinuation(실행 단위)을 큐로 푸시/팝해서 실행한다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉 ForkJoinPool은 carrier 스레드 pool 이자 스케줄러(work steal queue 기반) 역할.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2052&quot; data-start=&quot;1771&quot;&gt;&lt;b&gt;동작 과정&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2052&quot; data-start=&quot;1782&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1907&quot; data-start=&quot;1782&quot;&gt;virtual thread의 실제 실행은 runContinuation 같은 Runnable이 carrier의 work-queue에 들어가고, ForkJoinPool worker(=carrier) 가 이걸 가져가 실행.&lt;/li&gt;
&lt;li data-end=&quot;2005&quot; data-start=&quot;1910&quot;&gt;블로킹(park) 시에는 해당 virtual thread의 상태(continuation, stack frame 등)를 힙에 보관하고 carrier는 다른 작업을 수행.&lt;/li&gt;
&lt;li data-end=&quot;2052&quot; data-start=&quot;2008&quot;&gt;work-stealing 덕분에 carrier 사이에서 부하 균형을 맞춘다.&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 왜 virtual thread는 기존 플랫폼 스레드보다 메모리 덜 차지하는가?&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2742&quot; data-start=&quot;2400&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2535&quot; data-start=&quot;2400&quot;&gt;&lt;b&gt;OS 자원 미보유&lt;/b&gt;: 플랫폼 스레드는 OS 스레드당 고정된 네이티브 스택(수백 KB ~ 수 MB)을 갖는다. 가상스레드는 OS 네이티브 스택을 영구히 할당하지 않으므로(필요할 때만 carrier가 제공) 네이티브 스택 비용이 없다.&lt;/li&gt;
&lt;li data-end=&quot;2742&quot; data-start=&quot;2538&quot;&gt;&lt;b&gt;경량 객체화&lt;/b&gt;: VirtualThread는 JVM 내부 객체(힙에 할당되는 Thread 오브젝트 + continuation/광역 상태)로 관리되며, 전통적 플랫폼 스레드보다 per-thread 네이티브 오버헤드가 매우 작다. (따라서 수십만 ~ 백만 단위로도 메모리 사용량 급증이 덜함) &lt;span data-state=&quot;closed&quot;&gt;&lt;span data-testid=&quot;webpage-citation-pill&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 가상스레드를 어느정도로 생성했을 때 위험하며, 어떻게 방지할 수 있는가?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3124&quot; data-start=&quot;2914&quot;&gt;가상 스레드 생성을 제한하는 이유가 &lt;b&gt;메모리를 많이 차지하는 이유라면 잘못된 판단&lt;/b&gt;이라고 한다. 메모리 때문에 제한하기에는 수백만 개의 가상스레드를 무리 없이 생성하도록 만들어졌기 때문에 이슈가 생길 정도로 많이 생성되는 상황이 흔치 않을 것이다.&lt;/li&gt;
&lt;li data-end=&quot;3124&quot; data-start=&quot;2914&quot;&gt;주요 포인트는, 수 많은 가상 스레드가 제한 없이 만들어지면서 다음 경우가 발생하는 경우이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3124&quot; data-start=&quot;2914&quot;&gt;서버에서 외부로의 네트워크 요청이 급격히 늘어나는 경우&lt;/li&gt;
&lt;li data-end=&quot;3124&quot; data-start=&quot;2914&quot;&gt;DB/Cache/MQ 등의 미들웨어에 요청이 급격히 많아지는 경우&lt;/li&gt;
&lt;li data-end=&quot;3124&quot; data-start=&quot;2914&quot;&gt;서버 메모리 내의 공유 자원에 대한 접근이 많아지며 대기 시간이 길어지는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;위 문제들에 대해 &lt;b&gt;세마포어를 활용해 접근 가능한 스레드 개수를 제한&lt;/b&gt;하는 것을 권장한다고 한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;다만 다음 상황 발생 시 유의깊게 모니터링 필요
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3008&quot; data-start=&quot;2959&quot;&gt;jdk.VirtualThreadSubmitFailed 이벤트 발생(스케줄 실패).&lt;/li&gt;
&lt;li data-end=&quot;3067&quot; data-start=&quot;3011&quot;&gt;jdk.VirtualThreadPinned 이벤트가 잦음(pin 이슈로 carrier 고갈).&lt;/li&gt;
&lt;li data-end=&quot;3085&quot; data-start=&quot;3070&quot;&gt;GC 횟수/지연이 급증.&lt;/li&gt;
&lt;li data-end=&quot;3124&quot; data-start=&quot;3088&quot;&gt;시스템 전반(파일 디스크립터, DB 커넥션 등) 리소스 포화.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. virtual thread pool을 설정하지 않는 게 좋은 이유?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4029&quot; data-start=&quot;3664&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3811&quot; data-start=&quot;3664&quot;&gt;&lt;b&gt;기본 권장 패턴&lt;/b&gt;: 짧은 작업(블로킹 I/O 처리가 주된 요청)에는 newVirtualThreadPerTaskExecutor()(task-per-virtual-thread 방식)처럼 &lt;b&gt;매번 새 가상스레드 생성&lt;/b&gt;하는 게 단순하고 안전(구성 복잡성 낮음).&lt;/li&gt;
&lt;li data-end=&quot;4029&quot; data-start=&quot;3812&quot;&gt;이유:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4029&quot; data-start=&quot;3820&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3903&quot; data-start=&quot;3820&quot;&gt;virtual thread 자체가 저비용이므로 재사용/풀링으로 인한 복잡성(동기화, ThreadLocal 누수, 상태 공유 등)을 피할 수 있음. &lt;span data-state=&quot;closed&quot;&gt;&lt;span data-testid=&quot;webpage-citation-pill&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 가상스레드&amp;nbsp;활용&amp;nbsp;시&amp;nbsp;carrier&amp;nbsp;thread(platform&amp;nbsp;thread)&amp;nbsp;설정은&amp;nbsp;어떻게&amp;nbsp;할&amp;nbsp;것인가?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가상스레드의&amp;nbsp;carrier&amp;nbsp;thread&amp;nbsp;풀은&amp;nbsp;특수한&amp;nbsp;상황(리소스&amp;nbsp;제한/튜닝&amp;nbsp;목적)이&amp;nbsp;아니라면&amp;nbsp;직접&amp;nbsp;커스텀하지&amp;nbsp;않아도&amp;nbsp;되며,&amp;nbsp;JVM&amp;nbsp;default&amp;nbsp;설정에&amp;nbsp;맡길&amp;nbsp;것을&amp;nbsp;공식적으로&amp;nbsp;권장한다.&lt;/li&gt;
&lt;li&gt;carrier thread(플랫폼 스레드, 실제 OS 쓰레드) 수는 기본적으로 CPU 코어 수에 맞춰 자동으로 결정된다. 추가로 시스템 프로퍼티(jdk.virtualThreadScheduler.parallelism, jdk.virtualThreadScheduler.maxPoolSize)로 직접 설정 가능하다. 필요에 따라 컨테이너 환경(Kubernetes 등)에서는 visible CPU count나 관련 프로퍼티 옵션을 튜닝 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 기존 platform thread는 왜 pool로 설정하였는가?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4856&quot; data-start=&quot;4758&quot;&gt;전통적으로 OS 스레드는 생성&amp;middot;종료 비용이 크고, 시스템 리소스(네이티브 스택 등)를 사용하므로 &lt;b&gt;풀을 만들어 재사용&lt;/b&gt;하여 생성 비용과 컨텍스트 스위칭 비용을 줄여왔다.&lt;/li&gt;
&lt;li data-end=&quot;5001&quot; data-start=&quot;4857&quot;&gt;즉 플랫폼 스레드에 대한 풀은 전통적 방식(스레드 풀 패턴)의 자연스러운 결과 &amp;rarr; virtual thread 도입으로 풀 없이 스레드-per-task를 더 쉽게 쓸 수 있게 된 차이.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. virtual thread가 생성 되어지는 위치?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;virtual&amp;nbsp;thread의&amp;nbsp;생성과&amp;nbsp;관리는&amp;nbsp;JVM&amp;nbsp;내부에서&amp;nbsp;일어난다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;스레드 생성 API(Thread.ofVirtual().start 등)나 Executors.newVirtualThreadPerTaskExecutor()를 사용할 때, 이들은 힙 메모리에 스택 chunk를 두고, 실행이 필요할 때 JVM이 메인 carrier thread 풀(ForkJoinPool의 worker 등)에 virtual thread의 스택을 올려 실행시킨다.&lt;/li&gt;
&lt;li&gt;실제 OS에서는 carrier thread 만이 리소스를 점유한다. virtual thread 인스턴스의 스택은 필요 없을 때 힙에 저장되고, 실행될 때 carrier thread와 맞물려 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. virtual thread가 실행 되어지며 갖는 transaction, context는 어떻게 저장 되어져서 다른 carrier thread에서 실행 되어져도 데이터 일관성을 지킬 수 있는가?&lt;/h2&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 data-end=&quot;5726&quot; data-start=&quot;5523&quot;&gt;&lt;b&gt;ThreadLocal / InheritableThreadLocal&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5726&quot; data-start=&quot;5569&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5726&quot; data-start=&quot;5569&quot;&gt;가상스레드도 ThreadLocal을 지원한다. ThreadLocal 값은 virtual thread 객체 내부(힙)에서 유지되므로 &lt;b&gt;carrier가 바뀌어도 값은 유지&lt;/b&gt;된다(플랫폼 스레드와 달리 값이 carrier가 아닌 virtual thread 오브젝트에 속함).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;6089&quot; data-start=&quot;5727&quot;&gt;&lt;b&gt;Transaction / Context&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;6089&quot; data-start=&quot;5758&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5942&quot; data-start=&quot;5758&quot;&gt;트랜잭션 컨텍스트(예: JDBC/트랜잭션 스코프)는 보통 쓰레드-바운디드(resource per thread)로 구현된다. virtual thread는 thread-bound 컨텍스트를 유지하므로, 동일 virtual thread가 carrier 사이를 옮겨도 VM 레벨의 ThreadLocal 기반 컨텍스트는 계속 유지된다.&lt;/li&gt;
&lt;li data-end=&quot;6089&quot; data-start=&quot;5945&quot;&gt;다만 &lt;b&gt;외부 리소스(예: DB 커넥션 풀의 한정된 커넥션)&lt;/b&gt; 은 virtual thread가 무작정 늘어나면 고갈 가능 &amp;rarr; 트랜잭션 경계 관리는 semaphore 혹은 connection pool 으로 제어해야 함.&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-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.62em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;10. virtual thread 와 spring 과의 연동 되어지는 내부 과정?&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot 3.2 이상에선 spring.threads.virtual.enabled=true 같은 설정을 활용하면, Spring이 제공하는 내부 TaskExecutor(기존 ThreadPoolTaskExecutor 대신)를 virtual thread 기반 Executor로 자동 교체한다. 이는 Tomcat Executor, Servlet request, @Async, @Scheduled 작업 등 Spring이 관리하는 주요 비동기 작업에 다 적용된다.​&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;11. 실제로 spring 프레임워크와 연동 하였을 때 이슈가 없는가?&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;synchronized, native method 내 블로킹 등 특정 동기화/네이티브 호출, JNI 등은 virtual thread를 carrier thread에 pinned 상태로 만들어버린다. 이렇게 되면 carrier thread가 해제되지 않고, virtual thread의 장점이 사라진다.&lt;/li&gt;
&lt;li&gt;수많은 가상스레드가 제한 없이 생성되면 &lt;b&gt;배압조절 기능이 없다&lt;/b&gt;는 점을 유의해서 활용해야 한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 기존 thread와 구조가 달라졌는데 Spring도 업데이트되었는지 혹은 반영을 위한 업데이트가 필요 없었는지?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7725&quot; data-start=&quot;7582&quot;&gt;Spring은 &lt;b&gt;가상스레드를 고려한 업데이트&lt;/b&gt;를 일부 했음(문서&amp;middot;가이드&amp;middot;옵션). 하지만 &lt;b&gt;프레임워크가 대대적으로 내부 동작을 바꿀 필요는 적음&lt;/b&gt; &amp;mdash; virtual thread는 Thread API 유지 목표라 호환성이 좋음.&lt;/li&gt;
&lt;li data-end=&quot;8029&quot; data-start=&quot;7726&quot;&gt;다만 다음 영역에서 업데이트/주의 필요:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;8029&quot; data-start=&quot;7753&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7834&quot; data-start=&quot;7753&quot;&gt;&lt;b&gt;Embedded server integration&lt;/b&gt;: executor 주입(virtual thread executor 사용) 관련 구성.&lt;/li&gt;
&lt;li data-end=&quot;7901&quot; data-start=&quot;7837&quot;&gt;&lt;b&gt;Observability/metrics&lt;/b&gt;: 대량의 가상스레드에 적합한 모니터링/스레드 덤프 포맷 등 보완.&lt;/li&gt;
&lt;li data-end=&quot;8029&quot; data-start=&quot;7904&quot;&gt;&lt;b&gt;Spring Security / Transaction / AOP 등&lt;/b&gt;: ThreadLocal 기반 assumption이 있으면 패턴 점검 필요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;13. virtual thread 사용시에 synchronized를 사용하면 이슈가 발생 하는 이유?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;8332&quot; data-start=&quot;8096&quot;&gt;전통적으로 synchronized는 JVM 모니터를 사용하고, 모니터에 의해 스레드가 blocking 되면 &lt;b&gt;실제 플랫폼 스레드가 그 모니터를 획득한 상태&lt;/b&gt;로 남는다. 가상스레드가 synchronized 내부에서 blocking 되면 가상스레드가 &lt;b&gt;carrier에 pin&lt;/b&gt; 되어 carrier를 놓지 못하는 상황이 발생 &amp;rarr; carrier 풀 소진 &amp;rarr; 전체 가상스레드가 스케줄되지 못하는 문제.&lt;/li&gt;
&lt;li&gt;참고) JVM 옵션으로 pinning 감지 가능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;java&amp;nbsp;-Djdk.tracePinnedThreads=full&amp;nbsp;YourApp&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;14. jdk24 부터는 pin 이슈가 어떻게 해결되었으며 완벽히 pin 이슈가 해결된 것인가?&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;​&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-start=&quot;10511&quot; data-end=&quot;10779&quot;&gt;synchornized 에서의 pin 이슈는 해결되었다고 한다. synchronized로 인한 Object.wait() 시 blocking 될 때와 같이 carrier thread에서 unmount 되는 과정을 거치도록 수정되었다.&lt;/li&gt;
&lt;li data-start=&quot;10511&quot; data-end=&quot;10779&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;a href=&quot;https://openjdk.org/jeps/491&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JEP-491&lt;/a&gt; JDK24로 '거의 대부분'의 사례는 해결되었지만, 실제 운영 환경에서는 아직도 몇몇 특수 케이스(네이티브 라이브러리, 일부 에이전트, class-init/모니터 대기 복합 케이스)에서 pin-like 문제가 보고된다(버그 리포트 존재). 따라서&lt;span&gt; &lt;/span&gt;무조건 안전하다고 단정하긴 이르다. 실무에서는 JDK 버전&amp;middot;사용 라이브러리&amp;middot;에이전트 조합으로 철저한 테스트가 필요하다.&lt;/li&gt;
&lt;li data-start=&quot;10524&quot; data-end=&quot;10779&quot;&gt;jdk24에서 여전히 이슈가 발생한다는 보고:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8355036&quot;&gt;https://bugs.openjdk.org/browse/JDK-8355036&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;15. 주요 라이브러리 및 프레임워크에서 pin 이슈가 발생할 수 있는 예시는 어떤게 있나?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL Connector/J 8.0.32 이하
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오래된 JDBC 드라이버에 여러 사례가 있으며 최신 버전 업데이트 지원으로 해결되고 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HttpComponents 4.x: 5.x로 업그레이드 필요 / 참조: &lt;a href=&quot;https://blog.igooo.org/120&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.igooo.org/120&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;16. virtual thread 가 성능을 높이기 위한 기술인가?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;9394&quot; data-start=&quot;9278&quot;&gt;&lt;b&gt;목적은 throughput 과 단순성:&lt;/b&gt;&amp;nbsp;블로킹 I/O가 많은 서버에서 코드 변경(reactive 변환) 없이 동시성 처리량을 크게 늘리고 프로그래밍 모델을 단순화하는 게 목표이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;17. continuation이란 무엇이며 기존 플랫폼 스레드의 스택 구조와의 차이는 어떠한가?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;9854&quot; data-start=&quot;9671&quot;&gt;&lt;b&gt;Continuation&lt;/b&gt;: 실행의 재개 가능한 위치를 나타내는 개념(함수의 실행을 멈추고 저장한 상태). Project Loom의 구현에서는 virtual thread의 실행 스택(로컬 변수, 프레임 등)을 &lt;b&gt;힙 쪽의 continuation 형태로 저장/복원&lt;/b&gt;해서 &lt;b&gt;스택에 묶이지 않는 실행 단위&lt;/b&gt;를 만든다.&lt;/li&gt;
&lt;li data-end=&quot;9957&quot; data-start=&quot;9855&quot;&gt;기존 플랫폼 스레드:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;9957&quot; data-start=&quot;9871&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;9957&quot; data-start=&quot;9871&quot;&gt;네이티브 OS 스택에 모든 호출 스택과 로컬 변수가 저장되어 있고, 스레드가 blocking 되면 OS가 해당 스레드를 block 상태로 관리(스택은 네이티브에 고정).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;10199&quot; data-start=&quot;9958&quot;&gt;차이:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;10199&quot; data-start=&quot;9966&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;10090&quot; data-start=&quot;9966&quot;&gt;virtual thread는 blocking 시(park) &lt;b&gt;스택 내용을 JVM이 캡처(continuation)&lt;/b&gt; 하여 힙으로 옮기고 carrier를 해제 &amp;rarr; 다른 virtual thread에 같은 플랫폼 스레드를 할당 가능.&lt;/li&gt;
&lt;li data-end=&quot;10199&quot; data-start=&quot;10093&quot;&gt;결과적으로 스택이 네이티브에 고정되지 않으므로 수십만 스레드의 논리적 컨텍스트를 메모리-효율적으로 유지할 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>ohksj77</author>
      <guid isPermaLink="true">https://ohksj77.tistory.com/282</guid>
      <comments>https://ohksj77.tistory.com/282#entry282comment</comments>
      <pubDate>Sun, 2 Nov 2025 23:00:20 +0900</pubDate>
    </item>
    <item>
      <title>Java의 Structured Concurrency</title>
      <link>https://ohksj77.tistory.com/281</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결하고자 하는 기존 멀티스레드 방식의 문제점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;하나의 작업이 실패할 경우 자동으로 일괄 취소할 수 없음(각 수동 예외처리 필요)&lt;/li&gt;
&lt;li&gt;여러 작업 간의 명확한 관계가 표현되지 않음&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K3hKV/dJMcagcI5nL/DB1WInTYiiHZdWKklhwKiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K3hKV/dJMcagcI5nL/DB1WInTYiiHZdWKklhwKiK/img.png&quot; data-alt=&quot;어디로 튈지 모르며 일괄 취소가 어려운 기존의 멀티스레드 프로그래밍 방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K3hKV/dJMcagcI5nL/DB1WInTYiiHZdWKklhwKiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK3hKV%2FdJMcagcI5nL%2FDB1WInTYiiHZdWKklhwKiK%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;685&quot; height=&quot;302&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;어디로 튈지 모르며 일괄 취소가 어려운 기존의 멀티스레드 프로그래밍 방식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java Structured Concurrency 소개&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시성 작업을 부모-자식 관계로 구조화하여 작업 그룹을 하나의 단위로 관리하는 프로그래밍 패러다임
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오류 처리, 리소스 관리, 취소 기능을 단순화하여 안정적이고 예측 가능한 동시성 코드 작성을 목표로 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;여러 스레드 작업들이 작업 완료 시 모두 동일한 위치로 돌아온다는 특징&lt;/li&gt;
&lt;li&gt;Jdk25 기준으로 5th Preview 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장점&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;명확성: 일관된 패턴으로 멀티 스레드 코드 작성 가능&lt;/li&gt;
&lt;li&gt;예외 처리: 하나의 작업 실패로 다른 작업 취소 용이&lt;/li&gt;
&lt;li&gt;취소 전파: 상위 작업의 취소로 모든 하위 작업 취소 가능&lt;/li&gt;
&lt;li&gt;오류 원인 추론: 스레드 덤프에 더욱 명확히 작업의 계층구조 표시&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전체 구조&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;1162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KL9B3/dJMcajtKL6L/CFjP9Y6o9fTafpy2NhAns0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KL9B3/dJMcajtKL6L/CFjP9Y6o9fTafpy2NhAns0/img.png&quot; data-alt=&quot;Java25 기준 예제&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KL9B3/dJMcajtKL6L/CFjP9Y6o9fTafpy2NhAns0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKL9B3%2FdJMcajtKL6L%2FCFjP9Y6o9fTafpy2NhAns0%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;498&quot; height=&quot;445&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;1162&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Java25 기준 예제&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;scope 을 열어 내부에서 동시성 작업 수행&lt;/li&gt;
&lt;li&gt;scope.fork() 를 통해 비동기 작업 실행&lt;/li&gt;
&lt;li&gt;scope.join() 을 통해 원하는 방법으로 비동기 작업 일괄 처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;활용한 Joiner에 따라 달라지는 부분&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;978&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwzV0T/dJMcahioWtx/kxKjb09XWpbXmROXxOpHo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwzV0T/dJMcahioWtx/kxKjb09XWpbXmROXxOpHo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwzV0T/dJMcahioWtx/kxKjb09XWpbXmROXxOpHo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwzV0T%2FdJMcahioWtx%2FkxKjb09XWpbXmROXxOpHo1%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;511&quot; height=&quot;390&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;978&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;h3 data-ke-size=&quot;size23&quot;&gt;Joiner 종류와 그에 따른 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StructuredTaskScope.open() 의 인자로 원하는 Joiner를 넘겨줌&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;scope 내의 작업 처리 방식과 scope.join() 반환 타입을 정함&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;안넘겨주면 기본 값(awaitAlSuccessfulOrThrow)이 활용됨&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;span&gt;몇가지 Joiner 예시&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;b&gt;awaitAlSuccessfulOrThrow&lt;/b&gt;: 모든 scope 내의 작업이 성공하면 return, 작업 실패 시 나머지 작업 취소&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;alSuccessfulOrThrow&lt;/b&gt;: awaitAlSuccessfulOrThrow 와 동일하게 처리하지만 return 타입이 존재&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;anySuccessfulResultOrThrow&lt;/b&gt;: 하나의 scope 내의 작업이 성공하면 즉시 반환(나머지 작업 취소)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;awaitAll&lt;/b&gt;: 성공/실패에 관계없이 모든 작업이 완료한 이후 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Structured Concurrency 활용 방안&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;부모 scope 내에서 fork(생성) 한 작업 내에서 자식 scope를 생성하는 중첩된 구조일 때 다음 유즈케이스에 유용&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부모가 실패 시 자식의 작업까지 모두 중단&lt;/li&gt;
&lt;li&gt;자식의 작업 하나 실패 시 부모의 작업 모두 중단&lt;/li&gt;
&lt;li&gt;위 두 유즈케이스를 CompletableFuture로 처리하기는 꽤나 복잡할 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하나의 작업이 실패하면 다른 작업을 멈추어 리소스 낭비를 쉽게 방지&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특히 비동기 작업이 무거운 작업이라 중간에 일괄로 멈추면 도움이 되는 케이스에 유용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;여러 곳에서 데이터를 가져와야 하며 성능이 중요해 처음 가져온 데이터를 활용하는 케이스에 유용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 Joiner 중 anySuccessfulResultOrThrow 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복잡한 try-catch 문을 단순화하여 유지보수성을 높이고 싶은 경우에 유용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드가 간결해지며 여러 Joiner 처리 방식에도 일관된 스타일로 활용 가능&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;활용 시 유의할 점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업 취소 란 실행 중인 작업이 멈추는 것일 뿐, 이미 반영된 작업이 롤백되지는 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타 작업 실패로 작업 취소 시 Thread.inturrupt() 로 신호를 주기 때문에 Spring Data 의 트랜잭션 기본 설정으로는 자동 롤백이 되지 않을 것이다.&lt;/li&gt;
&lt;li&gt;트랜잭션이 아니더라도 heap 메모리에 어떤 값을 바꾼다고 하면 이미 처리된 작업이 롤백되지는 않는다.&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;p data-ke-size=&quot;size16&quot;&gt;참고한 레퍼런스, 이미지 출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://belief-driven-design.com/looking-at-java-21-structured-concurrency-39a81/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://belief-driven-design.com/looking-at-java-21-structured-concurrency-39a81/&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발-탐구</category>
      <author>ohksj77</author>
      <guid isPermaLink="true">https://ohksj77.tistory.com/281</guid>
      <comments>https://ohksj77.tistory.com/281#entry281comment</comments>
      <pubDate>Sat, 1 Nov 2025 15:06:20 +0900</pubDate>
    </item>
    <item>
      <title>Java 스레드의 발전 과정과 가상 스레드 (Virtual Thread)</title>
      <link>https://ohksj77.tistory.com/280</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;용어 정리&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;스레드 분류&lt;/h4&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;OS 커널에 의해 생성되고 관리되는 스레드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;OS 스레드, 네이티브 스레드 라고도 불림&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;커널 스레드를 프로그래밍 레벨에서 추상화한 스레드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;OS가 관리하는 커널스레드와 매핑되어 동작&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;h4 data-ke-size=&quot;size20&quot;&gt;스레드매핑모델&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yw20S/dJMcafES5kS/Cymd20hVEyKWCtfBgR89QK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yw20S/dJMcafES5kS/Cymd20hVEyKWCtfBgR89QK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yw20S/dJMcafES5kS/Cymd20hVEyKWCtfBgR89QK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyw20S%2FdJMcafES5kS%2FCymd20hVEyKWCtfBgR89QK%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;623&quot; height=&quot;279&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;596&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;b&gt;many-to-one model&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &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;one-to-one model&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &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;many-to-many model&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;다수의유저스레드를다수의커널스레드가처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Java 스레드의 발전 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Jdk 1.1&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;many-to-one model&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;CPU Core가 1개인 환경에서 설계된 방식, &lt;span&gt;그린 스레드&lt;/span&gt;라는 명칭의 유저 스레드 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;&lt;span&gt;단점&lt;/span&gt;: 커널스레드가 Blocking 되면 모든 유저 스레드가 작업을 하지 못한다.&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;Jdk 1.3&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;one-to-one model&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;OS스레드를Wrapping한&lt;span&gt;플랫폼스레드&lt;/span&gt;를활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;&lt;span&gt;단점&lt;/span&gt;: I/O 작업 시 Blocking 되며 Blocking 시 Context Switching 부담&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;플랫폼 스레드 I/O 블로킹을 고려한 대안&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Reactive Programming&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;여러 이벤트 루프를 활용한 Non-Blocking 모델을 활용, calback과 이벤트 기반 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;&lt;span&gt;단점&lt;/span&gt;: Blocking I/O 기반 라이브러리 활용의 어려움, 복잡한 코드 구조와 러닝커브&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;코루틴(Spring 생태계는Kotlin 한정)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;여러 루틴이 협력적으로 실행을 제어하는 패턴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;I/O 작업을 기다리는 동안 다른 작업을 처리할수 있어 CPU idle-time 최소화를 도움&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &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;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;해결하고 싶은 문제점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jdk1.3 부터 사용 가능한 플랫폼 스레드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;&lt;span&gt;단점&lt;/span&gt;: Blocking I/O 활용 -&amp;gt; 높은I/O 처리량 필요 시 부담&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactive Programming, 코루틴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;&lt;span&gt;단점&lt;/span&gt;: 특정적인 문법과 제약-&amp;gt;개발/유지보수 어려움&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가상 스레드 (Virtual Thread)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;714&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buQn7y/dJMcaacvBdM/cMktlUZIpnI0MmNlHwsOX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buQn7y/dJMcaacvBdM/cMktlUZIpnI0MmNlHwsOX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buQn7y/dJMcaacvBdM/cMktlUZIpnI0MmNlHwsOX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuQn7y%2FdJMcaacvBdM%2FcMktlUZIpnI0MmNlHwsOX1%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;297&quot; height=&quot;295&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;714&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;many-to-many model&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;Project Loom으로 시작된 경량 스레드 모델&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;Jdk21에 정식 feature로 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;[높은 I/O 처리량시부담, 개발+유지보수 어려움] 문제 해결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;스레드 모델 오버헤드 비교&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 84.4186%; height: 100px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Thread 1개 기준 (최댓값)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Thread&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Virtual Thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;메모리 사용량&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;~ 2MB&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&amp;nbsp;~ 50KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;생성 시간&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;~ 1ms&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&amp;nbsp;~ 10 &amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;컨텍스트 스위칭 시간&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;~ 100 &amp;mu;s&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&amp;nbsp;~ 10 &amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 스레드 생성 비용과 컨텍스트 스위칭 비용이 낮음&lt;br /&gt;-&amp;gt; OS가 아닌 JVM이 지원하기 때문&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. JVM 내 스레드 스케줄링을 통해 NonBlocking I/O 지원&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; Blocking 시 스레드가 대기하지 않고 작업 정보를 저장해두고 다른 작업을 처리하다 Blocking이 끝나면 재개&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;3. 기존 스레드를 상속하여 코드 호환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 기술에 한정된 문법이나 큰 수정 없이 사용 가능하다는 장점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전체 구조&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2090&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmbLzn/dJMb99Lrf1D/D6nyVwPQyuUWJv7HYjnGuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmbLzn/dJMb99Lrf1D/D6nyVwPQyuUWJv7HYjnGuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmbLzn/dJMb99Lrf1D/D6nyVwPQyuUWJv7HYjnGuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmbLzn%2FdJMb99Lrf1D%2FD6nyVwPQyuUWJv7HYjnGuK%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;707&quot; height=&quot;258&quot; data-origin-width=&quot;2090&quot; data-origin-height=&quot;762&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;&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;&lt;b&gt;플랫폼 스레드 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OS Level Context Switching&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;349&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cceXXC/dJMcaaXSK1A/kxRPIwIrPXBM5NqFAJKk40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cceXXC/dJMcaaXSK1A/kxRPIwIrPXBM5NqFAJKk40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cceXXC/dJMcaaXSK1A/kxRPIwIrPXBM5NqFAJKk40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcceXXC%2FdJMcaaXSK1A%2FkxRPIwIrPXBM5NqFAJKk40%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;792&quot; height=&quot;349&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;349&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;b&gt;가상 스레드 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM Level Context Switching&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;761&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oNrAB/dJMcacakdAV/piBEcokBkJnY4ZpSACaiTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oNrAB/dJMcacakdAV/piBEcokBkJnY4ZpSACaiTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oNrAB/dJMcacakdAV/piBEcokBkJnY4ZpSACaiTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoNrAB%2FdJMcacakdAV%2FpiBEcokBkJnY4ZpSACaiTK%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;761&quot; height=&quot;318&quot; data-origin-width=&quot;761&quot; data-origin-height=&quot;318&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;&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;&lt;b&gt;Fork Join Pool&lt;/b&gt; (Work Steal Queue 활용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;작업 처리 요청(submit)시 선점한 스레드의 Queue에 가상 스레드 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; 플랫폼&amp;nbsp;&lt;/span&gt;스레드(캐리어 스레드)는 각자의 Queue에 들어간 작업을 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;만약 본인의 Queue의 모든 작업을 처리했다면 다른 Queue에서 작업을 훔쳐와서(Steal) 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;&lt;b&gt;용어 정리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;&lt;b&gt;&lt;span&gt;Continuation&lt;/span&gt;&lt;/b&gt;: 가상 스레드가 실행해야 할 작업과 작업에 대한 진행 단계 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;&lt;b&gt;&lt;span&gt;StackChunk&lt;/span&gt;&lt;/b&gt;: Blocking 시 Heap에 저장되는 실행 정보(스택 프레임)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;&lt;b&gt;&lt;span&gt;캐리어 스레드&lt;/span&gt;&lt;/b&gt;: 작업을 수행하는 플랫폼 스레드로 각자 Queue를 가짐&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;714&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nSUfS/dJMcagjuD70/hA3cDj2xJhhZlIxzb1ThA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nSUfS/dJMcagjuD70/hA3cDj2xJhhZlIxzb1ThA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nSUfS/dJMcagjuD70/hA3cDj2xJhhZlIxzb1ThA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnSUfS%2FdJMcagjuD70%2FhA3cDj2xJhhZlIxzb1ThA0%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;670&quot; height=&quot;483&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;515&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;span&gt;&amp;bull; &lt;/span&gt;가상 스레드는 Queue에 저장되어 캐리어 스레드와 매핑되어 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;실행이 끝나면 캐리어 스레드는 다른 가상 스레드와 매핑&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;실행중 Blocking 되면 StackChunk를 Heap에 저장, &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;캐리어 스레드를 다른 가상 스레드에게 양보&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;Blocking이 끝나면 Heap에서 StackChunk를 불러와 중단지점부터 재개&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;h4 data-ke-size=&quot;size20&quot;&gt;기존 스레드를 상속하여 코드 호환&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2166&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3mfyN/dJMcae63aOd/YxyyfJVbcaQOK6H89SIVKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3mfyN/dJMcae63aOd/YxyyfJVbcaQOK6H89SIVKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3mfyN/dJMcae63aOd/YxyyfJVbcaQOK6H89SIVKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3mfyN%2FdJMcae63aOd%2FYxyyfJVbcaQOK6H89SIVKk%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;2166&quot; height=&quot;378&quot; data-origin-width=&quot;2166&quot; data-origin-height=&quot;378&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;bull;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;큰 수정 없이 활용 가능하며, SpringBoot는 application.yml 에 전역 설정 가능한 옵션 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;&lt;span&gt;&amp;bull; &lt;/span&gt;캐리어 스레드를 Block 하는 경우(synchronized 등)의 Pin 이슈로 Virtual Thread 활용 불가 (Jdk24 이후로는 이슈 없음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;라이브러리 내부 코드가 synchronized를 사용한다면 병목 가능성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;스레드풀 방식으로 활용하지 않는 것을 권장-&amp;gt; 생성 비용이 저렴하기 때문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;CPU Bound 작업의 경우 결국 Carrier Thread 위에서 동작하므로 Virtual Thread 활용은 성능 낭비&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;bull; &lt;/span&gt;배압(BackPressure) 조절 기능이 없기 때문에 과도한 생성으로 인한 과부하를 주의해야 함&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;h2 data-ke-size=&quot;size26&quot;&gt;학습하며 발생한 의문과 해답&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 의문들이 들었으며, 다음 포스트에 정리해두도록 하겠다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ohksj77.tistory.com/282&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ohksj77.tistory.com/282&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762092110995&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Java 가상 스레드 (Virtual Thread) 에 관한 오해 짚고 넘어가기&quot; data-og-description=&quot;목차왜 virtual thread를 many-to-many 모델이라고 부르는가?ForkJoinPool이 내부적으로 어떻게 활용되는가?왜 virtual thread는 기존 플랫폼 스레드보다 메모리 덜 차지하는가?가상스레드를 어느정도로 생성&quot; data-og-host=&quot;ohksj77.tistory.com&quot; data-og-source-url=&quot;https://ohksj77.tistory.com/282&quot; data-og-url=&quot;https://ohksj77.tistory.com/282&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dN5clV/hyZNdcWBTb/RO5DbFilhXdk2vsuHB7oj0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/uyPSO/hyZML3r69i/4nVgVMQ3wZbHhOB4SkwX9k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://ohksj77.tistory.com/282&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ohksj77.tistory.com/282&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dN5clV/hyZNdcWBTb/RO5DbFilhXdk2vsuHB7oj0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/uyPSO/hyZML3r69i/4nVgVMQ3wZbHhOB4SkwX9k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;Java 가상 스레드 (Virtual Thread) 에 관한 오해 짚고 넘어가기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;목차왜 virtual thread를 many-to-many 모델이라고 부르는가?ForkJoinPool이 내부적으로 어떻게 활용되는가?왜 virtual thread는 기존 플랫폼 스레드보다 메모리 덜 차지하는가?가상스레드를 어느정도로 생성&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ohksj77.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;왜&amp;nbsp;virtual&amp;nbsp;thread를&amp;nbsp;many-to-many&amp;nbsp;모델이라고&amp;nbsp;부르는가?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;ForkJoinPool이&amp;nbsp;내부적으로&amp;nbsp;어떻게&amp;nbsp;활용되는가?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;왜&amp;nbsp;virtual&amp;nbsp;thread는&amp;nbsp;기존&amp;nbsp;플랫폼&amp;nbsp;스레드보다&amp;nbsp;메모리&amp;nbsp;덜&amp;nbsp;차지하는가?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;가상스레드를&amp;nbsp;어느정도로&amp;nbsp;생성했을&amp;nbsp;때&amp;nbsp;위험하며,&amp;nbsp;어떻게&amp;nbsp;방지할&amp;nbsp;수&amp;nbsp;있는가?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;virtual&amp;nbsp;thread&amp;nbsp;pool을&amp;nbsp;설정하지&amp;nbsp;않는&amp;nbsp;게&amp;nbsp;좋은&amp;nbsp;이유?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;가상스레드 활용 시 carrier thread(platform thread) 설정은 어떻게 할 것인가?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;기존&amp;nbsp;platform&amp;nbsp;thread는&amp;nbsp;왜&amp;nbsp;pool로&amp;nbsp;설정하였는가?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;virtual&amp;nbsp;thread가&amp;nbsp;생성&amp;nbsp;되어지는&amp;nbsp;위치?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;virtual&amp;nbsp;thread가&amp;nbsp;실행&amp;nbsp;되어지며&amp;nbsp;갖는&amp;nbsp;transaction,&amp;nbsp;context는&amp;nbsp;어떻게&amp;nbsp;저장&amp;nbsp;되어져서&amp;nbsp;다른&amp;nbsp;carrier&amp;nbsp;thread에서&amp;nbsp;실행&amp;nbsp;되어져도&amp;nbsp;데이터&amp;nbsp;일관성을&amp;nbsp;지킬&amp;nbsp;수&amp;nbsp;있는가?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;virtual thread 와 spring 과의 연동 되어지는 내부 과정?&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;실제로&amp;nbsp;spring&amp;nbsp;프레임워크와&amp;nbsp;연동&amp;nbsp;하였을&amp;nbsp;때&amp;nbsp;이슈가&amp;nbsp;없는가?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;기존&amp;nbsp;thread와&amp;nbsp;구조가&amp;nbsp;달라졌는데&amp;nbsp;Spring도&amp;nbsp;업데이트되었는지&amp;nbsp;혹은&amp;nbsp;반영을&amp;nbsp;위한&amp;nbsp;업데이트가&amp;nbsp;필요&amp;nbsp;없었는지?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;virtual&amp;nbsp;thread&amp;nbsp;사용시에&amp;nbsp;synchronized를&amp;nbsp;사용하면&amp;nbsp;이슈가&amp;nbsp;발생&amp;nbsp;하는&amp;nbsp;이유?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;다른&amp;nbsp;주요&amp;nbsp;라이브러리&amp;nbsp;및&amp;nbsp;프레임워크에서&amp;nbsp;이슈가&amp;nbsp;발생할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;예시는&amp;nbsp;어떤게&amp;nbsp;있나?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;virtual&amp;nbsp;thread&amp;nbsp;가&amp;nbsp;성능을&amp;nbsp;높이기&amp;nbsp;위한&amp;nbsp;기술인가?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;continuation이란&amp;nbsp;무엇이며&amp;nbsp;기존&amp;nbsp;플랫폼&amp;nbsp;스레드의&amp;nbsp;스택&amp;nbsp;구조와의&amp;nbsp;차이는&amp;nbsp;어떠한가?&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;jdk24&amp;nbsp;부터는&amp;nbsp;pin&amp;nbsp;이슈가&amp;nbsp;어떻게&amp;nbsp;해결되었으며&amp;nbsp;완벽히&amp;nbsp;pin&amp;nbsp;이슈가&amp;nbsp;해결된&amp;nbsp;것인가?&lt;/li&gt;
&lt;/ol&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@suhongkim98/자바-언어의-태생적-한계와-극복을-위한-발전-과정-스레드편&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@suhongkim98/자바-언어의-태생적-한계와-극복을-위한-발전-과정-스레드편&lt;/a&gt; &lt;a href=&quot;https://techblog.lycorp.co.jp/ko/about-java-virtual-thread-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://techblog.lycorp.co.jp/ko/about-java-virtual-thread-1&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://xpmxf4.tistory.com/119&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://xpmxf4.tistory.com/119&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/15398/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://techblog.woowahan.com/15398/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발-탐구</category>
      <author>ohksj77</author>
      <guid isPermaLink="true">https://ohksj77.tistory.com/280</guid>
      <comments>https://ohksj77.tistory.com/280#entry280comment</comments>
      <pubDate>Sat, 1 Nov 2025 14:34:00 +0900</pubDate>
    </item>
    <item>
      <title>혹한기에 빅테크를 타겟팅한 백엔드 신입 취업 후기</title>
      <link>https://ohksj77.tistory.com/279</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;[총 1년 3개월의 취업 준비 기간]&lt;br /&gt;2024.04 ~ 2024.12 (9개월 - 쌩신입 취준)&lt;br /&gt;2025.01 ~ 2025.02 (인턴 근무)&lt;br /&gt;2025.03 ~ 2025.08 (6개월 - 인턴 경험 이후 취준)&lt;br /&gt;&lt;u&gt;2025.08 -&amp;gt; 취업&lt;/u&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;위 기간 동안 꼭 빅테크 기업에 가겠다는 의지(사실상 고집)을 가지고 끝까지 밀어붙인 취준/취업 성공 후기이다.&lt;br /&gt;스토리 형식으로 각 기간에 노력한 점 위주로 작성하고자 한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;1년 3개월이 아닌 4년 반의 노력이 들어간 결과&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;사실, 취업준비는 아니지만 &lt;b&gt;취업을 위한 노력은 4년 반 이전부터 시작&lt;/b&gt;됐다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;대학교 2학년에 들어가는 시점에 코딩 자체가 헷갈려 백준을 풀기 시작했다. 2학년 후반에는 solved ac 기준 골드2가 되었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이 과정에서 많은 변화가 있었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;1. 학교에서의 코드 관련 실습 과목은 모두 내게 쉬운 수준이 되었고, 학교 공부와 개인 공부를 병행하기에 무리가 없어졌다.&lt;br /&gt;2. 남는 시간에 개인 공부와 더불어 취업을 위한 진로를 알아볼 수 있었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그러던 중, 교내 프로그래밍 &lt;b&gt;동아리에서 우연찮게&lt;/b&gt; &lt;b&gt;Spring 스터디를 하게 되었다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;백엔드 개발의 첫 걸음이 된 Spring 스터디&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;스터디 내용은 &lt;b&gt;Spring 기초 위주였는데, 객체지향을 마음껏 프레임워크로 풀어냈다는 것을 알 수 있었다.&lt;/b&gt;&lt;br /&gt;당시 객체지향에 대해 많은 흥미를 갖고 Java 언어를 학습하던 시기였기에 &lt;b&gt;이러한 Spring 에 흥미를 안가질 수 없었다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;스터디 막바지에 간단한 API 서버를 만들어보며 더더욱 흥미를 느끼게 되었다.&lt;br /&gt;&lt;b&gt;그렇게 Spring 과 백엔드로의 지속적인 학습이 시작됐다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;프로젝트, 또 해? 이력서에 적을 정도로만 해도 되는거 아니야?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이후 학습을 하다가 좋은 기회에 프로젝트를 하게 되었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;첫 프로젝트에서 배포까지 완성하며 느낀 희열은 아직도 기억에 남는다.&lt;br /&gt;이 기억과 더불어 &lt;b&gt;프로젝트를 하면 할 수록 새로 배우는 점들이 많았기에&lt;/b&gt; &lt;b&gt;꾸준히 프로젝트에 참여하려고 노력&lt;/b&gt;했다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;다음 링크에는 24년도 8월까지 했던 프로젝트들을 정리해둔 포스트가 있다. &lt;a href=&quot;https://ohksj77.tistory.com/269&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;[프로젝트 소개 포스트]&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;사실 이 이후에도 몇몇 팀 프로젝트와 개인 프로젝트를 더 진행했다. 특히 오픈소스를 직접 구현한 프로젝트도 기억에 남는다.&lt;br /&gt;&lt;a href=&quot;https://ohksj77.tistory.com/276&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;[MySQL 직접 구현]&lt;/span&gt;&lt;/a&gt;, &lt;a href=&quot;https://ohksj77.tistory.com/277&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;[Git 직접 구현]&lt;/span&gt;&lt;/a&gt;, &lt;a href=&quot;https://ohksj77.tistory.com/278&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;[API Gateway 직접 구현]&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이러한 이유로 &lt;b&gt;졸업할 당시 10개 정도의 팀 프로젝트를 가지고 있었다.&lt;/b&gt; 이력서에 쓸 만한 프로젝트가 절반 이상이었다.&lt;br /&gt;그렇다고 양에만 집중하진 않았고 &lt;b&gt;개발 탐구를 깊게 하며 9개월 이상 진행한 프로젝트도 몇 개 있었다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;본격적인 취업 준비... 서류 검토관은 못말려...&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;취업 준비에 들어가는 시점에 코딩테스트와 개발 경험을 가지고 있었기에 &lt;b&gt;누구나 처음에 그렇듯 자신감이 넘쳤다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;하지만 반복되는 서류 탈락에 많은 고민을 하게 되었다. 그러면서 거의 한 달 동안은 이력서를 매일 고치며 현직자 분들을 찾아가 자문을 구하며 지냈다. 결과적으로 신기술을 이력서에서 빼고 오직 &lt;b&gt;깊은 개발 고민을 확실히 어필하는 이력서로 바뀌었다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이력서를 바꾸며 반신반의 하였지만, 그렇게 이력서를 지원한 직후 한 번에 빅테크 4곳에 서류가 붙었었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;4곳 모두 전형 중에 떨어졌지만, &lt;b&gt;이후 지속적인 서류 합격으로 IT 서비스 기업에 갈 수 있을 것이라는 생각이 점점 굳혀졌다. &lt;/b&gt;&lt;br /&gt;IT 서비스 기업만 지원하며 수시/상시 채용에 15% 가량의 서류 합격률을 계속 유지할 수 있었다. &lt;br /&gt;(SI 기업이나 제조업, 금융권 등은 전혀 지원하지 않았다.)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;근데 면접관도 못말리네??&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;그렇다. &lt;b&gt;면접관도 계속 말썽을 부렸다.&lt;/b&gt; (사실 내가 준비가 덜 되었던 것이다.)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;초반에는 면접에서 엄청 떨었다. 떨어지는게 당연해 보이는 면접들도 있었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;하지만, 이 과정에서 &lt;b&gt;계속해서 고민하며 면접 역량을 쌓아 나갔다.&lt;/b&gt; &lt;br /&gt;24년도 하반기에 약 30번 빅테크/유니콘 위주로 서비스 기업에 서류가 붙었기에 면접에서 계속 뚜둘겨 맞을 수 있었다(?).&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이렇게 계속 보완해 나갔으며 &lt;b&gt;24년도 10월 부터는 기술 면접에 전혀 떨어지지 않았다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;처음 맛보는 두 최종합격과 어마어마한 고민&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;24년도 연말에 두 기업에 최종합격을 하게 되었다.&lt;br /&gt;하나는 꽤나 큰 &lt;b&gt;외국계 서비스 기업의 인턴&lt;/b&gt;이었으며, 나머지 하나는 &lt;b&gt;초기 단계의 서비스 스타트업의 정규직&lt;/b&gt;이었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;스타트업은 내가 목표했던 바와 약간 차이가 있었고 인턴 이후에 좀 더 도전해보자는 엄청난 모험을 하며 &lt;b&gt;인턴을 선택했다.&lt;/b&gt;&lt;br /&gt;사실 이 시기에 이 선택이 어떤 결과를 가져다 줄지 정말로 몰랐다. (이후 생각해보면 좋은 영향을 주었다!)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;인턴 작업인데 인턴 작업이 아닌거 같아..??&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;운이 좋게도 인턴에서 &lt;b&gt;꽤나 챌린지한 업무&lt;/b&gt;를 받아 하게 되었다.&lt;br /&gt;무려 &lt;b&gt;대용량 데이터 처리&lt;/b&gt;를 해야 했으며 안정성을 위해 많이 고민해야 하는 작업이었다.&lt;br /&gt;&lt;a href=&quot;https://ohksj77.tistory.com/274&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;[인턴 개발 업무]&lt;/span&gt;&lt;/a&gt;, &lt;a href=&quot;https://ohksj77.tistory.com/275&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;[인턴 회고]&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;많은 고민을 하며 업무를 진행했고 다행히 문제를 잘 해결하였다.&lt;br /&gt;인턴을 하면서 시야가 많이 넓어졌다고 생각하며, 체험형 인턴이라 전환은 되지 않았다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;열심히 하셨잖아~&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;인턴 이후 &lt;b&gt;서류를 수정하는 작업을 진행&lt;/b&gt;했다. 이 쯤되면 너무 한게 많아서 오히려 선택과 집중을 위한 노력을 많이 했었다.&lt;br /&gt;그 결과 서류는 인턴 이후에도 여전히 지속적으로 붙었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이후 보는 면접들에서도 기술 면접들은 무난히 통과 했지만 &lt;b&gt;최종에서 떨어지는 경우가 계속 생겼다.&lt;/b&gt; &lt;br /&gt;여러 빅테크 기술 면접에 전혀 떨어지지 않고 계속 통과하며 자신감은 가득 차 있었지만, 아쉬운 결과가 생겨났다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;어느정도의 기간이 지나며 &lt;b&gt;눈을 낮춰야 하는지에 대해 고민하게 되었다. &lt;/b&gt;면접은 계속 보고 있었지만 뭔가 이유를 모르게 막히는 경우가 많았다. 면접을 정말 잘 보았는데 떨어지는 경우가 좀 있었고, 경력 공고에 신입이 지원해서 떨어지는지에 대한 생각도 많이 했다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;나 판교로 출근한다!!!&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;그러던 중, 크게 예상하지 못한 수시 채용 포지션에 &lt;b&gt;최종 합격 연락을 받았다. &lt;/b&gt;고민을 할 필요가 없어졌다.&lt;br /&gt;원하던 빅테크 IT 서비스 기업으로의 합격이었다. 정말 많이 기뻤고, 기뻤다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;+ 중니어(미들 레벨)을 뽑고자 했던 포지션이라고 한다. 역량과 가능성을 알아주셔서 정말 감사하다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;취업에 있어서 이전 인턴에서의 작업이 크게 작용했다고 생각한다.&lt;br /&gt;대용량 데이터 처리에서 나오는 문제를 고민하는 팀에 합류하게 되었다.&lt;br /&gt;합류하여 Spring 기반으로 개발을 할 예정이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;지금까지의 노력이 보상 받은 기분이었다. &lt;b&gt;이제 첫 출근을 앞두고 있다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;지금까지 하나의 목표를 가지고 밀어붙여 온게 의미가 있어서 좋다.&lt;/b&gt;&lt;br /&gt;이렇게 노력하며 배운 점들이 앞으로 생길 상황들에 많은 도움이 되지 않을까 한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;취준은 끝이지만 새로운 시작이 아닐까.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;취준은 끝이지만 앞으로 더욱 훌륭한 개발자가 되고 싶다는 생각이다.&lt;br /&gt;첫 출근 전까지 놀고 다시 열심히 역량을 쌓아야겠다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;궁금하신 분들을 위해 신입으로서의 &lt;b&gt;스펙&lt;/b&gt;을 적어보고자 한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;신입 공채가 아닌 이상&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;서비스 기업에 스펙이랄게 중요하다고 생각하지 않는다. 하지만 질문을 꽤나 받기에 적어둔다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;자격증이나 어학 점수, 부트캠프 등의 교육은 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 항목들 중 개발 문제 해결 경험이 가장 중요하다고 생각한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;- 경기권 4년제 컴공 (3점 중반대의 학점)&lt;br /&gt;- 외국계 서비스 기업 인턴&lt;br /&gt;- 오픈소스 기여 2건&lt;br /&gt;- 동아리 활동 2건 (모두 운영진)&lt;br /&gt;- 교내 수상 2건 / 교외 수상 2건 (각 해커톤, 공모전)&lt;br /&gt;- &lt;b&gt;개발 문제 해결 다수 &lt;/b&gt;&lt;a href=&quot;https://ohksj77.tistory.com/category/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%83%90%EA%B5%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;[탐구 과정 포스트 목록]&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;- 56권의 개발 서적과 30개 넘는 개발 강의로 다져온 개발 역량&lt;br /&gt;&amp;nbsp;&lt;br /&gt;내가 생각한 가장 주요했던 개발 문제 해결은 다음과 같다:&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://ohksj77.tistory.com/274&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;아이템 bulk 개봉과 API 서버의 자체 로드밸런싱&lt;/span&gt;&lt;/a&gt; &amp;lt;- 인턴 개발 업무&lt;br /&gt;&lt;a href=&quot;https://ohksj77.tistory.com/252&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;실시간 양방향 위치 공유 시스템 설계&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://ohksj77.tistory.com/267&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;실시간 통신 기술 상호 비교 및 분석 with 성능테스트&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;궁금하실까 하여 GitHub/LinkedIn 링크를 추가한다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/ohksj77&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/ohksj77&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://www.linkedin.com/in/ohksj77/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.linkedin.com/in/ohksj77&lt;/a&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;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;네이버웹툰, 토스뱅크, 넥슨코리아 - 모두 비슷한 시기에 수시/상시 채용 2차 면접을 보고 그 중 한 곳에 합류하였다.&lt;/span&gt;&lt;br /&gt;마지막으로 신입으로서의 서류 합격 내역을 공유하며 글을 마친다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;서류 합격하여 전형을 진행한 지원 목록 (클릭하여 더보기)&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상단부터 최신순이다. 2024.04 ~ 2024.12, 2025.03 ~ 2025.08 사이의 서류합 내역이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;423&quot; data-origin-height=&quot;985&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FSgQf/btsQ7yRy8EZ/wrrOl79wp4RxBlYggZWVe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FSgQf/btsQ7yRy8EZ/wrrOl79wp4RxBlYggZWVe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FSgQf/btsQ7yRy8EZ/wrrOl79wp4RxBlYggZWVe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFSgQf%2FbtsQ7yRy8EZ%2FwrrOl79wp4RxBlYggZWVe0%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;423&quot; height=&quot;985&quot; data-origin-width=&quot;423&quot; data-origin-height=&quot;985&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>기타/회고</category>
      <author>ohksj77</author>
      <guid isPermaLink="true">https://ohksj77.tistory.com/279</guid>
      <comments>https://ohksj77.tistory.com/279#entry279comment</comments>
      <pubDate>Fri, 29 Aug 2025 18:49:56 +0900</pubDate>
    </item>
    <item>
      <title>Kotlin으로 API Gateway 따라 만들기</title>
      <link>https://ohksj77.tistory.com/278</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GitHub&lt;/b&gt;: &lt;a href=&quot;https://github.com/ohksj77/api-gateway&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/ohksj77/api-gateway&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751846138294&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;GitHub - ohksj77/api-gateway: 직접 라우팅을 구현하며 api-gateway를 만들어보자&quot; data-og-description=&quot;직접 라우팅을 구현하며 api-gateway를 만들어보자. Contribute to ohksj77/api-gateway development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/ohksj77/api-gateway&quot; data-og-url=&quot;https://github.com/ohksj77/api-gateway&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dWeMKd/hyZjCxATkc/EflwoX8pU2lNu7d8eVj7Y1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/m0pGJ/hyZjvFeBGf/udd7SaF5hrb0YkJrMK7eqk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/ohksj77/api-gateway&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/ohksj77/api-gateway&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dWeMKd/hyZjCxATkc/EflwoX8pU2lNu7d8eVj7Y1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/m0pGJ/hyZjvFeBGf/udd7SaF5hrb0YkJrMK7eqk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - ohksj77/api-gateway: 직접 라우팅을 구현하며 api-gateway를 만들어보자&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;직접 라우팅을 구현하며 api-gateway를 만들어보자. Contribute to ohksj77/api-gateway development by creating an account on GitHub.&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;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;전체 코드는 위 Repository에서 확인할 수 있습니다. 이 포스트에서 전체 구현을 다루지는 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로서비스 아키텍처에서 API Gateway는 모든 클라이언트 요청의 진입점 역할을 하며, 라우팅, 로드 밸런싱, 인증, 모니터링 등 다양한 기능을 제공합니다. 이번 포스트에서는 Spring WebFlux와 Kotlin을 사용하여 API Gateway의 핵심 기능을 직접 구현한 프로젝트를 소개하고, 그 설계와 구현 방식을 분석해보겠습니다.&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;* I/O 작업을 적은 스레드 수로 효율적으로 처리하는 이점을 가져가고자 WebFlux를 채택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* WebFlux에 아직 익숙하지 않지만 API GW를 만드는데 사용되는 인터페이스 정도는 다룰 수 있었습니다. 아직 많은 학습이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 구조&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;api-gateway/
├── api-gateway/          # API Gateway 서버 (포트: 8080)
├── module1/             # 백엔드 서비스 1 (포트: 8081)
└── module2/             # 백엔드 서비스 2 (포트: 8082)
&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;module1, module2 는 api-gateway 의 동작을 확인하기 위해 간단히 추가한 서버입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 스택&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;언어&lt;/b&gt;: Kotlin&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프레임워크&lt;/b&gt;: Spring Boot 3.4.4, Spring WebFlux&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Java 버전&lt;/b&gt;: 21&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 기능 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 설정 기반 라우팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API Gateway는 YAML 설정 파일을 통해 라우팅 규칙을 정의합니다:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;607&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duLoxE/btsO7hrcTq0/NJ6NuswuJKUlu9KnZxZrSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duLoxE/btsO7hrcTq0/NJ6NuswuJKUlu9KnZxZrSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duLoxE/btsO7hrcTq0/NJ6NuswuJKUlu9KnZxZrSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduLoxE%2FbtsO7hrcTq0%2FNJ6NuswuJKUlu9KnZxZrSK%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;397&quot; height=&quot;607&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;607&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 데이터 모델 설계&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class App(
    val port: Int,
    val version: String,
    val name: String,
    val http: Http
) {
    fun createUrl(): String {
        return http.baseUrl + &quot;:&quot; + port
    }
}

class Http(
    val baseUrl: String,
    val routes: List&amp;lt;Route&amp;gt;
)

class Route(
    val method: String,
    val path: String,
    val header: Map&amp;lt;String, String&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;이 데이터 모델은 설정 파일의 구조를 반영하며, &lt;code&gt;createUrl()&lt;/code&gt; 메서드를 통해 백엔드 서비스의 전체 URL을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 리액티브 라우터 구현&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class Router(
    private val routerHandler: RouterHandler,
    private val app: App
) {
    private var webClient: WebClient = WebClient.builder()
        .baseUrl(app.createUrl())
        .build()

    fun route(): RouterFunction&amp;lt;ServerResponse&amp;gt; {
        val routes = app.http.routes.map { router -&amp;gt;
            when (router.method) {
                &quot;GET&quot; -&amp;gt; RouterFunctions.route(
                    RequestPredicates.GET(router.path),
                    routerHandler.get(router, webClient)
                )
                &quot;POST&quot; -&amp;gt; RouterFunctions.route(
                    RequestPredicates.POST(router.path),
                    routerHandler.post(router, webClient)
                )
                // ... 다른 HTTP 메서드들
            }
        }
        return routes.reduce { acc, next -&amp;gt;
            acc.and(next)
        }
    }
}
&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Spring WebFlux&lt;/b&gt;의&lt;b&gt; RouterFunction 사용&lt;/b&gt;: 함수형 엔드포인트 정의&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WebClient&lt;/b&gt;: 리액티브 HTTP 클라이언트로 백엔드 서비스와 통신&lt;/li&gt;
&lt;li&gt;&lt;b&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;h3 data-ke-size=&quot;size23&quot;&gt;4. 서킷 브레이커 패턴&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class RouterHandler(
    private val circuitBreaker: CircuitBreaker,
) {
    fun get(route: Route, webClient: WebClient): (ServerRequest) -&amp;gt; Mono&amp;lt;ServerResponse&amp;gt; = { request -&amp;gt;
        // ... 요청 처리 로직
        requestBuilder.retrieve()
            .bodyToMono(String::class.java)
            .transformDeferred(CircuitBreakerOperator.of(circuitBreaker))
            .flatMap { ServerResponse.ok().bodyValue(it) }
    }
}
&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;Resilience4j를 사용한 서킷 브레이커 설정:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;resilience4j:
  circuitbreaker:
    configs:
      default:
        slidingWindowSize: 10
        failureRateThreshold: 50.0
        waitDurationInOpenState: 5s
        permittedNumberOfCallsInHalfOpenState: 5
&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;slidingWindowSize&lt;/b&gt;: 10개의 요청을 윈도우로 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;failureRateThreshold&lt;/b&gt;: 50% 실패율 시 서킷 브레이커 활성화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;waitDurationInOpenState&lt;/b&gt;: 5초간 서킷 브레이커 열린 상태 유지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;permittedNumberOfCallsInHalfOpenState&lt;/b&gt;: 반열린 상태에서 5개 요청 허용&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 요청 처리 핸들러&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;fun get(route: Route, webClient: WebClient): (ServerRequest) -&amp;gt; Mono&amp;lt;ServerResponse&amp;gt; = { request -&amp;gt;
    logger.info(&quot;[REQUEST] get&quot;)
    val uri = buildUri(route.path, request)
    val queryParams = request.queryParams().toSingleValueMap()

    val requestBuilder = webClient.get()
        .uri { builder -&amp;gt;
            builder.path(uri)
            queryParams.forEach { (key, value) -&amp;gt; builder.queryParam(key, value) }
            builder.build()
        }

    route.header?.forEach { (key, value) -&amp;gt; requestBuilder.header(key, value) }

    requestBuilder.retrieve()
        .bodyToMono(String::class.java)
        .transformDeferred(CircuitBreakerOperator.of(circuitBreaker))
        .flatMap { ServerResponse.ok().bodyValue(it) }
}
&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Path Variable 처리&lt;/b&gt;: &lt;code&gt;{id}&lt;/code&gt; 형태의 경로 변수를 실제 값으로 치환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Query Parameter 전달&lt;/b&gt;: 클라이언트의 쿼리 파라미터를 백엔드로 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;헤더 설정&lt;/b&gt;: 라우트별로 정의된 헤더 정보 적용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리액티브 스트림&lt;/b&gt;: Mono를 사용한 비동기 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개선 가능한 부분&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;인증/인가&lt;/b&gt;: 현재 구현되지 않은 보안 기능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로드 밸런싱&lt;/b&gt;: 단일 인스턴스만 지원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모니터링/로깅&lt;/b&gt;: 상세한 메트릭 수집 부족&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐싱&lt;/b&gt;: 응답 캐싱 메커니즘 부재&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 직접 구현을 통해 API Gateway의 내부 동작 원리를 알아볼 수 있었습니다. 프로토타입 정도로 구현했기에 부족한 기능들을 추후 추가해보고 싶습니다. 또한, 활용했던 WebFlux 에 대해 더 학습해보고 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>오픈소스-직접-구현</category>
      <author>ohksj77</author>
      <guid isPermaLink="true">https://ohksj77.tistory.com/278</guid>
      <comments>https://ohksj77.tistory.com/278#entry278comment</comments>
      <pubDate>Mon, 7 Jul 2025 09:08:07 +0900</pubDate>
    </item>
    <item>
      <title>Kotlin으로 Git 따라 만들기: KGit</title>
      <link>https://ohksj77.tistory.com/277</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GitHub&lt;/b&gt;: &lt;a href=&quot;https://github.com/ohksj77/kgit&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/ohksj77/kgit&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751780380171&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;GitHub - ohksj77/kgit: Kotlin으로 만든 git 프로젝트&quot; data-og-description=&quot;Kotlin으로 만든 git 프로젝트. Contribute to ohksj77/kgit development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/ohksj77/kgit&quot; data-og-url=&quot;https://github.com/ohksj77/kgit&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/LH60S/hyZjmOW9Fa/8lMR7OKv0F5bzoa1w7rYLK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cQYbgH/hyZfWK9mtX/jNwZBOWfUZ0tH0MKVkfTR1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/ohksj77/kgit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/ohksj77/kgit&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/LH60S/hyZjmOW9Fa/8lMR7OKv0F5bzoa1w7rYLK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cQYbgH/hyZfWK9mtX/jNwZBOWfUZ0tH0MKVkfTR1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - ohksj77/kgit: Kotlin으로 만든 git 프로젝트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Kotlin으로 만든 git 프로젝트. Contribute to ohksj77/kgit development by creating an account on GitHub.&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;전체 코드는 위 Repository에서 확인할 수 있습니다. 이 포스트에서 전체 구현을 다루지는 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KGit은 Kotlin으로 구현된 Git 클론 프로젝트입니다. 실제 Git과 유사한 CLI 명령어를 제공하며, Git의 핵심 개념인 객체 모델과 참조 시스템을 구현했습니다. Git 내부 동작을 알아보고자 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;동작 예시&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brcYlb/btsO6Lsg4MJ/PYG7DHfIrTLthuediuKZn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brcYlb/btsO6Lsg4MJ/PYG7DHfIrTLthuediuKZn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brcYlb/btsO6Lsg4MJ/PYG7DHfIrTLthuediuKZn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrcYlb%2FbtsO6Lsg4MJ%2FPYG7DHfIrTLthuediuKZn0%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;1121&quot; height=&quot;294&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;294&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;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 구조&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;kgit/
├── src/main/kotlin/
│   ├── command/          # 명령어 처리 로직
│   │   ├── Command.kt    # 명령어 인터페이스 및 구현체들
│   │   ├── CommandFactory.kt  # Factory 패턴으로 명령어 생성
│   │   └── CommandType.kt     # 지원하는 명령어 타입 정의
│   ├── object/           # Git 객체 모델 (Blob, Tree, Commit, Tag)
│   │   ├── KgitObject.kt      # 기본 객체 구조 및 타입 정의
│   │   ├── HashObject.kt      # 객체 해시 생성 및 저장
│   │   ├── Tree.kt            # 트리 객체 구조
│   │   ├── WriteTree.kt       # 트리 객체 생성
│   │   ├── LsTree.kt          # 트리 내용 출력
│   │   ├── CommitTree.kt      # 커밋 객체 생성
│   │   ├── Tag.kt             # 태그 객체 생성
│   │   ├── CatFile.kt         # 객체 내용 확인
│   │   ├── UpdateIndex.kt     # 인덱스 업데이트
│   │   └── GitMode.kt         # 파일 모드 정의
│   ├── porcelain/        # 사용자 친화적 명령어
│   │   ├── RevParse.kt        # 참조 해석
│   │   └── Tag.kt             # 태그 명령어
│   ├── reference/        # 참조 시스템 (HEAD, 태그 등)
│   │   ├── SymbolicRef.kt     # 심볼릭 참조 처리
│   │   └── UpdateRef.kt       # 참조 업데이트
│   ├── config/           # 설정 관리
│   │   └── Config.kt          # 설정 파일 관리
│   └── Main.kt          # 진입점
├── build.gradle.kts     # Gradle 빌드 설정
├── kgit.sh              # 실행 스크립트
└── README.md

&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 기능&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Git 객체 모델&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KGit은 Git의 4가지 기본 객체 타입을 모두 구현했습니다:&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Blob (파일 데이터)&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;enum class Type(val value: String) {
    BLOB(&quot;blob&quot;),
    TREE(&quot;tree&quot;),
    COMMIT(&quot;commit&quot;),
    TAG(&quot;tag&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Tree (디렉토리 구조)&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Serializable
data class TreeEntry(
    val mode: Long = 0L,
    val file: String,
    val objectHash: String = &quot;&quot;
) {
    fun isDirectory(): Boolean = mode == GitMode.TREE_MODE
    fun isSymlink(): Boolean = mode == GitMode.SYMLINK_MODE
    fun isExecutable(): Boolean = mode == GitMode.EXECUTABLE_BLOB_MODE
    fun isRegularFile(): Boolean = !isDirectory() &amp;amp;&amp;amp; !isSymlink() &amp;amp;&amp;amp; mode != GitMode.SUBMODULE_MODE
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Commit (커밋 정보)&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Serializable
data class CommitObjectBody(
    val tree: String,
    val parent: List&amp;lt;String&amp;gt;,
    val author: String,
    val date: String,
    val message: String
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Tag (태그 정보)&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Serializable
data class TagBody(
    val `object`: String,
    val type: Type,
    val tag: String,
    val tagger: String
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 객체 저장 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git과 동일한 방식으로 SHA-1 해시를 사용한 객체 저장:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun newKey(typeVal: Type, data: ByteArray): String {
    val str = newContent(typeVal, data)
    val digest = MessageDigest.getInstance(&quot;SHA-1&quot;)
    digest.update(str)
    return digest.digest().joinToString(&quot;&quot;) { &quot;%02x&quot;.format(it) }
}

fun newContent(typeVal: Type, data: ByteArray): ByteArray {
    val header = &quot;${typeVal.value} ${data.size}\\\\u0000&quot;.toByteArray()
    val output = ByteArrayOutputStream()
    output.write(header)
    output.write(data)
    return output.toByteArray()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체는 .kgit/objects/ 디렉토리에 저장&lt;/li&gt;
&lt;li&gt;첫 2글자로 서브디렉토리 분할 (예: a1/b2c3d4...)&lt;/li&gt;
&lt;li&gt;zlib 압축으로 저장 공간 절약&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 명령어 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Factory 패턴을 사용한 명령어 처리:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;object CommandFactory {
    private val commandMap: EnumMap&amp;lt;CommandType, (String) -&amp;gt; Command&amp;gt; = EnumMap(CommandType::class.java)

    init {
        commandMap[CommandType.INIT] = { kgitDir -&amp;gt; InitCommand(kgitDir) }
        commandMap[CommandType.ADD] = { kgitDir -&amp;gt; AddCommand(kgitDir) }
        commandMap[CommandType.LS] = { kgitDir -&amp;gt; LsCommand(kgitDir) }
        commandMap[CommandType.CAT] = { kgitDir -&amp;gt; CatCommand(kgitDir) }
        commandMap[CommandType.WRITE] = { kgitDir -&amp;gt; WriteCommand(kgitDir) }
        commandMap[CommandType.COMMIT] = { kgitDir -&amp;gt; CommitCommand(kgitDir) }
        commandMap[CommandType.TAG] = { kgitDir -&amp;gt; TagCommand(kgitDir) }
    }

    fun getCommand(commandName: String, kgitDir: String): Command {
        val type = CommandType.from(commandName)
        return commandMap[type]?.invoke(kgitDir)
            ?: throw IllegalArgumentException(&quot;지원하지 않는 명령입니다: $commandName&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지원하는 명령어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 명령어&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;init: 저장소 초기화 (.kgit 디렉토리 및 하위 구조 생성)&lt;/li&gt;
&lt;li&gt;add &amp;lt;파일명&amp;gt;: 스테이징 영역에 파일 추가 (인덱스 업데이트)&lt;/li&gt;
&lt;li&gt;write: 트리 객체 생성 (현재 인덱스 기반)&lt;/li&gt;
&lt;li&gt;ls &amp;lt;트리해시&amp;gt;: 트리 구조 출력 (재귀적 출력 지원)&lt;/li&gt;
&lt;li&gt;commit &amp;lt;트리해시&amp;gt; &quot;메시지&quot;: 커밋 객체 생성&lt;/li&gt;
&lt;li&gt;cat pretty-print &amp;lt;해시&amp;gt;: 객체 내용 확인&lt;/li&gt;
&lt;li&gt;tag &amp;lt;태그명&amp;gt; &amp;lt;해시&amp;gt; &quot;메시지&quot;: 태그 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 예시&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;# 저장소 초기화
./kgit.sh init

# 파일 추가 및 커밋
./kgit.sh add example.txt
./kgit.sh write
./kgit.sh commit &amp;lt;트리해시&amp;gt; &quot;첫 번째 커밋&quot;

# 태그 생성
./kgit.sh tag v1.0 &amp;lt;커밋해시&amp;gt; &quot;릴리즈 v1.0&quot;

# 객체 내용 확인
./kgit.sh cat pretty-print &amp;lt;해시&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술적 특징&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Kotlin 기능 활용&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Data Class&lt;/b&gt;: 불변 객체 모델링으로 타입 안전성 보장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sealed Class&lt;/b&gt;: 타입 안전한 열거형과 패턴 매칭&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Extension Functions&lt;/b&gt;: 유틸리티 함수 확장으로 가독성 향상&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Result Type&lt;/b&gt;: 명시적 에러 처리 패턴으로 예외 상황 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kotlinx Serialization&lt;/b&gt;: JSON 직렬화로 설정 및 객체 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 함수형 프로그래밍&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun parseObject(kgitDir: String, objectHash: String): Result&amp;lt;KgitObject&amp;gt; {
    val path = objectHash.path(kgitDir)
    val file = File(path)

    if (!file.exists()) {
        return Result.failure(FileNotFoundException(&quot;Object file not found at $path&quot;))
    }

    return try {
        val data = file.readBytes()
        unmarshalObject(data)
    } catch (e: Exception) {
        Result.failure(e)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 에러 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Result&amp;lt;T&amp;gt; 타입을 사용한 명시적 에러 처리&lt;/li&gt;
&lt;li&gt;예외 상황에 대한 적절한 메시지 제공&lt;/li&gt;
&lt;li&gt;타입 안전한 에러 전파로 런타임 에러 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Git 모드 시스템&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 권한과 타입을 표현하는 모드 시스템:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;object GitMode {
    // 디렉토리 (Tree)
    const val TREE_MODE: Long = 0b010000000000000000000L // 0o040000

    // 일반 파일 (Blob)
    const val BLOB_MODE: Long = 0b00110001001010010101010101010101L // 0o100644

    // 실행 가능한 일반 파일 (Blob)
    const val EXECUTABLE_BLOB_MODE: Long = 0b00110001111011010101101101101L // 0o100755

    // 심볼릭 링크 (Blob)
    const val SYMLINK_MODE: Long = 0b101000000000000000000L // 0o120000

    // 서브모듈 (Commit)
    const val SUBMODULE_MODE: Long = 0b111000000000000000000L // 0o160000
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참조 시스템&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git의 참조 시스템을 구현하여 HEAD, 태그, 브랜치 관리:&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;enum class SymbolicRefType {
    HEAD,
    FETCH_HEAD,
    ORIG_HEAD,
    MERGE_HEAD;

    companion object {
        fun fromString(typeString: String): SymbolicRefType? {
            return entries.firstOrNull { it.name == typeString }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정 시스템&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 기반 설정 파일 관리:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Serializable
data class Config(
    val core: Core,
    val user: User
) {
    fun createConfigFile(kgitDir: String): Result&amp;lt;Unit&amp;gt; {
        val configFile = File(kgitDir, CONFIG_FILE_NAME)
        return try {
            val jsonString = Json.encodeToString(Config.serializer(), this)
            configFile.writeText(jsonString)
            Result.success(Unit)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빌드 및 실행&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요구사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kotlin 2.1.20+&lt;/li&gt;
&lt;li&gt;Java 17+&lt;/li&gt;
&lt;li&gt;Gradle&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;학습 포인트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Git 내부 구조 이해&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;객체 모델의 실제 구현&lt;/b&gt;: Blob, Tree, Commit, Tag의 구체적 구현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SHA-1 해시 기반 저장 시스템&lt;/b&gt;: Git의 핵심 저장 방식 이해&lt;/li&gt;
&lt;li&gt;&lt;b&gt;참조와 심볼릭 링크의 동작&lt;/b&gt;: HEAD, 태그 등의 참조 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Kotlin 기능&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;함수형 프로그래밍 패턴&lt;/b&gt;: Result 타입, 고차 함수 활용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타입 안전한 에러 처리&lt;/b&gt;: 예외 상황의 명시적 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;현대적인 직렬화 라이브러리 활용&lt;/b&gt;: JSON 기반 설정 및 객체 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 시스템 프로그래밍&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일 시스템 조작&lt;/b&gt;: 디렉토리 생성, 파일 읽기/쓰기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;바이너리 데이터 처리&lt;/b&gt;: 압축/압축 해제, 해시 계산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;압축 알고리즘 활용&lt;/b&gt;: zlib을 통한 저장 공간 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 설계 패턴&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Factory 패턴&lt;/b&gt;: 명령어 객체 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Strategy 패턴&lt;/b&gt;: 다양한 명령어 처리 방식&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Result 패턴&lt;/b&gt;: 에러 처리의 일관성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개선 가능한 부분&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;브랜치 기능&lt;/b&gt;: 현재는 기본적인 커밋만 지원, 브랜치 관리 기능 추가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;병합 기능&lt;/b&gt;: 여러 커밋 간의 병합 로직 구현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원격 저장소&lt;/b&gt;: 네트워크 통신을 통한 원격 저장소 지원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 최적화&lt;/b&gt;: 대용량 저장소에서의 성능 개선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 커버리지&lt;/b&gt;: 단위 테스트 및 통합 테스트 추가&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그냥 사용만 하던 Git 시스템에 대해 몰랐던 동작들을 알 수 있어 좋았습니다.&lt;/li&gt;
&lt;li&gt;GitHub 과 같은 remote repository에 연동해보고 싶은 마음도 있었으나 많은 고난(?)이 예상되어서 시도하지 못했습니다. 언젠가 시도해보고 싶습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>오픈소스-직접-구현</category>
      <author>ohksj77</author>
      <guid isPermaLink="true">https://ohksj77.tistory.com/277</guid>
      <comments>https://ohksj77.tistory.com/277#entry277comment</comments>
      <pubDate>Sun, 6 Jul 2025 14:43:21 +0900</pubDate>
    </item>
    <item>
      <title>Kotlin으로 MySQL 따라 만들기: KMySQL</title>
      <link>https://ohksj77.tistory.com/276</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GitHub&lt;/b&gt;: &lt;a href=&quot;https://github.com/ohksj77/kmysql&quot;&gt;https://github.com/ohksj77/kmysql&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751776647212&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;GitHub - ohksj77/kmysql: Kotlin으로 만든 MySQL 프로젝트&quot; data-og-description=&quot;Kotlin으로 만든 MySQL 프로젝트. Contribute to ohksj77/kmysql development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/ohksj77/kmysql&quot; data-og-url=&quot;https://github.com/ohksj77/kmysql&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bVEV7C/hyZfYoFNKG/gFlo7liXCDV2qy6YnuWGJ0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/3DxBp/hyZfVMdkIZ/CseO4ae6aZNwIwCuRKvK2K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/ohksj77/kmysql&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/ohksj77/kmysql&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bVEV7C/hyZfYoFNKG/gFlo7liXCDV2qy6YnuWGJ0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/3DxBp/hyZfVMdkIZ/CseO4ae6aZNwIwCuRKvK2K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - ohksj77/kmysql: Kotlin으로 만든 MySQL 프로젝트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Kotlin으로 만든 MySQL 프로젝트. Contribute to ohksj77/kmysql development by creating an account on GitHub.&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;  &lt;/span&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 응시한 면접에서 이러한 질문을 받은 적이 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;ldquo;직접 DB를 구현한다면 어떻게 Repeatable Read를 구현하고 싶으신가요?&amp;rdquo;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 계기로 언젠가는 간단하게라도 DBMS를 구현해보고 싶은 욕구가 생겼고, 이를 실제로 구현해봤습니다.&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;기술 스택&lt;/b&gt;: Kotlin 1.9+, Java 17+, Gradle 8.0+&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&amp;nbsp;동작 예시&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;455&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgXTaF/btsO7xNSxUt/lCrgsYmI94rJCaOzxHvGN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgXTaF/btsO7xNSxUt/lCrgsYmI94rJCaOzxHvGN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgXTaF/btsO7xNSxUt/lCrgsYmI94rJCaOzxHvGN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgXTaF%2FbtsO7xNSxUt%2FlCrgsYmI94rJCaOzxHvGN0%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;464&quot; height=&quot;455&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;455&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;h2 data-ke-size=&quot;size26&quot;&gt; ️ KMySQL의 핵심 컴포넌트들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 아키텍처 개요&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvVpHW/btsO7NwaA1G/vO3FVELkflJs5iNNjcrTC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvVpHW/btsO7NwaA1G/vO3FVELkflJs5iNNjcrTC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvVpHW/btsO7NwaA1G/vO3FVELkflJs5iNNjcrTC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvVpHW%2FbtsO7NwaA1G%2FvO3FVELkflJs5iNNjcrTC0%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;551&quot; height=&quot;370&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;370&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;h3 data-ke-size=&quot;size23&quot;&gt;각 컴포넌트의 역할&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;File Manager&lt;/b&gt;: 디스크 I/O 관리, 페이지 and 블록 단위 파일 접근&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Buffer Manager&lt;/b&gt;: 메모리 캐싱, LRU 기반 버퍼 교체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Transaction Manager&lt;/b&gt;: ACID 보장, 동시성 제어, 복구 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Index Manager&lt;/b&gt;: B-Tree, Hash 인덱스 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Query Planner&lt;/b&gt;: SQL 실행 계획 생성 및 최적화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JDBC Interface&lt;/b&gt;: 외부 애플리케이션 연동&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  트랜잭션 관리: ACID의 핵심&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ACID 속성 개념과 구현 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원자성 (Atomicity)&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;/li&gt;
&lt;li&gt;로그 기반 복구로 롤백 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;일관성 (Consistency)&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;/li&gt;
&lt;li&gt;제약조건과 트리거로 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;격리성 (Isolation)&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;/li&gt;
&lt;li&gt;MVCC와 락킹으로 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지속성 (Durability)&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;/li&gt;
&lt;li&gt;WAL(Write-Ahead Logging) 프로토콜로 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;격리 수준과 동시성 제어&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;격리 수준별 동시성 vs 일관성 트레이드오프

READ UNCOMMITTED  &amp;larr;─── 높은 동시성, 낮은 일관성
READ COMMITTED
REPEATABLE READ
SERIALIZABLE      &amp;larr;─── 낮은 동시성, 높은 일관성

&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;b&gt;MVCC (Multi-Version Concurrency Control)&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;/li&gt;
&lt;li&gt;읽기 작업이 쓰기 작업을 블록하지 않음&lt;/li&gt;
&lt;li&gt;메모리 사용량과 성능 간의 균형&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  버퍼 관리: 성능 최적화의 핵심&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 계층 구조&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;CPU Cache (L1, L2, L3)
    &amp;darr; (10-100배 빠름)
Main Memory (Buffer Pool)
    &amp;darr; (100,000-1,000,000배 빠름)
Disk Storage
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;버퍼 관리 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LRU (Least Recently Used) 교체 정책&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;/li&gt;
&lt;li&gt;지역성 원리(locality)를 활용한 성능 최적화&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이지가 사용 중일 때는 교체되지 않도록 보호&lt;/li&gt;
&lt;li&gt;참조 카운트로 안전한 메모리 해제 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지연 쓰기 (Lazy Writing)&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;/li&gt;
&lt;li&gt;버퍼가 가득 찰 때나 체크포인트 시에만 쓰기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ 인덱스: 검색 성능의 핵심&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스의 종류와 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;B-Tree 인덱스&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;/li&gt;
&lt;li&gt;삽입/삭제 시 자동으로 균형 유지&lt;/li&gt;
&lt;li&gt;검색 시간: O(log n)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hash 인덱스&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;/li&gt;
&lt;li&gt;매우 빠른 검색 속도: O(1)&lt;/li&gt;
&lt;li&gt;범위 검색은 비효율적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  로그 기반 복구: 데이터 안정성의 보장&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WAL (Write-Ahead Logging) 기법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 원칙&lt;/b&gt;: 데이터 페이지를 디스크에 쓰기 전에 로그를 먼저 써야 함&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;트랜잭션 실행 순서:
1. 로그 레코드 작성
2. 로그를 디스크에 강제 쓰기 (flush)
3. 데이터 페이지 수정
4. 커밋 로그 작성
5. 커밋 로그를 디스크에 강제 쓰기
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복구 시나리오&lt;/h3&gt;
&lt;p 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-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마지막 체크포인트부터 로그를 재실행 (REDO)&lt;/li&gt;
&lt;li&gt;커밋되지 않은 트랜잭션 롤백 (UNDO)&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 트랜잭션의 로그를 역순으로 읽어서 변경사항 되돌리기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  쿼리 처리: SQL에서 결과까지&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쿼리 처리 파이프라인&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;SQL 쿼리
    &amp;darr;
Lexical Analysis (토큰화)
    &amp;darr;
Parsing (구문 분석)
    &amp;darr;
Semantic Analysis (의미 분석)
    &amp;darr;
Query Optimization (최적화)
    &amp;darr;
Execution Plan (실행 계획)
    &amp;darr;
Query Execution (실행)
    &amp;darr;
Result Set (결과)

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 계획의 종류&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Table Scan&lt;/b&gt;: 전체 테이블을 순차적으로 읽기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Index Scan&lt;/b&gt;: 인덱스를 사용한 효율적인 검색&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Nested Loop Join&lt;/b&gt;: 중첩 루프를 이용한 조인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hash Join&lt;/b&gt;: 해시 테이블을 이용한 조인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sort Merge Join&lt;/b&gt;: 정렬 후 병합하는 조인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  실제 사용 시나리오&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대화형 SQL 클라이언트&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 데이터베이스 연결
Connect&amp;gt; kmysql_db

-- 테이블 생성 및 데이터 삽입
CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    email VARCHAR(100)
);

INSERT INTO users VALUES (1, '홍길동', 'hong@example.com');
INSERT INTO users VALUES (2, '김철수', 'kim@example.com');

-- 트랜잭션 내에서 작업
BEGIN;
UPDATE users SET name = '김영희' WHERE id = 2;
SAVEPOINT sp1;
DELETE FROM users WHERE id = 1;
ROLLBACK TO SAVEPOINT sp1;
COMMIT;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JDBC를 통한 프로그래밍&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 데이터베이스 연결
val driver = EmbeddedDriver()
val connection = driver.connect(&quot;kmysql_db&quot;, null)

try {
    connection.setAutoCommit(false)  // 트랜잭션 시작

    val statement = connection.createStatement()
    statement.executeUpdate(&quot;INSERT INTO users VALUES (3, '이철수', 'lee@example.com')&quot;)

    val resultSet = statement.executeQuery(&quot;SELECT * FROM users WHERE id = 3&quot;)
    while (resultSet.next()) {
        println(&quot;Name: ${resultSet.getString(&quot;name&quot;)}&quot;)
    }

    connection.commit()  // 트랜잭션 커밋

} catch (e: Exception) {
    connection.rollback()  // 오류 시 롤백
    throw e
} finally {
    connection.close()
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  성능과 안정성 테스트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동시성 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;데드락 감지&lt;/b&gt;: 무한 대기 방지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;락 타임아웃&lt;/b&gt;: 일정 시간 후 자동 롤백&lt;/li&gt;
&lt;li&gt;&lt;b&gt;격리 수준별 동작&lt;/b&gt;: 각 격리 수준에서의 일관성 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복구 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;체크포인트 복구&lt;/b&gt;: 마지막 체크포인트부터 복구&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그 재실행&lt;/b&gt;: 커밋된 트랜잭션 재실행&lt;/li&gt;
&lt;li&gt;&lt;b&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;h2 data-ke-size=&quot;size26&quot;&gt;  향후 발전 방향&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재는 Embedded DB 로만 동작할 수 있기에 단독 DB 서버로서 동작할 수 있도록 발전시키고 싶습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재는 서버 내부에서 파일 시스템을 활용해 동작 중입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;세부 구현들이 아직 실제 DBMS 수준에 미치지 못한 경우가 있어서 보완하고 싶습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt; &lt;/span&gt; Maven &amp;amp; Gradle 배포&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-06 오후 2.22.50.png&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAMbom/btsO5nGvPoY/o3F1qXTeja9WRApz2sneE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAMbom/btsO5nGvPoY/o3F1qXTeja9WRApz2sneE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAMbom/btsO5nGvPoY/o3F1qXTeja9WRApz2sneE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAMbom%2FbtsO5nGvPoY%2Fo3F1qXTeja9WRApz2sneE0%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;572&quot; height=&quot;104&quot; data-filename=&quot;스크린샷 2025-07-06 오후 2.22.50.png&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;299&quot; data-origin-height=&quot;105&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/copAC6/btsO7zrnGnK/kuAMi9VrTuGzYgLBEsmEJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/copAC6/btsO7zrnGnK/kuAMi9VrTuGzYgLBEsmEJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/copAC6/btsO7zrnGnK/kuAMi9VrTuGzYgLBEsmEJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcopAC6%2FbtsO7zrnGnK%2FkuAMi9VrTuGzYgLBEsmEJ0%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;299&quot; height=&quot;105&quot; data-origin-width=&quot;299&quot; data-origin-height=&quot;105&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Maven Repository에 Public하게 배포하는 방법은 절차가 다소 길기에 GitHub Package로 우선 배포해보았습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;  후기&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상상만 하지 않고 실현했다는 점이 가장 의미 깊었습니다.&lt;/li&gt;
&lt;li&gt;그 외로 DBMS의 내부 구현에 대해 더 깊게 알게 되었고, 상용 DBMS 제품들은 정말 많은 고민이 들어간 시스템이라는 것을 다시 한 번 깨닫게 되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>오픈소스-직접-구현</category>
      <author>ohksj77</author>
      <guid isPermaLink="true">https://ohksj77.tistory.com/276</guid>
      <comments>https://ohksj77.tistory.com/276#entry276comment</comments>
      <pubDate>Sun, 6 Jul 2025 14:26:24 +0900</pubDate>
    </item>
  </channel>
</rss>