<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://duyankim.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://duyankim.github.io/" rel="alternate" type="text/html" /><updated>2026-04-20T10:47:28+09:00</updated><id>https://duyankim.github.io/feed.xml</id><title type="html">Bank-End Chronicles</title><subtitle>우리는 읽은 것으로 만들어지고, 기록되지 않은 것은 기억되지 않는다.</subtitle><author><name>Duyan Kim</name></author><entry><title type="html">2025년 회고</title><link href="https://duyankim.github.io/2026/01/08/TIL83/" rel="alternate" type="text/html" title="2025년 회고" /><published>2026-01-08T20:40:08+09:00</published><updated>2026-01-08T20:40:08+09:00</updated><id>https://duyankim.github.io/2026/01/08/TIL83</id><content type="html" xml:base="https://duyankim.github.io/2026/01/08/TIL83/"><![CDATA[<p>연말이 순식간에 지나가고, 2026년의 초입에 2025년 회고를 남겨둔다.</p>

<h1 id="liked-좋았던-점">Liked (좋았던 점)</h1>
<blockquote>
  <p>프로젝트나 작업 중에 좋았던 점이나 성과를 나타내요.</p>
</blockquote>

<h2 id="일">일</h2>
<p>올해는 3월말에 프로젝트 오픈을 하고, 이후 운영업무를 처음으로 경험했다.<br />
주변 동료들과 개발자들로부터 SI일을 하는 것도 좋지만, 운영을 해보아야 한다는 피드백을 많이 들었기에 운영이 내심 궁금했다.<br />
궁금함과 동시에, 대략 15인 넘는 인원이 개발한 제품을 내가 모든 것을 파악하고 즉각적으로 bugfix를 할 수 있을까? 하는 걱정도 함께, 운영까지 함께 하게 되었다.</p>

<p>설계부터 함께한 프로젝트가 현장에서 제 할일을 다하며 누군가에게 소중하고 중요한 역할을 하는 것을 지켜보면서<br />
우리가 설계부터 목표로하고 바라던 것이 현실이 되었구나 새삼 느낀다.</p>

<p>bugfix와 feature 개발, 프로토콜 변화에 따른 대응을 4월 ~ 12월간 하면서 개발단계 때 보다도 더 안정성을 최우선순위로 고려하면서 개발을 하게 된다.<br />
그리고 실사용자들의 피드백을 들으면서 소프트웨어가 발전하게 된다.<br />
이번 경험이 내게 좋았던 것은 내가 일을 더 자세히 알게 된다는 점, 그리고 조금 더 자신이 생겼다는 점이다.<br />
내가 이 프로그램을 개발하면서 2년간 축적된 경험이 그대로 쌓인 것 같은 느낌이 든다.</p>

<h2 id="삶">삶</h2>
<p>삶의 국면에서 올해 좋았던 점은<br />
집이 멀어서 오가면서 도로에서 쌓인 축적된 피로를 해소하기 위해 독립을 한 점.<br />
내게 주어진 시간 덕에 아침 운동도 하고, 자전거로 출퇴근도 하고, 취미로 기타도 치고, 동료들과 밴드 합주도 하고,<br />
스트레스를 해소하는 활동을 적절히 해서 일 외적으로도 많은 기쁨이 있었다.</p>

<h1 id="learned-배운-점">Learned (배운 점)</h1>
<blockquote>
  <p>프로젝트 동안 새로 배운 것이나 개인적인 성장을 기록해요.</p>
</blockquote>

<h2 id="일-1">일</h2>
<p>올해의 일은 bitbucket과 jenkins를 활용해 배포를 하고, 실제 운영에 올라간 피쳐가 실수 없이 fix되었는지 확인하는 사이클이었다.<br />
오픈 초에 비해서 갈 수록 안정되어서 bug발생이 줄어들고 있음이 보이고, 이제 발전시키고 개선시키는 작업, 그리고 기술 부채를 해결하고 규제에 대응하는 일이 늘어나고 있다.</p>

<p>내가 개발하지 않은 코드도 잘 알기 위해서 코드와 문서를 읽으면서 그림을 그려보면서 이해를 하고 내 것으로 만드는 시간도 있었다.<br />
운영에 들어오기 전에는 내가 저걸 다 습득할 수 있을까?싶었는데 맘만 먹으면 되는 일이었다.<br />
다른 업무들을 들여다보면서 내가 전에 몰랐던 퍼즐들이 맞춰지는 구간도 있었고 (이를테면 전체적인 거래 흐름 같은 것)<br />
이런 배움들이 이썽서 전반적으로 하길 잘했다는 생각을 한다.</p>

<p>우리의 프로그램은 여기서 앞으로 몇 년을 굴러갈까? 지금처럼 15년을 잘 쓰고 난 뒤에 완전히 새로운 프레임워크로 재탄생할까?<br />
때때로 그런 상상을 한다. 기름칠을 잘 하고 부품을 잘 갈아끼우면서 오래 쓰면 좋을텐데 말이다.</p>

<p>일을 하는 방식도 많이 달라졌다. jira단위로 일을 기록하고, 주간 보고 회의를 통해 다같이 모여서 공유하는 식으로 말이다.<br />
이전에는 사실 각자 프로그램 짜야할 게 너무 너무 많은 탓에 그럴 시간이 없었다. 정확한 가이드가 없어서 다들 많이 혼란스러워하기도 했고.</p>

<p>지금은 각자 무슨 업무 하는지 알고 있고 가이드가 명확하다. 일의 방식에서도 배우는 점이 있다.</p>

<h2 id="삶-1">삶</h2>

<p>연차가 올라가서 그런지 가고 싶은 회사에 면접을 볼 기회가 몇 번 있었다.<br />
면접은 아직 내게 어려운 일이지만, 면접을 보기위해서 준비하고 면접을 보고 하는 그 사이클이 나를 성장시켰다.<br />
아 나 이걸 몰랐구나, 이런 역량이 부족하구나 하는 점을 깨닫는다.<br />
그리고 아주 직접적으로 피드백받는 과정이기도 하고, 회사 안에서 알 수 없는 것들을 알 수 있게 되는 점이 좋다.<br />
나도 누군가에게 같이 일하고 싶은 사람이 되길 바라는 중이다…</p>

<h1 id="lacked-부족했던-점">Lacked (부족했던 점)</h1>
<blockquote>
  <p>부족했던 점이나 개선이 필요한 부분을 기록해요.</p>
</blockquote>

<h2 id="일-2">일</h2>
<p>AI를 활용한 바이브코딩 같은걸 좀 더 시도해봤으면 좋을 것 같다.<br />
개발물량이 너무 많을 때는 잘 사용했는데 적을때는 그냥 내가 개발을 하기도 하기 때문..</p>

<h2 id="삶-2">삶</h2>
<p>사이드프로젝트나 개인 공부의 총량을 좀 더 늘렸으면 좋았을 것이다.</p>

<h1 id="longed-for--바람직한-점">Longed for  (바람직한 점)</h1>
<blockquote>
  <p>향후에 기대되는 것이나 바라는 점을 기록해요.</p>
</blockquote>

<p>12월에 시작하는 토스의 서버 개발자 멘토링 프로그램에 선정되어서 멘토링 진행중이다.<br />
나는 이 계기로 한 차례 더 성장했으면 한다. 굳이? 싶은 것들을 해서 개발하는 환경을 개선했으면 한다.<br />
그리고 그런 노력들이 쌓여서 더 나은 사람이 되었으면 한다.</p>

<p>이상으로 2025년의 회고를 마친다. 끝.</p>]]></content><author><name>Duyan Kim</name></author><category term="TIL" /><category term="TIL" /><summary type="html"><![CDATA[연말 회고 (4L 기법을 활용하여)]]></summary></entry><entry><title type="html">DB복제란?</title><link href="https://duyankim.github.io/2025/08/30/DB24/" rel="alternate" type="text/html" title="DB복제란?" /><published>2025-08-30T20:40:08+09:00</published><updated>2025-08-30T20:40:08+09:00</updated><id>https://duyankim.github.io/2025/08/30/DB24</id><content type="html" xml:base="https://duyankim.github.io/2025/08/30/DB24/"><![CDATA[<h1 id="복제란">복제란?</h1>

<p>네트워크로 연결된 여러 장비에 동일한 데이터 복사본을 유지한다는 의미다. <br />
복제에서 모든 어려움은 복제된 데이터의 변경 처리에 있으며, 이것을 <code class="language-plaintext highlighter-rouge">single leader</code>, <code class="language-plaintext highlighter-rouge">multi leader</code>, <code class="language-plaintext highlighter-rouge">leaderless</code>알고리즘으로 처리할 수 있다.</p>

<p>복제시 주요 고려 포인트</p>
<ul>
  <li>동기식 복제 vs 비동기식 복제</li>
  <li>잘못된 복제본을 어떻게 처리할 것인가?</li>
</ul>

<h3 id="데이터-복제가-필요한-이유">데이터 복제가 필요한 이유?</h3>

<ul>
  <li>지리적으로 사용자와 가깝게 데이터를 유지해 지연 시간을 줄인다.</li>
  <li>시스템의 일부에 장애가 발생해도 지속적으로 동작할 수 있게 해 가용성을 높인다.</li>
  <li>읽기 질의를 제공ㅇ하는 장비의 수를 확장해 읽기 처리량을 늘린다.</li>
</ul>

<blockquote>
  <p>복제: 각 장비에 전체 데이터셋의 복사본을 보유할 수 있을 때<br />
파티셔닝(샤딩): 단일 장비에는 너무 큰 데이터셋을 대상으로 할 때</p>
</blockquote>

<h2 id="모든-복제-서버에-모든-데이터가-있다는-사실을-어떻게-보장할-것인가">모든 복제 서버에 모든 데이터가 있다는 사실을 어떻게 보장할 것인가?</h2>

<p>데이터베이스의 모든 쓰기는 몯느 복제 서버에서 처리되어야 한다. 그렇지 않으면 복제 서버는 더 이상 동일한 데이터를 유지할 수 없다.<br />
=&gt; <code class="language-plaintext highlighter-rouge">leader-based replication</code> = <code class="language-plaintext highlighter-rouge">active-passive</code> = <code class="language-plaintext highlighter-rouge">master-slave</code> 구조가 필요한 이유이다.</p>

<h3 id="리더가-팔로워-복제-방식">리더가 팔로워 복제 방식</h3>

<ol>
  <li>복제 서버 중 하나를 <code class="language-plaintext highlighter-rouge">leader</code> = <code class="language-plaintext highlighter-rouge">master</code> = <code class="language-plaintext highlighter-rouge">primary</code>로 지정한다.</li>
  <li>클라이언트가 데이터베이스에 쓰기를 할 때 클라이언트 -&gt; 리더로 요청을 보낸다.</li>
  <li>리더는 먼저 로컬 저장소에 새로운 데이터를 기록한다.</li>
  <li>리더가 로컬 저장소에 새로운 데이터를 기록할 때마다 데이터 변경을 replication log나 change stream의 일부로 <code class="language-plaintext highlighter-rouge">follower</code> = <code class="language-plaintext highlighter-rouge">read replica</code> = <code class="language-plaintext highlighter-rouge">slave</code> = <code class="language-plaintext highlighter-rouge">seconary</code> = <code class="language-plaintext highlighter-rouge">hot standby</code> 에게 전송한다.</li>
  <li>각 팔로워가 리더로부터 로그를 받으면 리더가 처리한 것과 동일한 순서로 모든 쓰기를 적용해 그에 맞게 데이터베이스의 로컬 복사본을 갱신한다.</li>
</ol>

<ul>
  <li>클라이언트가 데이터베이스로부터 읽기를 할 때는 리더 또는 임의 팔로워에게 질의할 수 있다.</li>
  <li>하지만 쓰기는 리더에게만 허용된다. 팔로워는 클라이언트 관점에서는 읽기 전용이다.</li>
  <li>복제 모드는 PostgresQL, MySQL, Oracle Data Guard 같은ㄷ 데이터베이스에 내장된 기능이고, MongoDB같은 일부 비관계형 데이터베이스에서도 사용한다.</li>
  <li>리더 기반 복제는 카프카와 RabbitMQ의 고가용성 큐 같은 분산 메세지 브로커에도 사용된다.</li>
</ul>

<h2 id="동기식으로-복제할-것인가-비동기식으로-복제할-것인가">동기식으로 복제할 것인가? 비동기식으로 복제할 것인가?</h2>

<p>동기식:</p>
<ul>
  <li>팔로워가 쓰기를 수신했는지 확인해줄때까지 리더가 기다린다.</li>
  <li>확인이 끝나면 사용자에게 성공을 보고하고 다른 클라이언트에게 해당 쓰기를 보여준다.</li>
</ul>

<p>비동기식:</p>
<ul>
  <li>리더는 메세지를 전송하지만 팔로워의 응답을 기다리지는 않는다.</li>
</ul>

<h3 id="동기식">동기식</h3>

<p><strong>장점</strong>: 팔로워가 리더와 일관성 있게 최신 데이터 복사본을 가지는 것을 보장한다.<br />
갑자기 리더가 작동하지 ㅇ낳아도 데이터는 팔로워에서 계속 사용할 수 있음을 확신할 수 있다.</p>

<p><strong>단점</strong>: 어떤 문제로 인해 동기 팔로워가 응답하지 않는다면 쓰기가 처리될 수 없다.<br />
리더는 모든 쓰기를 block하고 동기 복제 서버가 다시 사용할 수 있을 때까지 기다려야 한다.</p>

<p>모든 팔로워가 동기식인 상황은 비현실적이다. 임의의한 노드 장애는 전체 시스템을 멈추게 할 수 있다.</p>
<ul>
  <li>현실적으로, 동기식 복제를 사용하려면 보통 팔로워 하나는 동기식으로 하고 그 밖에는 비동기식으로 하는 것이다.</li>
  <li>동기식 팔로워가 사용할 수 없게 되면 비동기식 팔로워 중 하나가 동기식이 된다.</li>
  <li>적어도 두 노드에(리더, 동기팔로워) 최신 데이터 복사본이 있다는 것을 보장하게 된다. 이것이 반동기식<code class="language-plaintext highlighter-rouge">semi-synchronous</code>다.</li>
</ul>

<h3 id="비동기식">비동기식</h3>

<p>보통 리더 기반 복제는 완전히 비동기식으로 구성한다.<br />
<strong>장점</strong>: 모든 팔로워가 잘못되더라도 리더가 쓰기 처리를 계속 할 수 있다.<br />
<strong>단점</strong>: 리더가 잘못되고 복구할 수 없으면 팔로워에게 아직 복제되지 않은 모든 쓰기는 유실되는데, 이것은 쓰기가 클라이언트에게 확인된 경우에도 지속성을 보장하지 않는다.</p>

<p>특히 많은 팔로워가 있거나 지리적으로 분산된 경우 비동기식 복제를 한다.</p>

<h2 id="새로운-팔로워를-설정해서-복제-서버-늘릴-때">새로운 팔로워를 설정해서 복제 서버 늘릴 때</h2>

<p>새로운 팔로워가 리더의 데이터 복제본을 정확히 가지고 있는지 보장하려면?</p>

<ul>
  <li>한 노드에서 다른 노드로 복사하는 것은 충분하지 않다. 데이터는 유동적이기 때문에, 복사 결과가 유효하지 않을 수 있다.</li>
  <li>데이터를 lock 잡아서 디스크 파일을 일관성있게 쓸 수 있긴 하지만 가용성이 저하된다.</li>
</ul>

<h3 id="팔로워-설정을-중단시간-없이-하는-방법">팔로워 설정을 중단시간 없이 하는 방법</h3>

<ol>
  <li>데이터베이스를 lock잡지 않고 리더의 스냅샷을 일정 시점에 가져온다. 대부분의 db는 이런 기능이 있다.</li>
  <li>스냅샷을 새로운 팔로워 노드에 복사한다.</li>
  <li>팔로워는 리더에 연결해 스냅샷 이후 발생한 모든 데이터 변경을 요청한다. 스냅샷이 리더의 복제 로그의 정확한 위치와 연관되어야 한다. (log sequence number, binlog coordinate등으로 부른다)</li>
  <li>팔로워가 스냅샷 이후 데이터 변경의 미처리분(backlog)를 모두 처리햇을 때 따라잡았다고 볼 수 있고, 이때부터 리더의 데이터 변화를 이어서 처리할 수 있다.</li>
</ol>

<h2 id="노드를-중단하고-처리해야-한다면">노드를 중단하고 처리해야 한다면?</h2>

<p>계획된 유지보수로 인한 중단 등을 염두해두고 어떻게 고가용성을 달성할 수 있을 것인가를 탐구한다.</p>

<h3 id="팔로워-장애의-경우---따라잡기를-복구한다">팔로워 장애의 경우 - 따라잡기를 복구한다.</h3>]]></content><author><name>Duyan Kim</name></author><category term="Database" /><category term="SQL" /><category term="DB" /><category term="데이터" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">HA Acitve-acitve, Active Passive</title><link href="https://duyankim.github.io/2025/08/18/Architecture01/" rel="alternate" type="text/html" title="HA Acitve-acitve, Active Passive" /><published>2025-08-18T20:40:08+09:00</published><updated>2025-08-18T20:40:08+09:00</updated><id>https://duyankim.github.io/2025/08/18/Architecture01</id><content type="html" xml:base="https://duyankim.github.io/2025/08/18/Architecture01/"><![CDATA[<h1 id="active-active-vs-active-passive-ha">Active-Active vs. Active-Passive HA</h1>
<p><strong>고가용성(HA)</strong>을 구현하는 방식에는 크게 <em>액티브-액티브(Active-Active)</em>와 <em>액티브-패시브(Active-Passive)</em> 구조가 있다.</p>

<ul>
  <li>액티브-액티브 구성은 여러 노드가 동시에 서비스 트래픽을 처리하여 부하를 분산하고, 한 노드가 장애가 나도 다른 노드들이 계속 서비스를 제공하는 방식이다.</li>
  <li>
    <p>반면 액티브-패시브 구성에서는 한 노드(주 노드)만 업무를 처리하고 다른 노드들은 대기 상태에 있다가 주 노드 장애 시에만 승격되어 서비스를 이어받는다.</p>
  </li>
  <li>액티브-패시브는 장애 복구 시 일정 지연이 발생할 수 있지만 구조가 단순하며, 액티브-액티브는 실시간 부하 분산과 무중단 성능이 뛰어난 대신 구성 복잡성이 높다.</li>
  <li>요구사항(예: RPO/RTO 목표)에 따라 두 방식 중 적절한 HA 구성을 선택해야 한다.</li>
</ul>

<h2 id="active-passive-architecture">Active-Passive Architecture</h2>

<ul>
  <li>액티브-패시브는 Standby 또는 Failover 아키텍처라고도 불린다. 동일한 구성의 여러 서버가 떠 있지만, 그 중 하나만 운영중이고, 만약 가동중인 시스템이 실패했을때 passive가 major로 승격하면서 운영하게 된다.</li>
</ul>

<h3 id="주요한-특징">주요한 특징</h3>

<ul>
  <li>Primary (Active) Server와 Standby (Passive) Server가 있다.</li>
  <li>Heartbeat mechanism이 필요하다. 무응답이나 시스템 문제를 감지할 수 있는 active server에 대한 hearbeat 체크를 할 수 있는 시스템이 필요하다.</li>
</ul>]]></content><author><name>Duyan Kim</name></author><category term="Architecture" /><category term="HA" /><summary type="html"><![CDATA[고가용성 시스템을 설계해보자]]></summary></entry><entry><title type="html">Heartbeat란?</title><link href="https://duyankim.github.io/2025/08/18/Architecture02/" rel="alternate" type="text/html" title="Heartbeat란?" /><published>2025-08-18T20:40:08+09:00</published><updated>2025-08-18T20:40:08+09:00</updated><id>https://duyankim.github.io/2025/08/18/Architecture02</id><content type="html" xml:base="https://duyankim.github.io/2025/08/18/Architecture02/"><![CDATA[<h1 id="heartbeat"><a href="https://en.wikipedia.org/wiki/Heartbeat_(computing)">Heartbeat</a></h1>

<p>Heartbeat란, 분산 시스템 환경 등에서, 서버나 서비스나 어떤 컴포넌트가 정상적으로 작동하고 있는지 주기적으로 체크하는 메세지 교환 방식이다. 지금 현재 실행 중인지, 네트워크에 접속 되어 있는지 node 들에 정기적으로 신호를 보내서 검증하는 방식이다. 일정 기간 안에 하트비트 체크 메세지가 응답이 안 올 경우에 시스템이 실패중이라고 파악할 수 있다.</p>

<h2 id="heartbeat-가-중요한-이유-fault-tolerance-reliablility-availability">Heartbeat 가 중요한 이유: fault tolerance, reliablility, availability</h2>
<ul>
  <li>실패 감지</li>
  <li>헬스 체크</li>
  <li>가용성 있는 컴포넌트를 파악하여 <strong>로드밸런싱</strong>에 활용</li>
  <li>네트워크 파티션 감지하여 잠재적인 네트워크 문제 파악</li>
  <li>일관성 유지</li>
</ul>

<h2 id="heartbeat-message로-어떤-것을-사용할까">heartbeat message로 어떤 것을 사용할까?</h2>
<ul>
  <li>seqence number로 예상한 번호가 응답으로 돌아오는지 확인하는 방법</li>
  <li>node/component의 id값 부여해서 unique id를 확인하는 방법</li>
  <li>timestamp로 freshness 확인하는 방법</li>
  <li>payload/data로 버젼 넘버나 설정 정보, 메세지 받는 쪽이 보내는 쪽의 상태를 체크할 수 있을만한 정보 넘겨주는 방법</li>
  <li>ack 메세지 검증하는 방법</li>
  <li>timeout limit 정해놓는 방법</li>
</ul>

<h2 id="heartbeat-protocol의-종류">Heartbeat Protocol의 종류</h2>

<h3 id="1-simple-heartbeat-protocol">1. Simple Heartbeat Protocol</h3>

<p>간단한 하트비트 메커니즘이 필요한 상황에서, 단순 메세지를 교환하여 정기적으로 노드의 가용성과 활성상태를 파악하는 방식</p>

<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20240313170247/Simple-Heartbeat-Protocol-(SHP).webp" alt="" /></p>

<h3 id="2-pingecho-protocol">2. Ping/Echo Protocol</h3>

<p>한 쪽에서 Ping을 보내고 수신 노드에서 echo 응답을 기다리는 것을 뜻하며<br />
네트워크 수준의 통신 (ICMP)를 사용하여 구현되고, 프로세스 간 통신의 경우 일반적으로 사용자 정의 애플리케이션 계층의 프로토콜을 사용하여 구현된다.<br />
네트워크 환경에서 보통 기본적인 연결 확인 및 상태 모니터링에 자주 사용된다.</p>

<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20240318093301/Ping-Echo-Protocol-(1).webp" alt="" /></p>

<h3 id="3-udp-기반-heartbeat-protocol">3. UDP 기반 Heartbeat Protocol</h3>

<p>UDP기반 노드간 통신을 원활하게 하기 위해, 일반적으로 하트비트 메세지가 포함된 가벼운 UDP 패킷을 주기적으로 전송한다. <br />
UDP 기반 하트비트 프로토콜은 낮은 지연 시간과 낮은 오버헤드가 요구되는 상황에 적합하다.</p>

<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20240313170325/UDP-based-Heartbeat-Protocol.webp" alt="" /></p>

<h3 id="4-tcp-기반-heartbeat-protocol">4. TCP 기반 Heartbeat Protocol</h3>

<p>TCP기반 노드들이 서로 메세지를 보낸다.</p>

<h3 id="5-apache-zookeeper-heartbeats">5. Apache ZooKeeper Heartbeats</h3>

<p>Apache Zookeeper 하트비트는 분산 환경에서 세션관리와 리더 선출에 쓰인다.</p>

<h6 id="reference">Reference</h6>

<p><img src="https://www.geeksforgeeks.org/system-design/what-are-heartbeat-messages/" alt="What are Heartbeat Messages?" /></p>]]></content><author><name>Duyan Kim</name></author><category term="Architecture" /><category term="Heartbeat" /><summary type="html"><![CDATA[고가용성 시스템을 설계해보자]]></summary></entry><entry><title type="html">Logging Deep Dive</title><link href="https://duyankim.github.io/2025/07/10/Spring33/" rel="alternate" type="text/html" title="Logging Deep Dive" /><published>2025-07-10T20:40:08+09:00</published><updated>2025-07-10T20:40:08+09:00</updated><id>https://duyankim.github.io/2025/07/10/Spring33</id><content type="html" xml:base="https://duyankim.github.io/2025/07/10/Spring33/"><![CDATA[<h1 id="스프링에서-로깅이란">스프링에서 로깅이란</h1>

<ul>
  <li>스프링은 원래 JCL <code class="language-plaintext highlighter-rouge">Jakarta  Commons Logging</code>을 사용해서 로깅을 했다고 한다.</li>
</ul>

<h2 id="jcl-commons-logging">JCL (commons-logging)</h2>

<ul>
  <li>JCL은 로깅 라이브러리가 아니라 로깅 추상화 라이브러리인데, 로깅 라이브러리의 선택권은 애플리케이션 개발자에게 있다.</li>
  <li>라이브러리나 프레임워크는 주로 로깅 추상화 라이브러리를 사용한다.</li>
  <li>JCL의 로깅 구현체를 찾는 방법은,
    <ul>
      <li>설정 파일에서 찾고</li>
      <li>애플리케이션 클래스패스에서 Log4j구현체를 찾아보고</li>
      <li>애플리케이션이 JDK 1.4에서 구동중인지 확인하고</li>
      <li>아무것도 못 찾으면 기본 구현체를 사용한다.</li>
    </ul>
  </li>
</ul>

<h3 id="jcl의-문제점">JCL의 문제점</h3>

<ul>
  <li>JCL이 구현체를 선택하는 시점이 <strong>런타임</strong>이라 클래스 로더에 의존적이다.</li>
  <li>따라서 가비지 컬렉션이 제대로 작동하지 않는 치명적인 문제점이 있다.</li>
  <li>문제 해결을 위해 클래스 로더 대신에 <strong>컴파일 시점</strong>에 구현체를 선택하도록 변경하면서 도입된 것이 <strong>SLF4J</strong>이다.</li>
</ul>

<h2 id="slf4j-simple-logging-facade-for-java"><a href="https://www.slf4j.org/manual.html">SLF4J</a> (Simple Logging Facade For Java)</h2>

<blockquote>
  <p>Facade란? 복잡한 시스템을 감추고 단순한 인터페이스 하나로 제공하는 방식.<br />
여러 로깅 구현체들의 복잡한 구조를 가려주고 공통된 껍데기만 보여주기 때문에 Simple Logging Facade인 것!</p>
</blockquote>

<ul>
  <li>SLF4J는 라이브러리 인터페이스 역할이고, <strong>Logback</strong>이 실질적인 로깅 라이브러리 구현체다.
    <ul>
      <li>각각 다른 로깅 라이브러리를 사용한 라이브러리들을 통합해서 로깅을 하고 싶은 목적에서 SLF4J를 사용해 인터페이스는 통일하되, 구현체를 갈아끼우는 형태인 것이다.</li>
    </ul>
  </li>
  <li>로깅 구현체를 런타임이 아닌 <strong>컴파일 타임</strong>에 정한다.</li>
  <li>세가지 모듈 Bridging, API, Binding을 제공한다.</li>
  <li>
    <p>SLF4J는 클래스 별로 Logger를 사용한다.</p>
  </li>
  <li>SLF4J의 이점: SLF4J 추상화를 사용하면 코드상에서는 org.slf4j.Logger 인터페이스만 의존하므로, 실제 로깅 프레임워크를 Logback에서 Log4j2로 교체하더라도 애플리케이션 코드 변경 없이 의존성 설정만 바꿔치기 하면 된다. 즉, SLF4J는 각기 다른 로깅 시스템을 통일된 방식으로 사용할 수 있게 해주는 계층이며, 바인딩 모듈이 그 다리를 놓아주는 원리이다.</li>
</ul>

<h3 id="staticloggerbinder">StaticLoggerBinder</h3>

<p>SLF4J는 내부적으로 StaticLoggerBinder라는 클래스를 통해 실제 로깅 구현체에 바인딩한다.<br />
각 로깅 구현 라이브러리(Logback, Log4j2 등)는 자신만의 StaticLoggerBinder를 제공하며, SLF4J API 호출시 이 바인더가 해당 구현체의 LoggerFactory를 반환하여 결과적으로 그 구현체를 이용해 로그를 기록한다.<br />
예를 들어 Logback의 경우 logback-classic JAR에 StaticLoggerBinder가 내장되어 SLF4J가 Logback의 LoggerContext에 연결되고, Log4j2를 사용하려면 log4j-slf4j-impl 같은 JAR를 추가하여 SLF4J가 Log4j2의 로그 시스템에 연결되도록 한다.</p>

<ul>
  <li>하나의 바인딩 선택: SLF4J 환경에서는 반드시 하나의 로거 바인딩만 클래스패스에 존재해야 한다. 동시에 둘 이상의 구현체가 존재하면 어떤 것을 써야 할지 모호해지므로 경고 또는 오류가 발생한다.</li>
</ul>

<h2 id="logback"><a href="https://logback.qos.ch/manual/architecture.html">Logback</a></h2>

<h3 id="로그를-구성하는-3요소-logger-appenders-layouts">로그를 구성하는 3요소 Logger, Appenders, Layouts</h3>

<ul>
  <li>Logger는 로그를 만들어주는 요소
    <ul>
      <li>Logger = logback-classic module</li>
    </ul>
  </li>
  <li>Appender는 로그를 어디에 보낼지 결정하는 요소 =&gt; 여러 appender를 동시에 등록 가능</li>
  <li>Layout은 보내기 전에 어떤 형식으로 로깅할지 결정하는 요소
    <ul>
      <li>Appender, Layout = logback-core</li>
    </ul>
  </li>
</ul>

<h4 id="logger">Logger</h4>

<p>모든 로거는 원하는 로거를 매개변수로 입력받아서 LoggerFactory를 이용해 생성한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Logger</span> <span class="n">rootLogger</span> <span class="o">=</span> <span class="nc">LoggerFactory</span><span class="o">.</span><span class="err">​</span><span class="n">getLogger</span><span class="o">(</span><span class="n">org</span><span class="o">.</span><span class="na">slf4j</span><span class="o">.</span><span class="na">Logger</span><span class="o">.</span><span class="err">​</span><span class="no">ROOT_LOGGER_NAME</span><span class="o">);</span>
</code></pre></div></div>

<p>로거는 아래와 같은 레벨로 사용할 수 있으며, default는 DEBUG이다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">org.slf4j</span><span class="o">;</span> 
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">Logger</span> <span class="o">{</span>

  <span class="c1">// Printing methods: </span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">trace</span><span class="o">(</span><span class="nc">String</span> <span class="n">message</span><span class="o">);</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">debug</span><span class="o">(</span><span class="nc">String</span> <span class="n">message</span><span class="o">);</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">info</span><span class="o">(</span><span class="nc">String</span> <span class="n">message</span><span class="o">);</span> 
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">warn</span><span class="o">(</span><span class="nc">String</span> <span class="n">message</span><span class="o">);</span> 
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">error</span><span class="o">(</span><span class="nc">String</span> <span class="n">message</span><span class="o">);</span> 
<span class="o">}</span>
</code></pre></div></div>

<p>log의 적용 규칙은 아래와 같다.</p>

<blockquote>
  <p>TRACE &lt; DEBUG &lt; INFO &lt;  WARN &lt; ERROR</p>
</blockquote>

<h5 id="loggercontext">LoggerContext</h5>

<p>로깅 시스템의 전체 설정과 Logger 인스턴스를 관리하는 컨테이너이자 중심 객체</p>

<ul>
  <li>모든 Logger와 Appender, Layout, 설정 상태를 담고 관리하는 중앙 컨트롤러</li>
  <li>
    <p>로거 등록, 설정 로딩, Appender 연결, 로깅 트리 계층 관리 등 전반적인 상태 및 생명주기 관리</p>
  </li>
  <li>내부적으로 LoggerContext를 통해 Logback을 제어하지만, 개발자는 보통 application.yml이나 logback-spring.xml만 수정하면 된다.</li>
</ul>

<h4 id="appender">Appender</h4>

<p>로그 메세지를 받아서 콘솔, 파일, DB, 원격서버 등 원하는 곳으로 출력하는 역할이다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Logger] → [Appender] → 출력 대상(Console, File, Socket, DB 등)
                   ↘ [Layout] 포맷 설정
</code></pre></div></div>

<h3 id="로깅을-일관되게-남기는-방법">로깅을 일관되게 남기는 방법</h3>

<h4 id="계층적으로-로그-남기기">계층적으로 로그 남기기</h4>

<p>보통 로거는 클래스 혹은 패키지명을 기준으로 생성되며, 로거 이름에 계층적 구조가 반영된다.</p>

<ul>
  <li>com.helloworld.moduleA.ClassA에서 발생한 로그는 com.helloworld.moduleA 패키지에 속하게 된다. 따라서 상위 패키지별로 로그 레벨을 통일하면 하위 모듈들까지 동일한 레벨이 적용되어 전체 호출 흐름을 한 수준에서 관찰할 수 있다.</li>
  <li>
    <p>여러 모듈이 서로 다른 패키지명을 가진다면, 설정 파일에서 각 모듈의 패키지에 대해 동일한 로그 레벨을 지정하여 균일한 출력이 이루어지도록 하자.</p>
  </li>
  <li>로거가 패키지 계층 구조를 따르고 있다면 상위 패키지의 레벨만 변경해도 하위가 따라가기 때문에 로그 레벨 전파 고려해야 한다.
    <ul>
      <li>모듈 A -&gt; B -&gt; C로 이어지는 호출 흐름에서, A에서 DEBUG 레벨 로그를 보고자 한다면 B와 C에서도 DEBUG 레벨 로그가 나와야 흐름을 완전히 이해할 수 있다. 따라서 특정 기능을 디버깅할 때는 관련된 모든 모듈의 로그 레벨을 일괄적으로 올리는 것이 좋다.</li>
    </ul>
  </li>
</ul>

<h4 id="mdcmapped-diagnositc-context-활용하기">MDC(Mapped Diagnositc Context) 활용하기</h4>

<ul>
  <li>
    <p>MDC를 사용하면 로그 이벤트에 공통 상호 연관 ID를 심어줄 수 있다.</p>
  </li>
  <li>
    <p>요청 시작 시 각 모듈의 코드에서 <code class="language-plaintext highlighter-rouge">MDC.put("traceId", 어떤값)</code>을 설정하고 로그 패턴에 <code class="language-plaintext highlighter-rouge">%X{traceId}</code>를 포함하면, 여러 모듈에 걸친 호출도 동일한 추적 ID를 로그에 남길 수 있다. 이렇게 하면 서로 다른 모듈이라도 같은 요청 흐름에서 발생한 로그를 식별하고 연결지어 볼 수 있어 연결성 있는 로그 분석이 가능하다.</p>
  </li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">org.apache.logging.log4j.ThreadContext</span><span class="o">;</span>

<span class="nc">ThreadContext</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"userId"</span><span class="o">,</span> <span class="s">"admin"</span><span class="o">);</span> <span class="c1">// 이 요청은 DEBUG 로그까지 출력됨</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s">"This is debug log for admin."</span><span class="o">);</span>
</code></pre></div></div>

<h5 id="dynamicthresholdfilter">DynamicThresholdFilter</h5>

<ul>
  <li>DynamicThresholdFilter는 Log4j 2에서 제공하는 동적 로그 필터 중 하나로, MDC값을 기반으로 로그 레벨 임계값을 다르게 적용할 수 있도록 도와주는 필터다.</li>
  <li>즉, 사용자/세션/요청 등의 MDC 키값에 따라 로그 레벨을 유동적으로 다르게 설정하고 싶을 때 사용한다.</li>
</ul>

<p>일반적인 로그 설정은 패키지명이나 클래스 기준으로 레벨을 고정하지만 다음과 같은 경우에는 요청이나 사용자 단위로 레벨을 다르게 적용하고 싶을 수 있다.</p>

<ul>
  <li>예시 상황:
운영 환경에서 전체 로그 레벨은 INFO로 유지하되,</li>
</ul>

<p>특정 사용자나 특정 요청에 대해서만 DEBUG 로그를 활성화하고 싶은 경우</p>

<p>이럴 때 DynamicThresholdFilter를 사용하면 MDC 값에 따라 로그 레벨 임계값을 조절할 수 있어 유용하다.</p>

<ul>
  <li>작동원리 : 로그 출력 시점에 MDC(예: traceId, userId, sessionId 등) 값을 확인하고 해당 키 값에 대해 미리 정의한 로그 레벨 기준과 비교, 설정된 기준보다 낮은 로그는 무시되고, 기준 이상인 로그만 출력된다.</li>
</ul>

<p>사용예시 log4j2.xml</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Filters&gt;</span>
  <span class="nt">&lt;DynamicThresholdFilter</span> <span class="na">key=</span><span class="s">"userId"</span> <span class="na">defaultThreshold=</span><span class="s">"INFO"</span> <span class="na">onMatch=</span><span class="s">"ACCEPT"</span> <span class="na">onMismatch=</span><span class="s">"DENY"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;KeyValuePair</span> <span class="na">key=</span><span class="s">"admin"</span> <span class="na">value=</span><span class="s">"DEBUG"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;KeyValuePair</span> <span class="na">key=</span><span class="s">"tester"</span> <span class="na">value=</span><span class="s">"TRACE"</span><span class="nt">/&gt;</span>
  <span class="nt">&lt;/DynamicThresholdFilter&gt;</span>
<span class="nt">&lt;/Filters&gt;</span>
</code></pre></div></div>

<ul>
  <li>key=”userId”: MDC에 저장된 userId 값을 기준으로 필터링</li>
  <li>admin 유저 → <code class="language-plaintext highlighter-rouge">DEBUG</code> 이상만 출력</li>
  <li>tester 유저 → <code class="language-plaintext highlighter-rouge">TRACE</code> 이상만 출력</li>
  <li>그 외는 기본 <code class="language-plaintext highlighter-rouge">INFO</code> 이상만 출력</li>
  <li><code class="language-plaintext highlighter-rouge">onMismatch="DENY"</code>: 기준 미달인 로그는 출력하지 않음</li>
</ul>

<h3 id="logback이-로깅을-하는-flow-예시">Logback이 로깅을 하는 flow 예시</h3>

<pre><code class="language-mermaid">flowchart TD
    A["Logger.info() 호출 (예: com.wombat)"] --&gt; B["1. TurboFilter 체인 실행&lt;br/&gt;(Marker, Level, Logger, 메시지, Throwable 기반 필터링)"]
    
    B --&gt;|FilterReply.DENY| Z1["로그 요청 DROP (종료)"]
    B --&gt;|FilterReply.ACCEPT| D
    B --&gt;|FilterReply.NEUTRAL| C["2. Basic Selection Rule 적용&lt;br/&gt;(Logger의 effective level vs 요청 level 비교)"]

    C --&gt;|레벨 불일치 Disabled| Z2["로그 요청 DROP (종료)"]
    C --&gt;|레벨 허용| D["3. LoggingEvent 객체 생성&lt;br/&gt;(Logger, Level, 메시지, 예외, 시간, 스레드, 클래스 정보, MDC 등)"]

    D --&gt; E["4. Appender 호출&lt;br/&gt;(Logger context로부터 상속된 모든 Appender에 doAppend 호출)"]
    
    E --&gt; E1["AppenderBase.doAppend() 동작:&lt;br/&gt;- synchronized 블록으로 Thread-safe 처리&lt;br/&gt;- Appender 전용 Filter 적용"]
    E1 --&gt; F["5. 출력 포맷팅&lt;br/&gt;(Layout을 사용해 LoggingEvent → 문자열 변환, 또는 직렬화)"]

    F --&gt; G["6. 로그 전송&lt;br/&gt;(Appender별 목적지: 콘솔, 파일, 소켓, DB 등)"]

    style Z1 fill:#ffdddd,stroke:#dd0000
    style Z2 fill:#ffdddd,stroke:#dd0000
</code></pre>

<ul>
  <li>TurboFilter: Context 전역 또는 조건부 필터링 (Marker, Level, Logger 등)</li>
  <li>Basic Selection Rule: 로거의 유효 레벨과 요청 레벨 비교</li>
  <li>LoggingEvent 생성: 요청 관련 모든 정보 + MDC</li>
  <li>Appender 호출: Thread-safe, Appender 전용 필터 가능</li>
  <li>포맷팅: Layout 사용 or 직접 직렬화</li>
  <li>전송: Appender 목적지로 출력</li>
</ul>

<h3 id="로그-레벨-독립-관리-시-주의사항">로그 레벨 독립 관리 시 주의사항</h3>

<ul>
  <li>로그 레벨에 따른 성능에 대해 생각하기
    <ul>
      <li>로그 레벨을 <code class="language-plaintext highlighter-rouge">DEBUG</code>/<code class="language-plaintext highlighter-rouge">TRACE</code>처럼 자세하게 설정하면 시스템 성능에 영향을 줄 수 있다. 상세 로그는 출력량 증가뿐만 아니라, 로그 메시지를 구성하는 연산 비용을 초래하는데, 예를 들어 문자열 연결이나 복잡한 메시지 생성이 로그 레벨 체크 없이 수행되면 불필요한 연산이 생긴다. 이를 완화하기 위해 SLF4J에서는 {} 플레이스홀더를 사용하는 지연 평가를 제공하지만, 경우에 따라 여전히 toString()같은 연산이 일어날 수 있다.</li>
      <li>logger.isDebugEnabled()로 레벨을 확인하고 나서 로그를 만드는 방법을 선택하면 불필요한 연산을 줄일 수 있다.</li>
    </ul>
  </li>
  <li>보안 및 민감 정보 노출
    <ul>
      <li>로그 레벨을 세분화하면 자칫 민감 정보가 노출될 위험이 있다. DEBUG 수준에서는 평소 출력되지 않던 내부 정보나 개인 정보까지 기록될 수 있으므로, 서비스별로 로그 레벨을 조정할 때 개인정보나 기밀 데이터가 로그로 남지 않도록 주의해야 한다. 특히 운영 중 동적 변경을 허용한 경우, 해당 기능(예: Actuator의 /loggers 엔드포인트 등)에 대한 접근 통제를 철저히 해야 한다. 예컨대 2021년 말에 발견된 Log4j2의 심각한 취약점(Log4Shell)은 로그 메시지 처리를 악용한 것이므로, Log4j2를 사용한다면 최신 보안 패치 버전으로 업그레이드하여 이러한 known 이슈를 해소해야 한다.</li>
    </ul>
  </li>
  <li>설정 충돌 및 일관성
    <ul>
      <li>하나의 JVM 내에서 복수의 로깅 프레임워크를 혼용하면 충돌이 발생하거나 의도치 않은 동작이 나올 수 있다. SLF4J 바인딩은 한 번에 하나만 존재해야 하므로, 각 서비스별로 어떤 로깅 구현을 쓸지 명확히 하고 불필요한 의존성은 제거해야 한다.</li>
      <li>예를 들어, Spring Boot 기반 서비스 A는 Logback을, 서비스 B는 Log4j2를 쓴다면 각각 종속성이 달라질 것이고, 한 서비스 내에서 두 가지를 함께 쓰지 않도록 해야 한다. 또한 여러 서비스가 한 서버에서 동작하며 공용 자원(예: 동일한 로그 파일 경로나 포트 등)을 사용한다면, 로그 설정이 서로 침범하지 않게 유의해야 한다. 서비스별로 로그 파일을 구분하거나, 컨테이너 환경이라면 콘솔 출력만 하고 중앙 로그 수집기로 모으는 편이 안전하다.</li>
      <li>환경별 설정 충돌도 피해야 한다. 예컨대 개발 환경에서는 특정 패키지를 DEBUG로 하고 운영에서는 INFO로 하는 등 프로파일별 설정을 관리하되, 이를 깜빡하고 운영에 디버그를 남겨두는 실수를 조심해야 한다.</li>
    </ul>
  </li>
</ul>]]></content><author><name>Duyan Kim</name></author><category term="Framework" /><category term="Spring" /><category term="Spring" /><summary type="html"><![CDATA[Logger? Logback? Slf4j?]]></summary></entry><entry><title type="html">분산 데이터베이스의 역사</title><link href="https://duyankim.github.io/2025/06/17/DB23/" rel="alternate" type="text/html" title="분산 데이터베이스의 역사" /><published>2025-06-17T20:40:08+09:00</published><updated>2025-06-17T20:40:08+09:00</updated><id>https://duyankim.github.io/2025/06/17/DB23</id><content type="html" xml:base="https://duyankim.github.io/2025/06/17/DB23/"><![CDATA[<h1 id="분산-데이터베이스의-역사와-핵심-개념">분산 데이터베이스의 역사와 핵심 개념</h1>

<p><strong>소개:</strong> 요즘의 애플리케이션은 대용량 데이터를 처리하고 고가용성을 유지하기 위해 <strong>분산 데이터베이스</strong> 구조를 채택하는 경우가 많다. 오늘 분산 DB에 주요 내용은 데이터베이스 <strong>복제</strong>(replication)의 종류와 이슈, <strong>Oracle RAC</strong>와 같은 클러스터링 기법, <strong>샤딩</strong>(sharding)을 통한 수평 확장, <strong>프록시 시스템</strong>(예: MariaDB MaxScale)의 역할, 분산 DB의 <strong>일관성</strong> 및 <strong>내결함성</strong> 개념, 그리고 <strong>MySQL Cluster</strong>의 구조 등을 포함한다.</p>

<h2 id="데이터베이스-복제replication">데이터베이스 복제(Replication)</h2>

<p><strong>데이터베이스 복제</strong>는 한 데이터베이스의 변경 내용을 다른 데이터베이스에 복사하여 여러 곳에 데이터를 유지하는 기술이다. 이를 통해 하나의 서버 장애에도 데이터가 사라지지 않도록 하거나, 읽기 부하를 여러 DB로 분산해 성능을 높일 수 있다. 복제에는 <strong>단방향(일대다) 복제</strong>와 <strong>양방향(다중 마스터) 복제</strong>가 있고, 데이터 변경 전파 방식에 따라 <strong>동기식</strong>과 <strong>비동기식</strong> 복제로 나뉜다.</p>

<h3 id="단방향-복제-마스터-슬레이브">단방향 복제 (마스터-슬레이브)</h3>
<ul>
  <li>하나의 <strong>주 데이터베이스(마스터)</strong>에서만 쓰기 연산이 이루어지고, 그 변경사항이 하나 이상의 <strong>보조 데이터베이스(슬레이브)</strong>로 복사되는 구조<br />
<img src="https://diadem.in/wp-content/uploads/2022/11/mysql-replication.png" alt="replication1" /></li>
  <li>위 그림은 클라이언트의 <strong>쓰기 요청</strong>은 마스터로 보내고, <strong>읽기 요청</strong>은 슬레이브들이 처리하도록 분산하는 구조를 보여준다.</li>
  <li>이 구성에서는 모든 데이터 변경이 한 곳에서 일어나므로 충돌 문제가 없고 구현이 비교적 간단하며, 슬레이브를 추가해 읽기 성능과 장애 대비를 향상시킬 수 있다는 장점이 있다.</li>
  <li>마스터-슬레이브 복제의 대표적인 활용 사례로는 <strong>읽기 부하 분산</strong>(예: 조회가 많은 서비스에서 슬레이브를 두어 읽기 처리)과 <strong>백업/고가용성</strong>(마스터 장애 시 슬레이브를 승격) 등이 있다.</li>
</ul>

<h3 id="양방향-복제-멀티-마스터">양방향 복제 (멀티-마스터)</h3>
<ul>
  <li>두 대 이상의 데이터베이스가 <strong>동등한 마스터</strong>로서 서로에게 변경 내용을 복제하는 방식<br />
<img src="https://severalnines.com/sites/default/files/blog/node_4698/image00.png" alt="replication2" /></li>
  <li>각 노드가 독립적으로 쓰기 작업을 받아들이고, 그 변경을 다른 마스터들에게 전달하여 <strong>모든 마스터가 최종적으로 동일한 상태</strong>를 갖도록 한다.</li>
  <li>이 구조의 이점은 <strong>쓰기를 분산</strong>함으로써 단일 노드에 집중되는 부하를 줄이고, 여러 지역에 마스터를 두어 <strong>지연 시간을 단축</strong>하거나 <strong>고가용성</strong>을 높일 수 있다는 점.</li>
  <li>하지만, 여러 노드에서 동일 데이터에 동시 수정이 발생하면 <strong>충돌 발생</strong> 가능성이 있으며, 이를 해결하기 위한 <strong>컨플릭트 해소 절차</strong>가 필요한다. 예를 들어 두 개의 마스터가 동시에 같은 레코드를 서로 다른 값으로 수정하면 <strong>“스플릿 브레인(split brain)”</strong> 상황이 되어 데이터가 불일치하게 된다.</li>
  <li>실제 사례로 한 쪽 마스터에서는 값 10으로, 다른 쪽에서는 30으로 변경했다면 어느 값이 옳은지 모호해집니다. 이러한 문제를 피하려면 <strong>최종 기록 승리(LWW)</strong> 같은 간단한 규칙을 적용하거나, Paxos/Raft와 같은 <strong>합의 프로토콜</strong>로 한 노드를 리더로 정해 <strong>쿼럼</strong> 합의를 거쳐 커밋하는 방식으로 일관성을 보장한다. 일반적으로 멀티-마스터 복제는 구현 복잡도와 충돌 해결 부담 때문에 필요한 경우에만 신중히 사용하는 것이 권장된다.</li>
</ul>

<h3 id="동기식-vs-비동기식-복제">동기식 vs 비동기식 복제</h3>
<ul>
  <li>복제는 데이터 변경을 <strong>얼마나 즉각적으로 다른 노드에 적용하느냐</strong>에 따라 동기식과 비동기식으로 구분된다.</li>
</ul>

<h4 id="동기식-복제"><strong>동기식 복제</strong></h4>
<ul>
  <li>마스터에서 트랜잭션을 커밋할 때 <strong>모든 복제 대상에게 변경사항이 기록되었다는 확인을 받은 후에</strong> 커밋을 완료하는 방식.</li>
  <li>예를 들어 두 노드 간 동기 복제에서는 한 노드에 쓰기가 발생하면 다른 노드에 해당 변경이 적용되고 <strong>확인 응답을 돌려받은 뒤에야</strong> 첫 노드의 트랜잭션이 완료된다.</li>
  <li>이러한 방식은 <strong>강한 일관성</strong>을 보장하여 한 노드 장애 시에도 데이터가 유실되지 않는 장점이 있지만, 매 쓰기마다 네트워크 왕복 지연이 추가되므로 <strong>쓰기 지연(latency)</strong> 이 늘어나는 단점이 있다.</li>
</ul>

<h4 id="비동기식-복제"><strong>비동기식 복제</strong></h4>
<ul>
  <li>마스터가 <strong>자신의 변경을 커밋한 후</strong>에야 다른 노드로 복제를 진행한다.</li>
  <li>마스터는 일단 로컬에 쓰기 성공을 응답하고, 그 다음에 별도 스레드가 변경 로그를 슬레이브로 전송한다. 이 경우 마스터의 성능과 응답 속도는 좋아지지만, 만약 <strong>마스터가 변경을 복제하기 전에 장애</strong>가 발생하면 해당 변경이 <strong>슬레이브에 적용되지 못하고 유실될 위험</strong>이 있다.</li>
  <li>예를 들어 어떤 업데이트가 마스터에 커밋된 직후 마스터가 다운되면, 슬레이브에는 그 업데이트가 전달되지 못했기 때문에 <strong>데이터 불일치</strong>가 생기고 해당 업데이트는 사라지게 된다.</li>
  <li>비동기 복제 환경에서는 이러한 데이터 손실을 수초 이내로 줄이는 것이 목표지만, 시스템 부하나 네트워크 상태에 따라 <strong>Replication Lag</strong>(복제 지연)이 커지면 최악의 경우 <strong>수분 이상의 업데이트</strong>가 한꺼번에 잃어버릴 위험도 존재한다.</li>
  <li>따라서 <strong>세미 동기식</strong>처럼 중간 해법(일부 슬레이브 확인만 대기)이나, 멀티-마스터 구조에서는 <strong>충분히 많은 노드에 기록된 후 성공으로 간주</strong>(쿼럼)하는 기법 등으로 타협하기도 한다.</li>
</ul>

<h3 id="복제-활용의-장단점">복제 활용의 장단점</h3>

<ul>
  <li>
    <p><strong>장점:</strong></p>

    <ul>
      <li><strong>고가용성 향상:</strong> 한 노드에 장애가 나도 다른 복제본으로 자동 <strong>페일오버</strong> 가능 (서비스 연속성).</li>
      <li><strong>읽기 성능 확장:</strong> 슬레이브로 읽기 부하를 분산하여 <strong>스케일 아웃</strong> 가능.</li>
      <li><strong>백업 용이:</strong> 실시간 복제로 <strong>백업</strong>을 유지하므로 데이터 복구 용이.</li>
    </ul>
  </li>
  <li>
    <p><strong>단점:</strong></p>

    <ul>
      <li><strong>일관성 지연:</strong> 비동기 복제의 경우 마스터-슬레이브 간 <strong>데이터 반영에 지연</strong>이 발생, 일시적 불일치가 생길 수 있음.</li>
      <li><strong>추가 지연 및 부하:</strong> 동기 복제 시 <strong>쓰기 지연 증가</strong> 및 시스템 부하 증가.</li>
      <li><strong>복잡성 증가:</strong> 다중 마스터나 다수 슬레이브 환경에서는 <strong>충돌 해결</strong>이나 <strong>복제 지연 관리</strong> 등의 복잡성이 추가됨.</li>
      <li><strong>Replication Storm:</strong> 큰 규모의 클러스터에서, 새로운 노드 추가나 네트워크 이상으로 <strong>대량의 복제 트래픽이 발생</strong>하면 전체 시스템에 부하를 주는 <em>복제 폭풍</em> 현상이 발생할 수 있다. 이런 상황에서는 복제 대기열이 급격히 쌓이고 네트워크가 포화되어 응답 지연이 커지며, 심한 경우 복제 지연이 더 누적되는 악순환이 생길 수 있다. 복제 폭풍은 보통 대규모 배포 환경에서 문제 되지만, <strong>증분 복제</strong> 조정이나 쓰로틀링으로 완화하도록 설계한다.</li>
    </ul>
  </li>
</ul>

<h4 id="split-brain-위험">Split-Brain 위험</h4>
<ul>
  <li>멀티-마스터 구성에서는 네트워크 단절 등이 발생해 <strong>두 개 이상의 노드가 자신을 “마스터”로 잘못 인식</strong>하는 상황이 생길 수 있다.</li>
  <li>이를 <em>스플릿 브레인</em>이라 하며, 데이터가 서로 다른 경로에서 동시에 변경되어 <strong>데이터 불일치</strong>로 이어집니다.</li>
  <li>이러한 문제를 피하려면 <strong>리더 선출</strong>(후술)로 한 순간에는 하나의 리더만 쓰기를 허용하거나, 충돌 발생 시 <strong>최신 타임스탬프 우선</strong>과 같은 정책으로 자동 조정하는 기능이 필요하다. 하지만 자동 정책은 잘못 적용하면 데이터 손실을 야기할 수 있으므로, <strong>Paxos/Raft 같은 합의 알고리즘</strong>을 통해 다수결 합의를 거쳐 단일 결정을 내리는 방식이 많이 사용된다.</li>
  <li>예를 들어 <strong>과반수 노드</strong>의 승인을 받아야 쓰기가 확정되도록 하면, 네트워크 분할 시 소수 파티션은 더 이상 쓰기를 받지 못하게 되어 하나의 일관된 쪽만 리더 역할을 하게 된다.</li>
</ul>

<blockquote>
  <p>요약하면, 복제는 분산 데이터베이스의 <strong>기본적인 고가용성/확장성 수단</strong>으로, 단방향 복제를 통해 손쉽게 읽기 성능을 높이거나 백업을 확보할 수 있고, 양방향 복제를 통해 여러 곳에서 쓰기를 처리할 수 있다. 다만 <strong>일관성-성능 트레이드오프</strong>(동기 vs 비동기)와 <strong>충돌 해결</strong>이라는 과제가 있으므로 시스템 요구사항에 맞게 신중히 설계해야 한다.</p>
</blockquote>

<h2 id="oracle-rac와-공유-디스크-클러스터링">Oracle RAC와 공유 디스크 클러스터링</h2>

<p><img src="https://github.com/user-attachments/assets/c9321a91-71ec-4e9c-be11-9bea747ec158" alt="image" /></p>

<h3 id="oracle-racreal-application-clusters">Oracle RAC(Real Application Clusters)</h3>

<ul>
  <li>오라클 데이터베이스에서 제공하는 클러스터링 솔루션으로, <strong>여러 대의 DB 서버가 하나의 공용 데이터베이스를 동시에 액세스</strong>하는 <strong>공유 디스크(shared-disk)</strong> 구조</li>
  <li>쉽게 말해, 여러 DB 인스턴스가 동일한 저장소(SAN 등의 공용 디스크)에 있는 데이터를 공동으로 읽고 쓰면서 하나의 데이터베이스처럼 동작하는 것.</li>
  <li>RAC의 목표는 <strong>인스턴스 수준의 고가용성</strong>과 <strong>확장성</strong>을 제공하는 것으로, 한 노드에 장애가 발생해도 다른 노드가 계속 서비스를 제공하고, 노드를 추가하여 성능을 높일 수 있다.</li>
</ul>

<h4 id="rac의-장점">RAC의 장점</h4>

<ul>
  <li>여러 DB 인스턴스가 동시에 운영되므로 <strong>노드 중 한 곳에 장애</strong>가 생겨도 서비스가 중단되지 않고 지속된다 (자동 페일오버). 이는 <strong>무중단 운영</strong>이 중요한 은행권 등의 핵심 시스템에 유용하다.</li>
  <li><strong>읽기 처리 능력 향상:</strong> 각 노드가 같은 데이터를 볼 수 있으므로 읽기 요청을 여러 서버로 분산해 처리할 수 있다. CPU와 메모리를 수평 확장(scale-out)하여 <strong>읽기 스루풋</strong>을 높일 수 있다.</li>
  <li>일정 범위 내에서는 노드를 추가하면 선형에 가까운 성능 향상을 얻을 수 있다고 하며, Oracle 공식 문헌상 <strong>최대 100개 노드</strong>까지 클러스터링을 지원한다.</li>
</ul>

<h4 id="rac의-한계와-비용">RAC의 한계와 비용</h4>

<ul>
  <li><strong>공유 저장소 병목:</strong> RAC의 근간은 모든 노드가 <strong>공동의 디스크</strong>에 읽고 쓴다는 점인데, 초대형 서비스 규모(Web-scale)에서는 이 중앙 저장소(SAN)가 <strong>성능 병목</strong>이 되고 전체 클러스터의 <strong>싱글 포인트 실패 지점</strong>이 될 수 있다.
    <ul>
      <li>Oracle은 이를 완화하기 위해 초고속 스토리지와 <strong>Cache Fusion</strong>(노드간 메모리 캐시 공유) 등의 기술을 쓰지만, 근본적으로 <strong>쓰기 작업은 한 데이터베이스 파일을 놓고 노드들이 락을 주고받으며 조정해야 하므로</strong> 노드가 늘어날수록 <strong>효율이 떨어질 수 있다</strong>. 실제로 “RAC로 노드를 많이 늘려도 결국 디스크나 락 경합 때문에 쓰기 성능은 늘어나지 않는다”는 업계 평가도 있다.</li>
    </ul>
  </li>
  <li><strong>복잡성 및 비용:</strong> RAC를 구축하려면 고성능의 공유 스토리지 인프라와 노드들 간의 초저지연 <strong>클러스터 네트워크(interconnect)</strong> 가 필요하며, Oracle 라이선스 비용도 매우 높다. 또한 설정과 운영이 복잡해서 전문 지식이 요구된다. 잘못 구성하면 오히려 <strong>단일 인스턴스보다 불안정</strong>해질 수 있고, 락 경합이나 캐시 동기화 문제로 <strong>지연 시간</strong>이 늘어나기도 한다.</li>
  <li><strong>확장성 한계:</strong> RAC는 <strong>수평 확장</strong>(scale-out)의 형태이지만, 완전한 공유-낫싱 시스템만큼 자유롭게 노드를 늘려 무한 확장하는 개념과는 다르다. 일반적으로 2~4개 노드 구성으로 <strong>고가용성</strong>을 확보하는 용도로 많이 쓰이고, 노드 수가 많아질수록 오버헤드가 커지기 때문에 10개 이상의 대규모 RAC는 드물다. Oracle도 권장사항으로 애플리케이션이 RAC 환경에 최적화되어야 성능 효율을 얻을 수 있다고 말한다.</li>
  <li>하나의 거대한 머신을 이용하려면 비용이 많이 든다는 것도 문제다.</li>
</ul>

<blockquote>
  <p>요약하면, Oracle RAC는 <strong>공유 디스크 기반 클러스터링</strong>의 대표 격으로서, <strong>고가용성과 일정 수준의 확장성</strong>을 얻기 위한 솔루션이다. 대기업 전사 시스템처럼 <strong>다운타임을 최소화해야 하는 경우</strong>나, 단일 호스트가 감당 못 할 정도로 CPU/메모리를 늘리고 싶을 때 고려된다. 
다만 <strong>구축/운영의 복잡성과 높은 비용</strong> 때문에, 웹 스타트업 등에서는 잘 사용하지 않으며 대신 이후 설명할 <strong>샤딩</strong> 같은 공유-낫싱 방식이나 클라우드 분산 DB를 선호한다. (실제로 Oracle 자체도 Web-scale 서비스에서는 RAC보다는 <strong>Data Guard</strong> (복제)나 <strong>Sharding 옵션</strong> 등을 권장하는 추세다.)</p>
</blockquote>

<h2 id="샤딩sharding--수평-분할을-통한-확장">샤딩(Sharding) – 수평 분할을 통한 확장</h2>

<ul>
  <li>대용량 데이터를 다루는 분산 시스템에서 흔히 사용하는 <strong>수평 파티셔닝(horizontal partitioning)</strong> 기법으로, <strong>테이블의 행(row)을 여러 분할로 나누어 각각 별도의 데이터베이스 인스턴스에 저장</strong>하는 방법이다.</li>
  <li>쉽게 말해, 하나의 거대한 DB를 여러 대의 작은 DB로 쪼개어 담는 것.</li>
  <li>각 샤드(분할된 DB 조각)는 동일한 스키마(테이블 구조)를 갖지만 저장하는 <strong>데이터 범위는 서로 다르고 중복되지 않으며</strong>, 모든 샤드의 데이터를 합치면 원래 전체 데이터집합이 된다.</li>
  <li>
    <p><strong>하나의 데이터베이스를 다수의 노드로 쪼개 분산</strong>함으로써 얻는 주된 이점은 <strong>수평 확장성</strong>과 <strong>비용 효율성</strong>.</p>
  </li>
  <li>일반적으로 샤드를 나누는 기준으로 <strong>샤드 키(shard key)</strong>를 사용하며, 예를 들어 <strong>사용자 ID 범위</strong>나 해시값 등을 기준으로 각 샤드에 할당한다. 이렇게 하면 각 샤드는 샤드 키에 대응되는 데이터만 저장하게 된다.</li>
  <li>이때 <strong>분산키 선택이 중요</strong>한데, 주로 <strong>특정 속성 값 범위나 해시</strong>를 기준으로 균등하게 나누어 <strong>모든 샤드에 부하가 고르게 분산</strong>되도록 한다.</li>
  <li>샤딩은 데이터가 <strong>쉐어드-낫싱(shared-nothing)</strong> 구조로 완전히 분리되므로, 한 샤드의 장애가 전체 시스템을 바로 마비시키지 않고 (해당 샤드 데이터에만 영향) 확장 시에도 다른 샤드에 영향이 적다.</li>
</ul>

<h3 id="샤딩을-사용하는-이유와-장점">샤딩을 사용하는 이유와 장점</h3>

<ul>
  <li><strong>무한에 가까운 수평 확장성:</strong> 단일 머신의 한계를 뛰어넘어 <strong>노드를 추가함으로써 저장용량과 처리량을 계속 늘려갈 수 있다</strong>.</li>
  <li>예를 들어 DB가 너무 커지면 더 큰 서버로 수직 확장하는 대신, 샤드를 하나 추가하고 데이터를 분산함으로써 선형적인 확장이 가능하다.</li>
  <li><strong>저렴한 비용으로 규모 확장:</strong> 특수한 대형 장비나 고가의 범용 서버 대신, <strong>일반 상용 하드웨어(commodity hardware)</strong> 여러 대를 조합해 대용량 데이터를 다룰 수 있으므로 <strong>가격 대비 성능</strong>이 좋다. 즉, 규모가 커질수록 비싼 고사양 한 대보다 저렴한 서버 여러 대로 구성하는 편이 경제적이다.</li>
  <li><strong>향상된 성능:</strong> 샤딩하면 각 샤드가 <strong>전체 데이터의 일부만</strong> 담고 있으므로, <strong>개별 쿼리의 대상 데이터 양이 줄어들어</strong> 조회 성능이 향상될 수 있다. 예를 들어 1억 건 테이블을 10개 샤드로 나누면 각 샤드는 1천만 건만 갖게 되어, 특정 샤드에 질의할 때 스캔해야 할 행 수가 줄어드는 효과가 있다. 또한 샤드들이 병렬로 쿼리를 처리하면 <strong>병렬 처리 효과</strong>도 얻을 수 있다.</li>
  <li><strong>지리적 분산 및 레이턴시 감소:</strong> 필요에 따라 각 샤드를 <strong>다른 데이터센터나 지역에 배포</strong>할 수 있다. 예를 들어 사용자 지역별로 샤드를 분리해 해당 지역에 가까운 서버에 위치시킴으로써, 그 지역 유저들의 <strong>지연 시간을 단축</strong>할 수 있다.</li>
  <li><strong>장애 격리:</strong> 한 샤드가 문제를 일으켜도 다른 샤드에는 영향이 덜 가므로, <strong>Fault Isolation</strong> 측면에서 유리하다. 전체 데이터의 일부분만 장애 영향권에 들기 때문에, 부분 장애로 서비스의 나머지 부분은 지속 운영할 수도 있다.</li>
</ul>

<h3 id="샤딩시-고려해야-할-단점과-도전과제">샤딩시 고려해야 할 단점과 도전과제</h3>

<ul>
  <li><strong>애플리케이션 복잡도 증가:</strong> 샤딩을 도입하면 <strong>어떤 데이터가 어느 샤드에 있는지</strong>를 애플리케이션이 인지하고 처리해야 한다. 응용 단에서 샤드 키를 기준으로 <strong>쿼리 라우팅 로직</strong>을 구현하거나, 모든 쿼리에 샤드 키 조건을 포함시키는 등의 개발 부담이 생긴다. (만약 이러한 로직을 애플리케이션 대신 해주는 미들웨어나 프록시를 사용한다면 복잡도를 위임할 수는 있다.)</li>
  <li><strong>운영 복잡성:</strong> 샤드 수가 늘어나면 관리해야 할 <strong>데이터베이스 인스턴스가 그만큼 많아지므로</strong>, 모니터링, 백업, 스키마 변경 등의 운영 작업이 복잡해진다. 예를 들어 테이블 스키마를 변경하려면 모든 샤드에 적용해야 하고, 샤드가 100개면 100개의 DB에 일관성 있게 변경을 반영해야 한다.</li>
  <li><strong>Cross-Shard 질의 제한:</strong> 샤딩 환경에서는 <strong>조인이나 글로벌 집계 등의 쿼리가 샤드를 넘어서</strong> 수행되어야 하는 경우 어려움이 있다. 한 샤드에 필요한 데이터가 모두 있으면 문제가 없지만, <strong>여러 샤드의 데이터를 모아야 하는 질의</strong>는 애플리케이션 계층에서 <strong>병렬 질의 후 결과를 합치는 로직</strong>을 짜거나 해야 한다. (일부 분산 SQL 솔루션은 프록시가 내부적으로 해주기도 하지만 일반적으로 복잡성이 높다.)</li>
  <li><strong>불균형 및 재샤딩 이슈:</strong> 초기에는 데이터가 균등하게 분산되도록 샤드를 나누어도, 시간이 지나면서 특정 샤드에 데이터가 몰리거나(예: 핫스팟) 부하가 치우칠 수 있다. 이 경우 <strong>샤드 재조정(resharding)</strong> 이나 분할이 필요해진다. 실시간으로 데이터를 이동시키면서 샤드 구조를 변경하는 것은 운영상 까다로운 작업이다.</li>
  <li><strong>트랜잭션 제약:</strong> 데이터가 서로 다른 샤드에 있으면 <strong>다중 샤드 간의 ACID 트랜잭션</strong>을 보장하기 어렵다. 분산 트랜잭션(2PC)를 도입하면 성능 저하와 복잡성이 급격히 커지기 때문에, 대부분의 샤딩 환경에서는 한 트랜잭션이 가급적 <strong>단일 샤드 내에서만 일어나도록</strong> 데이터 모델을 제한한다.</li>
  <li><strong>참조 데이터 중복:</strong> 모든 샤드에 공통으로 필요한 작은 테이블(예: 국가 코드표 등)은 <strong>각 샤드에 복제</strong>하여 저장하기도 한다. 이 경우 데이터 일관성 유지를 위해 변경 시 모든 샤드 업데이트가 필요하다.</li>
</ul>

<h2 id="샤딩-구현-방식">샤딩 구현 방식</h2>

<p>샤딩은 구현 방법에 따라 <strong>드라이버/애플리케이션 레벨</strong>과 <strong>프록시/미들웨어 레벨</strong>로 나눌 수 있다.</p>
<ul>
  <li><strong>애플리케이션 레벨 샤딩</strong>: 말 그대로 응용 프로그램 코드가 샤드 구분 로직을 담고 직접 각 DB 샤드에 질의를 보내는 방식.
    <ul>
      <li>예를 들어 <strong>사용자 ID % N</strong>으로 샤드를 결정하고, 각 쿼리를 보낼 DB 커넥션을 코드에서 선택하는 식</li>
      <li>이 방식은 단순하지만 언어별로 따로 구현해야 하고, 애플리케이션이 DB 구조를 많이 알아야 하기 때문에 유지보수 부담이 있다.</li>
    </ul>
  </li>
  <li><strong>프록시 레벨 샤딩</strong>: 애플리케이션은 하나의 가상 DB에 질의하듯이 하면, <strong>중간의 프록시 시스템</strong>이 쿼리를 받아 샤드로 라우팅해주는 방식.
    <ul>
      <li>프록시가 샤드 키를 분석해 해당 샤드로 질의를 전달하고, 각 샤드의 응답을 취합해 애플리케이션에 결과를 반환.</li>
      <li>프록시 레벨의 장점은 언어 독립적으로 동작하여 <strong>모든 애플리케이션에 투명하게 샤딩을 제공</strong>할 수 있다는 점.</li>
      <li>예로 <strong>Apache ShardingSphere-Proxy</strong>나 <strong>Vitess</strong>, <strong>MySQL Router</strong>, <strong>ProxySQL</strong> 등이 이러한 역할을 수행.</li>
    </ul>
  </li>
</ul>

<h3 id="프록시-시스템과-데이터베이스-접근-mariadb-maxscale-등">프록시 시스템과 데이터베이스 접근 (MariaDB MaxScale 등)</h3>

<p>분산 DB 환경에서는 애플리케이션과 여러 DB 노드 사이에 <strong>데이터베이스 프록시(DB proxy)</strong> 를 두어 접근을 제어하는 경우가 있다. 프록시는 쉽게 말해 <strong>클라이언트의 요청을 받아 적절한 DB 서버로 중계해주는 중간 계층</strong> 이다. 이러한 시스템을 사용하면 애플리케이션은 마치 단일 데이터베이스에 접속하듯이 프록시에게 질의하고, 프록시가 내부적으로 <strong>부하 분산</strong>, <strong>읽기/쓰기 분리</strong>, <strong>장애 조치(failover)</strong> 등을 수행하여 <strong>클러스터를 투명하게 관리</strong>해준다.</p>

<h4 id="mariadb-maxscale">MariaDB MaxScale</h4>

<p>대표적인 예로 <strong>MariaDB MaxScale</strong>을 들 수 있다.</p>
<ul>
  <li>MaxScale은 <strong>MySQL/MariaDB 전용 스마트 프록시</strong>로 개발되어, 클라이언트로부터 오는 SQL 쿼리를 파싱한 후 <strong>백엔드의 여러 DB 서버로 forwarding</strong>하는 역할을 한다.</li>
  <li>예를 들어 <strong>SELECT 쿼리는 슬레이브 노드들로 보내고, INSERT/UPDATE 등 쓰기 쿼리는 마스터 노드로 보내는</strong> 식으로 <strong>규칙 기반의 라우팅</strong>을 수행할 수 있다.</li>
  <li>또한 <strong>백엔드 서버들의 상태를 모니터링</strong>해서 마스터 장애 발생 시 자동으로 새로운 마스터로 연결을 돌리는 등 <strong>고가용성 기능</strong>도 제공한다.</li>
  <li>이러한 모든 처리는 애플리케이션 레벨에서는 보이지 않기 때문에, 개발자는 여러 DB 인스턴스를 일일이 관리하는 복잡성을 느끼지 않고 <strong>하나의 논리 DB에 접속하듯</strong> 코딩하면 된다.</li>
</ul>

<p>다른 예로 <strong>ProxySQL</strong>, <strong>MySQL Router</strong>(MySQL 공식 프록시) 등이 있으며, PostgreSQL 계열에는 <strong>PgBouncer</strong>, <strong>HAProxy</strong> 등을 조합해 사용하기도 한다. 또한 앞서 언급한 <strong>샤딩 프레임워크</strong>들도 프록시 역할을 수행하는 경우가 있다.</p>

<h3 id="프록시-시스템의-주요-기능">프록시 시스템의 주요 기능</h3>

<ul>
  <li><strong>부하 분산 (Load Balancing):</strong> 다수의 DB 노드들 사이에 쿼리를 분산시켜 특정 노드에 쏠리지 않도록 한다. 예를 들어 MaxScale의 <code class="language-plaintext highlighter-rouge">readwritesplit</code> 모드는 쓰기는 마스터로, 읽기는 슬레이브들로 자동 분배한다.</li>
  <li><strong>고가용성 및 장애 자동처리:</strong> 프록시는 백엔드 노드를 지속적으로 헬스 체크하여, 노드 장애 시 <strong>자동으로 연결을 다른 노드로 돌려주거나</strong> (Failover) 장애 노드를 서비스 풀에서 제외한다. 이를 통해 클라이언트는 별도 재시도 로직 없이도 장애에 대응할 수 있다.</li>
  <li><strong>연결 관리와 보안:</strong> 많은 애플리케이션 연결을 프록시가 받아 단일 또는 최소 연결로 백엔드에 연결하므로 <strong>DB 커넥션 자원</strong>을 효율적으로 사용할 수 있고, SQL 방화벽이나 감사(logging) 등의 정책을 프록시에서 적용할 수 있다.</li>
  <li><strong>쿼리 라우팅 / 변환:</strong> 샤딩된 환경에서는 프록시가 샤드 키를 분석해 <strong>해당 샤드로 쿼리를 전달</strong>하거나, 특정 DB로만 보내야 하는 쿼리를 식별해 처리한다. 일부 프록시는 읽기 전용 쿼리를 캐시하거나, 지연이 큰 노드에 대한 쿼리를 지양하는 등의 <strong>스마트 라우팅</strong>을 하기도 한다.</li>
</ul>

<p>물론 모든 경우에 프록시가 필요한 것은 아니다. 단순 Master-Slave 구조에서 <strong>클라이언트 드라이버가 여러 호스트를 지원</strong>하는 경우(예: 일부 드라이버는 읽기/쓰기 분리를 자체 지원), 혹은 어플리케이션에서 로드밸런싱 로직을 구현한 경우에는 프록시 없이도 운용이 가능하다. 하지만 <strong>이기종 언어/플랫폼 지원</strong>, <strong>운영 정책 중앙화</strong> 등의 이유로 중간 프록시를 두는 것이 관리에 용이한 경우가 있다.</p>

<p>한 마디로, 프록시 시스템은 <strong>분산 데이터베이스 환경의 접착제</strong> 역할을 하여, 여러 노드로 구성된 데이터 계층을 애플리케이션에는 <strong>마치 하나의 일관된 데이터베이스처럼 보이게 해주는</strong> 매우 유용한 도구다.</p>

<h2 id="분산-db의-일관성과-내결함성-consistency--fault-tolerance">분산 DB의 일관성과 내결함성 (Consistency &amp; Fault Tolerance)</h2>

<p>분산 데이터베이스에서는 <strong>데이터의 일관성(consistency)</strong> 과 <strong>내결함성(fault tolerance)</strong> 을 유지하는 것이 핵심 과제다. 분산 DB에서는 <strong>어떤 노드도 전체 데이터의 완전한 사본을 갖고 있지 않지만</strong>, <strong>각 데이터 조각은 충분한 중복 복제나 코딩으로 여러 노드에 걸쳐 저장되므로</strong> 일부 노드가 손실되어도 데이터를 잃지 않도록 한다.</p>

<h4 id="cassandra-예시">Cassandra 예시</h4>

<ul>
  <li>예를 들어, NoSQL 분산 DB인 <strong>Apache Cassandra</strong>의 경우 <strong>데이터를 일관된 해시 분배</strong>를 통해 클러스터 노드들에 분산 저장하며, <strong>하나의 데이터 항목을 여러 노드에 복제</strong>해 둔다.</li>
  <li><strong>복제 계수(replication factor)</strong> 를 3으로 설정했다면, 각 데이터 행(row)은 해시 링 상에서 정의된 3개의 노드에 저장되고 모든 노드는 자신이 담당하는 데이터의 복제본들을 유지한다.</li>
  <li>그 결과, <strong>한 노드가 다운되더라도 해당 노드에 있던 모든 데이터의 사본이 다른 두 노드에 남아</strong> 있어 데이터 손실이 니다. (Cassandra에서는 <strong>모든 복제본이 동등하며</strong> 특별한 마스터 노드가 없고, 설정한 복제 개수 만큼 각 데이터가 분산 저장된다.)</li>
  <li>따라서 4개 노드 클러스터에 복제 계수 3으로 운용하면, 겉보기에는 “각 노드가 전체 데이터의 75% 가량을 담고 있다”고 할 수 있지만, 사실은 <strong>각 데이터가 3개의 노드에 중복되어 있고 어느 하나의 25% 부분도 잃지 않도록 설계</strong>된 것이다.</li>
</ul>

<h4 id="cockroachdb-예시">CockroachDB 예시</h4>

<ul>
  <li><strong>CockroachDB</strong> 와 같은 NewSQL 분산 DB도 데이터를 작은 <strong>범위 단위(range)</strong> 로 쪼개어 <strong>다수 노드에 복제</strong>해 저장한다.</li>
  <li>CockroachDB는 내부적으로 <strong>Raft 합의 알고리즘</strong>을 사용하여, 각 데이터 범위(range)의 복제본들 중 하나를 <strong>리더(leaseholder)</strong> 로 선출하고 그를 통해 모든 쓰기/읽기가 이루어지게 하여 <strong>강한 일관성</strong>을 보장한다.</li>
  <li>기본 설정으로 <strong>3중 복제</strong>를 하여, 한 노드(리더) 장애 시 나머지 복제본 중 하나가 새로운 리더로 자동 승계되어 계속 서비스를 제공한다.</li>
  <li>CockroachDB의 경우 다수결(쿼럼) 쓰기가 기본이므로, <strong>3개 중 2개 노드만 살아있어도 데이터베이스 운영이 가능</strong>하지만(한 노드 장애 허용), 반대로 <strong>전체 노드 중 과반수 이상이 응답 불능이면 쓰기를 중단하여 일관성을 지킨다</strong> (CP 지향).</li>
</ul>

<p>정리하면, <strong>각 노드는 전체 데이터의 일부만을 저장</strong>하지만, <strong>데이터의 각 단위는 여러 노드에 겹쳐 저장</strong>되므로 <strong>어느 한 노드가 사라져도 데이터가 보전</strong> 된다. 이러한 <strong>중복성</strong>을 확보하는 방식으로 <strong>동일한 데이터를 여러 복제본으로 단순 저장</strong>하는 방법도 있고, <strong>erasure coding(소위 패리티)</strong> 기법을 활용하는 방법도 있다.</p>

<h2 id="복제replication"><strong>복제(replication)</strong></h2>

<ul>
  <li>가장 흔한 방식으로, <strong>데이터를 그 자체로 여러 노드에 복사</strong>해 두는 것.</li>
  <li>복제본 수를 늘릴수록 내결함성은 높아지지만 저장 공간이 그만큼 추가로 필요하다. Cassandra, CockroachDB, MongoDB 등 대부분의 분산 DB가 이 방식을 사용한다.</li>
  <li>예를 들어 <strong>Replication Factor = 노드 수</strong>로 설정하면 모든 노드가 전체 데이터를 갖게 되어 강한 내결함성을 얻지만, 저장/갱신 비용이 많이 들 것이다.</li>
  <li><strong>Erasure Coding(패리티 분산)</strong>: 일부 시스템은 데이터를 그대로 복제하지 않고, RAID 5/6와 유사하게 <strong>데이터를 조각내고 패리티 정보를 분산 저장</strong>한다.
    <ul>
      <li>예를 들어 원본 데이터를 3조각으로 나눠 서로 다른 3노드에 저장하고, 추가로 1개 패리티 조각을 4번째 노드에 저장하면, 네 노드 중 어느 하나가 사라져도 남은 조각들로 원본을 복원할 수 있다.</li>
      <li>이 경우 <strong>각 노드가 전체 데이터의 75%만 갖는</strong> 셈이지만, <strong>1개 노드 분량의 오버헤드(25%)</strong>로 내결함성을 확보하는 셈.</li>
      <li>Erasure coding은 저장 효율은 높이지만 복원 연산이 상대적으로 복잡하고 성능에 영향이 있어, 주로 대용량 분산 스토리지(HDFS, Ceph 등)에서 사용되고 <strong>실시간 DB 트랜잭션</strong> 시스템에서는 덜 쓰인다.</li>
    </ul>
  </li>
</ul>

<blockquote>
  <p>결과적으로, 분산 데이터베이스는 <strong>부분적인 데이터 분산</strong> + <strong>중복 저장</strong> 전략으로 <strong>확장성과 내결함성</strong>을 동시에 추구한다. 추가로, <strong>CAP 이론</strong>으로 알려져 있듯이 <strong>일관성(Consistency)</strong>과 <strong>가용성(Availability)</strong> 사이의 트레이드오프가 존재하는데, Cassandra같이 AP 지향 시스템은 일시적 불일치를 허용하면서 모든 노드 가용성을 높인 반면 CockroachDB같이 CP 지향 시스템은 장애 시 일부 서비스 중단을 감수하더라도 일관성을 우선한다. 이러한 차이는 서비스 요구사항(강한 일관성이 필요한 금융 vs 약한 일관성 허용 가능한 소셜 피드 등)에 따라 선택하게 된다.</p>
</blockquote>

<h2 id="mysql-cluster의-아키텍처와-특징-ndb-cluster">MySQL Cluster의 아키텍처와 특징 (NDB Cluster)</h2>

<p><strong>MySQL Cluster</strong>(정식 명칭: MySQL NDB Cluster)은 MySQL 데이터베이스에 내장된 분산 클러스터링 기술로, <strong>공유-낫싱(shard 기반)</strong> 다중 마스터 구조를 갖춘 시스템이다.</p>
<ul>
  <li>기본 아이디어는 <strong>여러 대의 데이터 노드(Data Node)</strong> 에 데이터를 자동으로 분할 저장하고, <strong>여러 SQL 노드(SQL Node)</strong> 를 통해 그 데이터를 접근하는 형태이다.</li>
  <li>각 SQL 노드는 MySQL 서버 프로세스이며, 클러스터의 데이터 노드에 네트워크로 접속하여 질의를 처리한다.</li>
  <li>그리고 별도로 <strong>관리 노드(Management Node)</strong> 가 있어서 클러스터 설정과 노드 모니터링을 담당한다.</li>
  <li>이러한 구조 덕분에 MySQL Cluster는 <strong>하나의 MySQL 논리 DB를 다중 물리 노드에 분산</strong>하여 <strong>고가용성과 확장성</strong>을 얻는다.</li>
</ul>

<p><img src="https://www.mysql.com/common/images/products/MySQL_Cluster_Scalability_v1.png" alt="" /></p>

<p><strong>데이터 노드</strong>들이 데이터를 분산 저장하며 서로 <strong>동기 복제</strong>로 데이터 일관성을 유지하고, <strong>SQL 노드(MySQL 서버)</strong>들은 애플리케이션의 질의를 받아 <strong>데이터 노드에 접근</strong>한다. <strong>관리 노드</strong>는 클러스터의 구성 정보를 보관하고 각 노드의 상태를 조율한다.</p>

<ul>
  <li><strong>자동 샤딩 및 다중 마스터:</strong> MySQL Cluster는 <strong>테이블의 프라이머리 키를 해싱하여 자동으로 데이터를 수평 분할</strong>하며, 이 과정은 사용자에게 <strong>투명</strong>하게 이루어진다.</li>
  <li>클러스터의 모든 <strong>데이터 노드</strong>들은 각자 할당받은 파티션의 데이터를 관리하고, <strong>모든 노드가 쓰기 연산을 받아들일 수 있는 멀티-마스터 구조</strong>다.</li>
  <li>즉 어떤 SQL 노드에서 INSERT를 하든, 해당 레코드가 속한 데이터 노드로 자동 전송되어 커밋되고, 그 변경이 즉시 다른 복제 노드들에도 적용된다.</li>
  <li>MySQL Cluster는 이처럼 <strong>어느 노드에서의 업데이트도 실시간으로 다른 노드에 전파</strong>되므로 모든 클라이언트가 최신 데이터를 공유할 수 있다.</li>
  <li>또한 하나의 트랜잭션이 여러 샤드(데이터 노드)에 걸쳐 있어도 내부적으로 <strong>이중 페이즈 커밋(2PC)</strong> 프로토콜로 모든 관련 노드에서 원자적으로 커밋을 수행하므로 <strong>분산 트랜잭션을 지원</strong>한다.</li>
  <li><strong>강력한 일관성과 동기 복제:</strong> MySQL Cluster의 데이터 복제는 기본적으로 <strong>동기식</strong>으로 이루어진다.</li>
  <li><strong>노드 그룹(node group)</strong>이라는 개념으로 2개 이상의 데이터 노드를 한 묶음으로 구성하고, 동일 그룹 내 노드들끼리 데이터를 완전 미러링한다.</li>
  <li>일반적으로 2개의 복제본(2노드 그룹)을 사용하며, 한 노드 그룹 내 한 노드가 장애 시 <strong>즉시 다른 노드가 대응</strong>하여 서비스 지속이 가능하다. 커밋 시에는 두 노드에 모두 기록이 확인된 후 완료되므로 <strong>단일 노드 장애로 인한 데이터 손실이 없다</strong>.</li>
  <li><strong>인메모리 지향 및 분산 인덱싱:</strong> MySQL Cluster의 <strong>NDB 스토리지 엔진</strong>은 <strong>메모리 기반</strong>으로 동작하도록 최적화되어 있다. <strong>모든 인덱싱된 컬럼은 분산된 메모리에 저장</strong>되고 관리되며, 비인덱스 컬럼은 옵션에 따라 메모리 또는 디스크에 저장할 수 있다 (디스크에 저장해도 인메모리 캐시를 통해 I/O를 최소화). 따라서 <strong>프라이머리 키 조회</strong>와 같은 인덱스 기반 질의는 네트워크 통신만 빼면 <strong>메모리 조회 속도</strong>로 매우 빠르게 처리된다. 흔히 <strong>“모든 노드가 모든 인덱스를 가진다”</strong>는 오해가 있지만, 사실은 <strong>“각 노드가 자신이 맡은 데이터의 인덱스를 메모리에 가지고 있다”</strong>는 의미입니다. MySQL Cluster는 <strong>데이터를 파티션</strong>하면서 <strong>해당 파티션의 인덱스도 함께 분산</strong>시키므로, <strong>모든 노드에 전체 인덱스를 복제하여 저장하는 것이 아니다다</strong> (그렇게 하면 메모리 낭비이자 일관성 유지도 어렵겠지요). 예를 들어 4개 노드로 데이터를 분산했다면 각 노드는 1/4 데이터와 그 부분의 인덱스만 메모리에 가지고 있다. 질의 시 MySQL 서버(SQL 노드)는 어떤 데이터가 어느 노드에 있는지 <strong>파티션 해시 알고리즘</strong>으로 계산하거나 <strong>클러스터 맵</strong>을 참조하여 해당 노드로 질의를 보내기 때문에, 인덱스 룩업은 정확한 노드에서 수행되고 매우 효율적.</li>
  <li><strong>쿼리 처리 및 조인:</strong> 분산된 데이터임에도 불구하고, MySQL Cluster를 사용할 때 애플리케이션은 <strong>일반 MySQL 질의</strong>를 사용한다. 분산된 다수의 SQL 노드가 내부적으로 협력하여 <strong>조인이나 집계 연산도 지원</strong>한다. 예를 들어 A테이블과 B테이블이 서로 다른 노드에 분산되어 있더라도, SQL 노드는 먼저 한쪽 노드에서 필요한 키를 추출하고 그 결과를 다른 노드로 보내 조인을 수행하는 방식으로 동작한다 (분산 조인). 물론 모든 조인이 이런 분산 수행을 효율적으로 처리할 수 있는 것은 아니므로, MySQL Cluster에서는 <strong>작업을 샤드 현지화(locality)</strong>하기 위해 <strong>파티셔닝 키를 잘 선택</strong>하는 것이 중요한다. 개발자가 <strong>의도적으로 관련 데이터가 같은 노드에 위치하도록</strong> 파티션 키를 설정하면 (예: 자주 조인되는 두 테이블에 동일한 파티션 키 사용), 네트워크를 가로지르는 조인을 줄여 성능을 높일 수 있다.</li>
  <li><strong>고가용성 및 무중단 운영:</strong> MySQL Cluster는 <strong>완전한 공유-낫싱 구조</strong>로 <strong>싱글 포인트 실패 지점이 없도록</strong> 설계되었다. 어느 하나의 노드나 디스크에 장애가 발생해도, 다른 노드들이 복제된 데이터를 가지고 있기 때문에 서비스가 중단되지 않습니다. 또한 <strong>롤링 업그레이드</strong>(노드를 하나씩 순차 업그레이드)나 온라인 노드 추가/제거 등을 지원하여, 클러스터를 내린 상태가 아니어도 <strong>동적으로 용량 확장과 유지보수</strong>가 가능한다. 다만 메이저 스키마 변경 등은 경우에 따라 잠시 락이 걸릴 수 있다. MySQL Cluster는 이러한 특성 덕분에 통신사의 Home Location Register 같은 24x365 서비스에 오랫동안 활용되어 왔다.</li>
</ul>

<blockquote>
  <p>요약하자면 MySQL Cluster는 <strong>MySQL의 분산 클러스터링 솔루션</strong>으로, <strong>자동 샤딩</strong>, <strong>동기식 복제에 의한 강한 일관성</strong>, <strong>인메모리 성능</strong>, <strong>멀티마스터 쓰기 처리</strong> 등을 특징으로 한다. 이는 앞서 설명한 일반적인 샤딩+복제 아키텍처를 <strong>데이터베이스 엔진 수준에서 구현</strong>한 것이며, 개발자가 특별한 샤딩 로직을 짜지 않아도 MySQL 쿼리를 통해 대용량 데이터를 처리할 수 있게 했다. 다만 운영 복잡성이 높고 메모리 자원 요구량이 많다는 단점이 있어, 반드시 필요한 경우에 사용된다. (예: 전세계적으로 분산된 사용자 데이터를 다루면서도 SQL 트랜잭션 일관성이 필요할 때.)</p>
</blockquote>]]></content><author><name>Duyan Kim</name></author><category term="Database" /><category term="SQL" /><category term="DB" /><category term="RDB" /><summary type="html"><![CDATA[분산DB의 역사 톺아보기를 했다]]></summary></entry><entry><title type="html">Artifactory란?</title><link href="https://duyankim.github.io/2025/06/10/Spring32/" rel="alternate" type="text/html" title="Artifactory란?" /><published>2025-06-10T20:40:08+09:00</published><updated>2025-06-10T20:40:08+09:00</updated><id>https://duyankim.github.io/2025/06/10/Spring32</id><content type="html" xml:base="https://duyankim.github.io/2025/06/10/Spring32/"><![CDATA[<h1 id="artifactory란">Artifactory란?</h1>

<ul>
  <li>패키지(라이브러리, 빌드 결과물 등)를 저장, 관리, 배포할 수 있는 저장소 관리 시스템(Repository Manager)</li>
  <li>Repository Manager (저장소 관리자)라는 개념을 JFrog에서 Artifactory라는 제품으로 내놓았는데, 저장소 관리의 대명사처럼 쓰임</li>
</ul>

<h2 id="사용-이유">사용 이유</h2>

<table>
  <thead>
    <tr>
      <th>이유</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>사내 라이브러리 공유</td>
      <td>팀 내부 또는 사내 공통 모듈(jar, tgz 등)을 올려두고 공유</td>
    </tr>
    <tr>
      <td>보안 및 안정성</td>
      <td>외부 중앙 저장소에 의존하지 않고, 사내 네트워크 내에서 관리</td>
    </tr>
    <tr>
      <td>빌드 속도 향상</td>
      <td>자주 사용하는 패키지를 캐싱하여 다운로드 시간 절감</td>
    </tr>
    <tr>
      <td>빌드 결과물 저장</td>
      <td>CI/CD에서 빌드된 아티팩트(.jar, .zip, .tar 등)를 저장</td>
    </tr>
  </tbody>
</table>

<h2 id="다양한-패키지-매니저-형식-지원">다양한 패키지 매니저 형식 지원</h2>

<table>
  <thead>
    <tr>
      <th>언어/도구</th>
      <th>패키지 매니저</th>
      <th>예시 확장자</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Java</td>
      <td>Maven / Gradle</td>
      <td><code class="language-plaintext highlighter-rouge">.jar</code>, <code class="language-plaintext highlighter-rouge">.pom</code></td>
    </tr>
    <tr>
      <td>JavaScript</td>
      <td>npm / yarn</td>
      <td><code class="language-plaintext highlighter-rouge">.tgz</code></td>
    </tr>
    <tr>
      <td>Python</td>
      <td>pip</td>
      <td><code class="language-plaintext highlighter-rouge">.whl</code>, <code class="language-plaintext highlighter-rouge">.tar.gz</code></td>
    </tr>
    <tr>
      <td>Docker</td>
      <td>Docker Registry</td>
      <td>Docker Image</td>
    </tr>
    <tr>
      <td>기타</td>
      <td>NuGet, RubyGems, Conan, Go modules 등</td>
      <td>다양한 포맷 지원</td>
    </tr>
  </tbody>
</table>]]></content><author><name>Duyan Kim</name></author><category term="Framework" /><category term="Spring" /><category term="Spring" /><summary type="html"><![CDATA[Repository Manager?]]></summary></entry><entry><title type="html">OS - 운영체제, 프로세스, 메모리</title><link href="https://duyankim.github.io/2025/04/19/OS01/" rel="alternate" type="text/html" title="OS - 운영체제, 프로세스, 메모리" /><published>2025-04-19T20:40:08+09:00</published><updated>2025-04-19T20:40:08+09:00</updated><id>https://duyankim.github.io/2025/04/19/OS01</id><content type="html" xml:base="https://duyankim.github.io/2025/04/19/OS01/"><![CDATA[<h1 id="운영체제란">운영체제란?</h1>

<h2 id="운영체제가-하는-일의-종류">운영체제가 하는 일의 종류</h2>

<ol>
  <li>프로세스를 관리하기
    <ul>
      <li>pc에 여러가지 프로그램을 한번에 켜 두면, 자연스럽게 지금 사용하지 않는 프로그램을 background에 둔다.</li>
    </ul>
  </li>
  <li>
    <p>메모리 관리</p>
  </li>
  <li>하드웨어 관리
    <ul>
      <li>사용자가 하드웨어에 직접적으로 접근하는 것을 금지하고, 운영체제가 대신 관리해준다.</li>
    </ul>
  </li>
  <li>파일 시스템 관리
    <ul>
      <li>하드디스크에 많은 데이터를 효율적으로 저장하기 위함</li>
    </ul>
  </li>
</ol>

<h2 id="운영체제의-구조">운영체제의 구조</h2>

<h3 id="커널">커널</h3>

<ul>
  <li>운영체제의 핵심</li>
  <li>프로세스, 메모리, 저장장치 관리</li>
  <li>사용자는 <code class="language-plaintext highlighter-rouge">인터페이스</code>를 통해 커널에 접근 가능. 인터페이스 = GUI, CLI
    <ul>
      <li>CLI: UNIX, LINUX가 기본적으로 제공하는 환경 (Command-Line Interace)</li>
    </ul>
  </li>
  <li>어플리케이션은 <code class="language-plaintext highlighter-rouge">시스템콜</code>을 통해 커널에 접근 가능.
    <ul>
      <li>시스템콜이 없이 어플리케이션에 직접 접근하면 중요한 데이터를 엎어씌워버릴 수도 있는데, 커널을 통해 write함수를 쓰면, 운영체제가 알아서 빈 메모리에 저장을 해줘서 안정성이 있다.</li>
    </ul>
  </li>
  <li>하드웨어 &lt;-&gt; 커널 인터페이스 : 드라이버</li>
</ul>

<h2 id="하드웨어의-구조">하드웨어의 구조</h2>

<h3 id="폰-노이만-구조">폰 노이만 구조</h3>

<p>폰 노이만은 하드웨어 중심의 프로그램을 개선하기 위해, CPU와 메모리<code class="language-plaintext highlighter-rouge">RAM</code>을 잇는 Bus(데이터를 전달하는 통로)를 만든다.<br />
프로그램은 메모리에 올려 실행하는데, 이를 <strong>프로그램 내장방식</strong>이라 한다.<br />
메모리에 올라간 프로그램은 배선을 바꾸는 대신, 소프트웨어만 바꿔주면 되도록 개선되었다.</p>

<h4 id="메인보드">메인보드</h4>

<ul>
  <li>다른 하드웨어를 연결하는 장치</li>
  <li>장치간에 데이터를 전달하는 것은 Bus가 담당</li>
</ul>

<h4 id="cpu-central-processing-unit-중앙처리장치">CPU <code class="language-plaintext highlighter-rouge">Central Processing Unit</code> 중앙처리장치</h4>

<ol>
  <li>산술논리 연산장치 <code class="language-plaintext highlighter-rouge">Arithmetic and Logic Unit</code>: CPU에서 실제로 데이터 연산을 담당함</li>
  <li>제어장치 <code class="language-plaintext highlighter-rouge">Control Unit</code>: 모든 장치들의 동작을 지시</li>
  <li>레지스터: CPU내에서 계산을 위해 임시로 보관하는 장치</li>
</ol>

<h4 id="메모리의-종류">메모리의 종류</h4>

<h5 id="ram-random-access-memory">RAM <code class="language-plaintext highlighter-rouge">Random Access Memory</code></h5>

<ul>
  <li>랜덤으로 데이터를 읽어도 저장된 위치와 상관없이 읽는 속도가 같다.</li>
  <li>전력이 끊기면 메모리를 잃기 때문에 메인메모리로 사용됨</li>
</ul>

<h5 id="rom-read-only-memory">ROM <code class="language-plaintext highlighter-rouge">Read Only Memory</code></h5>

<ul>
  <li>전력이 끊겨도 메모리를 갖고 있지만, 한번 쓰면 수정할 수 없음</li>
  <li>부팅과 관련된 바이오스를 저장하는데 주로 쓰인다.</li>
</ul>

<h2 id="컴퓨터를-켜면-일어나는-일">컴퓨터를 켜면 일어나는 일</h2>

<ol>
  <li>BIOS가 하드웨어가 괜찮은지 확인한다.</li>
  <li>하드웨어에 이상이 없다면, 하드디스크에 있는 마스터 부트 레코드에 저장된 부트로더를 메모리로 가져와서 실행시키낟.</li>
  <li>운영체제가 여러개 설치 되어 있는 경우, 운영체제를 선택하라는 화면이 나옴</li>
  <li>운영체제가 실행되면, 이제부터 실행되는 모든 프로그램은 메모리에 올라와서 운영체제가 관리한다.</li>
</ol>

<h1 id="프로그램과-프로세스">프로그램과 프로세스</h1>

<h2 id="프로그램이란">프로그램이란?</h2>

<p>프로그램이란 하드디스크 등과 같은 저장장치에 저장된 명령문의 집합체를 말한다.<br />
프로그램은 저장장치만 사용하는 수동적인 작업이다.</p>

<h2 id="프로세스란">프로세스란?</h2>

<p>프로세스란 실행중인 프로그램이다. 하드디스크에 올라간 프로그램이 메모리에 올라갔을 때, 실행중이라고 한다.<br />
프로세스는 CPU도 사용하고, 입출력 작업도 하기 때문에 능동적인 작업이다.</p>

<h3 id="code-영역">Code 영역</h3>

<p>프로세스를 실행하는 코드를 저장함</p>

<h3 id="data-영역">Data 영역</h3>

<p>전역변수와 Static 변수를 저장</p>

<h3 id="heap-영역">Heap 영역</h3>

<p>프로그래머가 런타임시 할당할 수 있는 메모리 공간</p>

<ul>
  <li>C언어
    <ul>
      <li>malloc() : 힙영역에 메모리 공간 할당</li>
      <li>free() : 할당된 메모리 공간 해제</li>
    </ul>
  </li>
</ul>

<h3 id="stack-영역">Stack 영역</h3>

<p>지역변수와 함수 호출을 했을 때 필요한 정보 저장</p>

<h3 id="컴파일-과정">컴파일 과정</h3>

<ol>
  <li>전처리기</li>
  <li>파일의 확장자가 i가 됨</li>
  <li>컴파일러가 컴파일 수행</li>
  <li>고수준인 c언어를 저수준인 어셈플리 언어로 바꿔줜다.</li>
  <li>컴파일러 확장자가 s가 됨</li>
</ol>

<h2 id="유니-프로그래밍과-멀티-프로그래밍">유니 프로그래밍과 멀티 프로그래밍</h2>

<h3 id="유니프로그래밍">유니프로그래밍</h3>

<ul>
  <li>메모리에 프로세스 1개</li>
  <li>하나의 프로세스를 끝내야 다른 프로세스를 시작할 수 있다.</li>
</ul>

<h3 id="멀티-프로그래밍">멀티 프로그래밍</h3>

<ul>
  <li>메모리에 프로세스 n개</li>
  <li>CPU가 쉬는 시간이 줄어들어 효율성이 높아진다</li>
  <li>멀티 태스킹: 한 프로세스를 짧게 실행하고, 다른 모든 프로세스도 짧게 실행하면서, 모든 프로세스를 동시에 실행시키는 것처럼 느껴지게 하는 기술</li>
  <li>멀티 프로세서: CPU가 여러개 있는 것</li>
  <li>멀티 프로그래밍: 멀티 프로세서로 실행하는 것</li>
</ul>

<h2 id="pcb-process-control-block">PCB <code class="language-plaintext highlighter-rouge">Process Control Block</code></h2>

<p>PCB란, 프로세스가 만들어지면 해당 프로세스의 정보를 가지고 있는 PCB를 저장한다. 여러개의 PCB는 연결리스트로 구성되어 있다.<br />
프로세스가 종료되면, 해당 프로세스의 PCB를 제거하고, 연결리스트는 유지한다.</p>

<p>PCB의 구성<br />
<img src="https://user-images.githubusercontent.com/56623911/195630556-93f98c1c-095b-4b82-a870-0c2a94213388.png" alt="" /></p>

<h3 id="프로세스-상태">프로세스 상태</h3>

<p>시분할을 사용하는 운영체제는 여러개의 프로세스를 돌아가면서 실행한다. 한 순간에 하나의 프로세스만 실행하지만, 속도가 빨라서 여러 개를 동시에 실행하는 것 처럼 보임</p>

<ul>
  <li><strong>생성 <code class="language-plaintext highlighter-rouge">New</code></strong>: PCB를 생성하고 메모리에 프로그램 적재를 요청한 상태</li>
  <li><strong>준비 <code class="language-plaintext highlighter-rouge">Ready</code></strong>: 메모리에 프로그램 적재를 승인 받으면 준비 상태로 넘어간다. CPU를 사용하기 위해 기다리는 상태. CPU 스케줄러에 의해 CPU가 할당됨. 대부분의 프로세는 준비상태이다.</li>
  <li><strong>실행 <code class="language-plaintext highlighter-rouge">Running</code></strong>: 준비상태에 있는 프로세스가 CPU 스케줄러에 의해 CPU를 할당받아 실행되는 상태. CPU가 n개라면, 실행상태에 있을 수 있는 프로세스의 수는 n개. 실행상태에 있는 프로세스도 부여된 시간만큼만 실행 가능. 시간이 끝나면 다시 준비상태로 들어간다.</li>
  <li><strong>대기 <code class="language-plaintext highlighter-rouge">Waiting</code></strong>: 프로세스가 입출력 요청을 하면 입출력이 완료될 때까지 대기상태로 빠진다. 입출력 작업이 끝나고 다시 준비 상태로 들어간다.</li>
  <li><strong>완료 <code class="language-plaintext highlighter-rouge">Terminated</code></strong>: 프로세스가 사용했던 데이터를 메모리에서 제거, 생성된 PCB도 제거</li>
</ul>

<p><img src="https://yanghs6.github.io/assets/img/computer_science/1003/1003_01_process_state.png" alt="" /></p>

<h2 id="컨텍스트-스위칭">컨텍스트 스위칭</h2>

<ul>
  <li>프로세스를 실행하는 중에 다른 프로세스를 실행하기 위해, 실행중인 프로세스의 상태를 저장하고, 다른 프로세스의 상태값으로 교체하는 작업</li>
  <li>컨텍스트 스위칭이 일어날 대, PCB의 아래 내용이 바뀜
    <ul>
      <li>프로세스 상태</li>
      <li>프로그램 카운터: 다음 실행할 명령어의 주소</li>
      <li>레지스터 정보</li>
      <li>메모리 관련 정보</li>
    </ul>
  </li>
</ul>

<h2 id="쓰레드">쓰레드</h2>

<ul>
  <li>운영체제가 작업을 처리하는 단위</li>
  <li>쓰레드는 프로세스 내에 존재한다. 1개의 프로세스 내에 n개의 쓰레드가 있을 수 있다.</li>
  <li>code, data, heap 영역은 쓰레드가 공유하고, stack은 각각 생성한다.</li>
  <li>Thread 별로 id가 있고, Thread Control Block이 관리한다.</li>
  <li>쓰레드를 사용하면 데이터 공유시 오버헤드가 적다. 그치만 데이터를 공유하기 때문에 문제가 생길 수도 있다.</li>
</ul>

<h1 id="cpu-스케줄링">CPU 스케줄링</h1>

<p>준비중인 프로세스들은 CPU를 할당받아 실행되는데, 이것을 CPU 스케줄링이라고 한다.</p>

<p>CPU스케줄러는 아래 내용을 고려해서 스케줄링을 한다.</p>

<ol>
  <li>어떤 프로세스에게 CPU를 할당할 것인가?</li>
  <li>프로세스는 얼마나 오래 CPU를 사용해도 되는가?</li>
</ol>

<h2 id="다중-큐">다중 큐</h2>

<p>프로세스가 대기하고 있는 준비상태, 대기상태는 큐로 관리된다. <strong>Queue</strong>는 FIFO 구조이다.</p>

<p>프로세스가 실행상태에서 준비상태로 돌아갈 때, 운영체제는 우선순위를 보고, PCB를 준비 큐에 넣는다.</p>

<p>CPU 스케줄러는 준비 큐에 있는 PCB 중에 적당한 PCB를 실행상태로 보낸다.</p>

<h2 id="스케줄링-목표">스케줄링 목표</h2>

<ol>
  <li>
    <p><strong>리소스 사용률</strong>: CPU의 사용률, I/O 디바이스의 사용률</p>
  </li>
  <li>
    <p><strong>오버헤드 최소화</strong>: 컨텍스트 스위칭을 너무 자주하면, 오버헤드가 발생</p>
  </li>
  <li>
    <p><strong>공평성</strong>: 모든 CPU에게 공평하게 할당되어야 한다. (공평함의 의미는 무조건 n분의 1은 아니다)</p>
  </li>
  <li>
    <p><strong>처리량</strong>: 같은 시간동안 내에 더 많은 것을 처리할 수 있어야 한다.</p>
  </li>
  <li>
    <p><strong>대기시간</strong>: 작업 요청 후 실제 실행 시간까지 대기시간이 짧아야 한다.</p>
  </li>
  <li>
    <p><strong>응답시간</strong>: 응답시간이 짧아야 한다.</p>
  </li>
</ol>

<h3 id="스케줄링-알고리즘">스케줄링 알고리즘</h3>

<p>스케줄링의 성능은 <strong>평균 대기시간</strong>으로 평가된다. 프로세스들이 모두 실행되기까지의 평균 대기시간이다.</p>

<h4 id="fifo">FIFO</h4>

<p>먼저 스케줄링 큐에 들어온 프로세스가 먼저 스케줄을 할당받는다.</p>

<p>ex) 마트 계산대</p>

<ul>
  <li>장점: 단순하고 직관적인다</li>
  <li>단점
    <ul>
      <li>한 프로세스가 끝나야 다음 프로세스가 실행되기 때문에, 실행시간이 짧은 프로세스도 앞 프로세스가 끝날 때까지 기다려야 한다.</li>
      <li>앞의 프로세스에 I/O 작업이 있을 경우 처리량이 떨어진다.</li>
      <li>프로세스의 Burst Time에 따라 성능 차이가 심하게 나서 실제로는 잘 쓰이지 않는다.</li>
    </ul>
  </li>
</ul>

<h4 id="sjf-shortest-job-first">SJF <code class="language-plaintext highlighter-rouge">Shortest Job First</code></h4>

<p>Burst Time이 짧은 프로세스 먼저 실행한다.</p>

<ul>
  <li>FIFO의 단점을 극복하기 위해 등장했다.</li>
  <li>단점
    <ul>
      <li>어떤 프로세스가 먼저 실행될지 예측하기 힘들다.</li>
      <li>Burst time이 긴 프로세스는 아주 오래 기다릴 수도 있다. 중간에 Burst time이 짧은 프로세스가 들어오면 계속 대기시간이 늘어지기 때문이다.</li>
      <li>실제로는 잘 쓰이지 않는다.</li>
    </ul>
  </li>
</ul>

<h4 id="rr-round-robin">RR <code class="language-plaintext highlighter-rouge">Round Robin</code></h4>

<p>한 프로세스에서 일정시간동안 CPU를 할당하고, 할당량이 지나면 강제로 다른 프로세스에게 일정시간만큼 CPU를 할당한다. 할당 받은 시간이 끝난 프로세스는 다시 맨 마지막 순서로 들어간다.</p>

<ul>
  <li><strong>타임 슬라이스</strong>: 프로세스가 할당하는 시간</li>
  <li>RR은 컨텍스트 스위칭 시간이 포함된다. 타임슬라이스 값에 따라 대기시간이 달라진다.</li>
  <li>같은 시간이 걸린다면 FIFO보다 RR이 비효율적이다.</li>
</ul>

<h4 id="mlfq-multi-level-feedback-queue">MLFQ <code class="language-plaintext highlighter-rouge">Multi Level Feedback Queue</code></h4>

<p>RR의 업그레이드된 버전</p>

<ul>
  <li>CPU Bound Process에 큰 크기의 타임슬라이스 적용</li>
  <li>I/O Bound Process에 작은 크기의 타임슬라이스 적용</li>
</ul>

<p>우선순위를 가진 큐를 여러개 준비해두고, 우선순위가 낮을수록 타임슬라이스가 커지고, 우선순위가 높을수록 타임슬라이스가 짧다.</p>

<h2 id="프로세스간-동기화">프로세스간 동기화</h2>

<h3 id="프로세스간-통신의-종류">프로세스간 통신의 종류</h3>

<ul>
  <li>한 컴퓨터 내에서 file과 pipe를 이용해서 통신하기</li>
  <li>한 프로세스 내에서 쓰레드간 통신하기</li>
  <li>다른 컴퓨터와 socket 통신, RPC(원격통신호출)</li>
</ul>

<h3 id="공유자원과-임계구역">공유자원과 임계구역</h3>

<ul>
  <li>임계구역 <code class="language-plaintext highlighter-rouge">Critical Section</code> : 여러 프로세스가 동시에 사용하면 안되는 영역</li>
  <li>상호배제 <code class="language-plaintext highlighter-rouge">Mutual Exclusion</code> 의 요구사항
    <ul>
      <li>임계영역엔 동시에 하나의 프로세스만 접근한다.</li>
      <li>여러 요청에도 하나의 프로세스의 접근만 허용한다.</li>
      <li>임계구역에 들어간 프로세스는 빠르게 나와야 한다.</li>
    </ul>
  </li>
</ul>

<h4 id="세마포어-semaphore">세마포어 <code class="language-plaintext highlighter-rouge">Semaphore</code></h4>

<p>임계구역에 진입하는 것을 lock을 통해 제어함</p>

<h4 id="모니터">모니터</h4>

<p>프로그래밍 언어 차원에서 지원</p>

<ul>
  <li>자바에서는 synchronized를 사용하면 모니터링 된다.</li>
  <li>하나의 변수에 synchronnized가 붙은 여러 함수(ex, increase, decrease)가 있다면, increase를 사용하는 중이라면 다른 프로세스에서 decrease도 접근 못하게 막는다.</li>
</ul>

<h2 id="교착상태">교착상태</h2>

<p>교착상태가 발생하는 이유는 공유자원 때문이다.</p>

<h3 id="식사하는-철학자">식사하는 철학자</h3>

<p>원탁에 3명의 철학자가 앉아서 식사를 하는데, 포크가 각각 2개 필요하지만 포크가 3개만 있을 때.<br />
한 명이 2 포크를 들고 식사를 하면 나머지 2명의 철학자는 사유를 하면서 대기를 한다.<br />
각자 1개의 포크를 들고 식사를 하려고 하면, 아무도 식사를 못하고 다른 사람이 먼저 먹고 포크를 반환하기를 기다린다.</p>

<h3 id="교착상태의-필요조건">교착상태의 필요조건</h3>

<ol>
  <li>상호배제</li>
  <li>비선점</li>
  <li>점유와 대기</li>
  <li>원형 대기</li>
</ol>

<h3 id="교착-상태의-해결-방법-교착상태-회피-deadlock-avoidance">교착 상태의 해결 방법: 교착상태 회피 <code class="language-plaintext highlighter-rouge">Deadlock avoidance</code></h3>

<ul>
  <li>프로세스들에게 자원을 할당할 때, 어느 정도이면 교착상태가 생기는지 파악해서 그것을 회피할 수 있을 정도로만 할당함</li>
  <li>은행원 알고리즘</li>
</ul>

<h4 id="가벼운-교착상태-검출">가벼운 교착상태 검출</h4>

<p>타이머를 이용하여 프로세스가 일정 시간동안 작업을 진행하지 않는다면, 교착상태가 발생했다고 간주하고 교착상태를 해결한다.<br />
일정시점마다 체크포인트를 만들어 저장하고, 타임아웃으로 교착상태가 발생했다면 마지막 저장한 체크포인트로 돌아간다.</p>

<h4 id="무거운-교착상태-검출">무거운 교착상태 검출</h4>

<p>자원할당 그래프를 사용하여 검출하는 방식<br />
현재 운영체제에서 프로세스가 어떤 자원을 사용하는지 지켜보고, 교착상태를 검출해낸다.<br />
교착상태 검출시 교착상태를 일으킨 프로세스를 강제 종료하고, 다시 실행시킬 때 체크포인트로 돌아간다.</p>

<h2 id="컴파일">컴파일</h2>

<ul>
  <li><strong>컴파일언어</strong>: 개발자가 코드를 작성하고 컴파일을 거쳐서 0과 1로 된 실행파일을 만든다. 컴파일 과정에서 개발자가 만든 문법 오류를 찾고, 기계어로 만든다.</li>
  <li><strong>인터프리터언어</strong>: 미리 검사하지 안혹, 실행시 코드를 한 줄씩 검사하며 실행한다. 속도가 컴파일 언어와 비교해 느리다. JS, Python이 이에 해당한다.</li>
</ul>

<h3 id="컴파일-과정-1">컴파일 과정</h3>

<p>test.c 파일 -&gt; 전처리기 -&gt; test.i가 됨 -&gt; 컴파일러 -&gt; test.s가 됨 -&gt; 어셈블러 -&gt; test.o가 됨 -&gt; 링커 -&gt; test.exe</p>

<p>운영체제는 exe파일에 있는 코드영역과 데이터 영역을 가져와서 데이터 영역에 할당하고 빈 상태의 heap과 stack을 할당한다.<br />
PCB를 만들어 관리되도록 하고, 프로그램 카운터를 생성한 프로세스의 코드영역 첫번째 주소를 할당한다.</p>

<p><img src="https://kwonsoonwoo.github.io/assets/cs50/%EC%BB%B4%ED%8C%8C%EC%9D%BC%20%EA%B3%BC%EC%A0%95.png" alt="" /></p>

<h2 id="컴퓨터의-메모리">컴퓨터의 메모리</h2>

<ul>
  <li>휘발성 메모리 (컴퓨터가 꺼지면 사라지는 메모리. 속도 빠르고 용량이 작고 가격이 비싸다.)
    <ul>
      <li>레지스터: CPU는 계산을 할 때 메인메모리에 있는 값을 레지스터로 가져와서 계산하고, 계산결과를 다시 메인메모리에 저장시킨다.</li>
      <li>캐시: 메인메모리에 있는 메모리를 레지스터로 옮기려면 시간이 오래 걸리기 때문에, 미리 데이터를 캐시에 옮겨둔다.
        <ul>
          <li>L1캐시, L2캐시 등등..</li>
        </ul>
      </li>
      <li>메인메모리<code class="language-plaintext highlighter-rouge">RAM</code>: 데이터를 저장하기보단 실행중인 프로그램을 올린다.</li>
    </ul>
  </li>
  <li>보조저장장치: 휘발되지 않는 메모리</li>
</ul>

<h3 id="메인-메모리">메인 메모리</h3>

<ul>
  <li>폰 노이만 구조는 모든 프로그램을 메모리에 올려서 실행한다.</li>
</ul>

<h6 id="reference">Reference</h6>

<blockquote>
  <p>그림으로 쉽게 배우는 운영체제</p>
</blockquote>]]></content><author><name>Duyan Kim</name></author><category term="CS" /><category term="OS" /><category term="OS" /><summary type="html"><![CDATA[그림으로 쉽게 배우는 운영체제 강의를 보고 정리]]></summary></entry><entry><title type="html">Java - 8부터 17까지의 변화</title><link href="https://duyankim.github.io/2025/04/18/JAVA53/" rel="alternate" type="text/html" title="Java - 8부터 17까지의 변화" /><published>2025-04-18T20:40:08+09:00</published><updated>2025-04-18T20:40:08+09:00</updated><id>https://duyankim.github.io/2025/04/18/JAVA53</id><content type="html" xml:base="https://duyankim.github.io/2025/04/18/JAVA53/"><![CDATA[<p>자바 8부터 17까지의 주요 변경사항에 대해 알아보자!</p>

<hr />

<h2 id="-java-8-2014년">✅ Java 8 (2014년)</h2>
<blockquote>
  <p><strong>기점이 되는 LTS (Long Term Support) 버전</strong>. 지금도 많이 쓰임.</p>
</blockquote>

<ul>
  <li><strong>람다식 (Lambda Expression)</strong>
    <ul>
      <li>데이터베이스 질의 언어에서 표현식을 처리하는 것처럼 병렬 연산을 지원하는 스트림이라는 새로운 API를 제공</li>
      <li>익명 함수 문법 도입. 함수형 프로그래밍 가능.</li>
      <li>데이터베이스 질의 언어에서 고수준 언어로 원하는 동작을 표현하면, 구현(자바에서는 스트림 라이브러리가 이 역할을 수행)에서 최적의 저수준 실행 방법을 선택하는 방식으로 동작한다</li>
      <li>에러를 자주 일으키는 멀티코어 CPU를 이용하는 것보다 비용이 훨씬 비싼 키워드 synchronized를 사용하지 않아도 된다.</li>
    </ul>
  </li>
  <li><strong>Stream API</strong>
    <ul>
      <li>컬렉션 처리 시 선언형 프로그래밍 가능.</li>
    </ul>
  </li>
  <li><strong>Optional 클래스</strong>
    <ul>
      <li>NullPointerException 방지용 도구.</li>
    </ul>
  </li>
  <li>
    <p><strong>인터페이스의 default, static 메서드</strong></p>
  </li>
  <li><strong>새로운 Date/Time API (<code class="language-plaintext highlighter-rouge">java.time</code>)</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">LocalDate</code>, <code class="language-plaintext highlighter-rouge">LocalDateTime</code>, <code class="language-plaintext highlighter-rouge">ZonedDateTime</code> 등</li>
    </ul>
  </li>
</ul>

<h2 id="-java-9-2017년">✅ Java 9 (2017년)</h2>

<ul>
  <li><strong>모듈 시스템 (Project Jigsaw)</strong>
    <ul>
      <li>애플리케이션을 모듈 단위로 쪼갤 수 있음.</li>
    </ul>
  </li>
  <li><strong>JShell</strong>
    <ul>
      <li>자바 REPL 지원. 인터랙티브 개발 가능.</li>
    </ul>
  </li>
  <li><strong>Stream API 개선 (<code class="language-plaintext highlighter-rouge">takeWhile</code>, <code class="language-plaintext highlighter-rouge">dropWhile</code>, <code class="language-plaintext highlighter-rouge">iterate</code>)</strong></li>
</ul>

<h2 id="-java-10-2018년">✅ Java 10 (2018년)</h2>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">var</code> 키워드 도입</strong>
    <ul>
      <li>지역 변수 타입 추론 가능</li>
    </ul>
  </li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">name</span> <span class="o">=</span> <span class="s">"abc"</span><span class="o">;</span> <span class="c1">// String으로 추론</span>
</code></pre></div></div>

<ul>
  <li><strong>GC 개선: G1 기본 GC</strong></li>
</ul>

<h2 id="-java-11-2018년--lts-버전">✅ Java 11 (2018년) — <strong>LTS 버전</strong></h2>

<ul>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">var</code>를 람다 파라미터에도 사용 가능</strong></p>
  </li>
  <li>
    <p><strong>HttpClient 정식 지원 (<code class="language-plaintext highlighter-rouge">java.net.http</code>)</strong></p>
  </li>
  <li>
    <p><strong>String API 추가 (<code class="language-plaintext highlighter-rouge">isBlank()</code>, <code class="language-plaintext highlighter-rouge">lines()</code>, <code class="language-plaintext highlighter-rouge">strip()</code>, <code class="language-plaintext highlighter-rouge">repeat()</code> 등)</strong></p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">Files.readString()</code> / <code class="language-plaintext highlighter-rouge">writeString()</code></strong> 간단한 파일 IO 지원</p>
  </li>
</ul>

<h2 id="-java-1214-20192020년">✅ Java 12~14 (2019~2020년)</h2>

<ul>
  <li><strong>Switch Expressions (프리뷰 - 정식)</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">yield</code> 키워드로 표현식 반환 가능</li>
    </ul>
  </li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="n">result</span> <span class="o">=</span> <span class="k">switch</span> <span class="o">(</span><span class="n">day</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">case</span> <span class="no">MONDAY</span> <span class="o">-&gt;</span> <span class="mi">1</span><span class="o">;</span>
  <span class="k">case</span> <span class="no">TUESDAY</span> <span class="o">-&gt;</span> <span class="mi">2</span><span class="o">;</span>
  <span class="k">default</span> <span class="o">-&gt;</span> <span class="k">throw</span> <span class="k">new</span> <span class="nc">IllegalStateException</span><span class="o">();</span>
<span class="o">};</span>
</code></pre></div></div>

<ul>
  <li><strong>Text Blocks (<code class="language-plaintext highlighter-rouge">"""</code>) — Java 13 프리뷰, 15 정식 도입</strong></li>
</ul>

<h2 id="-java-1516">✅ Java 15~16</h2>

<ul>
  <li><strong>Records (데이터 클래스)</strong>
    <ul>
      <li>불변 객체를 한 줄로 선언 가능</li>
    </ul>
  </li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">record</span> <span class="nf">Person</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="kt">int</span> <span class="n">age</span><span class="o">)</span> <span class="o">{}</span>
</code></pre></div></div>

<ul>
  <li><strong>Pattern Matching (프리뷰)</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">instanceof</code> 조건문 간결하게 가능</li>
    </ul>
  </li>
</ul>

<h2 id="-java-17-2021년--lts-버전">✅ Java 17 (2021년) — <strong>LTS 버전</strong></h2>
<blockquote>
  <p><strong>현재까지 가장 안정적인 LTS 버전 중 하나</strong>, Spring 3.x 기본 지원 버전.</p>
</blockquote>

<ul>
  <li><strong>Sealed Classes (제한된 상속)</strong>
    <ul>
      <li>상속 가능한 클래스를 명시적으로 제한</li>
    </ul>
  </li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sealed</span> <span class="kd">interface</span> <span class="nc">Shape</span> <span class="n">permits</span> <span class="nc">Circle</span><span class="o">,</span> <span class="nc">Square</span> <span class="o">{}</span>
</code></pre></div></div>

<ul>
  <li>
    <p><strong>Pattern Matching for <code class="language-plaintext highlighter-rouge">instanceof</code> 정식 채택</strong></p>
  </li>
  <li>
    <p><strong>JEP 356: 향상된 Pseudo-Random Number Generator API</strong></p>
  </li>
  <li>
    <p><strong>JEP 382: 새로운 macOS 렌더링 파이프라인</strong></p>
  </li>
</ul>

<hr />

<h2 id="-마무리-요약">🚀 마무리 요약</h2>
<p>| 버전 | 주요 특징 |
|——|———–|
| Java 8 | 람다, 스트림, Optional, 날짜 API |
| Java 9 | 모듈 시스템 |
| Java 10 | <code class="language-plaintext highlighter-rouge">var</code> 타입 추론 |
| Java 11 | HttpClient, String API 개선 |
| Java 13~14 | 텍스트 블록, switch 표현식 |
| Java 16 | Record, Pattern Matching |
| <strong>Java 17</strong> | Sealed Class, 안정된 LTS |</p>

<h6 id="reference">Reference</h6>
<blockquote>
  <p>모던 자바 인 액션</p>
</blockquote>]]></content><author><name>Duyan Kim</name></author><category term="Language" /><category term="Java" /><category term="stream" /><category term="java" /><summary type="html"><![CDATA[자바는 어떤 세월을 쌓아왔나?]]></summary></entry><entry><title type="html">MyBatis Generator 실행 중 OOM 디버깅 — classpath에 숨어있던 Lombok</title><link href="https://duyankim.github.io/2025/04/18/JAVA54/" rel="alternate" type="text/html" title="MyBatis Generator 실행 중 OOM 디버깅 — classpath에 숨어있던 Lombok" /><published>2025-04-18T20:40:08+09:00</published><updated>2025-04-18T20:40:08+09:00</updated><id>https://duyankim.github.io/2025/04/18/JAVA54</id><content type="html" xml:base="https://duyankim.github.io/2025/04/18/JAVA54/"><![CDATA[<h1 id="mybatis-generator-실행-중-oom-디버깅--classpath에-숨어있던-lombok">MyBatis Generator 실행 중 OOM 디버깅 — classpath에 숨어있던 Lombok</h1>

<p>Spring Boot 프로젝트에서 MyBatis Generator로 DTO/Mapper를 자동 생성하던 중 <code class="language-plaintext highlighter-rouge">OutOfMemoryError: Java heap space</code> 에러를 만났다. 힙 크기를 4GB까지 올려도 재현됐고, 테이블 필터링 범위를 좁혀도 마찬가지였다. 결국 원인은 엉뚱한 곳에 있었다. 이 글은 그 디버깅 과정과, 그 과정에서 정리한 개념들을 기록한 것이다.</p>

<h2 id="요약">요약</h2>

<ul>
  <li><strong>현상</strong>: <code class="language-plaintext highlighter-rouge">generateMyBatis</code> Gradle task 실행 시 <code class="language-plaintext highlighter-rouge">java.util.WeakHashMap.newTable</code>에서 OOM 발생. 힙 크기를 올려도 동일.</li>
  <li><strong>원인</strong>: Generator의 classpath에 애플리케이션 런타임 의존성을 통째로 얹는 설정(<code class="language-plaintext highlighter-rouge">sourceSets.main.runtimeClasspath</code>)이 있었고, 그 결과 <strong>Lombok 라이브러리가 Generator JVM에 함께 로드</strong>되면서 Lombok 내부의 <code class="language-plaintext highlighter-rouge">JavacAugments</code> static WeakHashMap이 누적되어 힙을 고갈시켰다.</li>
  <li><strong>해결</strong>: Generator task의 classpath를 필요한 최소한(MBG 본체 + JDBC 드라이버 + 커스텀 플러그인 jar)으로 축소. 커스텀 플러그인이 포함된 내부 모듈(<code class="language-plaintext highlighter-rouge">frw</code>)은 <code class="language-plaintext highlighter-rouge">transitive = false</code>로 선언하여 추이 의존성을 차단.</li>
</ul>

<h2 id="배경-프로젝트-구성">배경: 프로젝트 구성</h2>

<p>Oracle DB 기반 Spring Boot 멀티 모듈 프로젝트. <code class="language-plaintext highlighter-rouge">gen</code> 모듈이 DTO/Mapper 자동 생성을 담당하고, <code class="language-plaintext highlighter-rouge">build.gradle</code>에 다음과 같은 Gradle task가 정의되어 있었다.</p>

<div class="language-gradle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">configurations</span> <span class="o">{</span>
    <span class="n">generateMyBatis</span>
<span class="o">}</span>

<span class="k">dependencies</span> <span class="o">{</span>
    <span class="c1">// ... 수많은 application 런타임 의존성 (Spring, Lombok 등)</span>
    <span class="n">generateMyBatis</span> <span class="s1">'org.mybatis.generator:mybatis-generator-core:1.4.2'</span>
    <span class="n">generateMyBatis</span> <span class="s1">'com.oracle.database.jdbc:ojdbc17:23.26.1.0.0'</span>
<span class="o">}</span>

<span class="n">task</span> <span class="nf">generateMyBatis</span><span class="o">(</span><span class="nl">type:</span> <span class="n">JavaExec</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">classpath</span> <span class="o">=</span> <span class="n">configurations</span><span class="o">.</span><span class="na">generateMyBatis</span> <span class="o">+</span> <span class="k">sourceSets</span><span class="o">.</span><span class="na">main</span><span class="o">.</span><span class="na">runtimeClasspath</span>
    <span class="n">mainClass</span> <span class="o">=</span> <span class="s1">'org.mybatis.generator.api.ShellRunner'</span>
    <span class="n">args</span> <span class="o">=</span> <span class="o">[</span><span class="s1">'-configfile'</span><span class="o">,</span> <span class="s2">"${projectDir}/generatorConfig.xml"</span><span class="o">,</span> <span class="s1">'-overwrite'</span><span class="o">]</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">generatorConfig.xml</code>에서는 특정 스키마의 모든 테이블(<code class="language-plaintext highlighter-rouge">tableName="%"</code>)을 대상으로 코드 생성을 지시하고 있었고, 프로젝트 자체 커스텀 <code class="language-plaintext highlighter-rouge">LombokPlugin</code>(생성된 DTO에 <code class="language-plaintext highlighter-rouge">@Data</code>, <code class="language-plaintext highlighter-rouge">@Builder</code> 등을 주입)을 플러그인으로 사용하고 있었다.</p>

<h2 id="현상">현상</h2>

<p>IntelliJ의 Gradle Tool Window에서 <code class="language-plaintext highlighter-rouge">generateMyBatis</code> task를 실행하면 수 분간 돌다가 다음과 같이 실패했다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>java.lang.OutOfMemoryError: Java heap space
    at java.base/java.util.WeakHashMap.newTable(WeakHashMap.java:194)
    ...
</code></pre></div></div>

<p>프로젝트 루트에는 <code class="language-plaintext highlighter-rouge">java_pid13032.hprof</code>라는 약 <strong>847MB 크기의 힙 덤프 파일</strong>이 떨어졌다. <code class="language-plaintext highlighter-rouge">-XX:+HeapDumpOnOutOfMemoryError</code> JVM 옵션이 활성화되어 있었기에 OOM 순간 힙 스냅샷이 자동 저장된 것이다.</p>

<h2 id="디버깅-1단계-힙-크기-부족-의심-오답">디버깅 1단계: 힙 크기 부족 의심 (오답)</h2>

<p>처음엔 단순히 “테이블이 많아서 메타데이터 로드 중 힙이 부족한 것”이라고 판단했다. JavaExec가 기본 JVM 옵션으로 동작하기 때문에 힙 크기가 256~512MB 정도였을 것이라 보고, 최대 힙을 2GB, 이후 4GB로 상향했다.</p>

<div class="language-gradle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">task</span> <span class="nf">generateMyBatis</span><span class="o">(</span><span class="nl">type:</span> <span class="n">JavaExec</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">maxHeapSize</span> <span class="o">=</span> <span class="s1">'4g'</span>
    <span class="n">jvmArgs</span> <span class="o">=</span> <span class="o">[</span>
        <span class="s1">'-XX:+HeapDumpOnOutOfMemoryError'</span><span class="o">,</span>
        <span class="s2">"-XX:HeapDumpPath=${buildDir}/heapdumps"</span><span class="o">,</span>
    <span class="o">]</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>결과: 여전히 동일 지점에서 OOM.</strong> 힙 크기가 문제가 아니었다는 강력한 신호였다. 4GB를 다 먹고도 부족하다는 건, 어딘가에서 메모리가 계속 쌓이고 정리되지 않고 있다는 뜻이다.</p>

<h2 id="디버깅-2단계-jdbc-드라이버-캐시-의심-부분-오답">디버깅 2단계: JDBC 드라이버 캐시 의심 (부분 오답)</h2>

<p>스택 트레이스의 <code class="language-plaintext highlighter-rouge">WeakHashMap.newTable</code>이 단서였다. <code class="language-plaintext highlighter-rouge">WeakHashMap</code>은 <strong>key가 GC되면 자동으로 엔트리를 비워주는 Map</strong>이다. 정상 상황에서는 계속 성장하지 않는다. 만약 성장하고 있다면, key들이 어디선가 strong reference로 붙잡혀 GC되지 못하고 있다는 의미다.</p>

<p>당시 의심은 <strong>Oracle JDBC 드라이버(<code class="language-plaintext highlighter-rouge">ojdbc17:23.x</code>) 내부 캐시</strong>였다. Oracle JDBC는 Statement 캐시, 메타데이터 캐시 등을 내부적으로 운영하며, 과거 유사 사례에서 이런 캐시 누수로 OOM이 났다는 보고가 있다.</p>

<p>이에 따라 드라이버 캐시를 최소화하는 옵션을 추가했다.</p>

<div class="language-gradle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">jvmArgs</span> <span class="o">=</span> <span class="o">[</span>
    <span class="s1">'-Doracle.jdbc.implicitStatementCacheSize=0'</span><span class="o">,</span>
    <span class="s1">'-Doracle.jdbc.useThreadLocalBufferCache=false'</span><span class="o">,</span>
    <span class="s1">'-XX:+UseG1GC'</span><span class="o">,</span>
    <span class="s1">'-XX:SoftRefLRUPolicyMSPerMB=1'</span><span class="o">,</span>
<span class="o">]</span>
</code></pre></div></div>

<p><strong>결과: 또 같은 지점에서 OOM.</strong> 드라이버 캐시도 원인이 아니었다.</p>

<h2 id="디버깅-3단계-힙-덤프의-incoming-references를-따라가기-정답">디버깅 3단계: 힙 덤프의 Incoming References를 따라가기 (정답)</h2>

<p>이쯤 되어서야 힙 덤프를 제대로 분석하기 시작했다. IntelliJ의 Profiler에서 hprof 파일을 열면 여러 분석 뷰가 제공되는데, OOM 디버깅의 정석은 다음 순서다.</p>

<ol>
  <li><strong>Dominators</strong> 탭에서 힙을 가장 많이 점유한 객체 식별</li>
  <li>그 객체의 <strong>Incoming References</strong>를 따라 올라가며 “누가 이것을 strong reference로 붙잡고 있는가”를 역추적</li>
  <li>체인의 상위에서 나오는 <strong>클래스/패키지 이름으로 범인 라이브러리 특정</strong></li>
</ol>

<p>Dominator 최상위는 <code class="language-plaintext highlighter-rouge">java.util.WeakHashMap$Entry[2097152]</code>였다. 엔트리 테이블 크기가 2²¹, 즉 <strong>약 210만 슬롯</strong>. 일반적인 코드 생성 워크로드에서 나올 수 없는 규모다. Retained size는 121MB.</p>

<p>그 <code class="language-plaintext highlighter-rouge">WeakHashMap$Entry[]</code>의 Incoming References 체인을 따라가보니:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>java.util.WeakHashMap$Entry[2097152]
  table of java.util.WeakHashMap
    values of lombok.core.FieldAugment$MapWeakFieldAugment
      statically from JCTree_generatedNode of lombok.javac.JavacAugments
        java.lang.Object[366]
          elementData of java.util.ArrayList
            classes of lombok.launch.ShadowClassLoader (4.36 MB)
              statically from classLoader of lombok.launch.Main
</code></pre></div></div>

<p>체인에 <code class="language-plaintext highlighter-rouge">lombok.*</code> 패키지가 반복적으로 등장한다. 범인은 <strong>Lombok</strong>이었다.</p>

<h2 id="근본-원인-분석">근본 원인 분석</h2>

<h3 id="lombok의-javacaugments가-왜-이-맥락에서-성장하는가">Lombok의 JavacAugments가 왜 이 맥락에서 성장하는가</h3>

<p>Lombok은 일반 라이브러리와 동작 방식이 다르다. classpath에 로드되면 Java 컴파일러(javac) 내부에 끼어들어 AST(Abstract Syntax Tree, 구문 트리)를 조작할 준비를 한다. 이때 내부적으로 각 AST 노드에 대한 부가 정보를 <strong><code class="language-plaintext highlighter-rouge">lombok.javac.JavacAugments</code>라는 static 필드</strong>에 담긴 <code class="language-plaintext highlighter-rouge">WeakHashMap</code>에 매핑해둔다.</p>

<p>이 맵의 key는 AST 노드, value는 부가 메타데이터다. 정상적인 javac 컴파일 맥락에서는 각 컴파일 단위가 끝날 때 AST 노드들이 GC되고 맵도 자동으로 비워진다.</p>

<p>그런데 MyBatis Generator는 <strong>컴파일을 하지 않는다.</strong> 단순히 DB 메타데이터를 읽고 Java 소스 텍스트를 만들어 파일로 저장하는 도구다. 이 과정에서 많은 클래스 리플렉션과 동적 로딩이 일어나고, Lombok이 자기 초기화 과정에서 <code class="language-plaintext highlighter-rouge">ShadowClassLoader</code>를 통해 366개 클래스를 strong reference로 유지한다. 이 클래스들이 WeakHashMap의 key 체인을 붙잡으면서 <strong>GC되지 못한 엔트리가 계속 누적</strong>된다. 결국 테이블이 리사이즈될 때 다음 단계 크기(2²², 약 420만 슬롯)의 Entry 배열을 할당하려다 힙이 터진다.</p>

<p>즉, <strong>Generator에게는 필요조차 없는 Lombok이 우연히 classpath에 올라와 있었다는 것이 근본 원인</strong>이었다.</p>

<h3 id="왜-lombok이-generator-classpath에-있었나">왜 Lombok이 Generator classpath에 있었나</h3>

<p>원인은 다음 한 줄이었다.</p>

<div class="language-gradle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">classpath</span> <span class="o">=</span> <span class="n">configurations</span><span class="o">.</span><span class="na">generateMyBatis</span> <span class="o">+</span> <span class="k">sourceSets</span><span class="o">.</span><span class="na">main</span><span class="o">.</span><span class="na">runtimeClasspath</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sourceSets.main.runtimeClasspath</code>는 <strong>해당 모듈의 애플리케이션을 실제로 실행할 때 필요한 모든 라이브러리</strong>를 가리킨다. Spring Boot 기반 모듈이라면 Spring, 자동 구성된 각종 스타터, 그리고 Lombok까지 전부 포함된다.</p>

<p>하지만 MyBatis Generator는 <strong>애플리케이션이 아니라 별개의 코드 생성 도구</strong>다. 이 도구가 실제로 필요한 건:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">mybatis-generator-core</code> (Generator 본체)</li>
  <li>JDBC 드라이버 (DB 메타데이터 조회용)</li>
  <li>프로젝트에서 선언한 커스텀 플러그인이 있다면 그 클래스</li>
</ul>

<p>이게 전부다. Spring도, Lombok도, DevTools도 필요 없다. 그런데 “+<code class="language-plaintext highlighter-rouge">sourceSets.main.runtimeClasspath</code>“가 이 모든 것을 싸잡아 Generator JVM에 올렸고, 그 중 Lombok이 자기 몫의 메모리 문제를 일으킨 것이다.</p>

<h3 id="커스텀-lombokplugin과-lombok-라이브러리의-혼동">커스텀 LombokPlugin과 Lombok 라이브러리의 혼동</h3>

<p>중요한 구분이 하나 있다. 이 프로젝트에서 <code class="language-plaintext highlighter-rouge">generatorConfig.xml</code>의 다음 선언:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;plugin</span> <span class="na">type=</span><span class="s">"org.mybatis.generator.plugins.LombokPlugin"</span> <span class="nt">/&gt;</span>
</code></pre></div></div>

<p>이 <code class="language-plaintext highlighter-rouge">LombokPlugin</code>은 <strong>MyBatis Generator가 제공하는 빌트인 플러그인이 아니다.</strong> <code class="language-plaintext highlighter-rouge">frw</code> 모듈의 <code class="language-plaintext highlighter-rouge">src/main/java/org/mybatis/generator/plugins/LombokPlugin.java</code>에 자체 구현된 커스텀 클래스다. 패키지 이름 때문에 빌트인처럼 보이지만, <code class="language-plaintext highlighter-rouge">PluginAdapter</code>를 상속해 직접 만든 것이다.</p>

<p>이 플러그인이 하는 일은 단순하다. MyBatis Generator가 생성하는 Java 소스 텍스트에 <code class="language-plaintext highlighter-rouge">@Data</code>, <code class="language-plaintext highlighter-rouge">@Builder</code> 같은 <strong>어노테이션 문자열을 삽입</strong>할 뿐이다. Lombok 라이브러리를 호출하거나 AST를 조작하지 않는다. <code class="language-plaintext highlighter-rouge">@Data</code>는 생성된 DTO가 <strong>나중에 컴파일될 때</strong> Lombok이 처리하는 것이고, Generator 실행 시점에는 그저 문자열에 불과하다.</p>

<p>정리하면:</p>

<ul>
  <li>플러그인 실행에 <strong>필요한 것</strong>: 플러그인 클래스 파일(<code class="language-plaintext highlighter-rouge">LombokPlugin.class</code>)</li>
  <li>플러그인 실행에 <strong>필요 없는 것</strong>: Lombok 라이브러리 jar</li>
</ul>

<p>이 구분을 놓친 채 “Lombok 플러그인을 쓰려면 Lombok jar가 있어야지”라고 생각하기 쉽지만, 실제로는 그렇지 않다.</p>

<h2 id="해결">해결</h2>

<h3 id="1단계-classpath를-최소한으로-축소">1단계: classpath를 최소한으로 축소</h3>

<p>Generator JVM의 classpath에서 <code class="language-plaintext highlighter-rouge">sourceSets.main.runtimeClasspath</code>를 제거했다.</p>

<div class="language-gradle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">task</span> <span class="nf">generateMyBatis</span><span class="o">(</span><span class="nl">type:</span> <span class="n">JavaExec</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">classpath</span> <span class="o">=</span> <span class="n">configurations</span><span class="o">.</span><span class="na">generateMyBatis</span>
    <span class="n">mainClass</span> <span class="o">=</span> <span class="s1">'org.mybatis.generator.api.ShellRunner'</span>
    <span class="n">args</span> <span class="o">=</span> <span class="o">[</span><span class="s1">'-configfile'</span><span class="o">,</span> <span class="s2">"${projectDir}/generatorConfig.xml"</span><span class="o">,</span> <span class="s1">'-overwrite'</span><span class="o">]</span>
    <span class="n">maxHeapSize</span> <span class="o">=</span> <span class="s1">'4g'</span>
    <span class="n">jvmArgs</span> <span class="o">=</span> <span class="o">[</span>
        <span class="s1">'-XX:+HeapDumpOnOutOfMemoryError'</span><span class="o">,</span>
        <span class="s2">"-XX:HeapDumpPath=${buildDir}/heapdumps"</span><span class="o">,</span>
    <span class="o">]</span>
<span class="o">}</span>
</code></pre></div></div>

<p>이 변경만으로 OOM 원인인 Lombok이 Generator JVM에 로드되지 않게 된다.</p>

<h3 id="2단계-classnotfoundexception-대응">2단계: <code class="language-plaintext highlighter-rouge">ClassNotFoundException</code> 대응</h3>

<p>1단계 변경만으로는 문제가 하나 더 남는다. 커스텀 <code class="language-plaintext highlighter-rouge">LombokPlugin</code>이 있는 <code class="language-plaintext highlighter-rouge">frw</code> jar까지 classpath에서 빠지기 때문에, 플러그인 로딩 시 <code class="language-plaintext highlighter-rouge">ClassNotFoundException: org.mybatis.generator.plugins.LombokPlugin</code>이 발생한다.</p>

<p>해결: <code class="language-plaintext highlighter-rouge">frw</code>를 Generator 전용 configuration에 <strong>추이 의존성 없이</strong> 추가한다.</p>

<div class="language-gradle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">dependencies</span> <span class="o">{</span>
    <span class="n">generateMyBatis</span> <span class="s1">'org.mybatis.generator:mybatis-generator-core:1.4.2'</span>
    <span class="n">generateMyBatis</span> <span class="s1">'com.oracle.database.jdbc:ojdbc17:23.26.1.0.0'</span>
    <span class="n">generateMyBatis</span><span class="o">(</span><span class="s1">'com.jpmc.xxx:frw:1.+:plain'</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">transitive</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">transitive = false</code>는 “frw jar 자체만 가져오고, frw가 의존하는 다른 라이브러리는 무시하라”는 지시다. 이 프로젝트의 <code class="language-plaintext highlighter-rouge">frw</code>는 Lombok과 MyBatis Generator를 <code class="language-plaintext highlighter-rouge">compileOnly</code>로 선언하고 있어 추이 의존성이 원래도 Lombok을 물고 오지는 않지만, 혹시 모를 다른 전이 오염을 막기 위해 명시적으로 차단해두는 편이 안전하다.</p>

<p>결과적으로 Generator JVM의 classpath는 다음 세 개로 수렴한다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">mybatis-generator-core-1.4.2.jar</code></li>
  <li><code class="language-plaintext highlighter-rouge">ojdbc17-23.x.jar</code></li>
  <li><code class="language-plaintext highlighter-rouge">frw-1.x-plain.jar</code> (커스텀 플러그인만 포함)</li>
</ul>

<p>이 구성으로 OOM이 재발하지 않았고, 전체 스키마 테이블에 대해 문제없이 코드 생성이 완료됐다.</p>

<h2 id="덤으로-정리한-개념들">덤으로 정리한 개념들</h2>

<h3 id="outofmemoryerror-java-heap-space">OutOfMemoryError: Java heap space</h3>

<p>JVM은 시작 시 힙 메모리의 최대 크기를 지정받는다(<code class="language-plaintext highlighter-rouge">-Xmx</code>). 이 한계를 넘는 메모리를 할당하려 하면 OOM이 발생한다. 중요한 것은 <strong>힙이 가득 찼다는 사실 자체가 아니라 “왜 가득 찼는가”</strong>이다. 가득 찬 이유는 크게 두 가지다.</p>

<ul>
  <li><strong>정상 워크로드 대비 힙이 작다</strong>: 힙 크기를 올리면 해결된다.</li>
  <li><strong>어딘가에서 메모리가 해제되지 않고 누적된다 (leak)</strong>: 힙을 올려도 시간이 흐르면 다시 터진다. 이번 케이스가 여기에 해당했다.</li>
</ul>

<p>판별 기준: 힙을 여러 배 올려도 같은 지점에서 OOM이 재현되면 leak 쪽으로 무게를 두고 힙 덤프를 분석하는 게 맞다.</p>

<h3 id="weakhashmap과-reference-종류">WeakHashMap과 reference 종류</h3>

<p>Java는 객체 참조를 strong/soft/weak/phantom 네 종류로 나눈다.</p>

<ul>
  <li><strong>Strong reference</strong>: 일반 변수 참조. 이게 살아있는 한 GC 대상이 아니다.</li>
  <li><strong>Soft reference</strong>: 메모리가 부족할 때만 GC됨. 캐시 용도로 적합.</li>
  <li><strong>Weak reference</strong>: 다음 GC 때 바로 수거 가능.</li>
  <li><strong>Phantom reference</strong>: 객체가 수거된 후 후처리용.</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">WeakHashMap</code>은 key를 weak reference로 보관한다. 따라서 다른 곳에서 key를 strong reference로 붙잡고 있지 않다면 GC가 돌 때 엔트리가 자동 삭제된다. 반대로 <strong>어딘가에서 strong reference로 key를 붙잡고 있다면 WeakHashMap은 무한히 성장할 수 있다</strong>. 이번 케이스는 Lombok의 <code class="language-plaintext highlighter-rouge">ShadowClassLoader</code>가 classes ArrayList로 클래스들을 붙잡고 있어 weak key들이 수거되지 못하고 누적된 상태였다.</p>

<h3 id="gradle-classpath와-configuration">Gradle classpath와 configuration</h3>

<p>Gradle에서 <code class="language-plaintext highlighter-rouge">configuration</code>은 “같은 목적의 의존성들을 묶는 논리적 그룹”이다. 대표적인 configuration:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">implementation</code>: 애플리케이션 런타임에 필요한 일반 의존성</li>
  <li><code class="language-plaintext highlighter-rouge">compileOnly</code>: 컴파일 시에만 필요, 런타임 classpath에 없음</li>
  <li><code class="language-plaintext highlighter-rouge">annotationProcessor</code>: 컴파일 시 어노테이션 처리기</li>
  <li><code class="language-plaintext highlighter-rouge">runtimeOnly</code>: 런타임에만 필요</li>
</ul>

<p>여기에 우리가 직접 만든 <code class="language-plaintext highlighter-rouge">generateMyBatis</code> 같은 커스텀 configuration을 추가할 수 있다. 각 configuration은 독립된 classpath를 가지며, 어떤 작업에 어떤 configuration을 넘길지 선택할 수 있다.</p>

<p><code class="language-plaintext highlighter-rouge">sourceSets.main.runtimeClasspath</code>는 <code class="language-plaintext highlighter-rouge">main</code> 소스셋을 실제 실행할 때의 합산 classpath(= <code class="language-plaintext highlighter-rouge">implementation</code> + <code class="language-plaintext highlighter-rouge">runtimeOnly</code> + 컴파일된 <code class="language-plaintext highlighter-rouge">main</code> 출력 + 그 추이 의존성 전부)를 가리킨다. 이걸 그대로 Generator 같은 독립 도구의 classpath로 넘기는 건 <strong>도구에 불필요한 라이브러리를 잔뜩 주입</strong>하는 것과 같다. 도구가 그걸 무해하게 무시하면 다행이지만, 이번 Lombok처럼 로드되는 순간 자기 초기화를 시작하는 라이브러리가 섞이면 부작용이 발생한다.</p>

<h3 id="transitive-dependency">Transitive dependency</h3>

<p>의존성 A가 B를 필요로 하고, B가 C를 필요로 할 때, A만 선언해도 Gradle은 B와 C를 자동으로 가져온다. 이 자동 포함되는 간접 의존성을 <strong>추이(transitive) 의존성</strong>이라 한다.</p>

<p>편리한 기본 동작이지만, <strong>“A는 필요하지만 A가 딸고 오는 C는 원치 않는다”</strong> 는 경우에 대비해 <code class="language-plaintext highlighter-rouge">transitive = false</code> 옵션이 제공된다. 이번 해결에서는 <code class="language-plaintext highlighter-rouge">frw</code> jar에서 필요한 건 그 안의 커스텀 플러그인 클래스 하나뿐이었으므로, <code class="language-plaintext highlighter-rouge">frw</code>의 추이 의존성이 classpath를 오염시키는 일이 없도록 <code class="language-plaintext highlighter-rouge">transitive = false</code>를 명시했다.</p>

<h2 id="til">TIL</h2>

<ol>
  <li>
    <p><strong>OOM의 스택 트레이스가 <code class="language-plaintext highlighter-rouge">WeakHashMap.newTable</code>처럼 라이브러리 내부 지점을 가리키면, 힙 크기 조정보다 먼저 힙 덤프 분석부터 한다.</strong> 힙을 두 배, 세 배 올려봐서 동일 지점에서 터지면 누수임이 거의 확정이다.</p>
  </li>
  <li>
    <p><strong>힙 덤프 분석의 핵심은 Dominator → Incoming References → 라이브러리 식별이다.</strong> 메모리를 차지한 객체 자체보다 “누가 그 객체를 붙잡고 있는가”가 답을 준다. strong reference 체인을 따라 올라가다 나오는 패키지 이름이 범인이다.</p>
  </li>
  <li>
    <p><strong>“애플리케이션 실행용 classpath”와 “코드 생성 도구용 classpath”는 별개다.</strong> 두 context의 요구사항이 다르므로, 편의상 합쳐 쓰는 순간 예상치 못한 부작용이 생길 수 있다. Generator/Codegen 같은 독립 도구는 반드시 자기 전용 configuration만 받도록 격리한다.</p>
  </li>
  <li>
    <p><strong>Lombok처럼 JVM에 로드되자마자 자기 초기화를 시작하는 라이브러리는 classpath 오염에 민감하다.</strong> “거기 있어도 안 쓰면 그만”이라는 일반 라이브러리의 직관이 Lombok에는 적용되지 않는다.</p>
  </li>
  <li>
    <p><strong>라이브러리와 소스에서 쓰는 플러그인 클래스를 혼동하지 말 것.</strong> <code class="language-plaintext highlighter-rouge">LombokPlugin</code>(코드 텍스트에 어노테이션 문자열을 넣는 플러그인)과 Lombok 라이브러리(컴파일 시 AST를 조작하는 라이브러리)는 관계없다. 플러그인이 돌기 위해 라이브러리가 필요할 것이라는 직관을 먼저 의심해야 했다.</p>
  </li>
</ol>

<h2 id="참고">참고</h2>

<p>힙 덤프 분석 시 유용한 무료 도구:</p>

<ul>
  <li>IntelliJ IDEA Ultimate의 Profiler (이번에 사용)</li>
  <li>Eclipse Memory Analyzer (MAT)</li>
  <li>VisualVM</li>
</ul>

<p>모두 Dominator 트리와 Incoming References 조회를 지원한다. 어떤 도구를 쓰든 분석 절차는 동일하다.</p>]]></content><author><name>Duyan Kim</name></author><category term="Language" /><category term="Java" /><category term="Gradle" /><category term="MyBatis" /><category term="Lombok" /><category term="JVM" /><category term="OOM" /><summary type="html"><![CDATA[Gradle, MyBatis, Lombok, JVM]]></summary></entry></feed>