<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>JJ Garden</title>
    <link>https://jaejung.tistory.com/</link>
    <description>그냥 해봤습니다.</description>
    <language>ko</language>
    <pubDate>Tue, 30 Jun 2026 21:51:29 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>just just do it</managingEditor>
    <image>
      <title>JJ Garden</title>
      <url>https://tistory1.daumcdn.net/tistory/6112849/attach/2a45e00eb0d244ebace826efcfbc8763</url>
      <link>https://jaejung.tistory.com</link>
    </image>
    <item>
      <title>Socket.IO의 Namespace, Event, Room 개념 헷갈려서 정리해봤습니다</title>
      <link>https://jaejung.tistory.com/8</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script&gt;
(function() {
  function smoothScroll(id) {
    var el = document.getElementById(id);
    if (!el) return;
    var top = el.getBoundingClientRect().top + window.pageYOffset - 80;
    window.scrollTo({ top: top, behavior: 'smooth' });
  }

  function buildTOC() {
    var article = document.querySelector('.tt_article_useless_p_margin');
    if (!article || document.getElementById('auto-toc-nav')) return;
    var headings = article.querySelectorAll('h2, h3');
    if (!headings.length) return;

    headings.forEach(function(h, i) { h.id = 'toc-anchor-' + i; });

    var style = document.createElement('style');
    style.textContent = [
      '#auto-toc-nav{background:#fff;border:0.5px solid #e0e0e0;border-radius:12px;overflow:hidden;margin-bottom:28px;font-family:inherit}',
      '#toc-head{display:flex;align-items:center;gap:8px;padding:13px 18px;background:#f6f7f8;border-bottom:0.5px solid #e8e8e8;font-size:14px;font-weight:500;cursor:pointer;user-select:none}',
      '#toc-head .toc-count{font-size:11px;color:#888;background:#ececec;border-radius:4px;padding:1px 6px;margin-left:4px;font-weight:400}',
      '#toc-head .toc-caret{margin-left:auto;font-size:12px;color:#999;transition:transform .2s;display:inline-block}',
      '#toc-head .toc-caret.open{transform:rotate(180deg)}',
      '#toc-body{padding:10px 0}',
      '.toc-h2-row{display:flex;align-items:center;position:relative}',
      '.toc-h2-row .toc-bar{width:3px;min-width:3px;height:32px;background:#1D9E75;border-radius:0 2px 2px 0;opacity:0;transition:opacity .15s}',
      '.toc-h2-row:hover .toc-bar{opacity:1}',
      '.toc-arrow{display:flex;align-items:center;justify-content:center;width:28px;min-width:28px;height:32px;cursor:pointer;color:#aaa;font-size:11px;transition:transform .2s}',
      '.toc-arrow.open{transform:rotate(90deg)}',
      /* 밑줄·색상 강제 제거 */
      '#auto-toc-nav a{text-decoration:none !important;box-shadow:none !important}',
      '.toc-a2{flex:1;padding:5px 18px 5px 4px;font-size:14px;font-weight:500;color:#222 !important;cursor:pointer}',
      '.toc-a2:hover{color:#1D9E75 !important}',
      '.toc-sub{padding:2px 0 4px 48px;display:none}',
      '.toc-sub.open{display:block}',
      '.toc-a3{display:block !important;padding:4px 18px 4px 0;font-size:13px;color:#666 !important;cursor:pointer}',
      '.toc-a3::before{content:&quot;&quot;;display:inline-block;width:4px;height:4px;border-radius:50%;background:#ccc;margin-right:8px;vertical-align:middle;margin-bottom:1px}',
      '.toc-a3:hover{color:#1D9E75 !important}',
      '.toc-div{height:0.5px;background:#f0f0f0;margin:3px 18px}'
    ].join('');
    document.head.appendChild(style);

    var nav = document.createElement('nav');
    nav.id = 'auto-toc-nav';

    var totalLabel = headings.length + '개 항목';
    var head = document.createElement('div');
    head.id = 'toc-head';
    head.innerHTML = '&lt;span style=&quot;color:#1D9E75;font-size:16px;&quot;&gt;&amp;#9776;&lt;/span&gt; 목차 &lt;span class=&quot;toc-count&quot;&gt;' + totalLabel + '&lt;/span&gt;&lt;span class=&quot;toc-caret open&quot;&gt;&amp;#9660;&lt;/span&gt;';

    var bodyWrap = document.createElement('div');
    bodyWrap.id = 'toc-body';

    // 전체 접기/펼치기
    head.addEventListener('click', function() {
      var caret = head.querySelector('.toc-caret');
      var isOpen = caret.classList.contains('open');
      bodyWrap.style.display = isOpen ? 'none' : 'block';
      caret.classList.toggle('open', !isOpen);
    });

    nav.appendChild(head);

    var currentSub = null;
    var isFirst = true;

    headings.forEach(function(h, i) {
      var id = 'toc-anchor-' + i;

      if (h.tagName === 'H2') {
        if (!isFirst) {
          var div = document.createElement('div');
          div.className = 'toc-div';
          bodyWrap.appendChild(div);
        }
        isFirst = false;

        var row = document.createElement('div');
        row.className = 'toc-h2-row';

        var bar = document.createElement('div');
        bar.className = 'toc-bar';

        var arrow = document.createElement('span');
        arrow.className = 'toc-arrow'; // open 없음 = 기본 닫힘

        arrow.textContent = '▶';

        var a = document.createElement('a');
        a.className = 'toc-a2';
        a.textContent = h.innerText;
        a.href = 'javascript:void(0)';
        a.setAttribute('data-id', id);
        a.addEventListener('click', function() {
          smoothScroll(this.getAttribute('data-id'));
        });

        var sub = document.createElement('div');
        sub.className = 'toc-sub'; // open 없음 = 기본 닫힘

        arrow.addEventListener('click', function() {
          var isOpen = sub.classList.contains('open');
          sub.classList.toggle('open', !isOpen);
          arrow.classList.toggle('open', !isOpen);
        });

        row.appendChild(bar);
        row.appendChild(arrow);
        row.appendChild(a);
        bodyWrap.appendChild(row);
        bodyWrap.appendChild(sub);
        currentSub = sub;

      } else if (h.tagName === 'H3') {
        var target = currentSub || bodyWrap;
        var a3 = document.createElement('a');
        a3.className = 'toc-a3';
        a3.textContent = h.innerText;
        a3.href = 'javascript:void(0)';
        a3.setAttribute('data-id', id);
        a3.addEventListener('click', function() {
          smoothScroll(this.getAttribute('data-id'));
        });
        target.appendChild(a3);
      }
    });

    nav.appendChild(bodyWrap);
    article.insertBefore(nav, article.firstChild);
  }

  if (document.readyState === 'loading') {
    window.addEventListener('DOMContentLoaded', buildTOC);
  } else {
    buildTOC();
  }
})();
&lt;/script&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레츠고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Socket.IO로 실시간 기능을 설계하다 보면 Namespace, Event, Room이라는 세 가지 개념을 마주하게 된다. 이름만 봐서는 역할 구분이 헷갈릴 수 있는데, 각각의 목적이 명확히 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Namespace&lt;/b&gt;는 연결 공간을 분리하는 단위이고, &lt;b&gt;Event&lt;/b&gt;는 클라이언트와 서버가 주고받는 메시지의 이름이며, &lt;b&gt;Room&lt;/b&gt;은 특정 클라이언트들에게만 메시지를 보내기 위한 서버 측 그룹이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Namespace&lt;/td&gt;
&lt;td&gt;연결 영역 분리&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/chat&lt;/code&gt;, &lt;code&gt;/notification&lt;/code&gt;, &lt;code&gt;/dashboard&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event&lt;/td&gt;
&lt;td&gt;메시지 종류 구분&lt;/td&gt;
&lt;td&gt;&lt;code&gt;subscribe&lt;/code&gt;, &lt;code&gt;unsubscribe&lt;/code&gt;, &lt;code&gt;realtime_data&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Room&lt;/td&gt;
&lt;td&gt;메시지를 받을 클라이언트 그룹&lt;/td&gt;
&lt;td&gt;&lt;code&gt;project:123&lt;/code&gt;, &lt;code&gt;user:45&lt;/code&gt;, &lt;code&gt;dashboard:sales&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄로 요약하면 이렇다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Namespace로 기능 영역을 나누고, Room으로 수신 대상을 나누고, Event로 메시지 종류를 구분한다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;Namespace: /dashboard
Room:      dashboard:sales
Event:     realtime_data&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 구조는 &lt;code&gt;/dashboard&lt;/code&gt; 연결 공간 안에서, &lt;code&gt;dashboard:sales&lt;/code&gt; 대시보드를 보고 있는 사용자들에게, &lt;code&gt;realtime_data&lt;/code&gt; 이벤트로 실시간 데이터를 전송하는 방식이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Namespace&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Namespace는 Socket.IO 연결 공간을 기능 단위로 분리하는 개념이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 Namespace는 &lt;code&gt;/&lt;/code&gt;다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const socket = io(&quot;/&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 규모가 커지면 하나의 연결 공간에서 모든 이벤트를 처리하기보다, 기능별로 연결 공간을 나누는 편이 관리하기 훨씬 쉽다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const chatSocket         = io(&quot;/chat&quot;);
const notificationSocket = io(&quot;/notification&quot;);
const dashboardSocket    = io(&quot;/dashboard&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서도 각각의 Namespace를 따로 처리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;const chat = io.of(&quot;/chat&quot;);
chat.on(&quot;connection&quot;, (socket) =&amp;gt; {
  console.log(&quot;chat namespace connected&quot;);
});

const notification = io.of(&quot;/notification&quot;);
notification.on(&quot;connection&quot;, (socket) =&amp;gt; {
  console.log(&quot;notification namespace connected&quot;);
});

const dashboard = io.of(&quot;/dashboard&quot;);
dashboard.on(&quot;connection&quot;, (socket) =&amp;gt; {
  console.log(&quot;dashboard namespace connected&quot;);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Namespace는 다음처럼 &lt;b&gt;큰 기능 영역을 분리할 때&lt;/b&gt; 사용하는 것이 적합하다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;/chat           채팅 기능
/notification   알림 기능
/dashboard      실시간 대시보드 기능
/admin          관리자 기능&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⚠️ Namespace를 너무 잘게 나누는 것은 피해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 이벤트 하나마다 Namespace를 만들면 연결 관리가 불필요하게 복잡해진다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 지양
/message / /read / /typing / /online

# 권장
/chat  &amp;rarr;  내부에서 Event와 Room으로 세부 기능 구분&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Event&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Event는 클라이언트와 서버가 주고받는 메시지의 이름이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 서버로 메시지를 보낼 때도, 서버가 클라이언트에게 메시지를 보낼 때도 모두 Event를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 클라이언트가 특정 대시보드 데이터를 구독하고 싶다면 &lt;code&gt;subscribe&lt;/code&gt; 이벤트를 보낸다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;// 클라이언트 &amp;rarr; 서버
socket.emit(&quot;subscribe&quot;, {
  roomId: &quot;dashboard:sales&quot;
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 &lt;code&gt;subscribe&lt;/code&gt; 이벤트를 받아 처리한다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;// 서버에서 수신
socket.on(&quot;subscribe&quot;, (payload) =&amp;gt; {
  console.log(payload.roomId); // &quot;dashboard:sales&quot;
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 서버가 특정 클라이언트에게 실시간 데이터를 전달할 때는 &lt;code&gt;realtime_data&lt;/code&gt; 같은 이벤트를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// 서버 &amp;rarr; 특정 Room의 클라이언트 전체에 broadcast
io.of(&quot;/dashboard&quot;).to(&quot;dashboard:sales&quot;).emit(&quot;realtime_data&quot;, {
  type: &quot;sales_count&quot;,
  value: 1520,
  timestamp: &quot;2026-06-25T16:00:00+09:00&quot;
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event는 &lt;b&gt;&quot;이 메시지가 어떤 목적인가?&quot;&lt;/b&gt; 를 구분하는 이름이다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;subscribe       구독 요청
unsubscribe     구독 해제 요청
realtime_data   실시간 데이터 전달
message         채팅 메시지 전달
typing          입력 중 상태 전달
error           에러 응답&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Room&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Room은 같은 Namespace 안에서 특정 클라이언트들을 묶는 서버 측 그룹이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 여러 사용자가 같은 프로젝트 페이지를 보고 있다면, 서버는 해당 사용자들의 socket을 &lt;code&gt;project:123&lt;/code&gt; Room에 묶을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;tcl&quot;&gt;&lt;code&gt;socket.join(&quot;project:123&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 서버는 &lt;code&gt;project:123&lt;/code&gt; Room에 속한 클라이언트들에게만 메시지를 보낼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;io.of(&quot;/dashboard&quot;)
  .to(&quot;project:123&quot;)
  .emit(&quot;realtime_data&quot;, {
    projectId: 123,
    activeUsers: 8,
    updatedTaskCount: 24
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Room은 다음과 같은 상황에서 유용하다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;project:123         특정 프로젝트 화면을 보고 있는 사용자 그룹
chat:room:10        특정 채팅방에 들어와 있는 사용자 그룹
user:45             특정 사용자에게만 보내는 개인 알림 그룹
dashboard:sales     매출 대시보드를 보고 있는 사용자 그룹
document:777        같은 문서를 공동 편집 중인 사용자 그룹&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점이 두 가지 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫째, Room은 서버가 관리하는 그룹이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 &quot;이 데이터를 구독하고 싶다&quot;고 요청만 하고, 실제로 어떤 Room에 넣을지는 서버가 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘째, 클라이언트가 연결을 끊으면 모든 Room에서 자동으로 제거된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;disconnect&lt;/code&gt; 이벤트에서 직접 &lt;code&gt;socket.leave()&lt;/code&gt;를 호출하지 않아도 된다. 다만 사용자가 특정 화면을 벗어나는 것처럼 연결은 유지되면서 구독만 해제하는 경우에는 명시적으로 &lt;code&gt;socket.leave()&lt;/code&gt;를 호출해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실전 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 대시보드 기능을 설계하는 상황을 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 매출 대시보드 화면에 진입하면 해당 데이터를 구독하고, 화면을 벗어나면 구독을 해제하는 흐름이다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;Namespace: /dashboard
Room:      dashboard:sales
Events:    subscribe / unsubscribe / realtime_data&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클라이언트&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;const socket = io(&quot;/dashboard&quot;);

// 매출 대시보드 화면 진입 시 구독 요청
socket.emit(&quot;subscribe&quot;, {
  roomId: &quot;dashboard:sales&quot;
});

// 구독 완료 확인 수신
socket.on(&quot;subscribe_success&quot;, (data) =&amp;gt; {
  console.log(data.message); // &quot;구독이 완료되었습니다.&quot;
});

// 실시간 데이터 수신 &amp;rarr; 화면 갱신
socket.on(&quot;realtime_data&quot;, (data) =&amp;gt; {
  console.log(&quot;실시간 데이터 수신:&quot;, data);
  // 1. 매출 금액 업데이트
  // 2. 주문 수 업데이트
  // 3. 실시간 차트 갱신
});

// 매출 대시보드 화면 이탈 시 구독 해제 요청
socket.emit(&quot;unsubscribe&quot;, {
  roomId: &quot;dashboard:sales&quot;
});

// 구독 해제 완료 확인 수신
socket.on(&quot;unsubscribe_success&quot;, (data) =&amp;gt; {
  console.log(data.message); // &quot;구독이 해제되었습니다.&quot;
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const dashboard = io.of(&quot;/dashboard&quot;);

dashboard.on(&quot;connection&quot;, (socket) =&amp;gt; {

  // 구독 요청 처리
  socket.on(&quot;subscribe&quot;, (payload) =&amp;gt; {
    const { roomId } = payload;

    socket.join(roomId); // Room에 추가

    socket.emit(&quot;subscribe_success&quot;, {
      roomId,
      message: &quot;구독이 완료되었습니다.&quot;
    });
  });

  // 구독 해제 요청 처리
  socket.on(&quot;unsubscribe&quot;, (payload) =&amp;gt; {
    const { roomId } = payload;

    socket.leave(roomId); // Room에서 제거

    socket.emit(&quot;unsubscribe_success&quot;, {
      roomId,
      message: &quot;구독이 해제되었습니다.&quot;
    });
  });

});

// 매출 데이터 변경 시 해당 Room 전체에 broadcast
io.of(&quot;/dashboard&quot;)
  .to(&quot;dashboard:sales&quot;)
  .emit(&quot;realtime_data&quot;, {
    type: &quot;sales_summary&quot;,
    todaySales: 1250000,
    orderCount: 320,
    timestamp: &quot;2026-06-25T16:00:00+09:00&quot;
  });&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 흐름 요약&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 클라이언트가 /dashboard Namespace에 연결한다.
2. 사용자가 매출 대시보드 화면에 진입한다.
3. 클라이언트가 subscribe 이벤트로 dashboard:sales 구독을 요청한다.
4. 서버가 해당 socket을 dashboard:sales Room에 추가하고 subscribe_success를 응답한다.
5. 서버는 매출 데이터가 변경될 때마다 dashboard:sales Room 전체에 realtime_data를 broadcast한다.
6. 클라이언트는 realtime_data 이벤트를 받아 화면을 갱신한다.
7. 사용자가 화면을 이탈하면 unsubscribe 이벤트를 보낸다.
8. 서버가 해당 socket을 dashboard:sales Room에서 제거하고 unsubscribe_success를 응답한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 개념의 역할을 정리하면 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;실제 역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dashboard&lt;/code&gt; Namespace&lt;/td&gt;
&lt;td&gt;대시보드 기능 전용 연결 공간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dashboard:sales&lt;/code&gt; Room&lt;/td&gt;
&lt;td&gt;매출 대시보드를 보고 있는 사용자 그룹&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subscribe&lt;/code&gt; Event&lt;/td&gt;
&lt;td&gt;특정 대시보드 데이터 구독 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unsubscribe&lt;/code&gt; Event&lt;/td&gt;
&lt;td&gt;특정 대시보드 데이터 구독 해제 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;realtime_data&lt;/code&gt; Event&lt;/td&gt;
&lt;td&gt;서버가 Room 전체에 전송하는 실시간 데이터&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Socket.IO에서 Namespace, Event, Room은 각각 역할이 다르다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Namespace&lt;/b&gt; &amp;mdash; 연결 영역을 기능 단위로 분리한다. &lt;code&gt;/chat&lt;/code&gt;, &lt;code&gt;/notification&lt;/code&gt;, &lt;code&gt;/dashboard&lt;/code&gt;처럼 큰 기능 경계에서만 나눈다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Event&lt;/b&gt; &amp;mdash; 메시지의 목적을 구분하는 이름이다. &lt;code&gt;subscribe&lt;/code&gt;, &lt;code&gt;unsubscribe&lt;/code&gt;, &lt;code&gt;realtime_data&lt;/code&gt;처럼 &quot;이 메시지가 무엇을 하는가&quot;를 표현한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Room&lt;/b&gt; &amp;mdash; 서버가 관리하는 클라이언트 그룹이다. &lt;code&gt;dashboard:sales&lt;/code&gt;, &lt;code&gt;project:123&lt;/code&gt;처럼 메시지를 받을 대상을 묶는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설계 기준을 한 줄로 정리하면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;기능 단위   &amp;rarr;  Namespace
수신 대상   &amp;rarr;  Room
메시지 목적 &amp;rarr;  Event&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 가지 개념을 역할에 맞게 사용하면, 복잡한 실시간 기능도 명확하게 구조화할 수 있다.&lt;/p&gt;</description>
      <category> &amp;zwj;  SW</category>
      <author>just just do it</author>
      <guid isPermaLink="true">https://jaejung.tistory.com/8</guid>
      <comments>https://jaejung.tistory.com/8#entry8comment</comments>
      <pubDate>Thu, 25 Jun 2026 22:15:26 +0900</pubDate>
    </item>
    <item>
      <title>백엔드 프레임워크를 선택하기 전에 알아야 할 동시성 모델 레츠고</title>
      <link>https://jaejung.tistory.com/5</link>
      <description>&lt;script&gt;
(function() {
  function smoothScroll(id) {
    var el = document.getElementById(id);
    if (!el) return;
    var top = el.getBoundingClientRect().top + window.pageYOffset - 80;
    window.scrollTo({ top: top, behavior: 'smooth' });
  }

  function buildTOC() {
    var article = document.querySelector('.tt_article_useless_p_margin');
    if (!article || document.getElementById('auto-toc-nav')) return;
    var headings = article.querySelectorAll('h2, h3');
    if (!headings.length) return;

    headings.forEach(function(h, i) { h.id = 'toc-anchor-' + i; });

    var style = document.createElement('style');
    style.textContent = [
      '#auto-toc-nav{background:#fff;border:0.5px solid #e0e0e0;border-radius:12px;overflow:hidden;margin-bottom:28px;font-family:inherit}',
      '#toc-head{display:flex;align-items:center;gap:8px;padding:13px 18px;background:#f6f7f8;border-bottom:0.5px solid #e8e8e8;font-size:14px;font-weight:500;cursor:pointer;user-select:none}',
      '#toc-head .toc-count{font-size:11px;color:#888;background:#ececec;border-radius:4px;padding:1px 6px;margin-left:4px;font-weight:400}',
      '#toc-head .toc-caret{margin-left:auto;font-size:12px;color:#999;transition:transform .2s;display:inline-block}',
      '#toc-head .toc-caret.open{transform:rotate(180deg)}',
      '#toc-body{padding:10px 0}',
      '.toc-h2-row{display:flex;align-items:center;position:relative}',
      '.toc-h2-row .toc-bar{width:3px;min-width:3px;height:32px;background:#1D9E75;border-radius:0 2px 2px 0;opacity:0;transition:opacity .15s}',
      '.toc-h2-row:hover .toc-bar{opacity:1}',
      '.toc-arrow{display:flex;align-items:center;justify-content:center;width:28px;min-width:28px;height:32px;cursor:pointer;color:#aaa;font-size:11px;transition:transform .2s}',
      '.toc-arrow.open{transform:rotate(90deg)}',
      /* 밑줄·색상 강제 제거 */
      '#auto-toc-nav a{text-decoration:none !important;box-shadow:none !important}',
      '.toc-a2{flex:1;padding:5px 18px 5px 4px;font-size:14px;font-weight:500;color:#222 !important;cursor:pointer}',
      '.toc-a2:hover{color:#1D9E75 !important}',
      '.toc-sub{padding:2px 0 4px 48px;display:none}',
      '.toc-sub.open{display:block}',
      '.toc-a3{display:block !important;padding:4px 18px 4px 0;font-size:13px;color:#666 !important;cursor:pointer}',
      '.toc-a3::before{content:&quot;&quot;;display:inline-block;width:4px;height:4px;border-radius:50%;background:#ccc;margin-right:8px;vertical-align:middle;margin-bottom:1px}',
      '.toc-a3:hover{color:#1D9E75 !important}',
      '.toc-div{height:0.5px;background:#f0f0f0;margin:3px 18px}'
    ].join('');
    document.head.appendChild(style);

    var nav = document.createElement('nav');
    nav.id = 'auto-toc-nav';

    var totalLabel = headings.length + '개 항목';
    var head = document.createElement('div');
    head.id = 'toc-head';
    head.innerHTML = '&lt;span style=&quot;color:#1D9E75;font-size:16px;&quot;&gt;&amp;#9776;&lt;/span&gt; 목차 &lt;span class=&quot;toc-count&quot;&gt;' + totalLabel + '&lt;/span&gt;&lt;span class=&quot;toc-caret open&quot;&gt;&amp;#9660;&lt;/span&gt;';

    var bodyWrap = document.createElement('div');
    bodyWrap.id = 'toc-body';

    // 전체 접기/펼치기
    head.addEventListener('click', function() {
      var caret = head.querySelector('.toc-caret');
      var isOpen = caret.classList.contains('open');
      bodyWrap.style.display = isOpen ? 'none' : 'block';
      caret.classList.toggle('open', !isOpen);
    });

    nav.appendChild(head);

    var currentSub = null;
    var isFirst = true;

    headings.forEach(function(h, i) {
      var id = 'toc-anchor-' + i;

      if (h.tagName === 'H2') {
        if (!isFirst) {
          var div = document.createElement('div');
          div.className = 'toc-div';
          bodyWrap.appendChild(div);
        }
        isFirst = false;

        var row = document.createElement('div');
        row.className = 'toc-h2-row';

        var bar = document.createElement('div');
        bar.className = 'toc-bar';

        var arrow = document.createElement('span');
        arrow.className = 'toc-arrow'; // open 없음 = 기본 닫힘

        arrow.textContent = '▶';

        var a = document.createElement('a');
        a.className = 'toc-a2';
        a.textContent = h.innerText;
        a.href = 'javascript:void(0)';
        a.setAttribute('data-id', id);
        a.addEventListener('click', function() {
          smoothScroll(this.getAttribute('data-id'));
        });

        var sub = document.createElement('div');
        sub.className = 'toc-sub'; // open 없음 = 기본 닫힘

        arrow.addEventListener('click', function() {
          var isOpen = sub.classList.contains('open');
          sub.classList.toggle('open', !isOpen);
          arrow.classList.toggle('open', !isOpen);
        });

        row.appendChild(bar);
        row.appendChild(arrow);
        row.appendChild(a);
        bodyWrap.appendChild(row);
        bodyWrap.appendChild(sub);
        currentSub = sub;

      } else if (h.tagName === 'H3') {
        var target = currentSub || bodyWrap;
        var a3 = document.createElement('a');
        a3.className = 'toc-a3';
        a3.textContent = h.innerText;
        a3.href = 'javascript:void(0)';
        a3.setAttribute('data-id', id);
        a3.addEventListener('click', function() {
          smoothScroll(this.getAttribute('data-id'));
        });
        target.appendChild(a3);
      }
    });

    nav.appendChild(bodyWrap);
    article.insertBefore(nav, article.firstChild);
  }

  if (document.readyState === 'loading') {
    window.addEventListener('DOMContentLoaded', buildTOC);
  } else {
    buildTOC();
  }
})();
&lt;/script&gt;


&lt;h2&gt;동시성 모델을 왜 이해해야 하는가?&lt;/h2&gt;
&lt;p&gt;요즘 백엔드 프레임워크는 대부분 충분히 빠르다.&lt;/p&gt;
&lt;p&gt;일반적인 CRUD API나 단순 요청 처리만 놓고 보면 FastAPI, Spring Boot, Node.js, Go 중 무엇을 선택하더라도 프레임워크 자체가 병목이 되는 경우는 많지 않다. 실제 서비스에서 더 자주 문제가 되는 부분은 DB 쿼리, 외부 API 지연, 커넥션 풀 부족, 잘못된 worker 설정, CPU-heavy 작업 처리 방식 같은 요소다.&lt;/p&gt;
&lt;p&gt;그렇다고 해서 백엔드 프레임워크의 동시성 모델을 몰라도 된다는 뜻은 아니다.&lt;/p&gt;
&lt;p&gt;오히려 프레임워크 자체 성능이 충분히 좋아진 만큼, 성능 문제는 &amp;quot;어떤 프레임워크를 선택했는가&amp;quot;보다 &amp;quot;그 프레임워크를 어떻게 사용했는가&amp;quot;에서 발생하는 경우가 많다.&lt;/p&gt;
&lt;p&gt;백엔드 서버는 동시에 들어오는 여러 요청을 안정적으로 처리해야 한다. 하지만 &amp;quot;동시에 처리한다&amp;quot;는 말은 생각보다 단순하지 않다.&lt;/p&gt;
&lt;p&gt;어떤 서버는 요청마다 스레드를 하나씩 배정하고, 어떤 서버는 이벤트 루프 하나가 여러 요청을 번갈아 처리한다. 또 어떤 언어와 런타임은 고루틴, 코루틴, Virtual Thread 같은 경량 실행 단위를 제공한다.&lt;/p&gt;
&lt;p&gt;즉, 백엔드 프레임워크를 선택한다는 것은 단순히 다음 중 하나를 고르는 문제가 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FastAPI vs Spring Boot vs Node.js vs Go&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;더 본질적으로는 다음 질문에 답하는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;이 프레임워크는 여러 요청을 어떤 방식으로 처리하는가?
I/O 대기 중에는 실행 자원을 어떻게 다루는가?
CPU-heavy 작업이 들어오면 서버 전체에 어떤 영향을 주는가?
멀티코어를 어떻게 활용하는가?
worker를 여러 개 띄웠을 때 상태 관리는 안전한가?&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 모델을 이해하지 못하면 충분히 빠른 프레임워크를 사용하고도 다음과 같은 문제가 생길 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- async를 썼는데 오히려 서버가 느려짐
- CPU 작업 하나 때문에 이벤트 루프 전체가 멈춤
- 스레드 풀이 CPU 작업에 잠식되어 일반 API까지 지연됨
- worker를 여러 개 띄웠더니 메모리에 저장한 상태가 사라짐
- 프레임워크는 고성능인데 구현 방식 때문에 성능이 나오지 않음&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;따라서 동시성 모델을 이해한다는 것은 &amp;quot;가장 빠른 프레임워크를 고르기 위한 것&amp;quot;이 아니다.&lt;/p&gt;
&lt;p&gt;각 프레임워크가 어떤 방식으로 요청을 처리하고, 어떤 상황에서 병목이 생기며, 어떤 작업을 별도 worker나 process로 분리해야 하는지 이해하기 위한 것이다.&lt;/p&gt;
&lt;p&gt;결국 중요한 것은 특정 프레임워크가 무조건 빠르다는 결론이 아니다. 프로젝트의 워크로드와 운영 환경에 맞는 프레임워크를 선택하고, 그 프레임워크의 동시성 모델에 맞게 안정적으로 구현하는 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 먼저 알아야 할 핵심 개념&lt;/h2&gt;
&lt;p&gt;동시성 모델을 비교하기 전에 아래 개념을 먼저 구분해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1.1 동시성 Concurrency&lt;/h3&gt;
&lt;p&gt;동시성은 &lt;strong&gt;여러 작업을 겹쳐서 다루는 능력&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;작업들이 실제로 같은 순간에 동시에 실행되지 않더라도, 어떤 작업이 I/O 응답을 기다리는 동안 다른 작업을 처리하면 여러 작업이 함께 진행되는 것처럼 만들 수 있다.&lt;/p&gt;
&lt;p&gt;아래 그림은 CPU Core가 1개만 있는 상황을 가정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;gantt
    title 동시성 Concurrency - 하나의 Core가 여러 작업을 번갈아 처리
    dateFormat  YYYY-MM-DD HH:mm:ss
    axisFormat  %S초

    section CPU Core 1
    Request A 실행        :a1, 2026-01-01 00:00:00, 2s
    Request B 실행        :b1, 2026-01-01 00:00:02, 2s
    Request C 실행        :c1, 2026-01-01 00:00:04, 2s
    Request A 재개        :a2, 2026-01-01 00:00:06, 2s
    Request B 재개        :b2, 2026-01-01 00:00:08, 2s

    section Request A 상태
    A CPU 사용            :a_cpu1, 2026-01-01 00:00:00, 2s
    A I/O 대기            :a_wait, 2026-01-01 00:00:02, 4s
    A CPU 재사용          :a_cpu2, 2026-01-01 00:00:06, 2s

    section Request B 상태
    B 대기                :b_idle, 2026-01-01 00:00:00, 2s
    B CPU 사용            :b_cpu1, 2026-01-01 00:00:02, 2s
    B I/O 대기            :b_wait, 2026-01-01 00:00:04, 4s
    B CPU 재사용          :b_cpu2, 2026-01-01 00:00:08, 2s

    section Request C 상태
    C 대기                :c_idle, 2026-01-01 00:00:00, 4s
    C CPU 사용            :c_cpu1, 2026-01-01 00:00:04, 2s&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;동시성은 실제로 동시에 실행하는 것이 아니라, 여러 작업이 동시에 진행되는 것처럼 효율적으로 번갈아 처리하는 능력이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;1.2 병렬성 Parallelism&lt;/h3&gt;
&lt;p&gt;병렬성은 &lt;strong&gt;여러 작업이 실제로 같은 순간에 동시에 실행되는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;병렬성은 CPU Core가 여러 개 있을 때 가능하다. 각 Core가 서로 다른 작업을 같은 시간 구간에 실행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;gantt
    title 병렬성 Parallelism - 여러 Core가 여러 작업을 실제 동시에 실행
    dateFormat  YYYY-MM-DD HH:mm:ss
    axisFormat  %S초

    section CPU Core 1
    Request A 실행        :a1, 2026-01-01 00:00:00, 6s

    section CPU Core 2
    Request B 실행        :b1, 2026-01-01 00:00:00, 6s

    section CPU Core 3
    Request C 실행        :c1, 2026-01-01 00:00:00, 6s

    section CPU Core 4
    Request D 실행        :d1, 2026-01-01 00:00:00, 6s&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;병렬성은 여러 CPU Core가 여러 작업을 실제로 동시에 실행하는 능력이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;1.3 동시성과 병렬성 비교&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;gantt
    title 동시성 vs 병렬성 - 같은 시간축에서 비교
    dateFormat  YYYY-MM-DD HH:mm:ss
    axisFormat  %S초

    section 동시성 - Core 1개
    A 실행                :c_a1, 2026-01-01 00:00:00, 2s
    B 실행                :c_b1, 2026-01-01 00:00:02, 2s
    C 실행                :c_c1, 2026-01-01 00:00:04, 2s
    A 재개                :c_a2, 2026-01-01 00:00:06, 2s
    B 재개                :c_b2, 2026-01-01 00:00:08, 2s

    section 병렬성 - Core 1
    A 실행                :p_a, 2026-01-01 00:00:00, 6s

    section 병렬성 - Core 2
    B 실행                :p_b, 2026-01-01 00:00:00, 6s

    section 병렬성 - Core 3
    C 실행                :p_c, 2026-01-01 00:00:00, 6s&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;핵심 질문&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;동시성&lt;/td&gt;
&lt;td&gt;여러 작업을 어떻게 겹쳐서 다룰 것인가?&lt;/td&gt;
&lt;td&gt;하나의 실행 자원으로도 대기 시간을 활용해 여러 작업을 번갈아 처리할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;병렬성&lt;/td&gt;
&lt;td&gt;여러 작업을 실제로 동시에 실행할 수 있는가?&lt;/td&gt;
&lt;td&gt;여러 CPU Core가 있어야 실제 동시 실행이 가능하다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;동시성은 &lt;strong&gt;작업을 다루는 구조&lt;/strong&gt;의 문제이고, 병렬성은 &lt;strong&gt;실제 실행 자원&lt;/strong&gt;의 문제다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;1.4 비동기와 논블로킹&lt;/h3&gt;
&lt;p&gt;동시성 모델을 이해할 때 자주 나오는 개념이 비동기와 논블로킹이다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;비동기 Async&lt;/td&gt;
&lt;td&gt;작업 완료를 기다리지 않고, 완료되면 나중에 이어서 처리하는 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;논블로킹 Non-blocking&lt;/td&gt;
&lt;td&gt;호출한 스레드나 이벤트 루프를 멈추지 않고 즉시 제어권을 반환하는 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;둘은 비슷하게 쓰이지만 같은 말은 아니다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;async/await&lt;/code&gt;는 비동기 흐름을 표현하는 문법이다. 하지만 실제 I/O가 논블로킹으로 동작하려면 사용하는 DB driver, HTTP client, socket I/O도 non-blocking이어야 한다.&lt;/p&gt;
&lt;p&gt;즉, &lt;code&gt;async&lt;/code&gt;를 붙였다고 모든 코드가 자동으로 non-blocking이 되는 것은 아니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 백엔드 서버에서 중요한 두 가지 워크로드&lt;/h2&gt;
&lt;p&gt;동시성 모델을 비교할 때는 먼저 서버가 주로 어떤 작업을 하는지 구분해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2.1 I/O-bound 작업&lt;/h3&gt;
&lt;p&gt;I/O-bound 작업은 CPU 연산보다 &lt;strong&gt;기다리는 시간이 대부분인 작업&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DB 조회&lt;/li&gt;
&lt;li&gt;Redis 조회&lt;/li&gt;
&lt;li&gt;외부 API 호출&lt;/li&gt;
&lt;li&gt;파일 읽기/쓰기&lt;/li&gt;
&lt;li&gt;네트워크 통신&lt;/li&gt;
&lt;li&gt;메시지 브로커 publish/consume&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 작업에서는 CPU가 계속 바쁜 것이 아니라 DB나 네트워크 응답을 기다리는 시간이 길다.&lt;/p&gt;
&lt;p&gt;따라서 I/O-bound 서버에서는 다음이 중요하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;기다리는 동안 스레드나 이벤트 루프를 얼마나 효율적으로 다른 작업에 넘길 수 있는가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;2.2 CPU-bound 작업&lt;/h3&gt;
&lt;p&gt;CPU-bound 작업은 실제 CPU 연산이 많은 작업이다.&lt;/p&gt;
&lt;p&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이미지 처리&lt;/li&gt;
&lt;li&gt;영상 처리&lt;/li&gt;
&lt;li&gt;압축/해제&lt;/li&gt;
&lt;li&gt;암호화&lt;/li&gt;
&lt;li&gt;FFT&lt;/li&gt;
&lt;li&gt;대규모 JSON 직렬화/역직렬화&lt;/li&gt;
&lt;li&gt;머신러닝 전처리/후처리&lt;/li&gt;
&lt;li&gt;CPU 기반 모델 추론&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 작업은 기다리는 것이 아니라 실제로 CPU를 사용한다.&lt;/p&gt;
&lt;p&gt;따라서 CPU-bound 작업에서는 다음이 중요하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;CPU Core 수만큼 병렬로 실행할 수 있는가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;중요한 점은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Event Loop, Coroutine, Virtual Thread, Goroutine 같은 경량 동시성 모델은 I/O 대기에는 강하지만, CPU 연산을 마법처럼 빠르게 만들지는 않는다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;CPU-bound 작업은 결국 CPU Core 수, worker 수, process 수, GPU 사용 여부가 중요하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2.3 CPU-heavy 작업에 대한 공통 원칙&lt;/h3&gt;
&lt;p&gt;CPU-heavy 작업은 어떤 동시성 모델에서도 주의해야 한다. 다만 문제가 생기는 방식이 다르다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;실행 모델&lt;/th&gt;
&lt;th&gt;CPU-heavy 작업을 직접 실행하면&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Thread-per-Request&lt;/td&gt;
&lt;td&gt;해당 요청 스레드가 오래 점유된다. 많아지면 request thread pool이 고갈된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event Loop&lt;/td&gt;
&lt;td&gt;이벤트 루프 worker 전체가 막힌다. 같은 worker 안의 다른 요청까지 지연된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go Goroutine&lt;/td&gt;
&lt;td&gt;여러 goroutine이 CPU를 두고 경쟁한다. 결국 CPU Core 수가 한계다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kotlin Coroutine&lt;/td&gt;
&lt;td&gt;Dispatcher thread를 점유한다. 잘못 쓰면 dispatcher가 고갈된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual Thread&lt;/td&gt;
&lt;td&gt;CPU 작업 중에는 I/O 대기처럼 carrier thread를 반납하지 않는다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;일반적인 원칙은 다음과 같다.&lt;/p&gt;
&lt;p&gt;가벼운 CPU 작업은 짧고 제한적인 계산이라면 API 서버에서 직접 처리할 수 있다. 다만 요청 수가 많아지면 작은 CPU 작업도 누적되어 병목이 될 수 있다.&lt;/p&gt;
&lt;p&gt;반면 이미지 처리, 영상 처리, 압축, FFT, ML 추론처럼 무겁거나 오래 걸리는 CPU/GPU 작업은 API 서버의 주 요청 경로에서 분리하는 것이 안전하다. 이때 사용할 수 있는 방식은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;별도 worker&lt;/li&gt;
&lt;li&gt;process pool&lt;/li&gt;
&lt;li&gt;job queue&lt;/li&gt;
&lt;li&gt;Ray/Celery&lt;/li&gt;
&lt;li&gt;inference service&lt;/li&gt;
&lt;li&gt;GPU serving runtime&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;특히 Event Loop 기반 서버에서는 CPU-heavy 작업을 이벤트 루프 안에서 직접 실행하면 안 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Thread-per-Request&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;대표 예시: Spring MVC + Tomcat, Django + Gunicorn sync worker, Rails&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;3.1 핵심 개념&lt;/h3&gt;
&lt;p&gt;Thread-per-Request는 요청 하나를 하나의 스레드가 처리하는 방식이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;요청 1개 = 스레드 1개 배정&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;요청이 들어오면 스레드 풀에서 스레드 하나를 꺼내고, 그 스레드가 요청의 시작부터 끝까지 처리한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant C1 as Client 1
    participant C2 as Client 2
    participant C3 as Client 3
    participant TP as Thread Pool
    participant T1 as Thread 1
    participant T2 as Thread 2
    participant DB as Database

    C1-&amp;gt;&amp;gt;TP: 요청 도착
    TP-&amp;gt;&amp;gt;T1: Thread 1 배정

    C2-&amp;gt;&amp;gt;TP: 요청 도착
    TP-&amp;gt;&amp;gt;T2: Thread 2 배정

    C3-&amp;gt;&amp;gt;TP: 요청 도착
    Note over TP: 사용 가능한 스레드가 없으면 대기

    T1-&amp;gt;&amp;gt;DB: SELECT 요청
    Note over T1: DB 응답 대기 중&amp;lt;br/&amp;gt;스레드 풀 슬롯 점유

    T2-&amp;gt;&amp;gt;DB: SELECT 요청
    Note over T2: DB 응답 대기 중&amp;lt;br/&amp;gt;스레드 풀 슬롯 점유

    DB--&amp;gt;&amp;gt;T1: 결과 반환
    T1--&amp;gt;&amp;gt;C1: 응답

    TP-&amp;gt;&amp;gt;T1: 대기 중이던 C3 요청 배정&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;3.2 I/O-bound에서의 병목&lt;/h3&gt;
&lt;p&gt;Thread-per-Request에서 DB 응답을 기다리는 동안 CPU를 계속 사용하는 것은 아니다. 하지만 스레드는 다음 리소스를 점유한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스레드 풀의 한 자리&lt;/li&gt;
&lt;li&gt;스레드 스택 메모리&lt;/li&gt;
&lt;li&gt;DB 커넥션&lt;/li&gt;
&lt;li&gt;요청 컨텍스트&lt;/li&gt;
&lt;li&gt;커널 스케줄링 대상&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 I/O 대기가 길고 동시 요청이 많아지면 CPU 사용률은 높지 않은데도 스레드 풀이 고갈되어 요청이 밀릴 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A[동시 요청 1000개] --&amp;gt; B{스레드 풀 200개}
    B --&amp;gt;|200개 처리 중| C[DB/API 응답 대기&amp;lt;br/&amp;gt;스레드 점유]
    B --&amp;gt;|800개 대기| D[Queue 대기]
    C --&amp;gt; E[응답 도착 후 처리 재개]
    D --&amp;gt; F[스레드가 반환되면 처리 시작]

    style C fill:#ffcccc
    style D fill:#ffe0b3&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;3.3 CPU-bound에는 유리한가?&lt;/h3&gt;
&lt;p&gt;Thread-per-Request는 Event Loop와 비교하면 CPU-bound 작업에 상대적으로 유리하다. 이유는 요청마다 별도 스레드가 있기 때문이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Thread 1 → Request A CPU 작업
Thread 2 → Request B 처리
Thread 3 → Request C 처리
Thread 4 → Request D 처리&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Request A가 CPU 작업을 하더라도 Event Loop처럼 worker 전체가 즉시 멈추는 것은 아니다. 해당 요청을 처리하는 Thread 1이 점유될 뿐이고, 다른 스레드는 계속 다른 요청을 처리할 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 상대적으로 보면 다음과 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;CPU-bound 작업 직접 실행 시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Event Loop&lt;/td&gt;
&lt;td&gt;이벤트 루프 worker 전체가 막힘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thread-per-Request&lt;/td&gt;
&lt;td&gt;해당 요청 스레드가 막힘&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;이 점 때문에 Thread-per-Request는 CPU-bound 작업에 상대적으로 유리한 실행 모델이라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 한계도 명확하다. CPU-bound 작업은 결국 CPU Core를 사용한다. 예를 들어 8 Core 서버에서 CPU-heavy 요청 100개가 동시에 들어오면 실제로 동시에 빠르게 실행되는 작업은 대략 Core 수 근처다. 나머지 작업은 CPU 스케줄링을 기다리게 된다.&lt;/p&gt;
&lt;p&gt;또한 CPU-heavy 요청이 request thread pool을 많이 점유하면 일반 API 요청도 사용할 수 있는 스레드가 없어 대기할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Thread Pool 200개
CPU-heavy 요청 200개 점유
일반 API 요청 도착
→ 사용 가능한 request thread 없음
→ 일반 API도 지연&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;따라서 결론은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Thread-per-Request는 Event Loop보다 CPU-bound 작업에 상대적으로 유리하다. 하지만 무거운 CPU/GPU 작업을 request thread에서 오래 실행하면 thread pool 고갈이 발생할 수 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;즉, 짧고 제한적인 CPU 작업은 request thread에서 처리할 수 있지만, 이미지 처리, 영상 처리, ML 추론, 대용량 압축 같은 무거운 작업은 별도 worker/process/inference service로 분리하는 것이 안전하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3.4 장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;코드가 직관적이다.&lt;/li&gt;
&lt;li&gt;위에서 아래로 순차 실행되므로 이해하기 쉽다.&lt;/li&gt;
&lt;li&gt;디버깅이 상대적으로 쉽다.&lt;/li&gt;
&lt;li&gt;기존 blocking 라이브러리와 호환성이 좋다.&lt;/li&gt;
&lt;li&gt;Event Loop보다 CPU-bound 작업에 상대적으로 안전하다.&lt;/li&gt;
&lt;li&gt;Java/Spring MVC 같은 성숙한 생태계와 잘 맞는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;3.5 단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;I/O 대기 중에도 스레드 풀 슬롯을 점유한다.&lt;/li&gt;
&lt;li&gt;동시 요청 수가 스레드 풀 크기에 영향을 많이 받는다.&lt;/li&gt;
&lt;li&gt;스레드 수를 무작정 늘리면 메모리와 컨텍스트 스위칭 부담이 커진다.&lt;/li&gt;
&lt;li&gt;많은 동시 연결을 오래 유지하는 서비스에는 불리할 수 있다.&lt;/li&gt;
&lt;li&gt;CPU-heavy 작업이 많아지면 request thread pool이 잠식될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;3.6 적합한 경우&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;팀이 동기식 코드에 익숙한 경우&lt;/li&gt;
&lt;li&gt;요청 처리 흐름이 복잡하지만 순차적으로 표현하는 것이 중요한 경우&lt;/li&gt;
&lt;li&gt;blocking 라이브러리나 레거시 시스템과의 통합이 많은 경우&lt;/li&gt;
&lt;li&gt;요청 처리 시간이 짧은 일반 API 서버&lt;/li&gt;
&lt;li&gt;가벼운 CPU 작업이 포함된 API 서버&lt;/li&gt;
&lt;li&gt;무거운 CPU/GPU 작업은 별도 worker로 분리할 수 있는 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4. Event Loop&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;대표 예시: Node.js, FastAPI/asyncio, Nginx, Netty, Spring WebFlux&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;4.1 핵심 개념&lt;/h3&gt;
&lt;p&gt;Event Loop는 하나의 루프가 여러 I/O 작업을 번갈아 처리하는 방식이다.&lt;/p&gt;
&lt;p&gt;정확히는 보통 다음과 같이 이해하는 것이 좋다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;하나의 worker/process 안에서 이벤트 루프가 여러 비동기 작업을 처리한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;서버 전체가 반드시 스레드 하나만 쓴다는 뜻은 아니다. 운영 환경에서는 worker process를 여러 개 띄워 멀티코어를 활용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant C1 as Client 1
    participant C2 as Client 2
    participant C3 as Client 3
    participant EL as Event Loop
    participant DB as Database

    C1-&amp;gt;&amp;gt;EL: 요청 1 도착
    EL-&amp;gt;&amp;gt;DB: 비동기 DB 요청
    Note over EL: DB 응답을 기다리지 않고&amp;lt;br/&amp;gt;다음 작업 처리

    C2-&amp;gt;&amp;gt;EL: 요청 2 도착
    EL-&amp;gt;&amp;gt;DB: 비동기 DB 요청

    C3-&amp;gt;&amp;gt;EL: 요청 3 도착
    EL-&amp;gt;&amp;gt;DB: 비동기 DB 요청

    DB--&amp;gt;&amp;gt;EL: 요청 1 결과 도착
    EL--&amp;gt;&amp;gt;C1: 응답

    DB--&amp;gt;&amp;gt;EL: 요청 2 결과 도착
    EL--&amp;gt;&amp;gt;C2: 응답

    DB--&amp;gt;&amp;gt;EL: 요청 3 결과 도착
    EL--&amp;gt;&amp;gt;C3: 응답&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4.2 Event Loop가 잘 동작하는 조건&lt;/h3&gt;
&lt;p&gt;Event Loop가 성능을 내려면 중요한 조건이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이벤트 루프 안에서 오래 걸리는 blocking 작업을 하면 안 된다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;잘 맞는 작업&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;async DB driver&lt;/li&gt;
&lt;li&gt;async Redis client&lt;/li&gt;
&lt;li&gt;async HTTP client&lt;/li&gt;
&lt;li&gt;non-blocking socket I/O&lt;/li&gt;
&lt;li&gt;WebSocket 연결 관리&lt;/li&gt;
&lt;li&gt;SSE&lt;/li&gt;
&lt;li&gt;API Gateway&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;위험한 작업&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU-heavy 연산&lt;/li&gt;
&lt;li&gt;동기 blocking DB driver&lt;/li&gt;
&lt;li&gt;동기 HTTP client&lt;/li&gt;
&lt;li&gt;큰 파일을 동기 방식으로 읽고 쓰기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;time.sleep()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;무거운 이미지/영상 처리&lt;/li&gt;
&lt;li&gt;ML 추론&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;4.3 CPU-bound 작업에 특히 취약한 이유&lt;/h3&gt;
&lt;p&gt;Event Loop는 CPU-heavy 작업에 특히 취약하다.&lt;/p&gt;
&lt;p&gt;이유는 간단하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;CPU 작업 하나가 이벤트 루프를 점유하면, 같은 worker 안의 다른 요청까지 모두 밀린다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant C1 as Client 1
    participant C2 as Client 2
    participant WS as WebSocket
    participant EL as Event Loop
    participant CPU as Heavy CPU Function

    C1-&amp;gt;&amp;gt;EL: 요청 1
    EL-&amp;gt;&amp;gt;CPU: CPU-heavy 작업 직접 실행
    Note over EL,CPU: Event Loop가 점유됨&amp;lt;br/&amp;gt;다른 이벤트 처리 불가

    C2-&amp;gt;&amp;gt;EL: 요청 2
    Note over C2,EL: 대기

    WS-&amp;gt;&amp;gt;EL: WebSocket ping
    Note over WS,EL: 대기

    CPU--&amp;gt;&amp;gt;EL: CPU 작업 완료
    EL--&amp;gt;&amp;gt;C1: 응답
    EL-&amp;gt;&amp;gt;C2: 이제 요청 2 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thread-per-Request에서는 CPU 작업이 해당 요청 스레드를 막는다. 하지만 Event Loop에서는 CPU 작업이 event loop worker 전체를 막는다.&lt;/p&gt;
&lt;p&gt;따라서 Event Loop 기반 서버에서는 CPU-heavy 작업을 별도 worker/process/inference service로 분리하는 것이 사실상 필수다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4.4 멀티코어 활용&lt;/h3&gt;
&lt;p&gt;Event Loop는 보통 worker 하나당 하나의 이벤트 루프를 가진다. 따라서 멀티코어를 활용하려면 여러 worker를 띄운다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    LB[Load Balancer] --&amp;gt; W1[Worker 1&amp;lt;br/&amp;gt;Event Loop 1&amp;lt;br/&amp;gt;Core 1]
    LB --&amp;gt; W2[Worker 2&amp;lt;br/&amp;gt;Event Loop 2&amp;lt;br/&amp;gt;Core 2]
    LB --&amp;gt; W3[Worker 3&amp;lt;br/&amp;gt;Event Loop 3&amp;lt;br/&amp;gt;Core 3]
    LB --&amp;gt; W4[Worker 4&amp;lt;br/&amp;gt;Event Loop 4&amp;lt;br/&amp;gt;Core 4]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FastAPI를 예로 들면 다음과 같은 형태가 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;uvicorn app:app --workers 4&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4.5 장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;I/O 대기 시간이 많은 서버에 효율적이다.&lt;/li&gt;
&lt;li&gt;적은 수의 worker로 많은 연결을 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;WebSocket, SSE, 실시간 알림에 적합하다.&lt;/li&gt;
&lt;li&gt;API Gateway, BFF 구조에 잘 맞는다.&lt;/li&gt;
&lt;li&gt;높은 동시 연결을 처리하기 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;4.6 단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;blocking 코드 하나가 event loop 전체를 막을 수 있다.&lt;/li&gt;
&lt;li&gt;CPU-heavy 작업에 취약하다.&lt;/li&gt;
&lt;li&gt;async/await가 호출 체인에 전파된다.&lt;/li&gt;
&lt;li&gt;디버깅이 동기 코드보다 어려울 수 있다.&lt;/li&gt;
&lt;li&gt;사용하는 라이브러리도 async/non-blocking인지 확인해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;4.7 적합한 경우&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;DB/API 호출이 많은 I/O-bound API 서버&lt;/li&gt;
&lt;li&gt;WebSocket 기반 실시간 서비스&lt;/li&gt;
&lt;li&gt;채팅, 알림, streaming response&lt;/li&gt;
&lt;li&gt;API Gateway, BFF&lt;/li&gt;
&lt;li&gt;Python/FastAPI, Node.js 기반 서비스&lt;/li&gt;
&lt;li&gt;CPU-heavy 작업을 별도 worker로 분리할 수 있는 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;5. FastAPI를 사용할 때의 적용 규칙&lt;/h2&gt;
&lt;p&gt;FastAPI는 이벤트 루프 기반으로 동작할 수 있지만, 모든 코드가 자동으로 non-blocking이 되는 것은 아니다.&lt;/p&gt;
&lt;p&gt;FastAPI에서는 크게 두 가지 endpoint 스타일이 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5.1 &lt;code&gt;async def&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;async def&lt;/code&gt;는 이벤트 루프 위에서 실행된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.get(&amp;quot;/users/{id}&amp;quot;)
async def get_user(id: int):
    user = await async_db.fetch_user(id)
    return user&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 내부에서 &lt;code&gt;await&lt;/code&gt; 가능한 non-blocking I/O를 사용할 때 적합하다.&lt;/p&gt;
&lt;p&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;asyncpg&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aiomysql&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;httpx.AsyncClient&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aioredis&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aiofiles&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하지만 &lt;code&gt;async def&lt;/code&gt; 안에서 blocking 함수를 직접 호출하면 이벤트 루프가 막힌다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 좋지 않은 예
@app.get(&amp;quot;/bad&amp;quot;)
async def bad():
    result = heavy_cpu_work()
    return result&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;5.2 일반 &lt;code&gt;def&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;FastAPI에서 일반 &lt;code&gt;def&lt;/code&gt; endpoint는 threadpool에서 실행된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.get(&amp;quot;/legacy-users/{id}&amp;quot;)
def get_legacy_user(id: int):
    user = blocking_db.fetch_user(id)
    return user&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 blocking 라이브러리를 사용해야 한다면 무조건 &lt;code&gt;async def&lt;/code&gt;로 감싸는 것보다, 일반 &lt;code&gt;def&lt;/code&gt;를 사용하는 편이 더 안전할 수 있다.&lt;/p&gt;
&lt;p&gt;단, 일반 &lt;code&gt;def&lt;/code&gt;로 작성한 endpoint도 무한정 안전한 것은 아니다. threadpool 크기에는 한계가 있으므로, 오래 걸리는 CPU-heavy 작업을 일반 &lt;code&gt;def&lt;/code&gt; endpoint에서 계속 처리하면 threadpool이 고갈될 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5.3 FastAPI에서 blocking 작업과 CPU 작업 처리&lt;/h3&gt;
&lt;p&gt;CPU-heavy 작업은 API 이벤트 루프 안에서 직접 처리하지 않는 것이 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 좋지 않은 예
@app.post(&amp;quot;/predict&amp;quot;)
async def predict(data: InputData):
    result = heavy_ml_inference(data)
    return result&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;blocking I/O나 짧은 blocking 작업은 threadpool로 보낼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.post(&amp;quot;/process&amp;quot;)
async def process(data: InputData):
    result = await asyncio.to_thread(blocking_function, data)
    return result&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 순수 Python CPU-bound 작업은 threadpool로 보내도 GIL과 CPU Core 한계 때문에 처리량이 크게 늘지 않을 수 있다. 이런 작업은 ProcessPool, Ray, Celery, 별도 inference service로 분리하는 편이 더 적합하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A[FastAPI API Server] --&amp;gt;|요청 수신| B{작업 유형}
    B --&amp;gt;|가벼운 I/O| C[async/await]
    B --&amp;gt;|blocking I/O| D[sync def 또는 threadpool]
    B --&amp;gt;|CPU-heavy| E[ProcessPool / Ray / Celery]
    B --&amp;gt;|GPU inference| F[별도 Inference Service / Ray / Triton]

    E --&amp;gt; G[결과 반환]
    F --&amp;gt; G
    G --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;5.4 FastAPI worker와 상태 관리&lt;/h3&gt;
&lt;p&gt;FastAPI를 여러 worker로 실행하면 각 worker는 별도 프로세스다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    LB[Load Balancer] --&amp;gt; W1[Worker 1&amp;lt;br/&amp;gt;Process 1]
    LB --&amp;gt; W2[Worker 2&amp;lt;br/&amp;gt;Process 2]
    LB --&amp;gt; W3[Worker 3&amp;lt;br/&amp;gt;Process 3]

    W1 --&amp;gt; M1[Memory State A]
    W2 --&amp;gt; M2[Memory State B]
    W3 --&amp;gt; M3[Memory State C]

    R1[Request 1] --&amp;gt; W1
    R2[Request 2] --&amp;gt; W2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;첫 번째 요청은 worker 1로 가고, 두 번째 요청은 worker 2로 갈 수 있다. 이 경우 worker 1의 메모리에 저장된 상태를 worker 2는 알 수 없다.&lt;/p&gt;
&lt;p&gt;따라서 다음 기준을 사용할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;정합성이 중요한 상태: Redis, DB, 외부 state store에 저장&lt;/li&gt;
&lt;li&gt;단순 캐시: worker local memory 허용 가능&lt;/li&gt;
&lt;li&gt;요청 중 임시 상태: local variable 사용 가능&lt;/li&gt;
&lt;li&gt;장기 세션 상태: Redis/DB 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;중요한 점은 이것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Stateless라는 말은 메모리를 전혀 쓰지 말라는 뜻이 아니다. 여러 worker가 공유해야 하는 중요한 상태를 특정 worker 메모리에만 두지 말라는 뜻이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;6. Go Goroutine&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;대표 예시: Go net/http, Gin, Echo, Fiber&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;6.1 핵심 개념&lt;/h3&gt;
&lt;p&gt;Go는 goroutine이라는 경량 실행 단위를 제공한다.&lt;/p&gt;
&lt;p&gt;goroutine은 OS thread가 아니다. 하지만 OS thread 없이 실행되는 것도 아니다.&lt;/p&gt;
&lt;p&gt;정확히는 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Go runtime scheduler가 많은 goroutine을 적은 수의 OS thread 위에 배치해서 실행한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이것을 흔히 M:N 스케줄링이라고 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    subgraph &amp;quot;Go Runtime&amp;quot;
        G1[Goroutine 1]
        G2[Goroutine 2]
        G3[Goroutine 3]
        G4[Goroutine 4]
        G5[Goroutine 5]
        G6[Goroutine N]
    end

    subgraph &amp;quot;OS Threads&amp;quot;
        T1[OS Thread 1]
        T2[OS Thread 2]
        T3[OS Thread 3]
        T4[OS Thread 4]
    end

    G1 --&amp;gt; T1
    G2 --&amp;gt; T1
    G3 --&amp;gt; T2
    G4 --&amp;gt; T2
    G5 --&amp;gt; T3
    G6 --&amp;gt; T4

    T1 --&amp;gt; C1[CPU Core 1]
    T2 --&amp;gt; C2[CPU Core 2]
    T3 --&amp;gt; C3[CPU Core 3]
    T4 --&amp;gt; C4[CPU Core 4]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;goroutine이 가볍다고 해서 무제한으로 생성해도 된다는 뜻은 아니다. 요청 수, DB connection pool, 외부 API 처리량, channel 대기 상태를 고려하지 않으면 goroutine leak이나 backpressure 문제가 생길 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6.2 Go 코드의 특징&lt;/h3&gt;
&lt;p&gt;Go는 async/await 없이도 동기 코드처럼 작성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func handleRequest(w http.ResponseWriter, r *http.Request) {
    result, err := db.Query(&amp;quot;SELECT ...&amp;quot;)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }

    json.NewEncoder(w).Encode(result)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;겉으로는 blocking 코드처럼 보이지만, Go runtime은 네트워크 I/O 대기 중인 goroutine을 적절히 스케줄링한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6.3 CPU-bound 관점&lt;/h3&gt;
&lt;p&gt;Go goroutine은 CPU-bound 작업도 병렬로 실행할 수 있다. Go는 Python의 GIL 같은 제약이 없고, 여러 OS thread를 통해 멀티코어를 활용할 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 CPU-bound 작업도 결국 CPU Core 수가 한계다.&lt;/p&gt;
&lt;p&gt;goroutine을 많이 만든다고 CPU 작업 처리량이 무제한으로 늘어나지는 않는다. 무거운 CPU 작업이 많다면 worker pool, queue, backpressure, rate limit이 필요하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;6.4 장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;생성 비용이 작다.&lt;/li&gt;
&lt;li&gt;코드가 동기식처럼 읽힌다.&lt;/li&gt;
&lt;li&gt;async/await 전파가 없다.&lt;/li&gt;
&lt;li&gt;멀티코어 병렬 실행이 가능하다.&lt;/li&gt;
&lt;li&gt;네트워크 서버 구현에 강하다.&lt;/li&gt;
&lt;li&gt;단일 바이너리 배포가 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;6.5 주의할 점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;goroutine leak이 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;channel을 잘못 사용하면 deadlock이 생길 수 있다.&lt;/li&gt;
&lt;li&gt;공유 메모리 접근 시 race condition이 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;CPU-bound 작업은 결국 CPU Core 수가 한계다.&lt;/li&gt;
&lt;li&gt;DB connection pool, 외부 API 한계는 그대로 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;6.6 적합한 경우&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;고성능 API 서버&lt;/li&gt;
&lt;li&gt;네트워크 서버&lt;/li&gt;
&lt;li&gt;메시지 처리 서비스&lt;/li&gt;
&lt;li&gt;마이크로서비스&lt;/li&gt;
&lt;li&gt;edge 환경에서 단일 바이너리 배포가 중요한 경우&lt;/li&gt;
&lt;li&gt;I/O와 가벼운 CPU 작업이 섞인 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;7. Kotlin Coroutine&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;대표 예시: Ktor, Spring WebFlux + Kotlin Coroutine&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;7.1 핵심 개념&lt;/h3&gt;
&lt;p&gt;Kotlin coroutine은 suspend/resume 가능한 경량 실행 흐름이다.&lt;/p&gt;
&lt;p&gt;어떤 작업이 I/O를 기다려야 할 때 스레드를 계속 점유하지 않고, 중단되었다가 나중에 다시 이어서 실행될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant T as Thread
    participant C1 as Coroutine 1
    participant C2 as Coroutine 2
    participant DB as Database

    T-&amp;gt;&amp;gt;C1: Coroutine 1 실행
    C1-&amp;gt;&amp;gt;DB: suspend DB call
    Note over C1: suspend&amp;lt;br/&amp;gt;스레드 반납 가능

    T-&amp;gt;&amp;gt;C2: Coroutine 2 실행
    C2-&amp;gt;&amp;gt;DB: suspend DB call
    Note over C2: suspend&amp;lt;br/&amp;gt;스레드 반납 가능

    DB--&amp;gt;&amp;gt;C1: 결과 도착
    T-&amp;gt;&amp;gt;C1: Coroutine 1 재개

    DB--&amp;gt;&amp;gt;C2: 결과 도착
    T-&amp;gt;&amp;gt;C2: Coroutine 2 재개&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;7.2 중요한 오해&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;suspend&lt;/code&gt;를 붙인다고 내부 코드가 자동으로 non-blocking이 되는 것은 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;suspend fun badExample() {
    Thread.sleep(1000) // 실제 스레드를 막음
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;suspend&lt;/code&gt; 함수 안에서도 blocking 코드를 호출하면 실제 스레드는 점유된다.&lt;/p&gt;
&lt;p&gt;따라서 다음이 중요하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;non-blocking DB driver 사용&lt;/li&gt;
&lt;li&gt;non-blocking HTTP client 사용&lt;/li&gt;
&lt;li&gt;blocking 작업은 &lt;code&gt;Dispatchers.IO&lt;/code&gt;로 분리&lt;/li&gt;
&lt;li&gt;CPU 작업은 &lt;code&gt;Dispatchers.Default&lt;/code&gt; 또는 별도 worker로 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;7.3 구조적 동시성&lt;/h3&gt;
&lt;p&gt;Kotlin coroutine의 큰 장점 중 하나는 구조적 동시성이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;suspend fun getDashboard(userId: Long) = coroutineScope {
    val user = async { userService.findById(userId) }
    val orders = async { orderService.findByUserId(userId) }
    val profile = async { profileService.getProfile(userId) }

    Dashboard(
        user = user.await(),
        orders = orders.await(),
        profile = profile.await()
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여러 I/O 작업을 동시에 실행하고, 하나의 scope 안에서 생명주기를 관리할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;7.4 장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JVM 생태계를 그대로 활용할 수 있다.&lt;/li&gt;
&lt;li&gt;async/await보다 구조적인 동시성 표현이 좋다.&lt;/li&gt;
&lt;li&gt;여러 I/O 작업을 동시에 실행하고 조합하기 쉽다.&lt;/li&gt;
&lt;li&gt;Ktor, Spring WebFlux와 잘 어울린다.&lt;/li&gt;
&lt;li&gt;Android에서는 사실상 표준에 가깝다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;7.5 단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;suspend 함수가 호출 체인에 전파된다.&lt;/li&gt;
&lt;li&gt;Dispatcher, Scope, Job 개념을 이해해야 한다.&lt;/li&gt;
&lt;li&gt;blocking 라이브러리를 섞으면 성능 이점이 줄어든다.&lt;/li&gt;
&lt;li&gt;CPU-heavy 작업은 적절한 dispatcher나 worker로 분리해야 한다.&lt;/li&gt;
&lt;li&gt;기존 Spring MVC 스타일 팀에게는 학습 비용이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;7.6 적합한 경우&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Kotlin 기반 신규 백엔드&lt;/li&gt;
&lt;li&gt;Spring WebFlux 또는 Ktor 사용&lt;/li&gt;
&lt;li&gt;여러 I/O를 동시에 조합해야 하는 서비스&lt;/li&gt;
&lt;li&gt;JVM 생태계를 유지하면서 비동기 모델을 쓰고 싶은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;8. Java Virtual Threads&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;대표 예시: Java 21+, Spring Boot 3.2+ 환경의 Spring MVC&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;8.1 핵심 개념&lt;/h3&gt;
&lt;p&gt;Virtual Thread는 JVM이 관리하는 경량 스레드다.&lt;/p&gt;
&lt;p&gt;기존 platform thread는 OS thread와 거의 직접 연결된다. 반면 virtual thread는 JVM이 관리하고, 실제 실행이 필요할 때 carrier thread에 올라가 실행된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    subgraph &amp;quot;Platform Thread&amp;quot;
        PT1[Platform Thread 1] --&amp;gt; OS1[OS Thread 1]
        PT2[Platform Thread 2] --&amp;gt; OS2[OS Thread 2]
    end

    subgraph &amp;quot;Virtual Thread&amp;quot;
        VT1[Virtual Thread 1]
        VT2[Virtual Thread 2]
        VT3[Virtual Thread 3]
        VT4[Virtual Thread N]

        VT1 --&amp;gt; CT1[Carrier Thread 1]
        VT2 --&amp;gt; CT1
        VT3 --&amp;gt; CT2[Carrier Thread 2]
        VT4 --&amp;gt; CT2
    end

    CT1 --&amp;gt; Core1[CPU Core 1]
    CT2 --&amp;gt; Core2[CPU Core 2]&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;8.2 Virtual Thread가 해결하는 문제&lt;/h3&gt;
&lt;p&gt;기존 Thread-per-Request에서는 요청이 DB 응답을 기다리는 동안 platform thread를 점유했다.&lt;/p&gt;
&lt;p&gt;Virtual Thread는 많은 blocking I/O 상황에서 대기 중인 virtual thread를 carrier thread에서 내려놓을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant R as Request
    participant VT as Virtual Thread
    participant CT as Carrier Thread
    participant DB as Database

    R-&amp;gt;&amp;gt;VT: 요청 배정
    VT-&amp;gt;&amp;gt;CT: Carrier Thread에 mount
    CT-&amp;gt;&amp;gt;DB: Blocking DB call

    Note over VT,CT: I/O 대기 발생
    VT-&amp;gt;&amp;gt;CT: Carrier Thread에서 unmount
    Note over CT: Carrier Thread는 다른 Virtual Thread 실행 가능

    DB--&amp;gt;&amp;gt;VT: DB 결과 도착
    VT-&amp;gt;&amp;gt;CT: 다시 mount
    CT--&amp;gt;&amp;gt;R: 응답 반환&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;8.3 Spring Boot에서의 사용 예&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  threads:
    virtual:
      enabled: true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기존 Spring MVC 스타일 코드는 크게 바꾸지 않아도 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
public class UserController {

    @GetMapping(&amp;quot;/users/{id}&amp;quot;)
    public User getUser(@PathVariable Long id) {
        User user = userRepository.findById(id);
        Profile profile = profileClient.getProfile(id);
        return user.withProfile(profile);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;8.4 CPU-bound 관점&lt;/h3&gt;
&lt;p&gt;Virtual Thread는 I/O-bound 작업에 특히 유리하다.&lt;/p&gt;
&lt;p&gt;하지만 CPU-bound 작업에서는 I/O 대기처럼 carrier thread를 반납하지 않는다. CPU 작업을 하는 동안에는 실제 carrier thread와 CPU Core를 계속 사용한다.&lt;/p&gt;
&lt;p&gt;따라서 Virtual Thread를 사용한다고 CPU-heavy 작업 처리량이 자동으로 늘어나지는 않는다.&lt;/p&gt;
&lt;p&gt;무거운 CPU/GPU 작업은 Thread-per-Request와 마찬가지로 별도 worker, process pool, inference service로 분리하는 것이 안전하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;8.5 중요한 주의사항&lt;/h3&gt;
&lt;p&gt;Virtual Thread는 매우 유용하지만 모든 문제를 해결하지는 않는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU-bound 작업을 빠르게 만들어주는 기술은 아니다.&lt;/li&gt;
&lt;li&gt;DB connection pool 크기 제한은 그대로 존재한다.&lt;/li&gt;
&lt;li&gt;외부 API, downstream 서비스의 처리량 한계도 그대로 존재한다.&lt;/li&gt;
&lt;li&gt;Java 버전과 코드 패턴에 따라 carrier thread pinning을 주의해야 한다.&lt;/li&gt;
&lt;li&gt;특히 Java 21 환경에서는 &lt;code&gt;synchronized&lt;/code&gt; 블록/메서드 안에서 오래 걸리는 blocking I/O를 수행하면 pinning으로 확장성이 떨어질 수 있다.&lt;/li&gt;
&lt;li&gt;native call, foreign function, 일부 라이브러리 사용 시에도 pinning 여부를 확인하는 것이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, Virtual Thread의 핵심 장점은 다음이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;기존 동기식 Thread-per-Request 모델을 유지하면서 I/O 대기 중 platform thread 점유를 줄이는 것&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;8.6 장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;기존 동기식 코드 스타일을 유지할 수 있다.&lt;/li&gt;
&lt;li&gt;async/await를 도입하지 않아도 된다.&lt;/li&gt;
&lt;li&gt;Spring MVC와 잘 어울린다.&lt;/li&gt;
&lt;li&gt;I/O-bound API 서버의 동시성 개선에 유리하다.&lt;/li&gt;
&lt;li&gt;stack trace와 디버깅 모델이 상대적으로 친숙하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;8.7 단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Java 21 이상 환경이 필요하다.&lt;/li&gt;
&lt;li&gt;일부 라이브러리나 코드 패턴에서 pinning 이슈를 주의해야 한다.&lt;/li&gt;
&lt;li&gt;CPU-bound 작업에는 큰 이점이 없다.&lt;/li&gt;
&lt;li&gt;connection pool, rate limit, downstream 병목은 별도 설계가 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;8.8 적합한 경우&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;기존 Spring MVC 프로젝트&lt;/li&gt;
&lt;li&gt;Java 21 이상 사용 가능&lt;/li&gt;
&lt;li&gt;팀이 async/reactive 패러다임에 익숙하지 않은 경우&lt;/li&gt;
&lt;li&gt;I/O-bound API 서버&lt;/li&gt;
&lt;li&gt;기존 blocking 라이브러리를 많이 사용하는 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;9. 종합 비교&lt;/h2&gt;
&lt;h3&gt;9.1 실행 모델 비교&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    subgraph &amp;quot;Thread-per-Request&amp;quot;
        A1[Request] --&amp;gt; A2[OS/Platform Thread]
        A2 --&amp;gt; A3[Blocking 처리]
    end

    subgraph &amp;quot;Event Loop&amp;quot;
        B1[Requests] --&amp;gt; B2[Event Loop]
        B2 --&amp;gt; B3[Non-blocking I/O]
    end

    subgraph &amp;quot;Go Goroutine&amp;quot;
        C1[Requests] --&amp;gt; C2[Goroutines]
        C2 --&amp;gt; C3[Go Scheduler]
        C3 --&amp;gt; C4[OS Threads]
    end

    subgraph &amp;quot;Kotlin Coroutine&amp;quot;
        D1[Coroutine] --&amp;gt; D2[Suspend / Resume]
        D2 --&amp;gt; D3[Dispatcher Thread Pool]
    end

    subgraph &amp;quot;Virtual Thread&amp;quot;
        E1[Request] --&amp;gt; E2[Virtual Thread]
        E2 --&amp;gt; E3[Carrier Thread]
    end&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;9.2 비교표&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Thread-per-Request&lt;/th&gt;
&lt;th&gt;Event Loop&lt;/th&gt;
&lt;th&gt;Go Goroutine&lt;/th&gt;
&lt;th&gt;Kotlin Coroutine&lt;/th&gt;
&lt;th&gt;Java Virtual Thread&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;대표 환경&lt;/td&gt;
&lt;td&gt;Spring MVC, Django sync&lt;/td&gt;
&lt;td&gt;Node.js, FastAPI async, Netty&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Kotlin, Ktor, WebFlux&lt;/td&gt;
&lt;td&gt;Java 21+, Spring MVC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드 스타일&lt;/td&gt;
&lt;td&gt;동기식&lt;/td&gt;
&lt;td&gt;async/await&lt;/td&gt;
&lt;td&gt;동기식에 가까움&lt;/td&gt;
&lt;td&gt;suspend&lt;/td&gt;
&lt;td&gt;동기식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동시성 단위&lt;/td&gt;
&lt;td&gt;OS/Platform Thread&lt;/td&gt;
&lt;td&gt;Event/Task&lt;/td&gt;
&lt;td&gt;Goroutine&lt;/td&gt;
&lt;td&gt;Coroutine&lt;/td&gt;
&lt;td&gt;Virtual Thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I/O 대기 처리&lt;/td&gt;
&lt;td&gt;스레드 점유&lt;/td&gt;
&lt;td&gt;루프 반납&lt;/td&gt;
&lt;td&gt;런타임 스케줄러가 처리&lt;/td&gt;
&lt;td&gt;suspend/resume&lt;/td&gt;
&lt;td&gt;carrier thread 반납 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;멀티코어 활용&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;worker 여러 개 필요&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;dispatcher에 따라 가능&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU-bound&lt;/td&gt;
&lt;td&gt;Event Loop보다 상대적으로 유리&lt;/td&gt;
&lt;td&gt;루프에서 직접 처리 금지&lt;/td&gt;
&lt;td&gt;가능하지만 Core 수가 한계&lt;/td&gt;
&lt;td&gt;dispatcher 분리 필요&lt;/td&gt;
&lt;td&gt;이점 적음, 별도 분리 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;학습 난이도&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;낮음~중간&lt;/td&gt;
&lt;td&gt;중간~높음&lt;/td&gt;
&lt;td&gt;낮음~중간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주요 주의점&lt;/td&gt;
&lt;td&gt;스레드 풀 고갈&lt;/td&gt;
&lt;td&gt;blocking 코드 금지&lt;/td&gt;
&lt;td&gt;goroutine leak, race&lt;/td&gt;
&lt;td&gt;blocking 코드 주의&lt;/td&gt;
&lt;td&gt;pinning, pool 병목&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Virtual Thread도 CPU 작업을 실제로 실행할 때는 carrier thread와 CPU Core를 사용한다. 따라서 I/O 대기에는 유리하지만, CPU-bound 작업 처리량이 자동으로 늘어나는 것은 아니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;10. 어떤 상황에서 무엇을 검토할까?&lt;/h2&gt;
&lt;p&gt;아래 흐름은 절대적인 정답이 아니라, 기술 선택 시 참고할 수 있는 기준에 가깝다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
    A[백엔드 기술 선택] --&amp;gt; B{주 언어/플랫폼}

    B --&amp;gt;|Python| P{작업 성격}
    P --&amp;gt;|I/O-bound API| P1[FastAPI async 검토]
    P --&amp;gt;|blocking 라이브러리 많음| P2[FastAPI sync def / threadpool 검토]
    P --&amp;gt;|CPU/GPU 작업 많음| P3[FastAPI + Ray/Celery/별도 inference service 검토]

    B --&amp;gt;|Go| G1[Go net/http / Gin / Echo 검토]
    G1 --&amp;gt; G2[goroutine 기반 동시성]

    B --&amp;gt;|Java| J{Java 버전}
    J --&amp;gt;|Java 21+| J1[Spring MVC + Virtual Threads 검토]
    J --&amp;gt;|Java 17 이하| J2[Spring MVC / WebFlux 검토]

    B --&amp;gt;|Kotlin| K{비동기 모델 선호}
    K --&amp;gt;|Yes| K1[Ktor / WebFlux + Coroutine 검토]
    K --&amp;gt;|No| K2[Spring MVC + Kotlin 검토]

    B --&amp;gt;|Node.js| N1[Fastify / NestJS 검토]
    N1 --&amp;gt; N2[Event Loop 기반]

    style P1 fill:#d5e8ff
    style G1 fill:#d5f5d5
    style J1 fill:#fff2cc
    style K1 fill:#f8cecc&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;11. FastAPI 기반 서버 설계 시 적용 규칙&lt;/h2&gt;
&lt;p&gt;FastAPI 기반 API 서버를 설계할 때 가장 중요한 원칙은 다음이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;FastAPI API 서버는 요청 수신, 인증, 검증, 가벼운 I/O, orchestration에 집중하는 것이 좋다. CPU/GPU-heavy 작업은 API 이벤트 루프에서 직접 처리하지 않는 것이 안전하다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;11.1 권장 구조&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    Client[Client / UI] --&amp;gt; API[FastAPI API Server]

    API --&amp;gt;|가벼운 DB 조회| DB[(PostgreSQL / TimescaleDB)]
    API --&amp;gt;|캐시/상태 조회| Redis[(Redis)]
    API --&amp;gt;|메시지 발행| MQ[NATS / RabbitMQ / Kafka]

    API --&amp;gt;|CPU 작업 요청| Worker[Process Worker / Ray / Celery]
    API --&amp;gt;|GPU 추론 요청| Inference[Inference Service&amp;lt;br/&amp;gt;Ray / Triton / Python Worker]

    Worker --&amp;gt; DB
    Inference --&amp;gt; MQ
    Inference --&amp;gt; DB

    API --&amp;gt; Client&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;11.2 FastAPI 코딩 규칙&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;async def&lt;/code&gt; 안에서는 &lt;code&gt;await&lt;/code&gt; 가능한 non-blocking I/O만 직접 호출한다.&lt;/li&gt;
&lt;li&gt;blocking DB driver, blocking SDK, blocking HTTP client를 &lt;code&gt;async def&lt;/code&gt; 안에서 직접 호출하지 않는다.&lt;/li&gt;
&lt;li&gt;blocking 라이브러리를 써야 한다면 &lt;code&gt;sync def&lt;/code&gt; endpoint, threadpool, 별도 worker 중 하나를 선택한다.&lt;/li&gt;
&lt;li&gt;이미지 처리, 영상 처리, FFT, 압축, ML 추론 같은 CPU/GPU-heavy 작업은 API 서버에서 직접 실행하지 않는다.&lt;/li&gt;
&lt;li&gt;CPU-heavy 작업은 ProcessPool, Ray, Celery, 별도 worker로 분리한다.&lt;/li&gt;
&lt;li&gt;GPU inference는 별도 inference service 또는 Ray/Triton/Python worker로 분리한다.&lt;/li&gt;
&lt;li&gt;여러 worker가 공유해야 하는 상태는 worker 메모리에만 저장하지 않는다.&lt;/li&gt;
&lt;li&gt;정합성이 중요한 상태는 Redis, DB, 외부 state store에 둔다.&lt;/li&gt;
&lt;li&gt;단순 local cache는 허용 가능하지만, cache miss나 worker 변경을 감안해야 한다.&lt;/li&gt;
&lt;li&gt;성능 테스트 시 평균 응답시간뿐 아니라 p95/p99 latency, event loop blocking, worker별 CPU 사용률을 확인한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;11.3 좋지 않은 예&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.post(&amp;quot;/predict&amp;quot;)
async def predict(data: InputData):
    # CPU-heavy 또는 GPU inference를 API 이벤트 루프 안에서 직접 실행
    result = model.predict(data)
    return result&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;문제점은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이벤트 루프가 막힐 수 있다.&lt;/li&gt;
&lt;li&gt;같은 worker의 다른 요청도 지연된다.&lt;/li&gt;
&lt;li&gt;요청 수가 늘면 p95/p99 latency가 급격히 나빠질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;11.4 더 나은 예&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.post(&amp;quot;/predict&amp;quot;)
async def predict(data: InputData):
    # API 서버는 추론 작업을 별도 worker/service에 위임
    result = await inference_client.predict(data)
    return result&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또는 단순 blocking 작업이면 다음처럼 분리할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.post(&amp;quot;/process&amp;quot;)
async def process(data: InputData):
    result = await asyncio.to_thread(blocking_function, data)
    return result&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;12. 결론&lt;/h2&gt;
&lt;p&gt;동시성 모델을 이해한다는 것은 단순히 &lt;code&gt;async/await&lt;/code&gt;, &lt;code&gt;goroutine&lt;/code&gt;, &lt;code&gt;suspend&lt;/code&gt;, &lt;code&gt;virtual thread&lt;/code&gt; 문법을 아는 것이 아니다.&lt;/p&gt;
&lt;p&gt;중요한 질문은 다음이다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;서버의 병목은 I/O 대기인가, CPU/GPU 연산인가?&lt;/li&gt;
&lt;li&gt;요청 하나가 대기 중일 때 실행 자원을 점유하는가?&lt;/li&gt;
&lt;li&gt;CPU-heavy 작업이 들어왔을 때 어떤 범위까지 영향을 주는가?&lt;/li&gt;
&lt;li&gt;멀티코어를 어떻게 활용하는가?&lt;/li&gt;
&lt;li&gt;blocking 코드가 event loop나 worker 전체를 막지는 않는가?&lt;/li&gt;
&lt;li&gt;worker를 여러 개 띄웠을 때 상태 관리는 안전한가?&lt;/li&gt;
&lt;li&gt;DB connection pool, 외부 API, message broker 같은 downstream 병목은 고려했는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;패러다임&lt;/th&gt;
&lt;th&gt;핵심 요약&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Thread-per-Request&lt;/td&gt;
&lt;td&gt;이해하기 쉽고 CPU-bound에 상대적으로 유리하지만, I/O 대기 중 스레드 풀 슬롯을 점유한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event Loop&lt;/td&gt;
&lt;td&gt;I/O-bound에 강하지만 blocking 코드나 CPU 작업 하나가 루프를 막을 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go Goroutine&lt;/td&gt;
&lt;td&gt;동기식 코드 스타일과 경량 동시성을 잘 결합했다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kotlin Coroutine&lt;/td&gt;
&lt;td&gt;JVM 생태계에서 구조적 비동기 코드를 작성하기 좋다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java Virtual Thread&lt;/td&gt;
&lt;td&gt;기존 동기식 Java 코드를 유지하면서 I/O 동시성을 높이기 좋다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;최종적으로 중요한 것은 특정 패러다임이 무조건 좋다는 것이 아니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;프로젝트의 언어, 프레임워크, 라이브러리, 워크로드, 운영 환경에 맞는 동시성 모델을 선택하고, 그 모델의 한계를 알고 구현하는 것이 중요하다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;특히 FastAPI 기반 서버에서는 다음 원칙이 중요하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;API 서버는 이벤트 루프를 막지 않게 설계하고, CPU/GPU-heavy 작업은 별도 worker나 inference service로 분리한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;</description>
      <category> &amp;zwj;  SW</category>
      <category>동시성</category>
      <category>동시성 모델</category>
      <category>백엔드</category>
      <author>just just do it</author>
      <guid isPermaLink="true">https://jaejung.tistory.com/5</guid>
      <comments>https://jaejung.tistory.com/5#entry5comment</comments>
      <pubDate>Wed, 24 Jun 2026 22:28:14 +0900</pubDate>
    </item>
    <item>
      <title>Difference between Microkernel and Monolithic Kernel</title>
      <link>https://jaejung.tistory.com/3</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;When it comes to operating system design, there are two main approaches: microkernel and monolithic kernel. Both have their pros and cons, and in this article, we will explore the main differences between them.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJDW9g/btr2Os7VyFt/kSCMUW03SIAFuAF7Zscs8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJDW9g/btr2Os7VyFt/kSCMUW03SIAFuAF7Zscs8k/img.png&quot; data-alt=&quot;abstraction of each OS architecture&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJDW9g/btr2Os7VyFt/kSCMUW03SIAFuAF7Zscs8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJDW9g%2Fbtr2Os7VyFt%2FkSCMUW03SIAFuAF7Zscs8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;688&quot; height=&quot;176&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;abstraction of each OS architecture&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Monolithic Kernel&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A monolithic kernel is an operating system architecture where the &lt;u&gt;&lt;b&gt;entire operating system runs in kernel space&lt;/b&gt;&lt;/u&gt;. This means that all the basic system services such as process scheduling, memory management, and device drivers are part of the kernel. The monolithic kernel provides a &lt;u&gt;&lt;b&gt;single address space&lt;/b&gt;&lt;/u&gt;, which means that all the code and data are in the same place, and it is easier to share information between different parts of the kernel. The monolithic kernels &lt;u&gt;&lt;b&gt;use signals and sockets to communicate between inter-processes&lt;/b&gt;&lt;/u&gt;.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Advantages of the Monolithic Kernel:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;High performance&lt;/li&gt;
&lt;li&gt;Efficient communication between kernel modules&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Disadvantages of the Monolithic Kernel:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Difficult to maintain and upgrade due to interdependencies in the code&lt;/li&gt;
&lt;li&gt;Security, Reliability&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Microkernel&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A microkernel is an operating system architecture where the kernel is stripped down to its most basic functions, and other services are provided by separate processes, called servers. The microkernel &lt;u&gt;&lt;b&gt;provides a minimal set of services&lt;/b&gt;&lt;/u&gt;, such as inter-process communication and basic memory management. All other services, such as device drivers and file systems, run as separate processes in user space. Communication Microkernels &lt;u&gt;&lt;b&gt;use the messaging queues to achieve IPC&lt;/b&gt;&lt;/u&gt;.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Advantages of the Microkernel:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Easier to maintain and extend&lt;/li&gt;
&lt;li&gt;More secure, and reliable (because less code is running in kernel mode)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Disadvantages of the Microkernel:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Lower performance (because of overhead due to communication between user space and kernel space)&lt;/li&gt;
&lt;li&gt;More complex implementation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hybrid Systems&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Most modern operating systems are actually not one pure model, but hybrid systems.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Plus, many modern operating systems implement &lt;u&gt;&lt;b&gt;loadable kernel modules&lt;/b&gt;&lt;/u&gt;, so each core component is separate and each is loadable as needed within the kernel.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Linux kernels are monolithic by having the OS in a single kernel address space, but also modular for dynamic loading of functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In conclusion, the choice between a monolithic kernel and a microkernel depends on the requirements of the system. A monolithic kernel is generally faster and simpler to implement, but a microkernel is modular and easier to maintain. Both have their advantages and disadvantages, and it is up to the operating system designer to decide which architecture is best suited for their system.&lt;/p&gt;</description>
      <category> &amp;zwj;  SW</category>
      <category>Operating System</category>
      <author>just just do it</author>
      <guid isPermaLink="true">https://jaejung.tistory.com/3</guid>
      <comments>https://jaejung.tistory.com/3#entry3comment</comments>
      <pubDate>Wed, 8 Mar 2023 22:01:09 +0900</pubDate>
    </item>
  </channel>
</rss>