<?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>Sat, 20 Jun 2026 21:51:27 +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>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>
    <item>
      <title>EA코리아 2024 동계 인턴십 회고</title>
      <link>https://ohksj77.tistory.com/275</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2025.01 ~ 2025.02 두 달간의 인턴십 전반의 느낌과 배운 점에 대한 회고를 작성하고자 한다.&lt;br&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;체험형 인턴이라고 해도 궁금해할 만한 전환을 먼저 언급하자면 전환은 되지 않았다. 하지만, 이유가 내게 있지는 않았다. 처음부터 전환을 전제로 하지 않기도 했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;인턴 마무리 시점에 받은 평가는 과분할 정도로 정말 좋게 받았다. 특히 기술적인 항목들은 받을 수 있는 가장 높은 점수를 받는 등 전반적으로 좋은 점수를 받았던지라 아쉬웠다. 개발 작업 도중에 마음에 들어 하셨다는 점을 직접적으로 알 수 있기도 했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;체험형 인턴이었던 점에도 불구하고 팀에서 전환을 위해 알아봐 주셨지만 팀 사정상 아쉽게 되었다. 적응과 전반적인 부분에 도움을 주신 멘토님과 버디님께서 이런 점을 전달하며 많이 아쉬워하시던게 아직도 생생하다....&lt;br&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;2024년도 가을 쯤, 반가운 기업의 인턴십 모집 공고를 보았다. 바로 &lt;b&gt;EA코리아&lt;/b&gt; 였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;평소 게임 개발을 하였는가? 하면 &quot;아니다&quot; 라고 답변할 것이다. 하지만, 어렸을때부터 즐겨하던 FC Online (구 피파온라인4)이라는 관심 있는 서비스에 기여하며 실무를 익힐 수 있는 기회라 생각했다. 웹에서의 서버 개발과 같은 기술들을 활용한다는 점 또한 문제 없이 기여할 수 있을 것이라 기대하였다. 사람들에게 즐거움을 주는 서비스를 개발하는 것 또한 의미가 깊다고 생각하기도 했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;평소 학습해온 서버 개발을 할 수 있는 &lt;b&gt;Server Content Software Engineer 직무에 지원&lt;/b&gt;하였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;공식 블로그의 모집 소식 포스트: &lt;a href=&quot;https://blog.naver.com/eakblog/223614659714&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://blog.naver.com/eakblog/223614659714&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;당신의 경험을 플레이하라! EA코리아 2024 동계 체험형 인턴십 모집!&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;2024년 EA코리아 동계 인턴십 모집! 글로벌 게임 회사가 궁금하셨던 분들 모두 주목해 주세요! EA코리...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://blog.naver.com/eakblog/223614659714&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/P02cH/hyYIav58Yn/VuSo37AOMKAxMcYro1NST1/img.jpg?width=743&amp;amp;height=3715&amp;amp;face=0_0_743_3715&quot; data-og-url=&quot;https://blog.naver.com/eakblog/223614659714&quot;&gt;&lt;a href=&quot;https://blog.naver.com/eakblog/223614659714&quot; target=&quot;_blank&quot; data-source-url=&quot;https://blog.naver.com/eakblog/223614659714&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/P02cH/hyYIav58Yn/VuSo37AOMKAxMcYro1NST1/img.jpg?width=743&amp;amp;height=3715&amp;amp;face=0_0_743_3715')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;당신의 경험을 플레이하라! EA코리아 2024 동계 체험형 인턴십 모집!&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;2024년 EA코리아 동계 인턴십 모집! 글로벌 게임 회사가 궁금하셨던 분들 모두 주목해 주세요! EA코리...&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;blog.naver.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;서류 전형 &amp;gt; 코딩테스트 &amp;gt; 1차 인터뷰 &amp;gt; 2차 인터뷰를 거쳐 최종합격을 할 수 있었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1Nq4j/btsNAcMY9Yg/sB8P4fkd8YgqF6vAuYLhz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1Nq4j/btsNAcMY9Yg/sB8P4fkd8YgqF6vAuYLhz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1Nq4j/btsNAcMY9Yg/sB8P4fkd8YgqF6vAuYLhz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1Nq4j%2FbtsNAcMY9Yg%2FsB8P4fkd8YgqF6vAuYLhz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;674&quot; height=&quot;84&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&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;내게 맞는 역할과 앞으로의 커리어&lt;/b&gt;에 대해 고민해볼 기회를 갖고 싶었다. 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;서버 개발자도 Product, Platform 엔지니어 등으로 나뉘는 경우가 있다고 알고 있지만 추상적으로 아는 상태였다.&lt;/li&gt; 
   &lt;li&gt;커리어의 방향성에 대한 고민을 한다면 실제로 먼저 길을 밟아온 분들의 조언을 들으면 의미가 깊을 것이라 생각했다.&lt;/li&gt; 
  &lt;/ul&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;br&gt;결론적으로 1번에 대한 기대는 충분히 목표를 달성했으며, 2번도 실 서비스의 서버가 아닌 어드민 서버를 개발하였다 하더라도 목표는 달성했다고 생각한다. 3번은 아쉽지만 어드민 서버 개발로 만족해야 했다.&lt;br&gt;&amp;nbsp;&lt;br&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;&lt;b&gt;아이템 대량 개봉 어드민 개발&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;가장 주요했던 작업이다. 2달 중에 앞뒤 한 주씩을 제외한 모든 일정에 이 작업을 진행했다.&lt;br&gt;개발 과정 전반은 다음 포스트에서 확인할 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;인프라 구조를 바꾸거나 설정 값을 바꾸기 어려워 API 서버가 자체 로드밸런싱 하는 시스템을 설계하여 개발한 작업이다.&lt;br&gt;&lt;a href=&quot;https://ohksj77.tistory.com/274&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://ohksj77.tistory.com/274&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;아이템 bulk 개봉과 API 서버의 자체 로드밸런싱&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;로드밸런서가 아닌 API 서버에서 로드밸런싱을 수행할 수 밖에 없었던 작업 내용을 공유합니다.&amp;nbsp;요구사항아이템(선수팩, 랜덤박스 등)의 실제 개봉 확률을 의도한 확률과 비교하여 검증하고자 &quot; data-og-host=&quot;ohksj77.tistory.com&quot; data-og-source-url=&quot;https://ohksj77.tistory.com/274&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lzosg/hyYH9cUVpM/ZlT2xYOV9nAk0Ksm8pjm60/img.png?width=583&amp;amp;height=743&amp;amp;face=0_0_583_743,https://scrap.kakaocdn.net/dn/vu8OB/hyYMhARd2z/eZmKOjQR1axpPSBcJK4uX0/img.png?width=583&amp;amp;height=743&amp;amp;face=0_0_583_743,https://scrap.kakaocdn.net/dn/cOW4dL/hyYMZGEMpV/DCNENheWtuguQEP3WgZS31/img.png?width=583&amp;amp;height=743&amp;amp;face=0_0_583_743&quot; data-og-url=&quot;https://ohksj77.tistory.com/274&quot;&gt;&lt;a href=&quot;https://ohksj77.tistory.com/274&quot; target=&quot;_blank&quot; data-source-url=&quot;https://ohksj77.tistory.com/274&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lzosg/hyYH9cUVpM/ZlT2xYOV9nAk0Ksm8pjm60/img.png?width=583&amp;amp;height=743&amp;amp;face=0_0_583_743,https://scrap.kakaocdn.net/dn/vu8OB/hyYMhARd2z/eZmKOjQR1axpPSBcJK4uX0/img.png?width=583&amp;amp;height=743&amp;amp;face=0_0_583_743,https://scrap.kakaocdn.net/dn/cOW4dL/hyYMZGEMpV/DCNENheWtuguQEP3WgZS31/img.png?width=583&amp;amp;height=743&amp;amp;face=0_0_583_743')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;아이템 bulk 개봉과 API 서버의 자체 로드밸런싱&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;로드밸런서가 아닌 API 서버에서 로드밸런싱을 수행할 수 밖에 없었던 작업 내용을 공유합니다.&amp;nbsp;요구사항아이템(선수팩, 랜덤박스 등)의 실제 개봉 확률을 의도한 확률과 비교하여 검증하고자 &lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;ohksj77.tistory.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;사실 위 작업이 가장 주요했을 뿐, &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;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;Redis setnx 을 통해 임계영역을 설정한 경험&lt;/b&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;예외 케이스를 최대한 생각하며 on-demand 배치 작업에 안정성을 줄 수 있었다.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;개발 작업도 매우 중요했지만, &lt;b&gt;사전에 설계 개발 문서를 작성&lt;/b&gt;했던 경험과 팀원분들에게 따뜻한 &lt;b&gt;코드 리뷰를 받으며 의견을 주고 받았던 점&lt;/b&gt; 모두 좋은 경험이었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&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;직무 소개 세션과 커리어 세션&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;커리어 스킬 서적을 사내 개발자 분들과 함께 읽으며 앞으로의 커리어 방향성에 대해 고민해볼 수 있어 뜻깊었다.&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;개발자로 한정짓지 않고 다양한 직군의 분들과 커피챗을 할 수 있는 기회가 있어 참여했고, 이런저런 이야기를 나눌 수 있었다.&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그 외에도 다른 팀의 인턴 분들과 자주 이야기를 나누며 서로 응원하며 지냈던게 기억에 남는다. 그리고 같이 옆에서 서버 개발을 하였던 동료 인턴 분 한 분과 때로는 농담도 하고 개발 이야기도 하며 즐겁게 지냈던 부분도 감사했고 정말 기억에 많이 남는다.&lt;br&gt;&amp;nbsp;&lt;/p&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;/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;</description>
      <category>기타/회고</category>
      <author>ohksj77</author>
      <guid isPermaLink="true">https://ohksj77.tistory.com/275</guid>
      <comments>https://ohksj77.tistory.com/275#entry275comment</comments>
      <pubDate>Sat, 26 Apr 2025 17:42:29 +0900</pubDate>
    </item>
    <item>
      <title>아이템 bulk 개봉과 API 서버의 자체 로드밸런싱</title>
      <link>https://ohksj77.tistory.com/274</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;로드밸런서가 아닌 API 서버에서 로드밸런싱을 수행할 수 밖에 없었던 작업 내용을 공유합니다.&lt;/span&gt;&lt;/blockquote&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;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;100만 건의 아이템을 한 번에 개봉하여 로그로 결과를 확인&lt;/b&gt;하고자 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #434343;&quot;&gt;* 아이템 개봉이란 아이템을 사용해 정해진 확률을 기반으로 랜덤한 결과(선수, 재화 등)를 계정에 획득하는 것을 의미합니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;span style=&quot;color: #000000;&quot;&gt;실제 확률을 검증하기 위해 어드민에서 실 서비스에서 사용되는 개봉 메서드를 호출한다.&lt;/span&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; 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-filename=&quot;architecture.png&quot; data-origin-width=&quot;2392&quot; data-origin-height=&quot;755&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czBQ90/btsOLkiAMy6/ksGPlKjn113D93i3No0Q01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czBQ90/btsOLkiAMy6/ksGPlKjn113D93i3No0Q01/img.png&quot; data-alt=&quot;API Gateway는 Consul에서 알맞은 API 정보를 찾아서 MQ 기반의 RPC 통신으로 Admin Server에 요청합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czBQ90/btsOLkiAMy6/ksGPlKjn113D93i3No0Q01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczBQ90%2FbtsOLkiAMy6%2FksGPlKjn113D93i3No0Q01%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;2392&quot; height=&quot;755&quot; data-filename=&quot;architecture.png&quot; data-origin-width=&quot;2392&quot; data-origin-height=&quot;755&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;API Gateway는 Consul에서 알맞은 API 정보를 찾아서 MQ 기반의 RPC 통신으로 Admin Server에 요청합니다.&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;개발 과정 1: 테스트, chunk size, 그리고 두가지 문제점...&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;테스트 1. 메서드 1회 호출에 100만 건을 한 번에 요청하기&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맨 처음에는 다음과 같은 의문이 들었습니다: &lt;b&gt;아이템 개봉 메서드를 최소한으로 수정하여 100만 건을 개봉할 순 없을까?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이에 따라 로컬 환경에서 개봉 메서드의 한 번에 최대 개봉 가능한 개수인 10개 제한만 간단히 분기처리로 우회하여 100만 건을 한 번에 개봉해보았습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;아쉽게도 100만 건을 한 번에 개봉하다 부하가 심해지며 중간에 OOM과 함께 로컬 환경의 서버가 죽어버렸습니다...&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드 내부 로직에서 100만 번 반복하는 부분이 여러번 있었을 것이며 반복 내에서 MongoDB 와 상호작용하고 MQ 에 메시지를 보내는 등 데이터가 많이 오가고 있기 때문에 어쩌면 당연한 결과였습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;테스트 2. 메서드 1회 호출에 chunk 단위 만큼 요청하여 메서드를 반복 호출하기&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;chunk 를 테스트로 산정하여 메서드 호출 시 &lt;b&gt;chunk 단위만큼 요청&lt;/b&gt;하면서 &lt;b&gt;메서드 호출을 반복&lt;/b&gt;하고자 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부하가 문제 없는 정도&lt;/b&gt;의 chunk size 중에 &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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 전체 시간을 고려해 10만 건만 개봉하며 다음과 같은 단계로 chunk size를 찾아갔습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;chunk size 20000 부터는 부하가 심했기 때문에 &lt;b&gt;20000 미만의 size 가 적합&lt;/b&gt;했습니다.&lt;/li&gt;
&lt;li&gt;[10000, 5000, 1000, 500, 100, 50]을 chunk size로 설정하여 시간 여건 상 &lt;b&gt;3번씩만 실행하며 평균 속도를 비교&lt;/b&gt;하여 다음과 같은 적정값을 찾았습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;chunk size : 100&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-style=&quot;style2&quot;&gt;테스트하면서 알 수 있었던 두가지 문제점이 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;첫 번째 문제: 트랜잭션 예외 케이스&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사내에 구현된 트랜잭션은 트랜잭션 도중 동일 MongoDB Document 동시 접근 시 에러를 발생시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아이템 개봉 메서드를 병렬로 호출할 경우 동일한 Document로 동시 접근이 일어나기 때문에 에러가 필연적으로 발생합니다.&lt;br /&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;따라서 chunk size 만큼 메서드를 호출하는 작업을 모두 순차 처리해야 했습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&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;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;하나의 서버에서 100만 건을 chunk 단위로 분할하여 모두 순차 처리한 결과, 부하가 매우 심했습니다.&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;처리 전, 처리 중 사용률 증분&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;Memory: 360MB (8%) -&amp;gt; 2450MB (60%)&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;CPU: 0% -&amp;gt; 100%&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;blockquote data-ke-style=&quot;style3&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;개발 과정 2: 로드밸런서가 아닌 API 서버가 직접 로드밸런싱?!&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;어드민 클라이언트에서의 chunk 단위 반복 요청&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 서버가 모든 chunk를 처리할 수 없으므로 여러 서버가 처리에 참여해야 합니다.
&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;/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;클라이언트가 chunk 단위만큼 요청을 보내며 반복 요청을 하면 좋은 구조인지는 헷갈리지만 문제가 해결될 것으로 예상했습니다.&lt;/li&gt;
&lt;li&gt;이를 테스트하기 위해 Docker로 직접 실제 어드민 서버와 동일하게 CPU, Memory를 할당한 3대의 서버를 빠르게 띄워봤습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;확인 결과 &lt;b&gt;앞단의 API Gateway가 수행하는 RabbitMQ RPC 통신의 queue에 전역으로 prefetch size가 설정&lt;/b&gt;되어 있다는 사실을 알 수 있었으며, 이 prefetch size가 원인임을 유추할 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;다수의 메시지를 Consumer가 한 번에 가져오기 때문에 균등한 분배가 불가하다.&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;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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관리자 페이지에서 요청 시 즉시 수행되어야 하기에 &lt;b&gt;요청 시 bulk 처리용 서버를 새로 띄워서 사용하고 종료하는 방법은 어렵다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하나의 기능만을 위한 서버&lt;/b&gt;를 상시로 실행해두는 것은 &lt;b&gt;예외적인 서버를 두는 것이므로 관리가 어려울 것&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API Gateway 혹은 prefetch size 를 수정&lt;/b&gt;하기에는 오랫동안 전역으로 사용되는 기능과 설정값을 수정하는 것이고 정말 많은 고민이 들어가있을 터인데 &lt;b&gt;어드민 기능 하나 때문에 수정하기에는 부담이 크고 좋은 선택이 아니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&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;h4 data-ke-size=&quot;size20&quot;&gt;API 서버에서 반복 요청하며 로드밸런싱을 수행하도록 구현&lt;/h4&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;blockquote data-ke-style=&quot;style3&quot;&gt;API 서버가 반복 요청하면서 부하에 맞게 처리 여부를 선택할 순 없을까?&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API Gateway가 호출한 API 서버에서 직접 반복 요청하며 로드밸런싱 수행을 시도하기로 결정했습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커넥션을 유지할 이유는 없어서 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;우선 클라이언트에 응답한 이후&lt;span&gt;&amp;nbsp;이러한 처리를 수행하기로 했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;h4 data-ke-size=&quot;size20&quot;&gt;설계 1: 통신 기술 선택&lt;/h4&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;&lt;b&gt;순차 처리&lt;/b&gt;가 필요했으며 &lt;b&gt;하나의 서버만 요청을 받도록&lt;/b&gt; 하기 위해 이미 사용 중인 통신 방법인 &lt;b&gt;RPC를 선택&lt;/b&gt;했습니다.&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;나머지 선택지로는 Event Queue, Redis Pub/Sub 이 있었습니다.&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;서버의 API들은 RPC 기반이었으며 HTTP는 사용하는 곳이 없었기에 배제했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설계 2: 처리 여부 선택 알고리즘&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RPC 요청 큐의 Consumer가 폴링으로 가져오는 구조였기에 요청 보내는 시점에 처리 서버 결정이 불가했습니다.&lt;/li&gt;
&lt;li&gt;따라서 요청 받은 서버가 처리 여부를 선택하는 구조가 필요했으며, 처리 여부를 선택하는 알고리즘 또한 필요했습니다.&lt;/li&gt;
&lt;li&gt;부하 분산이 목적이었기에 &lt;b&gt;가장 최근에 처리한 &lt;span style=&quot;color: #006dd7;&quot;&gt;N&lt;/span&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;N&lt;/span&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;h4 data-ke-size=&quot;size20&quot;&gt;설계 3: 전체 흐름 설계&lt;/h4&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;API Gateway를 지나 API 서버가 요청을 받은 시점 부터의 흐름입니다.&lt;/li&gt;
&lt;li&gt;2개의 RPC기반 API를 구현해 전체 흐름을 구성했으며, API Gateway가 &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;API 1&lt;/span&gt;&lt;/b&gt;을 어드민 서버에 요청하면 해당 요청을 받은 서버가 &lt;b&gt;&lt;span style=&quot;color: #5733b1;&quot;&gt;API 2&lt;/span&gt;&lt;/b&gt; (아래 흐름에서의 RPC request)를 동기적으로 순차 반복 호출하며 진행됩니다.&lt;/li&gt;
&lt;li&gt;[Lock, 요청 ID, 저장소의 count] 등의 로직을 제외하고 확인하시면 좋을 것 같습니다. 다른 고민에 의해 구현한 부분입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;583&quot; data-origin-height=&quot;743&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Uut8Y/btsM8tULofr/L5T9X9KArzOTkvrSLuubzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Uut8Y/btsM8tULofr/L5T9X9KArzOTkvrSLuubzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Uut8Y/btsM8tULofr/L5T9X9KArzOTkvrSLuubzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUut8Y%2FbtsM8tULofr%2FL5T9X9KArzOTkvrSLuubzk%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;539&quot; height=&quot;687&quot; data-origin-width=&quot;583&quot; data-origin-height=&quot;743&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;&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;leftCount: 개봉해야 하는 잔여 아이템 개수&lt;/li&gt;
&lt;li&gt;RPC request: API 서버가 수행하는 &lt;b&gt;&lt;span style=&quot;color: #5733b1;&quot;&gt;API 2&lt;/span&gt;&lt;/b&gt; RPC 요청&lt;/li&gt;
&lt;li&gt;chunkSize: 테스트로 산정한 chunk 사이즈, 100&lt;/li&gt;
&lt;li&gt;response.usedCount: 한 번의 RPC 요청마다 개봉한 아이템 개수이자 &lt;b&gt;&lt;span style=&quot;color: #5733b1;&quot;&gt;API 2&lt;/span&gt;&lt;/b&gt; RPC 응답 데이터&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;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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 서버 본인의 식별자를 각자 어드민 서버 메모리 상에 가지고 있으며 &lt;b&gt;&lt;span style=&quot;color: #5733b1;&quot;&gt;API 2&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의&lt;/span&gt;&amp;nbsp;request와 response 마다 최대&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;N&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;크기의 리스트를 전달하며 진행됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #5733b1;&quot;&gt;API 2 &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청을 받은 서버는 &lt;/span&gt;리스트에 서버 본인의 식별자가 존재하는 경우 처리 가능하지 않다고 판단하고 처리하지 않고 응답합니다.&lt;/li&gt;
&lt;li&gt;처리 가능한 경우 서버 본인의 식별자를 리스트에 넣으며 리스트 크기가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;N&lt;/span&gt;&lt;/b&gt;을 넘어서면 가장 오래된 식별자 하나를 제거합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #5733b1;&quot;&gt;API 2 &lt;/span&gt;&lt;/b&gt;RPC request를 반복하는 &lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;API 1 &lt;/b&gt;&lt;/span&gt;는 서버 식별자 리스트를 (프로그래밍 언어의)변수로 관리하며 RPC를 통해 주고 받습니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;API 1&lt;/b&gt;&lt;/span&gt; 상세 흐름&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1a.png&quot; data-origin-width=&quot;1706&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EYbGI/btsOMQUBOve/10IvNdEAUdsgIGTE8FCsA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EYbGI/btsOMQUBOve/10IvNdEAUdsgIGTE8FCsA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EYbGI/btsOMQUBOve/10IvNdEAUdsgIGTE8FCsA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEYbGI%2FbtsOMQUBOve%2F10IvNdEAUdsgIGTE8FCsA1%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;1706&quot; height=&quot;864&quot; data-filename=&quot;1a.png&quot; data-origin-width=&quot;1706&quot; data-origin-height=&quot;864&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1B.png&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;425&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJM0x1/btsOMsfqZeD/kHdfhQKXs4qkLAbEI59lHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJM0x1/btsOMsfqZeD/kHdfhQKXs4qkLAbEI59lHK/img.png&quot; data-alt=&quot;* 트랜잭션 에러로 인해 동기적으로 동일 시점에 하나의 서버만 API 2 요청을 받아 처리합니다. (병렬 x)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJM0x1/btsOMsfqZeD/kHdfhQKXs4qkLAbEI59lHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJM0x1%2FbtsOMsfqZeD%2FkHdfhQKXs4qkLAbEI59lHK%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;531&quot; height=&quot;229&quot; data-filename=&quot;1B.png&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;425&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;* 트랜잭션 에러로 인해 동기적으로 동일 시점에 하나의 서버만 API 2 요청을 받아 처리합니다. (병렬 x)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #5733b1;&quot;&gt;&lt;b&gt;API 2&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;상세 흐름&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2a.png&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;866&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kIph3/btsOMSSrRWZ/Tk4kqJNylqdK7cW9lpKGP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kIph3/btsOMSSrRWZ/Tk4kqJNylqdK7cW9lpKGP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kIph3/btsOMSSrRWZ/Tk4kqJNylqdK7cW9lpKGP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkIph3%2FbtsOMSSrRWZ%2FTk4kqJNylqdK7cW9lpKGP0%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;1444&quot; height=&quot;866&quot; data-filename=&quot;2a.png&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;866&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2B.png&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kJGgV/btsOK8h0Oxo/eq8lSR64ynDhW85AT0Ma6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kJGgV/btsOK8h0Oxo/eq8lSR64ynDhW85AT0Ma6k/img.png&quot; data-alt=&quot;* 실제로는 개봉해야할 아이템 개수가 chunk_size 보다 작다면 그 만큼만 개봉합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kJGgV/btsOK8h0Oxo/eq8lSR64ynDhW85AT0Ma6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkJGgV%2FbtsOK8h0Oxo%2Feq8lSR64ynDhW85AT0Ma6k%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;507&quot; height=&quot;373&quot; data-filename=&quot;2B.png&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;674&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;* 실제로는 개봉해야할 아이템 개수가 chunk_size 보다 작다면 그 만큼만 개봉합니다.&lt;/figcaption&gt;
&lt;/figure&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;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;이전에 구성해둔 Docker 환경 기반으로 서버 3대를 사용해 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;N&lt;/span&gt;&lt;/b&gt;이 2인 경우를 테스트했으며, 부하 분산 수행하지 않았을 때와 다음과 같이 달랐습니다. (처리 중인 상태의 평균 값 기준 수치입니다.)&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;부하 분산을 수행하지 않았을 때&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&gt;Memory: &lt;b&gt;2450MB (60%)&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;CPU: &lt;b&gt;100%&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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&gt;Memory: &lt;b&gt;800MB (20%)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;CPU: &lt;b&gt;45%&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매우 유의미한 결과를 얻었습니다. &lt;b&gt;100만 건을 처리하는데 문제가 없었습니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가로, &lt;b&gt;9분 내로 100만 건이 처리됨 또한 확인&lt;/b&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;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 고민과 문제해결로 요구사항을 만족시켰으며 &lt;b&gt;단순 기능 개발 이상으로 다음과 같은 의미&lt;/b&gt;가 있었습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기술 관점 의미&lt;/h4&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;/ul&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;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;반복 작업과 한 번에 최대 10건 개봉 가능했던 불편함을 개선&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;기회가 된다면 개선하고 싶은 점&lt;/h4&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;span style=&quot;color: #5733b1;&quot;&gt;API 2&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #5733b1;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에서의 처리 여부 결정 조건의 고도화가 필요하다고 느껴집니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개봉 실패에 대한 에러 핸들링을 더욱 고도화하면 좋을 것이라 생각이 듭니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트-탐구/아이템 가상 bulk 개봉</category>
      <author>ohksj77</author>
      <guid isPermaLink="true">https://ohksj77.tistory.com/274</guid>
      <comments>https://ohksj77.tistory.com/274#entry274comment</comments>
      <pubDate>Fri, 4 Apr 2025 01:30:56 +0900</pubDate>
    </item>
  </channel>
</rss>