<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>깃짱코딩</title>
    <link>https://engineerinsight.tistory.com/</link>
    <description>연새데학교 컴퓨터과학과 &amp;amp; 우아한테크코스 5기 백엔드 스타라이토 깃짱</description>
    <language>ko</language>
    <pubDate>Tue, 12 May 2026 09:43:46 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>깃짱</managingEditor>
    <image>
      <title>깃짱코딩</title>
      <url>https://tistory1.daumcdn.net/tistory/5311754/attach/47c7c93c86a44bda9e68551c66748419</url>
      <link>https://engineerinsight.tistory.com</link>
    </image>
    <item>
      <title>[LLM] Claude vs Claude Code 뭐가 달라요?</title>
      <link>https://engineerinsight.tistory.com/470</link>
      <description>&lt;!doctype html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
&lt;meta charset=&quot;utf-8&quot; /&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;width=1080, initial-scale=1&quot; /&gt;
&lt;title&gt;Claude vs Claude Code — 세로 배너&lt;/title&gt;
&lt;link rel=&quot;preconnect&quot; href=&quot;https://cdn.jsdelivr.net&quot; crossorigin /&gt;
&lt;link href=&quot;https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.css&quot; rel=&quot;stylesheet&quot; /&gt;
&lt;style&gt;
  :root {
    --bg: #FFFFFF;
    --surface: #F7F7F5;
    --surface-2: #EFEEEA;
    --line: #E3E2DC;
    --ink: #121212;
    --ink-2: #3D3D3D;
    --ink-3: #7A7A78;
    --accent: #FF6B2C;
    --accent-2: #7B5CE6;
    --warn-bg: #FFF6DB;
    --warn-line: #F3C94A;
    --warn-ink: #6B4B00;
  }

  * { box-sizing: border-box; }
  html, body { margin: 0; padding: 0; background: #EDECE8; }
  body {
    font-family: 'Pretendard Variable', 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    color: var(--ink);
    -webkit-font-smoothing: antialiased;
    text-rendering: geometricPrecision;
    display: flex;
    justify-content: center;
  }

  .banner {
    width: 1080px;
    min-height: 4000px;
    background:
      radial-gradient(1200px 700px at 8% 4%, rgba(255,107,44,0.06), transparent 60%),
      radial-gradient(1000px 900px at 95% 40%, rgba(123,92,230,0.05), transparent 60%),
      var(--bg);
    position: relative;
    overflow: hidden;
  }

  /* 공통 */
  .section { padding: 0 80px; position: relative; }
  .eyebrow {
    font-size: 16px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-3);
    display: inline-flex;
    align-items: center;
    gap: 12px;
  }
  .eyebrow::before {
    content: '';
    width: 24px; height: 2px;
    background: var(--ink-3);
    display: inline-block;
  }
  .h1 { font-size: 56px; line-height: 1.2; font-weight: 800; letter-spacing: -0.025em; margin: 20px 0; color: var(--ink); }
  .h2 { font-size: 38px; line-height: 1.25; font-weight: 800; letter-spacing: -0.02em; margin: 0 0 20px; color: var(--ink); }
  .h3 { font-size: 26px; line-height: 1.3; font-weight: 700; letter-spacing: -0.015em; margin: 0 0 14px; color: var(--ink); }
  .body { font-size: 19px; line-height: 1.7; color: var(--ink-2); font-weight: 400; }
  .body + .body { margin-top: 14px; }
  .body strong { color: var(--ink); font-weight: 700; }
  .caption { font-size: 16px; line-height: 1.6; color: var(--ink-3); }

  /* 1. HERO */
  .hero { padding: 110px 80px 70px; }
  .hero .logo-row {
    display: flex; align-items: center; justify-content: space-between;
    margin-bottom: 64px;
  }
  .brand {
    display: flex; align-items: center; gap: 14px;
    font-weight: 700; font-size: 20px; letter-spacing: -0.01em;
    color: var(--ink);
  }
  .brand-dot {
    width: 34px; height: 34px; border-radius: 10px;
    background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
  }
  .hero-tag {
    font-size: 15px; color: var(--ink-2);
    border: 1px solid var(--line);
    padding: 8px 14px; border-radius: 999px;
    background: var(--bg);
  }
  .q-block {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 22px;
    padding: 40px 44px;
    margin-top: 36px;
    position: relative;
  }
  .q-label {
    display: inline-block;
    font-weight: 800;
    font-size: 16px;
    color: #FFF;
    background: var(--ink);
    padding: 6px 14px;
    border-radius: 8px;
    margin-bottom: 20px;
    letter-spacing: 0.08em;
  }
  .q-text {
    font-size: 22px;
    line-height: 1.65;
    color: var(--ink);
    font-weight: 500;
  }
  .q-text em {
    font-style: normal;
    background: linear-gradient(180deg, transparent 62%, rgba(255,107,44,0.28) 62%);
    padding: 0 2px;
  }

  /* 2. 핵심 비교 */
  .compare { padding: 70px 80px; }

  /* 비교표 */
  .compare-table {
    margin-top: 32px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 20px;
    overflow: hidden;
    box-shadow: 0 4px 20px rgba(0,0,0,0.04);
  }
  .compare-table .row {
    display: grid;
    grid-template-columns: 160px 1fr 1fr;
    border-bottom: 1px solid var(--line);
  }
  .compare-table .row:last-child { border-bottom: 0; }
  .compare-table .row.head { background: var(--surface); }
  .compare-table .cell {
    padding: 20px 22px;
    font-size: 16px;
    line-height: 1.6;
    color: var(--ink);
    border-right: 1px solid var(--line);
  }
  .compare-table .cell:last-child { border-right: 0; }
  .compare-table .cell.label {
    font-weight: 700;
    color: var(--ink-3);
    font-size: 14px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    background: var(--surface);
    display: flex; align-items: center;
  }
  .compare-table .cell strong { font-weight: 700; }
  .compare-table .row.head .cell {
    padding: 22px;
    font-size: 22px;
    font-weight: 800;
    letter-spacing: -0.015em;
  }
  .compare-table .row.head .cell.chat { color: var(--accent-2); }
  .compare-table .row.head .cell.code { color: var(--accent); }
  .compare-table .row.head .cell .role {
    display: block;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.16em;
    color: var(--ink-3);
    text-transform: uppercase;
    margin-bottom: 6px;
  }
  .compare-table .cell.chat strong { color: var(--accent-2); }
  .compare-table .cell.code strong { color: var(--accent); }

  /* 정의 인용구 */
  .definition {
    margin-top: 32px;
    padding: 28px 32px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-left: 4px solid var(--accent);
    border-radius: 0 14px 14px 0;
    font-size: 19px;
    line-height: 1.75;
    color: var(--ink);
  }
  .definition strong { font-weight: 700; }

  /* 5. 바이브 코딩 예시 */
  .vibe { padding: 70px 80px 0; }
  .quote-list { display: flex; flex-direction: column; gap: 16px; margin-top: 28px; }
  .quote {
    background: var(--surface);
    border-left: 4px solid var(--accent);
    border-radius: 0 14px 14px 0;
    padding: 22px 26px;
    font-size: 18px;
    line-height: 1.65;
    color: var(--ink-2);
    font-style: italic;
    font-weight: 400;
  }
  .quote strong {
    color: var(--ink);
    font-style: normal;
    font-weight: 700;
    background: linear-gradient(180deg, transparent 65%, rgba(255,107,44,0.24) 65%);
    padding: 0 2px;
  }

  /* 6. 기존 vs Claude Code */
  .flow { padding: 70px 80px 0; }
  .flow-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
    margin-top: 28px;
  }
  .flow-col {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 18px;
    padding: 28px 26px;
  }
  .flow-col.old { background: var(--surface-2); }
  .flow-col.new {
    border-color: rgba(255,107,44,0.3);
    background: linear-gradient(180deg, #FFF5EE 0%, #FFFAF6 100%);
  }
  .flow-tag {
    display: inline-block;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.14em;
    padding: 6px 12px;
    border-radius: 6px;
    margin-bottom: 16px;
  }
  .flow-col.old .flow-tag { color: var(--ink-3); border: 1px solid var(--line); background: var(--bg); }
  .flow-col.new .flow-tag { color: var(--accent); border: 1px solid rgba(255,107,44,0.4); background: rgba(255,107,44,0.08); }
  .flow-title { font-size: 18px; font-weight: 700; margin: 0 0 14px; color: var(--ink); }
  .flow-steps { list-style: none; padding: 0; margin: 0; }
  .flow-steps li {
    font-size: 16px;
    line-height: 1.55;
    color: var(--ink-2);
    padding: 10px 0;
    border-bottom: 1px dashed var(--line);
    display: flex; align-items: flex-start; gap: 12px;
  }
  .flow-steps li strong { color: var(--ink); font-weight: 700; }
  .flow-steps li:last-child { border-bottom: 0; }
  .flow-steps .num {
    min-width: 26px; height: 26px;
    border-radius: 50%;
    background: var(--line);
    color: var(--ink);
    font-size: 13px; font-weight: 700;
    display: flex; align-items: center; justify-content: center;
  }
  .flow-col.new .flow-steps .num { background: var(--accent); color: #FFF; }

  /* 7. 주의사항 */
  .warn-box {
    margin: 70px 80px 0;
    background: var(--warn-bg);
    border: 1px solid var(--warn-line);
    border-radius: 22px;
    padding: 36px 40px;
    display: grid;
    grid-template-columns: 64px 1fr;
    gap: 24px;
    align-items: start;
  }
  .warn-icon {
    width: 64px; height: 64px;
    border-radius: 14px;
    background: var(--warn-line);
    color: #FFF;
    font-size: 36px; font-weight: 900;
    display: flex; align-items: center; justify-content: center;
    line-height: 1;
  }
  .warn-box h3 { font-size: 24px; font-weight: 800; margin: 0 0 12px; color: var(--warn-ink); }
  .warn-box p { font-size: 17px; line-height: 1.7; color: var(--ink); margin: 0; }
  .warn-box p + p { margin-top: 14px; }
  .warn-box p strong { font-weight: 700; }

  /* 8. 타사 비교 */
  .others { padding: 70px 80px 0; }
  .others-list { display: flex; flex-direction: column; gap: 12px; margin-top: 26px; }
  .other-row {
    display: grid;
    grid-template-columns: 220px 1fr;
    gap: 24px;
    padding: 20px 22px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 14px;
    align-items: center;
  }
  .other-name { font-size: 19px; font-weight: 700; color: var(--ink); }
  .other-desc { font-size: 16px; line-height: 1.55; color: var(--ink-2); }

  /* 9. CTA */
  .cta {
    margin: 90px 80px 0;
    padding: 56px 44px;
    background: linear-gradient(135deg, rgba(255,107,44,0.10), rgba(123,92,230,0.10));
    border: 1px solid var(--line);
    border-radius: 26px;
    text-align: center;
  }
  .cta .eyebrow { color: var(--ink-2); }
  .cta .eyebrow::before { background: var(--ink-2); }
  .cta-title {
    font-size: 42px; font-weight: 800; letter-spacing: -0.025em; margin: 16px 0 22px;
    background: linear-gradient(90deg, var(--accent), var(--accent-2));
    -webkit-background-clip: text; background-clip: text; color: transparent;
  }
  .cta-summary {
    display: flex; flex-direction: column; gap: 14px;
    max-width: 820px; margin: 0 auto 28px;
    text-align: left;
  }
  .cta-summary .row {
    padding: 20px 22px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 14px;
    font-size: 17px;
    line-height: 1.65;
    color: var(--ink);
  }
  .cta-summary .row strong { color: var(--accent); font-weight: 800; }
  .cta-summary .row.chat strong { color: var(--accent-2); }
  .cta-summary .row span { color: var(--ink-2); }
  .cta-summary .row span b { color: var(--ink); font-weight: 700; }
  .cta-final {
    font-size: 19px; color: var(--ink-2); line-height: 1.7;
    max-width: 760px; margin: 20px auto 0;
  }
  .cta-final b { color: var(--ink); font-weight: 700; }

  /* Footer */
  .footer {
    margin: 70px 80px 90px;
    padding-top: 28px;
    border-top: 1px solid var(--line);
    display: flex; justify-content: space-between; align-items: flex-end;
  }
  .sign-title { font-size: 14px; color: var(--ink-3); letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 8px; }
  .sign-name { font-size: 20px; font-weight: 700; color: var(--ink); }
  .sign-sub { font-size: 16px; color: var(--ink-2); margin-top: 4px; }
  .page-n { font-size: 13px; color: var(--ink-3); letter-spacing: 0.2em; font-weight: 600; }

  .grid-bg {
    position: absolute; inset: 0;
    background-image:
      linear-gradient(rgba(18,18,18,0.03) 1px, transparent 1px),
      linear-gradient(90deg, rgba(18,18,18,0.03) 1px, transparent 1px);
    background-size: 60px 60px;
    pointer-events: none;
    mask-image: linear-gradient(180deg, transparent, #000 6%, #000 94%, transparent);
  }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div class=&quot;banner&quot;&gt;
    &lt;div class=&quot;grid-bg&quot;&gt;&lt;/div&gt;

    &lt;!-- 1. HERO --&gt;
    &lt;section class=&quot;section hero&quot;&gt;
      &lt;div class=&quot;logo-row&quot;&gt;
        &lt;div class=&quot;hero-tag&quot;&gt;#자주받는질문 · Vol.01&lt;/div&gt;
      &lt;/div&gt;

      &lt;div class=&quot;eyebrow&quot;&gt;Claude vs Claude Code&lt;/div&gt;
      &lt;h1 class=&quot;h1&quot;&gt;
        &lt;span style=&quot;color: var(--accent-2)&quot;&gt;클로드(Claude)&lt;/span&gt;랑&lt;br /&gt;
        &lt;span style=&quot;color: var(--accent)&quot;&gt;클로드 코드(Claude Code)&lt;/span&gt;랑&lt;br /&gt;
        뭐가 달라요?
      &lt;/h1&gt;

      &lt;div class=&quot;q-block&quot;&gt;
        &lt;div class=&quot;q-label&quot;&gt;Q.&lt;/div&gt;
        &lt;div class=&quot;q-text&quot;&gt;
          둘 다 &lt;em&gt;&quot;클로드&quot;&lt;/em&gt;라는데 왜 이름이 두 개예요?&lt;br /&gt;&lt;br /&gt;
          제가 쓰는 건 그냥 창에 질문 치면 답해주는 건데, 옆자리 개발자님은 까만 창 띄워놓고 &lt;em&gt;&quot;클로드 코드&quot;&lt;/em&gt;라는 걸 쓰시더라고요.&lt;br /&gt;&lt;br /&gt;
          이거 같은 건가요 다른 건가요? &lt;strong&gt;저도 그거 써야 되는 거예요?&lt;/strong&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    &lt;!-- 2. 핵심 비교 --&gt;
    &lt;section class=&quot;section compare&quot;&gt;
      &lt;div class=&quot;eyebrow&quot;&gt;A. 아주 자주 받는 질문입니다&lt;/div&gt;
      &lt;h2 class=&quot;h2&quot;&gt;같은 AI 모델이지만,&lt;br /&gt;사용하는 방식과 용도가 다른 제품입니다.&lt;/h2&gt;

      &lt;p class=&quot;body&quot;&gt;
        두 가지는 &lt;strong&gt;Claude라는 AI 모델&lt;/strong&gt;을 쓰지만,&lt;br /&gt;
        &lt;strong&gt;사용하는 방식과 용도가 다른 제품&lt;/strong&gt;입니다.
      &lt;/p&gt;
      &lt;p class=&quot;body&quot;&gt;
        쉽게 말씀드리면, 같은 AI 모델이지만&lt;br /&gt;
        한쪽은 &lt;strong&gt;&quot;조언자&quot;&lt;/strong&gt;, 다른 한쪽은 &lt;strong&gt;&quot;실무자&quot;&lt;/strong&gt;라고 보시면 됩니다.
      &lt;/p&gt;

      &lt;div class=&quot;definition&quot;&gt;
        &lt;strong style=&quot;color: var(--accent-2);&quot;&gt;Claude(채팅)&lt;/strong&gt; = 대화창에 질문을 던지면 &lt;strong&gt;말로 답해주는&lt;/strong&gt; 채팅 도구&lt;br /&gt;&lt;br /&gt;
        &lt;strong style=&quot;color: var(--accent);&quot;&gt;Claude Code&lt;/strong&gt; = AI가 &lt;strong&gt;내 컴퓨터 안에서 직접 파일을 열고 고쳐주는&lt;/strong&gt; 도구
      &lt;/div&gt;

      &lt;div class=&quot;compare-table&quot;&gt;
        &lt;div class=&quot;row head&quot;&gt;
          &lt;div class=&quot;cell label&quot;&gt;항목&lt;/div&gt;
          &lt;div class=&quot;cell chat&quot;&gt;
            &lt;span class=&quot;role&quot;&gt;조언자&lt;/span&gt;
            Claude
          &lt;/div&gt;
          &lt;div class=&quot;cell code&quot;&gt;
            &lt;span class=&quot;role&quot;&gt;실무자&lt;/span&gt;
            Claude Code
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;row&quot;&gt;
          &lt;div class=&quot;cell label&quot;&gt;한 줄 정의&lt;/div&gt;
          &lt;div class=&quot;cell chat&quot;&gt;대화창에 질문하면&lt;br /&gt;&lt;strong&gt;말로 답해주는&lt;/strong&gt; 채팅 도구&lt;/div&gt;
          &lt;div class=&quot;cell code&quot;&gt;AI가 내 컴퓨터 안에서&lt;br /&gt;&lt;strong&gt;직접 파일을 열고 고쳐주는&lt;/strong&gt; 도구&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;row&quot;&gt;
          &lt;div class=&quot;cell label&quot;&gt;역할&lt;/div&gt;
          &lt;div class=&quot;cell chat&quot;&gt;&lt;strong&gt;조언자&lt;/strong&gt;&lt;br /&gt;&quot;이렇게 하시면 됩니다&quot;&lt;/div&gt;
          &lt;div class=&quot;cell code&quot;&gt;&lt;strong&gt;실무자&lt;/strong&gt;&lt;br /&gt;&quot;제가 직접 고쳐드릴게요&quot;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;row&quot;&gt;
          &lt;div class=&quot;cell label&quot;&gt;어디서&lt;/div&gt;
          &lt;div class=&quot;cell chat&quot;&gt;웹 브라우저&lt;br /&gt;(claude.ai)&lt;/div&gt;
          &lt;div class=&quot;cell code&quot;&gt;터미널&lt;br /&gt;VS Code 같은 개발 도구&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;row&quot;&gt;
          &lt;div class=&quot;cell label&quot;&gt;주 사용자&lt;/div&gt;
          &lt;div class=&quot;cell chat&quot;&gt;누구나&lt;/div&gt;
          &lt;div class=&quot;cell code&quot;&gt;개발자 +&lt;br /&gt;&lt;strong&gt;업무 자동화 원하는 일반 임직원&lt;/strong&gt;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;row&quot;&gt;
          &lt;div class=&quot;cell label&quot;&gt;대표 용도&lt;/div&gt;
          &lt;div class=&quot;cell chat&quot;&gt;문서 요약&lt;br /&gt;이메일 초안&lt;br /&gt;아이디어 정리&lt;/div&gt;
          &lt;div class=&quot;cell code&quot;&gt;코드 작성/수정&lt;br /&gt;여러 파일 동시 작업&lt;br /&gt;&lt;strong&gt;바이브 코딩 업무 자동화&lt;/strong&gt;&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    &lt;!-- 3. 바이브 코딩 예시 --&gt;
    &lt;section class=&quot;section vibe&quot;&gt;
      &lt;div class=&quot;eyebrow&quot;&gt;Vibe Coding — 실제 지시 예시&lt;/div&gt;
      &lt;h2 class=&quot;h2&quot;&gt;당장 코딩은 못하더라도,&lt;br /&gt;자연어로 &lt;span style=&quot;color: var(--accent)&quot;&gt;프로덕트를 만듭니다.&lt;/span&gt;&lt;/h2&gt;

      &lt;p class=&quot;body&quot;&gt;
        당장 코딩은 못하더라도 자연어로 지시만 하면&lt;br /&gt;
        실제로 동작하는 프로덕트를 만들어낼 수 있습니다.
      &lt;/p&gt;

      &lt;div class=&quot;quote-list&quot;&gt;
        &lt;div class=&quot;quote&quot;&gt;
          &quot;산출방법서를 업로드하면 &lt;strong&gt;곧바로 이노룰즈(InnoRules)에 넣을 수 있는 형태의 표로 변환해주는&lt;/strong&gt; 시스템을 만들어주세요.&quot;
        &lt;/div&gt;
        &lt;div class=&quot;quote&quot;&gt;
          &quot;고객의 나이·성별·기왕력 같은 기본 정보를 입력하면 &lt;strong&gt;해당 군집의 암 발생률과 그 군집이 가장 많이 가입한 보험 Top 5를 보여주는&lt;/strong&gt; 애플리케이션을 만들어주세요.&quot;
        &lt;/div&gt;
        &lt;div class=&quot;quote&quot;&gt;
          &quot;방금 만든 애플리케이션의 서버를 실행하고, &lt;strong&gt;팀원들이 브라우저에서 접속할 수 있는 주소를 알려주세요.&lt;/strong&gt;&quot;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;p class=&quot;caption&quot; style=&quot;margin-top: 24px;&quot;&gt;
        → 한 번 쓰고 버리는 스크립트가 아니라,&lt;br /&gt;
        &lt;strong style=&quot;color:var(--ink)&quot;&gt;팀이 계속 돌려 쓸 수 있는 &quot;프로그램&quot;&lt;/strong&gt;을 자연어만으로 만들 수 있습니다.
      &lt;/p&gt;
    &lt;/section&gt;

    &lt;!-- 4. 기존 방식 vs Claude Code 방식 --&gt;
    &lt;section class=&quot;section flow&quot;&gt;
      &lt;div class=&quot;eyebrow&quot;&gt;Claude Code 방식의 장점&lt;/div&gt;
      &lt;h2 class=&quot;h2&quot;&gt;내가 가장 잘 아는 나의 업무를,&lt;br /&gt;직접 구현할 수 있게 됩니다.&lt;/h2&gt;

      &lt;div class=&quot;flow-grid&quot; style=&quot;grid-template-columns: 1fr;&quot;&gt;
        &lt;div class=&quot;flow-col new&quot;&gt;
          &lt;span class=&quot;flow-tag&quot;&gt;Claude Code 방식&lt;/span&gt;
          &lt;div class=&quot;flow-title&quot;&gt;현업이 직접 구현 · 즉시 검증&lt;/div&gt;
          &lt;ul class=&quot;flow-steps&quot;&gt;
            &lt;li&gt;&lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;&lt;span&gt;본인이 직접 &lt;strong&gt;기능 구체화&lt;/strong&gt;&lt;/span&gt;&lt;/li&gt;
            &lt;li&gt;&lt;span class=&quot;num&quot;&gt;2&lt;/span&gt;&lt;span&gt;&quot;맞나? 틀렸나?&quot; 즉시 판단&lt;/span&gt;&lt;/li&gt;
            &lt;li&gt;&lt;span class=&quot;num&quot;&gt;3&lt;/span&gt;&lt;span&gt;그 자리에서 &quot;이 부분은 이렇게 바꿔주세요&quot;&lt;/span&gt;&lt;/li&gt;
            &lt;li&gt;&lt;span class=&quot;num&quot;&gt;4&lt;/span&gt;&lt;span&gt;수정 요청서 · 회신 대기 없음&lt;/span&gt;&lt;/li&gt;
            &lt;li&gt;&lt;span class=&quot;num&quot;&gt;5&lt;/span&gt;&lt;span&gt;&lt;strong style=&quot;color:var(--accent)&quot;&gt;내 업무를 내가 직접 구현&lt;/strong&gt;&lt;/span&gt;&lt;/li&gt;
          &lt;/ul&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    &lt;!-- 5. 주의사항 --&gt;
    &lt;div class=&quot;warn-box&quot;&gt;
      &lt;div class=&quot;warn-icon&quot;&gt;!&lt;/div&gt;
      &lt;div&gt;
        &lt;h3&gt;다만 한 가지 주의할 점이 있습니다.&lt;/h3&gt;
        &lt;p&gt;
          만들어진 결과물을 &lt;strong&gt;안정적으로 운영하고 조금씩 개선해 나가시려면&lt;/strong&gt;,&lt;br /&gt;
          결국은 AI가 짜놓은 코드를 읽고 &lt;strong&gt;대략적인 흐름을 이해하는 능력&lt;/strong&gt;이 어느 정도 필요합니다.
        &lt;/p&gt;
        &lt;p&gt;
          처음부터 모든 것을 이해하실 필요는 없지만, 쓰시면서 &lt;strong&gt;조금씩 감을 잡아가시는 것&lt;/strong&gt;을 권장드립니다.
        &lt;/p&gt;
        &lt;p&gt;
          &quot;AI가 다 해주니까 나는 아예 몰라도 된다&quot;는 접근은 장기적으로는 한계가 있습니다.
        &lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- 6. 타사 비교 (축약) --&gt;
    &lt;section class=&quot;section others&quot;&gt;
      &lt;p class=&quot;caption&quot; style=&quot;margin: 0;&quot;&gt;
        참고 · 이런 것도 있습니다 — Cursor, Windsurf, Gemini CLI, OpenAI Codex, GitHub Copilot
      &lt;/p&gt;
    &lt;/section&gt;

    &lt;!-- 7. 한 줄 정리 + CTA --&gt;
    &lt;div class=&quot;cta&quot;&gt;
      &lt;div class=&quot;eyebrow&quot; style=&quot;justify-content:center; display:flex;&quot;&gt;One-line Summary&lt;/div&gt;
      &lt;div class=&quot;cta-title&quot;&gt;한 줄 정리&lt;/div&gt;

      &lt;div class=&quot;cta-summary&quot;&gt;
        &lt;div class=&quot;row chat&quot;&gt;
          &lt;strong&gt;Claude(채팅)&lt;/strong&gt;
          &lt;span&gt; = AI와 &quot;대화&quot;로 일하는 도구&lt;/span&gt;&lt;br /&gt;
          &lt;span&gt;— 문서 요약 · 메일 초안 같은 일상 업무용&lt;/span&gt;
        &lt;/div&gt;
        &lt;div class=&quot;row&quot;&gt;
          &lt;strong&gt;Claude Code&lt;/strong&gt;
          &lt;span&gt; = AI가 &quot;직접 손을 써서&quot; 일해주는 도구&lt;/span&gt;&lt;br /&gt;
          &lt;span&gt;— 개발자뿐 아니라 &lt;b&gt;바이브 코딩으로 업무 자동화&lt;/b&gt;에도 사용&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;p class=&quot;cta-final&quot;&gt;
        클로드 코드를 활용하기 위해&lt;br /&gt;
        &lt;b&gt;당장은 개발을 몰라도 괜찮습니다.&lt;/b&gt;
      &lt;/p&gt;
      &lt;p class=&quot;cta-final&quot; style=&quot;margin-top: 10px;&quot;&gt;
        &lt;b&gt;&quot;시키는 법&quot;&lt;/b&gt;을 익히면서, 천천히 배워나가면 됩니다.
      &lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</description>
      <category>AI/LLM</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/470</guid>
      <comments>https://engineerinsight.tistory.com/470#entry470comment</comments>
      <pubDate>Mon, 11 May 2026 11:48:50 +0900</pubDate>
    </item>
    <item>
      <title>[AI/LLM] LangChain &amp;amp; LangGraph: RAG 구현(LLM 파이프라인 설계)의 고통을 아시나요?</title>
      <link>https://engineerinsight.tistory.com/467</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  인트로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 기반 서비스를 만들 때 가장 핫한 기술 중 하나는 RAG 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 실제로 구현해보면 모델을 불러서 돌리는 것보다, 파이프라인을 어떻게 연결할지, 메모리를 어디에 둘지, 프롬프트 변형은 어떤 조건에서 할지 같은 구성 문제가 훨씬 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangChain과 LangGraph는 이런 복잡한 LLM 워크플로우를 안정적으로 만들기 위해 등장한 도구입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 사전지식: RAG(Retrieval-Augmented Generation)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG(Retrieval-Augmented Generation)는 &lt;b&gt;모델이 답을 생성하기 전에, 외부 지식 저장소에서 관련 문서를 검색해 함께 넣어주는 방식&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 기본적으로 학습 시점까지의 정보만 알고 있어 최신 데이터나 사내 데이터는 알 수 없기 때문에, RAG는 모델이 모르는 것을 아는 것처럼 보이게 하는 핵심 기술입니다. LLM이 가진 추론 능력 위에 &lt;b&gt;검색 기반의 지식 보강 레이어&lt;/b&gt;를 붙인 구조입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csrGV1/dJMcafE2Kqi/knesv477FNSj6T7Ko56KL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csrGV1/dJMcafE2Kqi/knesv477FNSj6T7Ko56KL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csrGV1/dJMcafE2Kqi/knesv477FNSj6T7Ko56KL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsrGV1%2FdJMcafE2Kqi%2Fknesv477FNSj6T7Ko56KL1%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;649&quot; height=&quot;290&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;542&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 &lt;code&gt;검색 &amp;rarr; 전처리 &amp;rarr; 프롬프트 &amp;rarr; 후처리 &amp;rarr; 메모리 관리&lt;/code&gt; 전체가 직접 짜기엔 꽤 복잡하다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;LLM모델이 좋은 응답을 하게 만드는 것에는 크게 Fine Tuning과 RAG가 있습니다. 모델 자체를 튜닝하는 것은 GPU 자원이 기본적으로 많이 필요하기 때문에 비용이 많이 들기도 하고, 이후에 업데이트에 추가적으로 계속 대응하면서 모델을 학습시킨다는 것이 대다수의 기업에는 한계가 있기 때문에 비교적 간편하고 비용이 저렴한 RAG 방식이 자주 사용됩니다. 과거에는 RAG 방식을 사용해 관련 문서의 내용을 추가한 프롬프트가 길어지면 긴 컨텍스트를 인지하지 못하는 문제가 종종 발생했는데, 현대(2025년)의 LLM은 이런 문제가 거의 개선되었기 때문에 RAG 방식이 더 큰 장점을 가지게 되었습니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  AI Agent&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Agent는 단순히 모델을 호출하는 것이 아니라, &lt;b&gt;사용자의 요청을 해석하고 적절한 작업 흐름(Flow)을 설계&amp;middot;실행하는 &amp;lsquo;조정자(Orchestrator)&amp;rsquo;&lt;/b&gt; 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 도구(Vector DB, API, 모델 등)를 언제 호출할지 결정하고 흐름을 구성하고 중간 결과를 바탕으로 다음 행동을 선택하고 필요하면 프롬프트를 재구성하고 실패 상황에서는 다른 경로로 우회하거나 재시도하고 전체 컨텍스트를 유지하면서 상태를 관리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대충 생각해도 좀 복잡한 것 같은데, RAG나 LLM 기능을 직접 만들다보면 다음과 같은 문제가 흔히 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 문제 상황&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;검색, 모델 호출, 문서 파싱, 후처리&lt;/b&gt; 모두를 직접 연결해야 함
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조금만 플로우가 복잡해져도 if-else가 난무하는 문제가 발생함니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;상태 저장&amp;middot;컨텍스트 관리가 어려움
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대화형 시스템일수록 어떤 정보를 기억해야 하는지가 점점 꼬임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;병렬 처리 / 재시도 / 분기 로직을 안정적으로 작성하기 까다로움
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 API가 실패하면 어디서 다시 처리해야 하는지 직접 구현해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파이프라인을 변경할 때마다 전체 코드를 뜯어고쳐야 함
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;검색 &amp;rarr; 요약 &amp;rarr; 답변&lt;/code&gt; 구조에서 &lt;code&gt;검색 &amp;rarr; 필터링 &amp;rarr; 정형화 &amp;rarr; 요약 &amp;rarr; 답변&lt;/code&gt;으로 바꾸고 싶으면 거의 다시 만들다시피 해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예시를 통한 감정이입(?)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Agent를 직접 구성해본 적이 없는 분이라면 위의 상황이 잘 와닿지 않겠는데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번 예시를 통해 감정이입(?)해 보겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 회사는 클라이언트 매뉴얼(PDF/문서)을 업로드하면 &lt;b&gt;AI가 고객이 묻는 질문에 자동으로 답변해주는 SaaS&lt;/b&gt;를 만들고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 버전은 이렇게 단순합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;사용자 질문 &amp;rarr; 문서 검색 &amp;rarr; LLM 호출 &amp;rarr; 답변&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 버전 코드는 대략 이런 느낌입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;query = user_input
docs = vector_db.search(query) # 문서내 검색
parsed = parse_docs(docs) # 살짝의 전처리
prompt = make_prompt(query, parsed) # LLM에 날릴 프롬프트 만들기 = 유저가 물어본거 + 정확한 자료
answer = llm(prompt) # llm 호출
cleaned = postprocess(answer) # 살짝의 후처리
return cleaned&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지는 괜찮아 보이지만, 클라이언트 요구가 이렇게 오기 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&amp;ldquo;검색된 문서가 3개 이상이면 요약해서 넣어주세요&amp;rdquo;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&amp;ldquo;문서가 없으면 FAQ API를 먼저 호출해주세요&amp;rdquo;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&amp;ldquo;답변이 너무 길면 다시 줄여주세요&amp;rdquo;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&amp;ldquo;할루시네이션 감지 후 재생성 해주세요&amp;rdquo;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&amp;ldquo;특정 키워드는 마스킹해주세요&amp;rdquo;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 suddenly&amp;hellip;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;if docs:
    if len(docs) &amp;gt; 3:
        parsed = summarize(docs)
    else:
        parsed = parse(docs)
else:
    faq = search_faq(query)
    if faq:
        parsed = faq
    else:
        parsed = &quot;No data&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 &lt;b&gt;지옥의 if-else&lt;/b&gt;가 생기기 시작합니다. &lt;i&gt;(아직은 애교일수도,,?)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rArr; 검색, 모델 호출, 문서 파싱, 후처리 모두를 직접 연결해야 함&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 클라이언트가 &lt;i&gt;&amp;ldquo;대화형 모드로 만들어주세요&amp;rdquo;&lt;/i&gt;라고 요청하면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;사용자: 로깅 설정 어떻게 하나요?
AI: ~~ 설명 ~~
사용자: 그럼 Spring Boot 기준에서 어떻게 적용하죠?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 AI가 이전 질문의 문맥을 자연스럽게 이어가려면 이전 답변, 이전 검색 결과, 대화 히스토리, 요약된 컨텍스트 등을 계속 관리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 &lt;b&gt;전부 개발자가 직접 저장하고 재조립&lt;/b&gt;해야 하는데, 대화가 5~10턴만 넘어가면 컨텍스트가 꼬여버립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rArr; 상태 저장&amp;middot;컨텍스트 관리가 어려움&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 DB나 LLM API는 종종 실패합니다. &lt;i&gt;(Timeout, Rate limit, Empty response, 500 오류)&lt;/i&gt;&lt;i&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &amp;ldquo;어디서 다시 호출해야 하는지&amp;rdquo; 로직을 직접 구현해야 합니다. &lt;i&gt;(= 외부 API 실패 시 흐름 제어를 전부 직접 관리해야 함)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1) 먼저 벡터 DB 검색 시도
   - 실패하면 캐시 검색
   - 그것도 실패하면 웹 검색
   - 결과가 없으면 default 답변

2) LLM 호출 후 응답이 이상하면
   - 프롬프트 재구성
   - retry(3)
   - fallback 모델로 전환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 다 직접 처리하면 비즈니스 로직이 아니라 오류 처리 코드가 60%가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rArr; 병렬 처리 / 재시도 / 분기 로직을 안정적으로 작성하기 까다로움&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 우리가 처음 만든 구조가 이거였다고 합시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;검색 &amp;rarr; 요약 &amp;rarr; 답변&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 클라이언트가 이렇게 요청합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&amp;ldquo;검색 결과가 너무 많아서 답변이 부정확하니 먼저 &lt;code&gt;필터링 &amp;rarr; 정형화 &amp;rarr; 그걸 요약&lt;/code&gt;해서 넣어주세요.&amp;rdquo;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 파이프라인이 이렇게 변경됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;검색 &amp;rarr; 필터링 &amp;rarr; 정형화 &amp;rarr; 요약 &amp;rarr; 답변&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 설계가 단계 3개를 가정하고 만들어져 있기 때문에 새로운 단계가 끼면 &lt;b&gt;모든 코드의 흐름 제어 부분을 다 수정&lt;/b&gt;해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 &lt;b&gt;전체 재개발에 가까운 작업&lt;/b&gt;이 발생합니다. &lt;i&gt;(근데 실제 운영 환경에서는 파이프라인 수시로 바뀜)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rArr; 파이프라인을 변경할 때마다 전체 코드를 뜯어고쳐야 함&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1400&quot; data-origin-height=&quot;933&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfwHHd/dJMcahCQ7sr/eIpF3LZYiWrqJMBOXBKst1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfwHHd/dJMcahCQ7sr/eIpF3LZYiWrqJMBOXBKst1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfwHHd/dJMcahCQ7sr/eIpF3LZYiWrqJMBOXBKst1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfwHHd%2FdJMcahCQ7sr%2FeIpF3LZYiWrqJMBOXBKst1%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;647&quot; height=&quot;431&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;933&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  LangChain&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangChain은 LLM을 중심으로 한 앱을 쉽게 구성할 수 있도록 만든 &lt;b&gt;모듈식 프레임워크&lt;/b&gt;입니다. &lt;i&gt;(RAG나 Agent 기능 만들 때 반복되는 구성 요소들을 표준 라이브러리로 정리해둔 느낌입니다.)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트 템플릿, 메모리 관리, 벡터스토어, 모델 호출, 체이닝(Chain) 등 LLM 기능을 구성하는 모든 요소를 &lt;b&gt;표준화된 인터페이스로 제공&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;LLM 파이프라인을 레고처럼 조립할 수 있게 해줍니다!&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 LangChain은 Chain보다 LCEL 기반으로 흐름을 구성합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 기능&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프롬프트 템플릿 관리&lt;/li&gt;
&lt;li&gt;벡터스토어 연동 및 RAG 구성&lt;/li&gt;
&lt;li&gt;Tool 호출 구조 정의(검색, DB조회 등)&lt;/li&gt;
&lt;li&gt;에이전트(Agent) 기반 워크플로우 구성&lt;/li&gt;
&lt;li&gt;대화 메모리 관리(Chat History)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ LCEL(LangChain Expression Language)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 Chain을 상속해서 만들고, 함수 여러 개 연결하는 방식이었지만, 요즘 LangChain은 &lt;b&gt;LCEL이라는 표현식 기반으로 파이프라인을 구성하는 방식&lt;/b&gt;을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프라인을 함수 합성하듯 깔끔하게 만들 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;chain = retriever | format_docs | prompt | llm&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 &lt;b&gt;|&lt;/b&gt; 로 연결하면 바로 파이프라인이 됩니다. &lt;i&gt;(매우 편하쥬?)&lt;/i&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  LangGraph&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 개념&lt;/h3&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;1354&quot; data-origin-height=&quot;1016&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r01lB/dJMcag41w73/WJMcKy2wXasXNP94jFKYx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r01lB/dJMcag41w73/WJMcKy2wXasXNP94jFKYx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r01lB/dJMcag41w73/WJMcKy2wXasXNP94jFKYx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr01lB%2FdJMcag41w73%2FWJMcKy2wXasXNP94jFKYx1%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;567&quot; height=&quot;425&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;1016&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangGraph는 LangChain의 상위 개념으로, &lt;b&gt;LLM 기반 흐름을 그래프 형태로 정의&lt;/b&gt;하는 프레임워크입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 파이프라인을 직접 코드로 연결했지만, LangGraph는 Node와 Edge로 RAG pipeline을 선언적으로 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangGraph를 활용하면 여러 에이전트를 포함한 애플리케이션을 만들 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Node(노드)&lt;/b&gt;: 파이프라인 안에서 하나의 단계
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: &lt;i&gt;검색하기&lt;/i&gt;, &lt;i&gt;필터링하기&lt;/i&gt;, &lt;i&gt;프롬프트 만들기&lt;/i&gt;, &lt;i&gt;LLM 호출하기&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Edge(엣지)&lt;/b&gt;: 각 단계를 어떤 순서로 이어줄지를 나타냄
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: &lt;i&gt;검색 &amp;rarr; 필터링 &amp;rarr; 프롬프트 &amp;rarr; LLM&lt;/i&gt; 같은 흐름을 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, LangGraph는 &lt;b&gt;내가 RAG를 어떻게 실행하고 싶은지 연결해두면&lt;/b&gt; 그 순서대로 동작하도록 자동으로 제어해주는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 기능&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노드(Node) 단위로 기능 정의&lt;/li&gt;
&lt;li&gt;분기/루프/병렬 등을 그래프 형태로 선언&lt;/li&gt;
&lt;li&gt;오류 발생 시 재시도/롤백 같은 제어 가능&lt;/li&gt;
&lt;li&gt;여러 모델/도구/상태를 상태머신처럼 관리&lt;/li&gt;
&lt;li&gt;여러 턴을 거치는 에이전트 시스템을 안정적으로 구축&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangChain Agent는 단일 턴 위주, LangGraph Agent는 멀티스텝&amp;middot;지속형에 적합합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 Agent 시스템을 만들 때는 대부분 LangGraph를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;LangChain&lt;/b&gt;: &lt;i&gt;LLM 기능을 &amp;ldquo;부품처럼 조립&amp;rdquo;할 수 있게 해주는 프레임워크&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LangGraph&lt;/b&gt;: &lt;i&gt;그 조립된 기능들이 안정적으로 &amp;ldquo;흐름대로 실행&amp;rdquo;되도록 관리해주는 워크플로우 엔진&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스팅에서는 랭체인, 랭그래프를 활용해 RAG 기반 희귀품종 무화과 챗봇을 개선해 보겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI/LLM</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/467</guid>
      <comments>https://engineerinsight.tistory.com/467#entry467comment</comments>
      <pubDate>Fri, 28 Nov 2025 10:00:17 +0900</pubDate>
    </item>
    <item>
      <title>[MSA] MSA 운영 이슈를 해결하는 패턴: SAGA/CQRS/Outbox/Bulkhead/Circuit Breaker/Event Sourcing</title>
      <link>https://engineerinsight.tistory.com/466</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글을 읽기 전 먼저 &lt;a href=&quot;https://engineerinsight.tistory.com/465&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 포스팅(MSA 운영에서 발생하는 이슈들)&lt;/a&gt;에 대해서 읽고 오시길 바랍니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  인트로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 포스팅에서 MSA를 실제로 운영할 때 자연스럽게 발생하는 대표적인 문제들을 정리했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ MSA 운영 이슈들&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;조인 불가:&lt;/b&gt; 단일 DB가 아니기 때문에 서비스 간 조인이 불가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 불확장:&lt;/b&gt; 트랜잭션이 서비스 경계를 넘어서 확장되지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장애 전파:&lt;/b&gt; 한 서비스의 장애가 네트워크 타고 전체로 전파됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이벤트 불일치:&lt;/b&gt; 이벤트 전달이 실패하거나 순서가 바뀔 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조회 성능 저하:&lt;/b&gt; 조회가 여러 서비스 호출로 변하면서 성능 문제가 생깁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 이러한 문제들을 해결하기 위해 현업에서 가장 널리 사용되는 패턴들을 정리해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SAGA, CQRS, Outbox, Bulkhead, Circuit Breaker, Event Sourcing은 MSA 아키텍처를 구성할 때 등장하는 개념이며, 이번 포스팅에서 이게 왜 필요하고 어떻게 문제를 해결하는지 정리해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 상황을 가정해볼게용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 이해하기 위해서 아래에서는 이 상황에 대입해서 생각해보겠습니다. &lt;i&gt;(지난 포스팅에서의 상황과 동일합니다)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 커머스에서 흔히 볼 수 있는 구조를 생각해봅시당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/en5FKH/dJMcaiIuvI9/gXyEVUBiWckFj9eJax03i1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/en5FKH/dJMcaiIuvI9/gXyEVUBiWckFj9eJax03i1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/en5FKH/dJMcaiIuvI9/gXyEVUBiWckFj9eJax03i1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fen5FKH%2FdJMcaiIuvI9%2FgXyEVUBiWckFj9eJax03i1%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;449&quot; height=&quot;302&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문 서비스&lt;/li&gt;
&lt;li&gt;재고 서비스&lt;/li&gt;
&lt;li&gt;매장 서비스 (오프라인 매장 재고 포함)&lt;/li&gt;
&lt;li&gt;결제 서비스&lt;/li&gt;
&lt;li&gt;배송 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  SAGA 패턴&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결하려는 문제: 트랜잭션 적용 불가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 하나의 비즈니스를 처리할 때 여러 서비스가 연쇄적으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주문 서비스 &amp;rarr; 결제 서비스 &amp;rarr; 재고 서비스 &amp;rarr; 배송 서비스 같은 흐름이 있는데, 이 과정 전체를 하나의 트랜잭션으로 묶을 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 &lt;b&gt;중간 단계에 실패가 발생하면 앞 단계에서 이미 처리된 성공 작업을 되돌릴 방법이 없는 문제가 생깁니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법: &lt;b&gt;&lt;i&gt;실패하면 앞단 성공 작업을 취소하는 보상 API를 실행한다&lt;/i&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SAGA 패턴은 비즈니스 트랜잭션을 여러 개의 로컬 트랜잭션으로 나누고, 실패 시 &lt;b&gt;보상 트랜잭션&lt;/b&gt;을 수행하여 상태를 되돌리는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;실패하면 앞단 성공 작업을 취소하는 API를 순서대로 호출하는 구조입니다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AK47a/dJMcacO49Fw/1voOtROw1MKgY8xAdWptB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AK47a/dJMcacO49Fw/1voOtROw1MKgY8xAdWptB1/img.png&quot; data-alt=&quot;실패 시 보상 API 실행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AK47a/dJMcacO49Fw/1voOtROw1MKgY8xAdWptB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAK47a%2FdJMcacO49Fw%2F1voOtROw1MKgY8xAdWptB1%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;784&quot; height=&quot;187&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;220&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실패 시 보상 API 실행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성공 흐름: T1 &amp;rarr; T2 &amp;rarr; T3&lt;/li&gt;
&lt;li&gt;실패 흐름: T3 실패 &amp;rarr; 보상(T2 취소) &amp;rarr; 보상(T1 취소)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 상황에 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주문 흐름이 이렇게 되어 있다고 해봅시다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;주문 생성 (T1)&lt;/li&gt;
&lt;li&gt;결제 승인 (T2)&lt;/li&gt;
&lt;li&gt;재고 차감 (T3)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 재고 차감(T3)에서 실패했다면???&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결제 승인 취소(T2 보상)&lt;/li&gt;
&lt;li&gt;주문 취소(T1 보상)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 앞에서 했던 일을 역순으로 취소하는 API를 호출하는 게 바로 SAGA입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SAGA에서는 보상 트랜잭션을 누가 실행하느냐에 따라 두 가지 방식이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1815&quot; data-start=&quot;1721&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1772&quot; data-start=&quot;1721&quot;&gt;&lt;b&gt;오케스트레이션:&lt;/b&gt; 중앙 orchestrator가 취소 API들을 순차적으로 실행 &lt;i&gt;(재고 서비스는 실패했다는 신호만 보냄)&lt;/i&gt;&lt;/li&gt;
&lt;li data-end=&quot;1815&quot; data-start=&quot;1773&quot;&gt;&lt;b&gt;코레오그래피:&lt;/b&gt; 서비스끼리 이벤트 기반으로 보상 트랜잭션을 이어나감&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방식 모두 역방향으로 취소가 일어나지만, 누가 그 흐름을 조율하느냐가 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무튼 이렇게 보상 트랜잭션이 실행되면서 전체 비즈니스 일관성이 유지됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  CQRS 패턴&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결하려는 문제: 조회 성능 저하&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 서비스마다 DB를 따로 가지고 있기 때문에 조회 시 여러 서비스로부터 데이터를 조합해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커머스 프론트 &amp;ldquo;주문 상세&amp;rdquo; 화면을 보려 할 때 필요한 정보는 보통 이렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문 서비스: 주문 ID, 주문일, 주문 상태, 총액&lt;/li&gt;
&lt;li&gt;결제 서비스: 결제 수단, 결제 승인 여부, 승인 시간&lt;/li&gt;
&lt;li&gt;재고 서비스: 출고 준비 가능 여부&lt;/li&gt;
&lt;li&gt;매장 서비스: 매장 재고가 있는지 여부&lt;/li&gt;
&lt;li&gt;배송 서비스: 배송 상태, 배송 예정일&lt;/li&gt;
&lt;li&gt;추천 서비스: 관련 추천 상품&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모놀리식이라면 단일 DB에서 조인 한 번이면 끝날 문제지만, 각 서비스가 자기 DB를 가지고 있기 때문에 조인이 불가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 프론트는 이렇게 호출한 결과를 합쳐야만 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;/order/{id}
/payment/{orderId}
/inventory/check/{orderId}
/store-stock/{itemId}
/shipment/{orderId}
/recommendation/order/{id}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;6~7개 서비스의 데이터를 합쳐야 주문 상세 화면 하나가 그려집니다. = 지연폭발(latency explosion)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &lt;b&gt;서비스 버전의 N+1 문제&lt;/b&gt;라고 부르며, 조회 응답 속도가 크게 저하됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법: &lt;i&gt;조회용 테이블을 아예 따로 만들어둔다&lt;/i&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CQRS(Command Query Responsibility Segregation)는 &lt;b&gt;쓰기 모델과 조회 모델을 분리&lt;/b&gt;하는 방식입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쓰기 모델: 정규화, 도메인 규칙 기반 저장&lt;/li&gt;
&lt;li&gt;조회 모델: 화면에 최적화된 &lt;i&gt;의도적인&lt;/i&gt; 비정규화 구조 (Redis, ES, 별도 DB 활용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;CQRS의 조회 모델은 여러 서비스의 상태를 실시간 이벤트 기반으로 미리 합쳐둔 비정규화 데이터 구조이며, 프론트는 이 모델 한 번만 조회하면 화면 전체를 구성할 수 있습니다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 모델은 여러 서비스의 데이터를 합쳐놓은 형태일 수 있어 서비스 간 조인이 필요 없어져 조회 성능이 크게 개선됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 상황에 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CQRS에서는 &lt;b&gt;조회 전용(read model) DB&lt;/b&gt;를 별도로 구성합니다. 이 DB는 정규화되어 있지 않고, 화면에 최적화된 형태로 구조가 잡혀 있습니다. 주문 상세에 필요한 데이터를 미리 한 테이블에 담아두는 식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;order_read_model
────────────────────────────────────────────
order_id
order_date
order_status
total_amount

payment_method
payment_approved_at

inventory_available
store_stock_available

shipment_status
estimated_delivery

recommended_items (JSON 배열)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 의도적으로 비정규화된 테이블에는 화면에 필요한 모든 정보가 한 행(row)에 들어있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CQRS에서는 이 화면에 필요한 조회 모델을 미리 만들어두고, 프론트는 &lt;b&gt;조회 모델 DB 한 번만 조회&lt;/b&gt;하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 그러면 이 Read Model은 누가 채우나요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;이벤트로 채웁니다.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 모델(도메인 서비스들)이 각각 이런 이벤트를 발행합니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문 서비스 &amp;rarr; &lt;code&gt;OrderCreated&lt;/code&gt;, &lt;code&gt;OrderUpdated&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;결제 서비스 &amp;rarr; &lt;code&gt;PaymentApproved&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;재고 서비스 &amp;rarr; &lt;code&gt;InventoryChecked&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;배송 서비스 &amp;rarr; &lt;code&gt;ShipmentUpdated&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;추천 서비스 &amp;rarr; &lt;code&gt;RecommendationUpdated&lt;/code&gt;&lt;/li&gt;
&lt;/ul&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;1158&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8bEnd/dJMcahv5EQg/yILkg7RDtizM0k9y5AYVo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8bEnd/dJMcahv5EQg/yILkg7RDtizM0k9y5AYVo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8bEnd/dJMcahv5EQg/yILkg7RDtizM0k9y5AYVo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8bEnd%2FdJMcahv5EQg%2FyILkg7RDtizM0k9y5AYVo1%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;720&quot; height=&quot;154&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CQRS의 조회 모델 빌더는 이 이벤트들을 순서대로 소비하면서 read model DB를 채웁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(실제로는 이벤트 순서 보장 문제가 있어 별도 정렬&amp;middot;타임스탬프 전략 필요합니다. )&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;각 서비스가 이벤트를 흘려보내고 조회 모델 빌더가 이를 받아 READ 모델을 점진적으로 완성하는 구조&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Outbox 패턴&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결하려는 문제: 이벤트 불일치 (누락 방지)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 기반 아키텍처에서는 DB 저장과 Kafka에 이벤트 발행이 별개 시스템입니다. 따라서 다음과 같은 &lt;b&gt;일관성 깨짐 문제가 발생할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;주문 DB 저장 성공&lt;/li&gt;
&lt;li&gt;Kafka로 OrderCreated 이벤트 발행 실패&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 다른 서비스들은 주문 생성 사실을 평생 모르게 됩니다. 이건 데이터 일관성 붕괴, 재고/배송 오류, 커머스 장애까지 이어지는 심각한 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;기존에 트랜잭션을 통해서 atomic 하게 운영되던 부분을 이벤트 기반으로 비동기적으로 나누어 놓았는데, 모든 정합성이 깨지면서도 그게 깨지는지도 알기 어려운 상황,,,&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법: 이벤트 발행할 내용을 먼저 우리 DB에 atomic하게 기록해두고 나중에 Kafka로 보낸다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Kafka에 보냈는지 여부를 우리 DB에 직접 기록해두고, 그걸 기반으로 안전하게 재발행하는 구조&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;DB에 저장한 사실과 이벤트 발행 여부를 반드시 일치시키기 위해, 이벤트를 발행할 내용을 먼저 우리 DB에 써두고 그것을 나중에 Kafka로 보내는 방식입니다.&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sVQxg/dJMcacIj512/nXNSikH8kKRiZEKh5A9pB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sVQxg/dJMcacIj512/nXNSikH8kKRiZEKh5A9pB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sVQxg/dJMcacIj512/nXNSikH8kKRiZEKh5A9pB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsVQxg%2FdJMcacIj512%2FnXNSikH8kKRiZEKh5A9pB0%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;530&quot; height=&quot;275&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;1) 먼저 &amp;ldquo;보낼 이벤트&amp;rdquo;를 Outbox 테이블에 저장&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계는 &lt;b&gt;주문 저장과 같은 DB 트랜잭션 안에 포함&lt;/b&gt;되어 atomicity가 보장됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문 저장 성공 = Outbox 이벤트 저장 성공&lt;/li&gt;
&lt;li&gt;주문 저장 실패 = Outbox 이벤트 저장 rollback&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;DB는 항상 완전한 상태&lt;/b&gt;를 가지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;2) Outbox Processor가 DB의 이벤트를 읽어서 Kafka로 발행&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka가 잠깐 장애여도 상관 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;3) Kafka 발행 성공하면 Outbox 테이블의 status를 &lt;code&gt;SENT&lt;/code&gt;로 업데이트&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka가 장애였다면 계속 재시도합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;4) Outbox 테이블에서 오래된 이벤트는 삭제하거나 보관 전략 실행&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Outbox Processor는 실패하면 재시도 가능하도록 설계하고, 이 방법을 사용한다면 이벤트 누락을 방지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 상황에 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 서비스가 주문 생성 후 이벤트를 발행하는 경우라면&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;주문 서비스는 주문 DB 저장 + Outbox 테이블에 &lt;code&gt;OrderCreated&lt;/code&gt; 이벤트 기록&lt;/li&gt;
&lt;li&gt;동일 트랜잭션이므로 둘 중 하나만 성공할 수 없음&lt;/li&gt;
&lt;li&gt;Outbox Processor가 Outbox 이벤트를 Kafka로 안전하게 전달&lt;/li&gt;
&lt;li&gt;재고 서비스, 배송 서비스는 Kafka로부터 이벤트를 안전하게 수신&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 주문 정보 저장과 이벤트 발행의 일관성이 깨지지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Bulkhead 패턴&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결하려는 문제: 장애 전파&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 한 서비스의 장애가 다른 서비스로 쉽게 전파됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 재고 서비스가 느려지면 주문 서비스의 모든 스레드가 재고 응답을 기다리다가 &lt;b&gt;주문 서비스 전체가 멈추는 상황&lt;/b&gt;이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법: 서비스마다 리소스 풀을 분리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bulkhead 패턴은 &lt;b&gt;서비스마다 리소스 풀을 분리해 격리하는 구조&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;&lt;i&gt;말 그대로 격벽을 만들어서 하나의 기능이 다른 기능을 무너뜨리지 못하게 하는 방식입니다.&lt;/i&gt;&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 애플리케이션 내부라면 주문 서비스가 재고 서비스 API를 호출하든, 결제 서비스를 호출하든, 매장 서비스를 호출하든 &lt;b&gt;모두 같은 스레드 풀에서 실행됩니다.&lt;/b&gt; 그래서 한 요청이 외부 API에서 오래 대기하면 그 요청을 담당하던 스레드가 계속 묶이게 되고, 다른 서비스를 호출할 수도 없게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bulkhead는 외부 API 호출을 &lt;b&gt;요청 처리 스레드와 분리된 전용 스레드 풀에서 실행&lt;/b&gt;해 장애를 격리하는 패턴입니다. 특정 기능이 느려져도 그 전용 스레드 풀만 막히기 때문에 서비스 전체 스레드가 고갈되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 한 기능의 장애가 다른 기능으로 전파되지 않고, 서비스 전체가 멈추는 상황을 예방할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 상황에 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 주문 요청을 보낼 때, 다음과 같은 상황을 가정해 보겠습니당&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결제 API: 정상&lt;/li&gt;
&lt;li&gt;매장 재고 조회 API: 정상&lt;/li&gt;
&lt;li&gt;재고 서비스 API: 느림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Bulkhead 없음 (기본 구조)]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문 서비스 스레드 풀 200개&lt;/li&gt;
&lt;li&gt;재고 API가 느려서 150개 스레드가 재고 응답 기다리는 중&lt;/li&gt;
&lt;li&gt;남은 50개도 매장/결제/배송 요청이 들어오면 금방 스레드 고갈&lt;/li&gt;
&lt;li&gt;결과: 주문 서비스 전체 503 상태(응답 불가)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Bulkhead 있음]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재고 API 스레드 풀: 20개&lt;/li&gt;
&lt;li&gt;매장 API 스레드 풀: 20개&lt;/li&gt;
&lt;li&gt;배송 API 스레드 풀: 20개&lt;/li&gt;
&lt;li&gt;주문 서비스 main 스레드 풀(Tomcat): 200개&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재고 API가 느려진다 할지라도, 재고 전용 스레드 20개만 대기하고 나머지 180개의 요청 스레드는 동작하기 때문에 주문 조회, 주문 취소, 결제 취소 등 정상 작동하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재고 전용 스레드 풀이 고갈되더라도, 주문 서비스의 다른 기능(주문 조회, 결제 취소 등)은 정상 처리되어 전체 장애가 전파되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Circuit Breaker 패턴&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결하려는 문제: 장애 전파&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고장난 서비스로 계속 요청을 보내면 호출하는 쪽의 스레드가 모두 묶이면서 전체 장애가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 주문&amp;ndash;결제&amp;ndash;재고&amp;ndash;배송처럼 강하게 연결된 서비스에서는 &lt;b&gt;연쇄 장애&lt;/b&gt;가 쉽게 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법: 실패율이 높아지면 회로를 강제로 끊어 요청하지 않고 즉시 실패한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Circuit Breaker는 일정 비율 이상 실패율이 발생하면 &lt;b&gt;회로를 강제로 끊어(Open 상태)&lt;/b&gt; 해당 서비스로의 요청을 즉시 실패시키는 방식입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실패율이 높다면 &amp;rarr; Open &amp;rarr; 요청 즉시 실패&lt;/li&gt;
&lt;li&gt;일정 시간 후 &amp;rarr; Half-Open &amp;rarr; 테스트 호출 몇 개 시도&lt;/li&gt;
&lt;li&gt;성공률이 회복되면 &amp;rarr; Closed &amp;rarr; 정상 호출 복귀&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 사용하면 장애 전파를 막고 시스템을 보호할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 상황에 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재고 서비스가 다운된 상황에서 Bulkhead만 있다면 스레드는 일부 보호되지만 끊임없이 재고 API를 재시도하게 되지만, Circuit Breaker가 함께 있으면 일정 실패 이후 재고 호출을 즉시 차단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 서비스는 &amp;ldquo;재고 시스템 점검 중&amp;rdquo; 같은 빠른 오류 반환 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(오류가 없다면 베스트겠지만 이건 이미 오류가 났다는 상황 아래에 어떻게 최대한 잘 대처할 수 있는가에 대한 방법입니다)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bulkhead, Circuit Breaker 두 패턴이 함께 사용될 때 안정성이 크게 올라갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Event Sourcing 패턴&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결하려는 문제: 이벤트 순서/복구 불가 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 기반 구조에서는 최신 상태만 저장하면 다음과 같은 문제가 생깁니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트 순서 문제&lt;/li&gt;
&lt;li&gt;상태 복구 불가능&lt;/li&gt;
&lt;li&gt;감사 로그 부족&lt;/li&gt;
&lt;li&gt;여러 서비스가 상태를 재구성할 수 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 스트림을 잃으면 전체 상태를 재구성할 수 없는 구조가 되어버립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법: &lt;b&gt;상태 변화의 이벤트를 그대로 기록한다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event Sourcing은 &lt;b&gt;현재 상태가 아니라 상태 변화의 이벤트를 그대로 기록&lt;/b&gt;하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zgnHB/dJMcacO49MO/fS55VB7KknBdBsyEaypjUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zgnHB/dJMcacO49MO/fS55VB7KknBdBsyEaypjUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zgnHB/dJMcacO49MO/fS55VB7KknBdBsyEaypjUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzgnHB%2FdJMcacO49MO%2FfS55VB7KknBdBsyEaypjUK%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;312&quot; height=&quot;288&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주문의 상태를 하나의 필드로 저장하는 것이 아니라,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OrderCreated&lt;/li&gt;
&lt;li&gt;PaymentApproved&lt;/li&gt;
&lt;li&gt;ShipmentStarted&lt;/li&gt;
&lt;li&gt;ShipmentDelivered&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 이벤트를 순서대로 append-only 형태로 기록합니다. 시스템이 재시작되거나 새로운 서비스가 합류해도 이 이벤트 스트림을 replay하면 최신 상태를 재구성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 상황에 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배송 서비스가 장애로 일시 중단되었다고 가정해봅시다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;주문 서비스는 &lt;code&gt;OrderCreated&lt;/code&gt; 이벤트를 기록&lt;/li&gt;
&lt;li&gt;결제 서비스는 &lt;code&gt;PaymentApproved&lt;/code&gt; 이벤트를 기록&lt;/li&gt;
&lt;li&gt;배송 서비스가 재시작되면 Kafka의 이벤트 스트림을 순서대로 읽고 상태를 다시 정확하게 복원할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event Sourcing은 CQRS의 조회 모델 구성과 함께 조합이 좋슴니당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 포스팅에 등장한 패턴들은 MSA 환경에서 반복적으로 등장하는 이슈들에 대한 해결 방법입니다.&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;SAGA:&lt;/b&gt; 트랜잭션이 서비스 간 확장되지 않는 문제를 해결합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CQRS:&lt;/b&gt; 조회가 여러 서비스 호출로 변해 성능이 저하되는 문제를 해결합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Outbox:&lt;/b&gt; 이벤트 전달 실패, 중복, 순서 꼬임 등 이벤트 불일치 문제를 해결합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Bulkhead:&lt;/b&gt; 한 서비스의 장애가 전체로 전파되는 문제를 방지합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Circuit Breaker:&lt;/b&gt; 장애난 서비스로 계속 요청을 보내 발생하는 연쇄 장애를 차단합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Event Sourcing:&lt;/b&gt; 이벤트 순서 문제, 상태 복구 불가 문제를 해결합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 패턴은 단독으로도 좋지만, 실제 운영에서는 보통 여러 패턴이 결합됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 11월 27일 오후 03_59_04.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFrxir/dJMcagKICnf/k1sbkDnF0Vqhs4WlAMqdJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFrxir/dJMcagKICnf/k1sbkDnF0Vqhs4WlAMqdJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFrxir/dJMcagKICnf/k1sbkDnF0Vqhs4WlAMqdJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFrxir%2FdJMcagKICnf%2Fk1sbkDnF0Vqhs4WlAMqdJK%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;433&quot; height=&quot;433&quot; data-filename=&quot;ChatGPT Image 2025년 11월 27일 오후 03_59_04.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>아키텍처+MSA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/466</guid>
      <comments>https://engineerinsight.tistory.com/466#entry466comment</comments>
      <pubDate>Thu, 27 Nov 2025 16:30:38 +0900</pubDate>
    </item>
    <item>
      <title>[MSA] MSA 운영에서 발생하는 이슈들: 데이터 일관성/트랜잭션 적용 불가/장애 전파/이벤트 불일치/조회 성능</title>
      <link>https://engineerinsight.tistory.com/465</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  인트로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA는 Microservice Architecture(마이크로서비스 아키텍처)의 약자로, 하나의 큰 애플리케이션을 여러 개의 독립적인 작은 서비스로 나누어 개발&amp;middot;배포&amp;middot;운영하는 아키텍처입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서비스는 자체 DB와 로직을 가지며 서로 API나 이벤트로 통신합니다. 이렇게 분리하면 기능별 확장과 독립 배포가 쉬워지지만, 서비스 간 협력이 복잡해지고 일관성&amp;middot;장애 관리 문제가 함께 따라옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에 대해서는 크게 장점으로는 &lt;i&gt;나누어져 있으니 일부만 scale out 할 수 있어 비용이 절감된다&lt;/i&gt;, 그리고 단점으로는 &lt;i&gt;운영이 복잡하다&lt;/i&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;  MSA 운영에서 발생하는 이슈들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA 구조를 실제로 운영해보면, 처음에 기대했던 서비스를 나눠서 확장 쉽게 한다는 포부와는 다르게 복잡한 문제가 곳곳에서 터지게 됩니다. 근본적인 이유는 하나의 비즈니스를 여러 서비스가 나눠 들고 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 커머스에서 흔히 볼 수 있는 구조를 생각해봅시당&lt;/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;680&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9iYht/dJMcai2M31l/VsZvNP2LrnQtKK04wKVaw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9iYht/dJMcai2M31l/VsZvNP2LrnQtKK04wKVaw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9iYht/dJMcai2M31l/VsZvNP2LrnQtKK04wKVaw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9iYht%2FdJMcai2M31l%2FVsZvNP2LrnQtKK04wKVaw1%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;467&quot; height=&quot;335&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문 서비스&lt;/li&gt;
&lt;li&gt;재고 서비스&lt;/li&gt;
&lt;li&gt;매장 서비스 (오프라인 매장 재고 포함)&lt;/li&gt;
&lt;li&gt;결제 서비스&lt;/li&gt;
&lt;li&gt;배송 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보기에는 역할이 명확하게 분리된 것 같지만, 실제 요청 흐름에서는 이 서비스들이 서로 강하게 연결되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 데이터 일관성 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 고객이 상품을 주문할 때는 반드시 재고가 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 MSA에서는 재고 정보가 &lt;b&gt;재고 서비스&lt;/b&gt;의 DB에 존재하고, 주문 정보는 &lt;b&gt;주문 서비스&lt;/b&gt;의 DB에 존재합니다. 두가지 DB는 다른 서버에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 단일 DB였다면 재고 조회, 주문 생성, 재고 감소를 모두 하나의 트랜잭션 안에서 처리해 어느 한 단계에서 오류가 나면 전체가 깔끔하게 롤백됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 MSA에서는 각 서비스가 서로 다른 DB를 사용하기 때문에, 더 이상 조인을 수행할 수 없습니다. 다음과 같은 쿼리는 물리적으로 불가능합니다. &lt;i&gt;(외부 DB 간 조인은 지원되지 않습니당)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 실시간 JOIN은 불가능하며, 애플리케이션 레벨 조합&lt;i&gt;(A 서비스+ B 서비스 조회 후 백엔드에서 결합)&lt;/i&gt;하거나 조회 전용 모델을 별도로 구성해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;SELECT * FROM order_db.orders o
JOIN stock_db.stocks s ON ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 주문을 생성하는 시점에 재고 서비스에서 재고를 조회해보고 믿는 방식으로만 처리할 수밖에 없습니다. 이 과정에서 &lt;b&gt;데이터 일관성 문제가 발생합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문은 성공했지만 재고 차감은 실패 &amp;rarr; 판매량은 증가했지만 재고는 그대로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재고 차감은 성공했지만 주문은 실패 &amp;rarr; 재고만 빠진 상태가 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 트랜잭션 적용 문제&lt;/h3&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;하나의 DB 커넥션 안에서만&lt;/b&gt; 동작합니다. 따라서 주문 DB와 재고 DB를 한 번에 묶어서 commit 또는 rollback하는 것은 원칙적으로 불가능합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 A는 성공&lt;/li&gt;
&lt;li&gt;서비스 B는 실패&lt;/li&gt;
&lt;li&gt;두 서비스의 상태가 어긋남&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 주문/결제/재고처럼 서로 강하게 연관된 작업에서 특히 심각하게 드러납니다. &lt;i&gt;(그래서 뒤에 등장할 SAGA 패턴이 등장했습니다.)&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 장애가 전체로 전파되는 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA의 가장 치명적인 문제는 &lt;b&gt;한 서비스의 느림이나 장애가 전체로 퍼진다는 점&lt;/b&gt;입니다. 예시 상황을 들어보겟습니당.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세일 시간에 재고 확인 요청이 폭주합니다.&lt;/li&gt;
&lt;li&gt;재고 서비스가 느려지거나 타임아웃이 발생합니다.&lt;/li&gt;
&lt;li&gt;주문 서비스는 재고 서비스를 계속 호출합니다.&lt;/li&gt;
&lt;li&gt;호출이 모두 타임아웃되면서 주문 서비스의 스레드 풀을 소모합니다.&lt;/li&gt;
&lt;li&gt;결국 주문 서비스 전체가 멈추고, 결제 서비스까지 장애가 확산됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;하나의 서비스가 느려지게 되면 타임아웃 동안 해당 서비스를 호출한 서비스의 스레드가 대기하기 때문에, 호출한 쪽의 스레드 풀이 소진되고, 이건 결국 전체 요청 실패로 이어지는 장애 전파의 대표적인 형태입니다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 모놀리식 구조에서는 같은 프로세스 내부에서 최적화할 수 있지만, MSA는 네트워크 호출에 의존하기 때문에 작은 장애도 전체 장애로 이어질 가능성이 높습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(이 문제를 해결하기 위해 Bulkhead, Circuit Breaker 같은 패턴이 사용됩니다.)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 이벤트 전달과 동기화 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 간 DB를 공유할 수 없기 때문에 대부분 이벤트 기반(Kafka 등)으로 상태를 동기화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이벤트 전달 과정에서 이런 문제들이 발생할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재고 감소 이벤트가 Kafka 장애로 누락됨 &lt;i&gt;(흔치 않긴 하지만)&lt;/i&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 data-ke-size=&quot;size16&quot;&gt;이런 문제가 발생하면 서비스 간 상태가 쉽게 불일치하게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 재고는 0인데, 주문 서비스 화면에는 재고가 있는 것처럼 보임&lt;/li&gt;
&lt;li&gt;주문 서비스는 &amp;ldquo;결제 완료&amp;rdquo; 상태인데 결제 서비스는 아직 처리 안 됨&lt;/li&gt;
&lt;li&gt;두 명이 동시에 같은 상품을 주문해버림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(이런 문제를 해결하기 위해 Outbox 패턴, Event Sourcing 패턴 등이 등장합니다.)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 조회 성능 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 화면을 그리기 위해 여러 서비스를 호출해야 하는 상황이 생깁니다. 예를 들어 주문 상세 페이지를 띄우려면 다음 정보가 필요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문 정보 (주문 서비스)&lt;/li&gt;
&lt;li&gt;상품 정보 (상품 서비스)&lt;/li&gt;
&lt;li&gt;재고 정보 (재고 서비스)&lt;/li&gt;
&lt;li&gt;매장 재고 정보 (매장 서비스)&lt;/li&gt;
&lt;li&gt;배송 옵션 (배송 서비스)&lt;/li&gt;
&lt;li&gt;쿠폰 및 할인 정책 (쿠폰 서비스)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 DB였다면 &lt;code&gt;JOIN&lt;/code&gt; 또는 여러 서브쿼리로 한 번에 가져올 수 있지만, MSA에서는 네트워크를 통한 서비스 호출이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;각 서비스를 별도로 호출해야 하므로 화면 하나 로딩에 6~8개의 API가 호출되는 &amp;lsquo;서비스 버전의 N+1 문제&amp;rsquo;가 발생합니다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일일이 다른 API로 해당 정보들을 모아오려면 서비스 호출이 많아 응답이 느려지고, 이에 따라 병목 구간이 생기게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 트래픽이 많은 상황이라면 특정 서비스 장애가 전체 요청을 실패시킬 수도 있고, 화면 렌더링이 불안정해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(그래서 조회 전용 모델을 따로 두는 CQRS 패턴이나, 비정규화된 검색 엔진(Elasticsearch&amp;middot;Redis)을 사용하는 방식이 등장합니다.)&lt;/i&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;MSA 구조를 운영할 때 발생하는 문제들은 대부분 다음 다섯 가지가 발생합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 DB가 아니기 때문에 조인이 불가능합니다.&lt;/li&gt;
&lt;li&gt;트랜잭션이 서비스 간에 확장되지 않습니다.&lt;/li&gt;
&lt;li&gt;장애가 네트워크 타고 전체로 번집니다.&lt;/li&gt;
&lt;li&gt;이벤트 전달이 실패하거나 순서가 바뀔 수 있습니다.&lt;/li&gt;
&lt;li&gt;조회가 여러 서비스 호출로 변하면서 성능 문제가 생깁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제들을 해결하기 위해 SAGA, CQRS, Outbox, Bulkhead, Circuit Breaker, Event Sourcing 같은 패턴이 등장했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스팅에서는 MSA에서 발생하는 문제들을 해결하기 위한 패턴들에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 11월 27일 오후 03_59_04.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOyHPr/dJMcabWWqWF/SZZ84VXYBoO94unUtTeKP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOyHPr/dJMcabWWqWF/SZZ84VXYBoO94unUtTeKP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOyHPr/dJMcabWWqWF/SZZ84VXYBoO94unUtTeKP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOyHPr%2FdJMcabWWqWF%2FSZZ84VXYBoO94unUtTeKP0%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;602&quot; height=&quot;602&quot; data-filename=&quot;ChatGPT Image 2025년 11월 27일 오후 03_59_04.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>아키텍처+MSA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/465</guid>
      <comments>https://engineerinsight.tistory.com/465#entry465comment</comments>
      <pubDate>Thu, 27 Nov 2025 16:00:25 +0900</pubDate>
    </item>
    <item>
      <title>[AI/LLM] MCP(Model Context Protocol): LLM이 곧바로 DB에서 최신 데이터도 읽고, 메일도 보내준다고?</title>
      <link>https://engineerinsight.tistory.com/464</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  MCP(Model Context Protocol)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;AI 모델이 외부 시스템, 데이터, 도구와 상호작용하는 방식을 표준화한 통신 규약&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 개념&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JIPL5/dJMcai2KSrj/vWMPu0DxKeJHwwld51bdcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JIPL5/dJMcai2KSrj/vWMPu0DxKeJHwwld51bdcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JIPL5/dJMcai2KSrj/vWMPu0DxKeJHwwld51bdcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJIPL5%2FdJMcai2KSrj%2FvWMPu0DxKeJHwwld51bdcK%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;696&quot; height=&quot;272&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP는 비교적 최근(2024년 말부터 2025년 초까지) Anthropic에서 발표한 프로토콜로, LLM 기반 에이전트 시대에 필요한 모델 중심 프로토콜입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 인터넷 프로토콜(HTTP, WebSocket 등)은 네트워크 통신 자체만 정의했지만, MCP는 &lt;b&gt;AI 모델이 어떤 메시지를 주고받을지 자체를 정의&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 적당히 똑똑하지만, 모델 자체가 학습 시점의 지식에 고착되어 있고, 외부 세계와 상호작용할 수 없습니다. 즉, 실시간 데이터에 액세스하거나 회의 예약, 고객 기록 업데이트와 같은 작업을 수행할 수 없습니다.&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;600&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caqPpt/dJMcahCOY2y/BwLq9qVDSVIAWA97GYaZo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caqPpt/dJMcahCOY2y/BwLq9qVDSVIAWA97GYaZo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caqPpt/dJMcahCOY2y/BwLq9qVDSVIAWA97GYaZo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaqPpt%2FdJMcahCOY2y%2FBwLq9qVDSVIAWA97GYaZo0%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;456&quot; height=&quot;456&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[LLM이 못 하는 것]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 파일 읽기&amp;middot;쓰기&lt;/li&gt;
&lt;li&gt;인터넷 검색 후 결과 정리&lt;/li&gt;
&lt;li&gt;회사 내부 API 호출&lt;/li&gt;
&lt;li&gt;데이터베이스 조회&amp;middot;수정&lt;/li&gt;
&lt;li&gt;반복 작업 자동 실행&lt;/li&gt;
&lt;li&gt;여러 단계를 연결한 워크플로우 수행&lt;/li&gt;
&lt;li&gt;도구 실행 결과를 다시 이용해 다음 행동 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;주로 외부의 서비스에 직접 손대는 것들을 못하죠?&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Model Context Protocol(MCP)은 이러한 문제를 해결하기 위해 설계된 개방형 표준입니다. 2024년 11월 &lt;a href=&quot;https://cloud.google.com/products/model-garden/claude&quot;&gt;Anthropic&lt;/a&gt;에서 도입한 MCP는 LLM이 외부 데이터, 애플리케이션, 서비스와 통신할 수 있는 안전하고 표준화된 '언어'를 제공합니다. AI가 정적 지식을 넘어 현재 정보를 검색하고 조치를 취할 수 있는 동적 &lt;a href=&quot;https://cloud.google.com/discover/what-are-ai-agents&quot;&gt;에이전트&lt;/a&gt;가 되도록 지원하는 브리지 역할을 하여 AI의 정확성, 유용성, 자동화를 높입니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 AI가 단순히 입력에 대한 응답을 생성하는 수준을 넘어, 필요한 데이터를 스스로 서버에 요청하거나 외부 시스템을 호출할 수 있게 만드는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로는 정확한 정보를 바탕으로 추론해 더 정확한 답변을 생성할 수 있어 할루시네이션을 억제하는 효과가 있습니당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ RAG와의 차이점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG는 응답 생성 전!!!에 관련 정보를 검색해 LLM 프롬프트에 함께 제공해 응답의 품질을 높이는 방법입니다. 여전히 외부와 상호작용할 수는 없습니다. 그냥 LLM이 좀더 잘 말하게 하는 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP는 LLM이 외부 도구와 서비스에 엑세스하고 상호작용할 수 있도록, 정보도 검색해 읽고 쓰기 작업도 할 수 있도록 하는 &lt;b&gt;양방향 상호작용 레이어&lt;/b&gt;를 제공하는 프로토콜입니다. &lt;i&gt;(웹소켓식 양방향은 아님! 나중에 한다고는 하는 것 같음)&lt;/i&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;  MCP 아키텍처&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ MCP 호스트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 AI를 사용하는 공간&lt;/li&gt;
&lt;li&gt;LLM이 포함된 전체 실행 환경&lt;/li&gt;
&lt;li&gt;MCP 호스트 = LLM + MCP 클라이언트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ MCP 클라이언트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LLM &amp;harr; MCP 서버의 통신&lt;/li&gt;
&lt;li&gt;MCP에 대한 LLM의 요청을 변환하고, LLM에 대한 MCP의 응답을 변환&lt;/li&gt;
&lt;li&gt;사용 가능한 MCP 서버를 찾아 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ MCP 서버&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LLM에 컨텍스트, 데이터, 기능 등을 제공하는 외부 서비스&lt;/li&gt;
&lt;li&gt;외부 서비스에 연결해 LLM의 응답을 LLM이 이해할 수 있는 형식으로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 전송 계층&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSON-RPC 2.0 메세지 사용해 클라이언트 &amp;harr; 서버 통신&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[전송 방법]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;표준 입력/출력(stdio):&lt;/b&gt; 로컬 리소스에 적합하며 빠른 동기식 메시지 전송을 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버 전송 이벤트(SSE):&lt;/b&gt; 원격 리소스에 선호되며 효율적인 실시간 데이터 스트리밍을 지원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  MCP 작동 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&amp;ldquo;DB에서 최신 판매 보고서 찾아서 관리자한테 이메일로 보내줘&amp;rdquo;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;1) 요청 분석&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 자신이 할 수 있는 것 없는 것을 분리할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;할 수 있는 것: 판매 자료 찾기, 이메일 내용 작성&lt;/li&gt;
&lt;li&gt;할 수 없는 것: DB 검색, 이메일 보내기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신이 할 수 없는 것은 MCP 클라이언트를 사용해 사용 가능한 도구를 검색하고, MCP 서버에 등록된 관련 도구 2개(&lt;code&gt;database_query&lt;/code&gt; 도구와 &lt;code&gt;email_sender&lt;/code&gt; 도구)를 찾습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;2) 도구 호출&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 도구 호출을 위해 요청을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보고서 이름을 지정하여 &lt;code&gt;database_query&lt;/code&gt; 도구를 호출합니다. 그러면 MCP 클라이언트가 이 요청을 적절한 MCP 서버로 보냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;3) 외부 작업 및 데이터 반환&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP 서버는 요청을 수신하고 이를 회사의 데이터베이스에 대한 보안 SQL 쿼리로 변환하여 판매 보고서를 검색합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 다음 이 데이터를 포맷하여 LLM에 다시 보냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;4) 두 번째 작업 및 응답 생성&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보고서 데이터를 확보한 LLM은 &lt;code&gt;email_sender&lt;/code&gt; 도구를 호출하여 관리자의 이메일 주소와 보고서 콘텐츠를 제공합니다. 이메일이 전송된 후 MCP 서버는 작업이 완료되었음을 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;5) 최종 확인&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 최종 응답을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;'최신 판매 보고서를 찾아서 관리자에게 이메일로 보냈습니다.'&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  너무 모호합니다. MCP는 어떻게 생겼슴니까!!!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리에게 익숙한 HTTP는 이렇게 생긴 문자 기반 규격입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /users HTTP/1.1
Host: example.com
Authorization: Bearer ...
Content-Type: application/json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요청 라인 &amp;rarr; 헤더 &amp;rarr; 바디&lt;/b&gt; 이런 구조가 &lt;i&gt;표준&lt;/i&gt;인 것처럼, MCP는 아래와 같은 구조를 가집니당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ MCP 요청/응답 형태&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP는 &lt;b&gt;JSON-RPC 2.0 기반&lt;/b&gt;입니다. &lt;i&gt;(= 모든 요청이 JSON 객체 형태입니다.)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[MCP 요청 예시]&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;id&quot;: &quot;23&quot;,
  &quot;method&quot;: &quot;tools.call&quot;,
  &quot;params&quot;: {
    &quot;tool&quot;: &quot;query_database&quot;,
    &quot;arguments&quot;: {
      &quot;query&quot;: &quot;SELECT * FROM sales WHERE date = '2025-01-01';&quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[MCP 응답 예시]&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;id&quot;: &quot;23&quot;,
  &quot;result&quot;: {
    &quot;content&quot;: [
      { &quot;id&quot;: 1, &quot;amount&quot;: 10000, &quot;region&quot;: &quot;서울&quot; },
      { &quot;id&quot;: 2, &quot;amount&quot;: 15000, &quot;region&quot;: &quot;부산&quot; }
    ],
    &quot;is_final&quot;: true}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 메세지의 타입이 5가지로 표준화되어 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ MCP 메세지 타입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) message&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 받거나 보내는 일반 텍스트 메시지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;message&quot;,
  &quot;role&quot;: &quot;user&quot;,
  &quot;content&quot;: &quot;서울 날씨 알려줘&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) tool.call&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 외부 도구 호출을 요청하는 메시지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;tool.call&quot;,
  &quot;tool&quot;: &quot;search_weather&quot;,
  &quot;arguments&quot;: { &quot;city&quot;: &quot;Seoul&quot; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) tool.result&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구가 수행된 후 MCP 서버가 보내는 결과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;tool.result&quot;,
  &quot;tool&quot;: &quot;search_weather&quot;,
  &quot;content&quot;: { &quot;temp&quot;: 21, &quot;status&quot;: &quot;Cloudy&quot; },
  &quot;is_final&quot;: true}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) resource.*&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일/DB/문서 등 &amp;ldquo;리소스&amp;rdquo; 읽고 쓰는 메시지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;resource.read&quot;,
  &quot;uri&quot;: &quot;file:///workspace/readme.md&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5) stream.chunk&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트리밍 중간 결과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;stream.chunk&quot;,
  &quot;content&quot;: &quot;데이터 분석 중... 42% 완료&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서는 모두 소개하지 못하지만, Anthropic은 MCP 스펙을 다음처럼 구성해 공개했으니 한번 찾아가서 살펴보셔도 좋을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://modelcontextprotocol.io/docs/getting-started/intro&quot;&gt;https://modelcontextprotocol.io/docs/getting-started/intro&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Message Types&lt;/li&gt;
&lt;li&gt;RPC Methods&lt;/li&gt;
&lt;li&gt;Sessions&lt;/li&gt;
&lt;li&gt;Resource API&lt;/li&gt;
&lt;li&gt;Tool Call Workflow&lt;/li&gt;
&lt;li&gt;Event 구조&lt;/li&gt;
&lt;li&gt;Transport 규약&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  HTTP 그냥 쓰면 안됨? 왜 굳이 MCP를 만든걸까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 AI 모델 역시 http 통신을 통해 외부 시스템과 통신할 수 있었습니다만,&lt;br /&gt;여러 가지 한계가 있기에 새로운 프로토콜을 제안하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;1) HTTP는 Stateless함&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP는 본질적으로 stateless입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 떄문에 모델이 이전 요청의 컨텍스트를 기억할 수 없어 매 요청마다 전체 프롬프트를 다시 보내야 하는 문제가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 무상태성은 서버와 클라이언트 통신에서는 서버를 자유롭게 확장할 수 있게 해 확장성에는 유리하지만, AI 모델과의 통신에서는 단점이 됩니다. (비용 증가 + 지연 증가 발생)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;2) 도구 호출 형식이 서비스마다 모두 달랐음 (표준 부재)&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI, Anthropic, Google, AWS 모두 function calling 규격이 서로 달랐습니다.&lt;br /&gt;각 벤더마다 다른 형태의 JSON을 사용했고, 외부 서비스를 연결할 때마다 커스텀 어댑터를 새로 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;3) 양방향 스트리밍&amp;middot;이벤트 기반 구조가 부족함&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 HTTP는 단방향 구조이기 때문에 비효율성을 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP는 요청-응답만 지원하기 때문에 모델이 도구 실행 결과를 기다리는 동안 멈추게 되고, 서버가 모델에게 변화를 알릴 방법이 없었습니다. 또 스트리밍은 있어도 이벤트 구조가 없기 때문에 AI 모델이 외부 데이터를 스트리밍으로 받아가며 추론하는 데에는 적합하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;4) 반복적인 요청 오버헤드&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP는 요청마다 &lt;b&gt;모든 메타데이터(헤더, 인증정보, 옵션)&lt;/b&gt; 를 다시 전송합니다. LLM은 많은 기능을 호출할 때 매우 짧은 주기의 요청을 수십~수백 번 반복하는데, 이런 구조에서는 불필요한 네트워크 패킷이 누적될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이 오버헤드 자체가 치명적인 문제는 아니지만, &lt;b&gt;AI 시스템 특성상 더 가볍고 연속적인 통신 구조가 필요하다는 한계를 보여줍니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;5) HTTP는 양방향/실시간 통신에 적합하지 않음!&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP는 본질적으로 &lt;b&gt;Request &amp;rarr; Response&lt;/b&gt; 구조로 설계된 단방향 프로토콜입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니깐! 클라이언트 &amp;rarr; 서버 방향만 주도권이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;= HTTP에서는 통신을 시작할 수 있는 주체가 오직 클라이언트뿐입니다.&lt;br /&gt;= 서버는 스스로 말 걸기를 할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 에이전트가 도구 실행 후 결과를 조금씩 알려야 할 때에 적합하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 서버에서 실시간 업데이트가 있을 때 모델에게 알릴 방법이 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일이 수정되거나 DB 값이 바뀌었을 때에 서버는 모델에게 현재 변화가 생겼다고 전달해야 하는지, HTTP에서는 이런 과정이 불가합니다. &lt;i&gt;(정확히는 자연스럽지 않습니다. SSE나 WebSocket 기반 이벤트, 폴링 등을 사용해야 하고, 규격이 제각각인데다가 표준 메세지 형태가 없습니다)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 구조적 한계 때문에, AI 모델이 &lt;b&gt;실시간&amp;middot;상태 유지형 통신&lt;/b&gt;을 수행하기에는 HTTP만으로는 부족합니다. 그 대안으로 등장한 것이 바로 &lt;b&gt;MCP&lt;/b&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;  MCP의 효능&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;할루시네이션 감소
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MCP를 사용하면 LLM이 실시간 정보, 확실한 정보를 가지고 응답을 생성할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AI 유용성/자동화 향상
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;많은 작업들을 자동화할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AI를 외부 서비스와 간편하게 연결할 수 있음&lt;/li&gt;
&lt;li&gt;작업의 추적성 증가 (traceability)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io/docs/getting-started/intro&quot;&gt;https://modelcontextprotocol.io/docs/getting-started/intro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/modelcontextprotocol/modelcontextprotocol&quot;&gt;https://github.com/modelcontextprotocol/modelcontextprotocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/discover/what-is-model-context-protocol?hl=ko&quot;&gt;https://cloud.google.com/discover/what-is-model-context-protocol?hl=ko&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bcho.tistory.com/1470&quot;&gt;https://bcho.tistory.com/1470&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI/LLM</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/464</guid>
      <comments>https://engineerinsight.tistory.com/464#entry464comment</comments>
      <pubDate>Fri, 21 Nov 2025 15:30:21 +0900</pubDate>
    </item>
    <item>
      <title>[아키텍처/캐시] CDN 완전 정복: 개념&amp;middot;헤더&amp;middot;보안, CloudFront vs Cloudflare 비교까지</title>
      <link>https://engineerinsight.tistory.com/463</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  CDN(Content Delivery Network)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 개념&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/59bX4/dJMcacBv2mU/OD6ONgsmem80L6rgaoOyBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/59bX4/dJMcacBv2mU/OD6ONgsmem80L6rgaoOyBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/59bX4/dJMcacBv2mU/OD6ONgsmem80L6rgaoOyBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F59bX4%2FdJMcacBv2mU%2FOD6ONgsmem80L6rgaoOyBk%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;727&quot; height=&quot;409&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;정적 콘텐츠를 전송하는 데 쓰이는 지리적으로 분산된 서버의 네트워크&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 평소 쓰는 정적 파일(이미지, JS, CSS, 비디오 등)을 캐시해, &lt;b&gt;더 가까운 곳에서 보내주기 위해 존재하는 인프라&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;del&gt;(최근에는 동적 컨텐츠를 캐싱하는 데도 CDN을 사용하지만, 이 경우는 다음 포스팅에서 소개하겠습니다)&lt;/del&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적인 파일을 미국 서버에서 가져오면 200ms 이상 걸리지만, 서울에 있는 POP(Point of Presence)에서 가져오면 10~20ms 정도로 끝나기 때문에 속도가 확 달라지는 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Origin Server: 원본 컨텐츠를 저장하는 중앙 서버&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Edge Server: 전세계 곳곳에 위치한 분산 서버로, 오리진 서버의 컨텐츠를 캐싱함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 서버: 오리진 서버에 요청을 보내면 DNS 서버가 가장 가까운 엣지 서버로 라우팅함&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1LYAE/dJMcaiVYRyg/EekcFMgABistJ1Pzrdp1C0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1LYAE/dJMcaiVYRyg/EekcFMgABistJ1Pzrdp1C0/img.png&quot; data-alt=&quot;이미지 출처: https://docs.tosspayments.com/resources/glossary/cdn&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1LYAE/dJMcaiVYRyg/EekcFMgABistJ1Pzrdp1C0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1LYAE%2FdJMcaiVYRyg%2FEekcFMgABistJ1Pzrdp1C0%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;732&quot; height=&quot;333&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 출처: https://docs.tosspayments.com/resources/glossary/cdn&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ CDN 서비스&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AWS CloudFront&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Google Cloud CDN&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Azure CDN (Azure Front Door 포함)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Oracle Cloud CDN&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  CDN의 특징&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 지리적으로 가까우닛깐 빠르다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDN을 쓰면 서울에서 한 요청을 미국 서버에서 가져오는 것이 아닌, 그보다 가까운 서울/도쿄의 CDN으로부터 가져오게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;사용자 &amp;rarr; 가까운 서울/도쿄 POP &amp;rarr; 응답&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;***POP(Point of Presence): CDN이 전 세계에 설치한 지역 캐시 서버 거점&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 이미 캐싱해둔 파일을 바로 준다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 누가 읽어온 파일은 POP에 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 사용자들은 origin 서버를 거치지 않고 바로 POP에서 응답받습니다. = 빠르다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 압축, 최적화, HTTP/2/3 등을 적용해 빠르다&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Brotli 압축
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CDN은 파일을 사용자에게 보내기 전에 자동으로 압축합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;i&gt;더 작은 파일 = 더 빠른 속도&lt;/i&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HTTP/2 멀티플렉싱
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비교적 최신 프토콜을 활용해 브라우저가 더 많은 파일을 더 빠르게 받아올 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;keep-alive 재활용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저와 서버가 한번 연결한 TCP 연결을 계속 유지해서 여러 요청에 재사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이미지 포맷 자동 변환(WebP 등)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CDN이 파일 자체를 변환해서 더 빠르게 만드는 기능&lt;/li&gt;
&lt;li&gt;Cloudflare, Akamai, CloudFront 등은 이미지 포맷 자동 변환, 불필요한 메타데이터 제거, JS/CSS 자동 Minify(공백이나 주석을 제거함), 큰 이미지를 리사이징, HTML 최적화 등등을 제공합니당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 따로 설정하지 않아도 빠르게 동작합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  CDN 캐싱 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDN을 설정하더라도, 캐시 여부와 자세한 캐시 설정은 결국 서버가 보내는 헤더로 결정됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDN은 서버가 보낸 캐시 관련 헤더들을 보고 캐시를 할지, 언제까지 할지, 검증을 어떻게 할지 등등의 세세한 세팅을 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;결국 정책은 서버가 만들고, CDN은 서버가 정한대로 캐싱만 함!!!&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Cache-Control 헤더&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;파일&amp;middot;응답을 얼마나 어떻게 캐시할지를 결정하는 핵심 헤더&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;public:&lt;/b&gt; CDN&amp;middot;브라우저 모두 캐시 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;private:&lt;/b&gt; 브라우저만 캐시 가능 (CDN 캐시 금지, 개인화된 응답인 경우)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;no-store:&lt;/b&gt; 어떤 캐시에도 저장 금지 (민감 데이터)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;no-cache:&lt;/b&gt; 캐시는 해도 되지만 사용 전 반드시 서버 재검증 필요 (HTML에서 많이 사용)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CDN은 원본에 HEAD 요청을 보내 ETag, Last-Modified 헤더 등을 확인하고 같으면(=캐시 히트) POP 캐시를 재사용하고, 다른다면 다시 Origin에서 가져오게 됩니당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;max-age=초:&lt;/b&gt; 캐시 유효기간(초 단위)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정적 파일은 보통 1년(31536000초)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;s-maxage=초:&lt;/b&gt; CDN(공유 캐시)에만 적용되는 max-age
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저와 다르게 설정할 때 사용&lt;/li&gt;
&lt;li&gt;CDN/프록시만 이 값을 인식하고 브라우저는 이 값을 무시하고 max-age만 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;must-revalidate:&lt;/b&gt; 만료된 캐시는 반드시 재검증해야 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;immutable:&lt;/b&gt; 파일이 절대 바뀌지 않음(재검증도 안 함)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌드된 JS/CSS 파일에 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;stale-while-revalidate=초:&lt;/b&gt; 만료 캐시 사용 + 백그라운드 갱신&lt;/li&gt;
&lt;li&gt;&lt;b&gt;stale-if-error=초:&lt;/b&gt; 서버 에러 시 만료 캐시로 대체 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용 예시&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Cache-Control: public, max-age=31536000&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정적 파일(JS/CSS/이미지). 누구에게나 동일한 콘텐츠.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cache-Control: private, max-age=0&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자별로 다른 응답(마이페이지, 장바구니 등).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cache-Control: no-store&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;민감 데이터(로그인/결제/개인정보). 절대 저장 금지.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cache-Control: no-cache&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTML 같은 자주 변경되는 문서. 사용 전 항상 서버 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cache-Control: public, max-age=600&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;짧게 캐싱해도 되는 공용 API(인기글, 환율 등).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cache-Control: public, s-maxage=300, max-age=0&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CDN에는 캐싱하되 브라우저는 캐싱 금지할 때.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cache-Control: max-age=60, must-revalidate&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만료된 데이터 절대 쓰면 안 되는 경우.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cache-Control: public, max-age=31536000, immutable&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌드된 JS/CSS 처럼 절대 바뀌지 않는 정적 파일.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cache-Control: public, max-age=10, stale-while-revalidate=30&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;약간 오래된 데이터를 먼저 주고 백그라운드로 갱신하고 싶은 API.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cache-Control: public, max-age=10, stale-if-error=300&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 오류일 경우 캐시된 데이터라도 보여야 할 때.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ ETag 헤더&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;파일의 버전&amp;middot;식별자 역할을 하는 헤더&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ETag:&lt;/b&gt; 파일의 버전/식별자&lt;/li&gt;
&lt;li&gt;&lt;b&gt;If-None-Match:&lt;/b&gt; ETag가 같으면 304(변경 없음) 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용 예시&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버: &lt;code&gt;ETag: &quot;abc123&quot;&lt;/code&gt; / 클라이언트: &lt;code&gt;If-None-Match: &quot;abc123&quot;&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일이 &amp;ldquo;바뀌었는지&amp;rdquo; 확인할 때. 변경이 잦은 파일.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동작 방식&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 &lt;code&gt;ETag: &quot;abc123&quot;&lt;/code&gt; 로 응답&lt;/li&gt;
&lt;li&gt;브라우저가 다음 요청에 &lt;code&gt;If-None-Match: &quot;abc123&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;서버가 같으면 &lt;code&gt;304 Not Modified&lt;/code&gt; 응답 (다운로드 생략)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;Last-Modified 헤더&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;파일이 마지막으로 수정된 시간을 나타내는 헤더&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Last-Modified:&lt;/b&gt; 파일의 마지막 수정 시간&lt;/li&gt;
&lt;li&gt;&lt;b&gt;If-Modified-Since:&lt;/b&gt; 수정 여부 비교 후 304 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용 예시&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버: &lt;code&gt;Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;클라이언트: &lt;code&gt;If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;날짜 기반으로 변경 여부만 체크하면 되는 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동작 방식&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일이 안 바뀌면 304 응답&lt;/li&gt;
&lt;li&gt;바뀌면 새 파일 내려줌&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Expires 헤더 [Deprecated]&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Cache-Control 이전에 쓰던 구 방식으로 지정한 날짜나 시간까지 캐싱한다&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Expires:&lt;/b&gt; 캐시 만료일을 특정 날짜로 지정 (Cache-Control보다 우선순위 낮음)&lt;/li&gt;
&lt;li&gt;현대 CDN은 &lt;b&gt;Cache-Control이 우선&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Cache-Control이 없을 때만 사용됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Vary 헤더&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;캐시를 구분할 기준을 지정하는 헤더&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Accept-Encoding:&lt;/b&gt; 압축 방식별로 캐시 분리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Accept-Language:&lt;/b&gt; 언어별로 캐시 분리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;User-Agent:&lt;/b&gt; 기기별 캐시 분리(PC/모바일 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cookie:&lt;/b&gt; 요청마다 캐시가 달라져 캐싱 거의 불가(주의)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용 예시&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Vary: Accept-Encoding&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gzip/br 등 압축 포맷별로 캐시 구분&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Vary: Accept-Language&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;언어별로 다른 HTML을 제공할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Vary: User-Agent&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모바일과 데스크탑을 다르게 캐싱할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Vary: Cookie&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거의 하지 않음. 요청마다 캐시가 달라져 캐싱 무효화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Content-Encoding 헤더&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;서버/CDN이 응답을 어떤 방식으로 압축했는지 나타내는 헤더&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;gzip:&lt;/b&gt; 일반적 압축&lt;/li&gt;
&lt;li&gt;&lt;b&gt;br:&lt;/b&gt; Brotli 압축(가장 효율적)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;deflate:&lt;/b&gt; 오래된 압축 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  CloudFront vs Cloudflare&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ CloudFront = CDN&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AWS 생태계에 최적화된 CDN&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;CDN 역할이 중심인 일반적인 CDN이라 origin IP가 그대로 노출될 수 있음. &lt;i&gt;(단독 사용은 보안에 쪼매 취약)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구조&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;S3, ALB, EC2 등 Origin을 내가 완전히 소유할 수 있는 구조&lt;/li&gt;
&lt;li&gt;클라이언트가 직접 CloudFront URL에 접속&lt;/li&gt;
&lt;li&gt;DNS는 Route53과 자연스럽게 연동&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Cloudflare = CDN + 보안 + 프록시 + 네트워크&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;보안 + 프록시 + CDN + Edge 컴퓨팅이 결합된 서비스&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안 중심 설계&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;웹사이트 전체 앞단을 보호하는 WAF 프록시 역할(리버스 프록시)을 통해서 Origin IP를 숨길 수 있음&lt;/li&gt;
&lt;li&gt;WAF, Rate Limit, Bot Management, Zero Trust, Firewall 전부 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  CDN 쓸 때는 보안을 조심해야 한다!!!&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ CDN의 Origin IP를 감춰야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Origin IP를 감추는 것이 가장 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cloudflare 같은 서비스를 앞에 두는 목적은 공격을 대신 받아주는 것인데, 만약 누군가 Origin 서버의 IP를 알게 되면 Cloudflare를 우회해서 직접 서버에 공격을 보낼 수 있습니다. 그러면 다른 정보를 읽을 수도, CDN 서버에 DDoS 공격을 보내게 될 수도 있으므로 이렇게 되면 CDN을 쓰는 의미가 거의 사라지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Origin CDN의 방화벽에서는 Cloudflare의 IP Range만 허용하고 나머지는 전부 차단해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 개인화된 데이터는 캐시에 들어가면 안 됨&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 데이터들은 다른 사람과 공유하지 않습니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인한 사용자가 보는 HTML&lt;/li&gt;
&lt;li&gt;쿠키 기반 인증 페이지&lt;/li&gt;
&lt;li&gt;개인정보 포함된 GET 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 실수로 &lt;code&gt;Cache-Control: public&lt;/code&gt;을 달아버리면 다른 사용자에게 같은 HTML이 그대로 전달됩니다. &lt;i&gt;(실제로 이런 사고가 꽤 많습니다.)&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ HTTPS 반드시 유지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDN &amp;harr; Origin 간도 꼭 HTTPS로 해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Signed URL / Signed Cookie 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;URL/Coolie에 &amp;lsquo;유효기간 + 파일경로 + 비밀키로 만든 서명&amp;rsquo;을 붙여서 만료되면 쓸 수 없게 만드는 기술&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CloudFront: Signed URL / Signed Cookies&lt;/li&gt;
&lt;li&gt;Cloudflare: Tokenized URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 안 쓰면 이미지 링크가 퍼지는 순간 누구나 다운받을 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  CDN을 써도 왜 서버 부하가 줄어들지 않을까?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 캐시 MISS가 나면 결국 origin으로 간다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시에 없으면 무조건 원 서버(origin)로 갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일명이 자주 바뀐다든지, 매번 쿼리 파라미터가 달라진다든지(Hard Cache Busting) 하면 미스가 계속 납니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 정적 파일이 아닌 요청(API, HTML)은 캐시가 안 됨&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 API는 캐시되지 않습니다. 개인화된 HTML도 캐시가 어렵습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Cache-Control 설정이 잘못된 경우&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;Cache-Control: private&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 CDN이 절대 캐시할 수 없습니다. 많이 발생하는 실수입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 자주 변하는 파일을 캐싱에 태우면 효과가 없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 배포할 때마다 JS/CSS 파일명을 안 바꾸고 내용만 바꾸는 경우에는 CDN에서는 기존 파일이 변한 걸 몰라서 매번 origin으로 확인하러 감&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>아키텍처+MSA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/463</guid>
      <comments>https://engineerinsight.tistory.com/463#entry463comment</comments>
      <pubDate>Fri, 21 Nov 2025 14:00:53 +0900</pubDate>
    </item>
    <item>
      <title>[아키텍처/캐시] Redis 캐시 완전 정복: 개념&amp;middot;전략&amp;middot;TTL&amp;middot;일관성&amp;middot;모니터링 총정리</title>
      <link>https://engineerinsight.tistory.com/462</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  캐시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 캐시의 필요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 서비스를 운영할 때 가장 큰 문제는 &lt;b&gt;데이터베이스의 부하&lt;/b&gt;입니다. 사용자가 많아질수록 모든 요청이 DB로 향하게 되고, DB는 디스크 기반으로 동작하기 때문에 속도가 한계에 부딪힙니다. 특히 동일한 데이터를 여러 사용자가 반복해서 조회하는 경우, 매번 DB를 거치는 것은 불필요한 낭비입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 캐시(Cache)는 자주 조회되는 데이터를 메모리(RAM)에 저장해, 다음 요청 때는 DB가 아닌 캐시에서 바로 반환하도록 하는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 DB 부하를 크게 줄이고, 응답 속도를 몇십 배까지 향상시킬 수 있습니다. 웹 서비스에서는 상품 목록, 배너, 사용자 정보처럼 변경이 적고 조회가 잦은 데이터들이 대표적인 캐시 대상입니다. 이런 데이터들은 DB보다는 캐시에서 처리하는 것이 훨씬 효율적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 &lt;b&gt;&lt;code&gt;메모리 기반의 저장소&lt;/code&gt;&lt;/b&gt;로, &lt;b&gt;DB보다 빠르고 애플리케이션 메모리보다는 느린 중간 계층&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[애플리케이션 메모리]  &amp;rarr;  [Redis 캐시]  &amp;rarr;  [데이터베이스]
   (가장 빠름)            (빠름)           (느림)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 내부 메모리는 빠르지만 서버의 전원이 내려가거나 서버가 종료되면 사라지게 되고(휘발성!!!), 서버가 늘어나면 서버끼리의 데이터 공유가 어렵습니다. DB는 데이터를 안전하게 저장하지만 속도가 느리고 부하에 취약합니다. Redis는 이 둘의 장점을 절충한 형태로, &lt;b&gt;빠른 조회 속도와 일정 수준의 데이터 공유, 확장성&lt;/b&gt;을 모두 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  캐시 전략&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Cache Aside (Lazy Loading)&lt;/h3&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;1478&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAPI6T/dJMcafrsLwe/YKS8rL8uvK0Cq12kMQBiD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAPI6T/dJMcafrsLwe/YKS8rL8uvK0Cq12kMQBiD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAPI6T/dJMcafrsLwe/YKS8rL8uvK0Cq12kMQBiD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAPI6T%2FdJMcafrsLwe%2FYKS8rL8uvK0Cq12kMQBiD0%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;527&quot; height=&quot;291&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache Aside는 &lt;b&gt;가장 일반적인 캐시 전략&lt;/b&gt;으로, 데이터를 필요할 때 캐시에 채워 넣는 방식입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;요청이 들어오면 먼저 캐시에서 데이터를 찾습니다.&lt;/b&gt; &amp;rarr; 캐시에 있으면 그대로 반환합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐시에 없다면(Cache Miss)&lt;/b&gt; DB에서 데이터를 가져옵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가져온 데이터를 캐시에 저장한 뒤, 사용자에게 응답을 보냅니다.&lt;/b&gt; &lt;i&gt;(후속 요청부터는 캐시 히트)&lt;/i&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 동작하기 때문에 캐시에 자주 쓰는 데이터만 남고, 잘 쓰이지 않는 데이터는 자연스럽게 제거됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 캐시가 비어 있을 때(서비스 재시작 직후 등)는 DB를 계속 조회해야 해서 첫 요청이 느릴 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Write Through&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;239&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcJNXD/dJMcaajpNat/mIXfJOF2MYt4jKBVgWqEMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcJNXD/dJMcaajpNat/mIXfJOF2MYt4jKBVgWqEMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcJNXD/dJMcaajpNat/mIXfJOF2MYt4jKBVgWqEMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcJNXD%2FdJMcaajpNat%2FmIXfJOF2MYt4jKBVgWqEMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;674&quot; height=&quot;211&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;239&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Write Through는 데이터를 &lt;b&gt;DB와 캐시에 동시에 저장하는 방식&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 과정에서 두 저장소가 항상 같은 값을 유지하므로, 언제 캐시를 읽어도 최신 데이터가 보장됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;클라이언트가 데이터를 수정하거나 추가합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;애플리케이션이 캐시와 DB에 동시에 쓰기를 수행합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이후 읽기 요청은 캐시에서 바로 처리합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 최신 데이터가 캐시에 있으므로 읽기 속도가 빠르고, DB와의 불일치가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 매번 두 곳에 쓰기 때문에 속도가 다소 느려질 수 있고, DB가 장애일 경우 쓰기 전체가 실패할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 주문 내역이나 결제 상태처럼 정확도가 중요한 서비스에서 사용됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Write Back (Write Behind)&lt;/h3&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;1494&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMtoCK/dJMcaap9MGI/xKfUNbjYBvfHzZLb4kJBL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMtoCK/dJMcaap9MGI/xKfUNbjYBvfHzZLb4kJBL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMtoCK/dJMcaap9MGI/xKfUNbjYBvfHzZLb4kJBL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMtoCK%2FdJMcaap9MGI%2FxKfUNbjYBvfHzZLb4kJBL1%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;741&quot; height=&quot;262&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Write Back은 &lt;b&gt;쓰기 요청을 먼저 캐시에만 저장하고&lt;/b&gt;, 나중에 일정 시간마다 DB로 한꺼번에 저장하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 성능이 매우 빠르고, 순간적인 트래픽 폭주를 막을 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;클라이언트가 데이터를 저장하거나 변경합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;애플리케이션은 Redis 캐시에만 데이터를 기록하고, 사용자에게 바로 응답합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redis가 일정 주기마다(혹은 버퍼가 찼을 때) DB로 데이터를 일괄 전송합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 빠르지만, 캐시에 장애가 생기면 DB에 반영되지 않은 데이터가 사라질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 완전한 안정성보다 속도를 중시하는 로그, 통계, 세션 정보 등에 자주 사용됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Read Through&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmlr5N/dJMcabiiD8Z/yoG7hUckwXcbVEZpwmnV31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmlr5N/dJMcabiiD8Z/yoG7hUckwXcbVEZpwmnV31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmlr5N/dJMcabiiD8Z/yoG7hUckwXcbVEZpwmnV31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcmlr5N%2FdJMcabiiD8Z%2FyoG7hUckwXcbVEZpwmnV31%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;673&quot; height=&quot;234&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Read Through는 &lt;b&gt;캐시가 DB를 스스로 조회해서 갱신하는 방식&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache Aside와 비슷하지만, 차이점은 애플리케이션이 DB를 직접 접근하지 않는다는 점입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;요청이 들어오면 캐시를 먼저 확인합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐시에 없을 경우, 캐시 자체가 DB에서 데이터를 읽어옵니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐시가 그 값을 저장하고 사용자에게 반환합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 계층이 DB 조회 로직까지 담당하기 때문에, 애플리케이션은 DB 접근 코드를 신경 쓸 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 &lt;code&gt;@Cacheable&lt;/code&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;  TTL / Eviction 정책&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 메모리에 데이터를 저장하기 때문에, &lt;b&gt;언제 데이터를 지우고, 어떤 데이터를 남길지&lt;/b&gt;가 굉장히 중요합니다. 이 두 가지를 결정하는 핵심 요소가 바로 &lt;b&gt;TTL(Time To Live)&lt;/b&gt; 과 &lt;b&gt;Eviction 정책&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ TTL&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL은 각 데이터(Key)에 설정된 유효 기간으로, 데이터가 언제까지 살아있을지를 정하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 상품 목록을 5분마다 갱신해야 한다면 TTL을 300초로 설정하고, 5분이 지나면 Redis가 해당 데이터를 자동으로 삭제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL은 캐시의 신선도(freshness)를 보장해줍니다. 한 번 저장된 데이터가 너무 오래 남아 있으면, 이미 DB 값은 바뀌었는데 캐시는 예전 데이터를 반환하는 문제가 생기는데, 이걸 방지하기 위해 TTL을 설정해 일정 주기마다 데이터를 갱신하게 만드는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 TTL이 너무 짧게 설정되면 매번 캐시가 지워지고, 다시 DB에서 데이터를 가져와야 하므로 캐시 미스(Cache Miss)가 늘어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, TTL은 너무 짧으면 DB 부하가 증가하고, 너무 길면 데이터가 낡기 때문에 서비스 특성에 맞게 조정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Eviction&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eviction은 &lt;b&gt;Redis의 메모리가 꽉 찼을 때, 어떤 데이터를 지울지 결정하는 방식&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 기본적으로 메모리에 모든 데이터를 저장하므로, 메모리 용량을 넘기면 오래된 데이터부터 삭제하는데, 이걸 자동으로 관리해주는 것이 바로 &lt;b&gt;Eviction Policy&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 여러 가지 Eviction 방식을 지원합니다.&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;noeviction&lt;/b&gt; : 메모리가 가득 차면 더 이상 데이터를 추가하지 않고 오류를 반환합니다. &lt;i&gt;(Redis를 캐시가 아닌 데이터 저장소로 쓸 때 설정)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;allkeys-lru&lt;/b&gt; : 모든 키 중 &lt;b&gt;가장 오랫동안 사용되지 않은(Key access가 가장 오래된)&lt;/b&gt; 데이터를 삭제합니다. 가장 일반적으로 많이 쓰이는 설정입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;volatile-lru&lt;/b&gt; : TTL이 설정된 키들 중에서만 LRU 정책으로 삭제합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;allkeys-lfu&lt;/b&gt; : &lt;b&gt;가장 적게 사용된(Key access 빈도가 낮은)&lt;/b&gt; 데이터를 삭제합니다. 최근보다는 &amp;lsquo;얼마나 자주 사용되었는가&amp;rsquo;를 기준으로 판단합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;random&lt;/b&gt; : 임의의 키를 무작위로 삭제합니다. &lt;i&gt;(테스트용이나 단순 환경에서만 사용)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;LRU(Least Recently Used)&lt;/b&gt; 는 &amp;ldquo;최근에 사용되지 않은 데이터&amp;rdquo;를 지우는 방식이고, &lt;b&gt;LFU(Least Frequently Used)&lt;/b&gt; 는 &amp;ldquo;사용 빈도가 낮은 데이터&amp;rdquo;를 지우는 방식입니다. LRU는 단기적으로 자주 쓰는 데이터가 유리한 구조(뉴스 메인 페이지 캐시)에 적합하고, LFU는 일정 기간 동안 꾸준히 요청되는 데이터(인기 상품 목록)에 더 효율적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Cache Hit / Cache Miss&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 캐시가 얼마나 잘 작동하고 있는지를 판단할 때 가장 중요한 지표가 바로 &lt;b&gt;캐시 적중률(Cache Hit Ratio)&lt;/b&gt; 입니다. 이 수치는 전체 요청 중에서 얼마나 많은 요청이 캐시에서 바로 처리되었는지를 의미합니다. 적중률이 높을수록 DB 접근 횟수가 줄고, 응답 속도도 크게 개선됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 &lt;b&gt;Cache Miss&lt;/b&gt;는 캐시에 데이터가 없어 DB를 직접 조회해야 하는 상황입니다. 이 경우 네트워크 지연과 디스크 I/O가 추가로 발생하기 때문에 전체 응답 시간이 길어지고, 트래픽이 많을 때는 DB 부하가 급격히 증가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;캐시 미스가 자주 발생하는 이유는 다양합니다.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;TTL이 너무 짧게 설정된 경우&lt;br /&gt;&lt;/b&gt;데이터가 너무 자주 만료되면, 캐시가 데이터를 저장해도 금방 지워져서 매번 DB를 조회하게 됩니다.&lt;br /&gt;&lt;i&gt;(이럴거면 캐시 왜 만듦?ㅋㅋ)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;키의 다양성이 지나치게 높은 경우&lt;br /&gt;&lt;/b&gt;사용자 입력 검색어처럼 매번 다른 키로 캐시를 저장하면 재사용률이 낮아지고, Hit율은 떨어집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cold Start(콜드 스타트)&lt;br /&gt;&lt;/b&gt;서버가 재시작되면 캐시 메모리는 비워지기 때문에 초기 몇 분 동안은 캐시가 쌓이지 않아 DB 요청이 몰립니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Eviction(데이터 자동 삭제)&lt;/b&gt; 이 자주 일어나는 환경&lt;br /&gt;Redis 메모리가 부족하면 오래된 데이터를 제거하기 때문에 캐시에 있어야 할 데이터가 이미 삭제된 상태일 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;키 네이밍 불일치&lt;/b&gt;&lt;br /&gt;&lt;code&gt;user:1&lt;/code&gt;과 &lt;code&gt;User:1&lt;/code&gt;, &lt;code&gt;user_01&lt;/code&gt;처럼 동일한 데이터를 서로 다른 키로 저장하면 캐시가 여러 개로 분산되어 Hit율이 낮아집니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache Miss가 왜 발생하는지를 주기적으로 점검하고, TTL, 키 설계, 메모리 정책을 함께 조정하는 것이 Redis 캐시 효율을 극대화하는 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;캐시를 언제 도입하면 좋을까요?&lt;/i&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;캐시는 모든 서비스에 무조건 필요한 기술이 아닙니다. 득일지 실일지를 잘 생각한 후 도입해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 캐시가 필요한 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;&amp;ldquo;자주 조회되지만 자주 바뀌지 않는 데이터&amp;rdquo;&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;읽기(Read) 요청이 매우 많은 서비스&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: 상품 목록, 게시글 조회, 랭킹 페이지 등&lt;/li&gt;
&lt;li&gt;동일한 데이터를 여러 사용자가 반복적으로 요청한다면, 캐시가 없을 때 DB 부하가 급격히 늘어납니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터가 자주 바뀌지 않는 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: 카테고리 목록, 설정 정보, 배너 이미지 등&lt;/li&gt;
&lt;li&gt;변경 빈도가 낮은 데이터는 한 번만 DB에서 가져와 캐시에 보관하는 것이 훨씬 효율적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외부 API나 복잡한 연산 결과를 재사용할 때&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: 외부 환율 API, 추천 알고리즘 결과 등&lt;/li&gt;
&lt;li&gt;매번 계산하거나 외부 호출을 하는 대신, 결과를 캐싱해두면 시스템 부하를 크게 줄일 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 캐시가 문제를 일으킬 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시는 잘못 쓰면 오히려 시스템을 불안정하게 만들 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 일관성이 중요한 시스템&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: 금융 거래, 결제, 주문 상태&lt;/li&gt;
&lt;li&gt;캐시가 DB보다 늦게 갱신되면 &amp;ldquo;입금했는데 잔액이 그대로&amp;rdquo; 같은 오류가 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 변경이 너무 자주 일어나는 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: 실시간 센서 데이터, 채팅 메시지 등&lt;/li&gt;
&lt;li&gt;캐시를 갱신하는 비용이 DB 조회보다 커져 오히려 손해입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TTL 설정이 부적절할 때&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;너무 짧으면 캐시가 자주 비워지고, 너무 길면 낡은 데이터가 남아 있습니다.&lt;/li&gt;
&lt;li&gt;이로 인해 캐시 Hit율이 떨어지고, DB와의 불일치가 심해집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐시 서버 장애 시 의존성 과다&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시 서버가 죽으면 모든 요청이 DB로 몰리며 폭주가 일어날 수 있습니다.&lt;/li&gt;
&lt;li&gt;따라서 캐시 장애 시에도 서비스가 최소한으로 버티도록 Fallback 전략을 설계해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Redis는 왜 빠른가?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 싱글 스레드: 락 충돌과 스레드 전환 비용이 없음&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 싱글 스레드 기반으로 동작합니다. 하나의 스레드만 사용하므로 겉보기에는 멀티스레드보다 느릴 것처럼 보이지만, 실제로는 대부분의 경우 오히려 훨씬 빠르게 처리됩니다. 관련해서는 &lt;a href=&quot;https://engineerinsight.tistory.com/235&quot;&gt;이 포스팅&lt;/a&gt;을 보면 자세한 내용을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djpvOT/dJMcag4YLyD/QMUDXKKK8XCWK2jFby6YFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djpvOT/dJMcag4YLyD/QMUDXKKK8XCWK2jFby6YFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djpvOT/dJMcag4YLyD/QMUDXKKK8XCWK2jFby6YFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjpvOT%2FdJMcag4YLyD%2FQMUDXKKK8XCWK2jFby6YFK%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;702&quot; height=&quot;430&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;770&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글 스레드 구조는 락(lock) 충돌이나 컨텍스트 스위칭이 발생하지 않는다는 장점이 있습니다.&lt;br /&gt;멀티스레드 환경에서는 여러 스레드가 같은 데이터를 동시에 접근하며 락을 걸어야 하지만, Redis는 한 번에 하나의 명령만 순서대로 처리하기 때문에 이런 병목이 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;메모리 기반 연산&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis가 모든 연산을 메모리(RAM)에서 수행하기 때문입니다. 디스크 접근이 없고, 명령 실행 속도가 거의 &lt;code&gt;O(1)&lt;/code&gt;에 가까워서 단일 스레드로도 초당 수만~수십만 건의 요청을 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;이벤트 루프 기반의 비동기 처리(epoll)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 Redis는 epoll 기반의 이벤트 루프(Event Loop) 를 사용합니다.&lt;br /&gt;요청이 들어올 때마다 새로운 스레드를 생성하지 않고, 하나의 스레드가 수천 개의 클라이언트 요청을 비동기적으로 처리할 수 있습니다. 즉, I/O가 많은 환경에서도 효율적으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;결과적으로 Redis는 구조가 단순하지만, 락이 없고, 컨텍스트 스위칭이 없고, I/O가 빠른 최적의 설계 덕분에&lt;br /&gt;멀티스레드보다 더 빠른 응답 속도와 안정성을 제공합니다.&lt;/i&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  캐시 일관성(Cache Consistency) 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 일관성 문제란, &lt;b&gt;DB와 캐시의 데이터가 일시적으로 불일치하는 현상&lt;/b&gt;을 말합니다. 즉, DB의 값은 변경되었지만 캐시에는 이전 값이 남아 있는 상황이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 주로 &lt;b&gt;Cache Aside 패턴&lt;/b&gt;을 사용할 때 발생합니다. 일반적으로 DB &amp;rarr; 캐시 순으로 데이터 흐름이 이루어지는데, DB를 업데이트한 직후 캐시를 지우기 전에 다른 요청이 들어오면 캐시가 다시 오래된 값을 DB에서 가져와 저장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 현상을 &lt;b&gt;Cache Stampede(캐시 스탬피드)&lt;/b&gt; 또는 &lt;b&gt;데이터 정합성 문제&lt;/b&gt;라고 부릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 발생 원인&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;DB 업데이트와 캐시 삭제 사이의 시간 차이&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션이 완료되기 전, 다른 요청이 캐시를 갱신해버리는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TTL이 만료되어 캐시 미스 발생&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시에 여러 요청이 들어오면 수십, 수백 개의 DB 쿼리가 한꺼번에 발생 &lt;i&gt;(Thundering Herd 현상)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비동기 처리나 큐 기반 캐시 갱신 지연&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시 반영 속도가 느릴 경우 최신 데이터 반영이 늦어짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;뮤텍스 키(Mutex Key) 사용&lt;br /&gt;&lt;/b&gt;캐시가 비어 있을 때(DB에서 데이터를 가져오는 중) 다른 요청이 동시에 DB에 접근하지 못하도록 Lock을 겁니다.&lt;br /&gt;첫 번째 요청만 DB에서 데이터를 가져오게 하고, 나머지는 대기하거나 캐시 데이터가 채워질 때까지 기다립니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Thundering Herd 방지 로직&lt;br /&gt;&lt;/b&gt;캐시 미스 시 모든 요청이 동시에 DB로 향하지 않도록 하나의 요청만 허용하고, 나머지는 짧은 지연(백오프) 후 재시도하도록 설계합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TTL 분산(Random TTL) 적용&lt;br /&gt;&lt;/b&gt;모든 캐시 키가 동시에 만료되지 않도록 TTL에 &amp;plusmn; 몇 초의 랜덤 값을 더해 캐시 만료 시점을 분산합니다.&lt;br /&gt;예를 들어 TTL을 300초로 설정했다면, &lt;code&gt;300 &amp;plusmn; 30초&lt;/code&gt; 형태로 설정해 트래픽 집중을 완화합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Redis 장애 대응 및 모니터링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 메모리 기반 시스템이기 때문에 &lt;b&gt;속도는 매우 빠르지만&lt;/b&gt;, 운영 환경에서는 메모리 초과나 서버 장애 복구 속도 같은 이슈가 빈번하게 발생할 수 있습니다. 이런 문제를 예방하기 위해서는 주요 지표를 꾸준히 모니터링하고, 각 상황에 맞는 대응 전략을 미리 설계해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 주요 모니터링 지표&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Hit Ratio (캐시 효율)&lt;/b&gt;&lt;br /&gt;캐시에서 바로 응답된 요청 비율입니다.&lt;br /&gt;값이 낮아지면 캐시가 자주 비워지거나 TTL이 너무 짧을 가능성이 있습니다.&lt;br /&gt;TTL을 늘리거나, 자주 사용하는 데이터를 사전 로딩(pre-warming)하는 방식으로 개선합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Memory Usage (메모리 사용량)&lt;/b&gt;&lt;br /&gt;Redis가 사용 중인 메모리 양입니다.&lt;br /&gt;사용률이 90% 이상으로 올라가면 Eviction(자동 삭제)이 자주 발생해 캐시 효율이 떨어집니다.&lt;br /&gt;불필요한 키 정리, TTL 설정 강화, 또는 메모리 증설이 필요합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Evicted Keys (자동 삭제된 키 수)&lt;/b&gt;&lt;br /&gt;Redis가 메모리 부족으로 삭제한 키의 개수를 의미합니다.&lt;br /&gt;값이 급격히 증가한다면 캐시 데이터가 강제로 밀려나고 있는 상태입니다.&lt;br /&gt;maxmemory-policy를 점검하고, LRU 대신 LFU 정책으로 전환하거나 TTL 정책을 재설계합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Connected Clients (동시 연결 수)&lt;/b&gt;&lt;br /&gt;현재 Redis에 접속 중인 클라이언트 수입니다.&lt;br /&gt;수치가 비정상적으로 많다면 커넥션 누수나 서비스 과부하 가능성이 높습니다.&lt;br /&gt;커넥션 풀 크기 조정, maxclients 제한 설정, 타임아웃(timeout) 값 점검으로 대응합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Replication Lag (복제 지연)&lt;/b&gt;&lt;br /&gt;Master와 Replica 간 데이터 동기화 지연 시간입니다.&lt;br /&gt;Replica가 뒤처지면 장애 시 데이터 유실 가능성이 커집니다.&lt;br /&gt;네트워크 대역폭 확인, 복제 버퍼(repl-backlog-size) 확장, Sentinel 알림 활성화로 예방합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 장애 유형별 해석과 대응 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 메모리 초과 (Out of Memory)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;현상&lt;/b&gt;: Redis가 &lt;code&gt;OOM command not allowed when used memory &amp;gt; 'maxmemory'&lt;/code&gt; 오류를 반환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원인&lt;/b&gt;: 캐시 데이터가 너무 많거나 TTL이 없어 만료되지 않음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대응 방법&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;maxmemory&lt;/code&gt; 설정값 상향&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxmemory-policy&lt;/code&gt; 확인 (&lt;code&gt;volatile-lru&lt;/code&gt;, &lt;code&gt;allkeys-lfu&lt;/code&gt; 등 적절히 조정)&lt;/li&gt;
&lt;li&gt;불필요한 키 삭제 및 TTL 설정 철저히 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2)&lt;/b&gt; &lt;b&gt;Eviction 폭주 (자동 삭제 급증)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;현상&lt;/b&gt;: 캐시 Hit율 급격히 하락, DB 부하 증가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원인&lt;/b&gt;: 메모리 부족, LRU 정책으로 인해 자주 쓰는 키도 삭제됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대응 방법&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TTL 조정 &amp;rarr; 너무 짧으면 캐시 자주 만료&lt;/li&gt;
&lt;li&gt;캐시 대상 최소화 &amp;rarr; 자주 조회되는 데이터만 캐싱&lt;/li&gt;
&lt;li&gt;필요 시 메모리 증설 및 LFU 정책 전환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) Replication Lag (복제 지연)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;현상&lt;/b&gt;: Master에서 Write는 성공했는데 Replica가 늦게 반영&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원인&lt;/b&gt;: 네트워크 대역폭 부족, Replica CPU 부하&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대응 방법&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replica 노드 수 조정 (읽기 분산)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;repl-backlog-size&lt;/code&gt; 확장&lt;/li&gt;
&lt;li&gt;네트워크 대역폭 및 스냅샷 주기 점검&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) Sentinel 장애 전환 (Failover)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;현상&lt;/b&gt;: Master 장애 시 자동으로 Replica가 Master로 승격되지 않음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원인&lt;/b&gt;: Sentinel 설정 오류, quorum 불일치&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대응 방법&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Sentinel 최소 3대 이상 구성 (과반수 투표 구조)&lt;/li&gt;
&lt;li&gt;각 노드 간 &lt;code&gt;sentinel monitor&lt;/code&gt;, &lt;code&gt;quorum&lt;/code&gt; 값 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;down-after-milliseconds&lt;/code&gt; 값 적정 조정 (너무 짧으면 오탐, 너무 길면 전환 지연)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  분산 환경에서 Redis 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 단일 인스턴스로는 한계가 있기 때문에, 대규모 트래픽 환경에서는 &lt;b&gt;복제와 클러스터링&lt;/b&gt;으로 확장성을 확보합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Master&amp;ndash;Replica 구조&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Master가 쓰기, Replica가 읽기를 담당&lt;/li&gt;
&lt;li&gt;Replica 장애 시 자동 승격 가능 (Sentinel 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redis Sentinel&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Master의 상태를 감시하고 장애 시 Replica를 자동 승격&lt;/li&gt;
&lt;li&gt;고가용성(HA) 구조의 핵심&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redis Cluster&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 여러 노드에 분산 저장 (샤딩)&lt;/li&gt;
&lt;li&gt;최대 16,384개의 슬롯을 기반으로 분할 처리&lt;/li&gt;
&lt;li&gt;노드 일부 장애 시에도 서비스 지속 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  휘발성 Redis 캐시를 데이터베이스로 사용하기?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Redis는 메모리 기반이기 때문에 서버가 꺼지면 데이터가 사라집니다. 하지만 캐시를 설정을 통해 영속형 데이터베이스로 활용할 수도 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ RDB (Redis Database Snapshot)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis의 메모리 상태를 &lt;b&gt;주기적으로 디스크에 &lt;code&gt;스냅샷&lt;/code&gt;으로 저장&lt;/b&gt;합니다. 예를 들어 &lt;code&gt;save 900 1&lt;/code&gt; 설정을 하면, 900초(15분) 동안 최소 1개의 변경이 있을 때 스냅샷을 생성합니다. 따라서 서버가 재시작되면 마지막 저장 시점의 데이터를 복원할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점: 백업 용도로 간단하고 빠름&lt;/li&gt;
&lt;li&gt;단점: 마지막 저장 시점 이후 변경된 데이터는 복구되지 않음 &lt;i&gt;(계속 스냅샷을 때려?)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ AOF (Append Only File)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis가 처리한 &lt;b&gt;모든 쓰기 명령을 순차적으로 파일에 기록&lt;/b&gt;합니다. 장애 발생 시, 파일을 읽어 모든 명령을 다시 실행함으로써 데이터 복구가 가능합니다. 또 &lt;code&gt;appendfsync always | everysec | no&lt;/code&gt; 설정으로 기록 주기를 조절할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점: 데이터 손실 가능성이 거의 없음&lt;/li&gt;
&lt;li&gt;단점: 디스크 I/O가 늘어나고 파일 크기가 커짐&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ RDB + AOF 함께 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지를 병행 사용한다면 훨씬 안정적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDB는 빠른 재시작용 스냅샷으로 활용하고, AOF는 세밀한 데이터 복구용 로그로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 7.0 이후에는 두 기능이 결합된 &lt;b&gt;Mixed Persistence&lt;/b&gt; 모드도 지원되어, 더 빠르고 안정적인 영속성 확보가 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>아키텍처+MSA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/462</guid>
      <comments>https://engineerinsight.tistory.com/462#entry462comment</comments>
      <pubDate>Thu, 20 Nov 2025 16:30:57 +0900</pubDate>
    </item>
    <item>
      <title>[아키텍처] 로드밸런싱 완전 정복: L4/L7 구조부터 SPOF 해결까지</title>
      <link>https://engineerinsight.tistory.com/461</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  로드 밸런싱&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 트래픽을 다루는 모든 서비스에서 로드 밸런서는 필수적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;여러 대의 서버로 들어오는 요청을 균등하게 분산시켜 전체 시스템의 안정성과 처리량을 높이는 기술&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KC8hd/dJMb99LyfwD/YZ4I9eIglTkd4mG5tSOKyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KC8hd/dJMb99LyfwD/YZ4I9eIglTkd4mG5tSOKyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KC8hd/dJMb99LyfwD/YZ4I9eIglTkd4mG5tSOKyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKC8hd%2FdJMb99LyfwD%2FYZ4I9eIglTkd4mG5tSOKyK%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;703&quot; height=&quot;268&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모의 트래픽을 다루기 위해서는 단순히 서버 여러 대를 늘리는 것만으로는 충분하지 않고, &lt;b&gt;부하를 지능적으로 분배하고 장애를 격리하는 구조&lt;/b&gt;가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 중앙에서 분산을 담당하는 장치나 소프트웨어가 바로 로드 밸런서(Load Balancer)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서는 크게 두 가지 역할을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 트래픽 분산&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 대의 서버 중 한 곳에 요청이 몰리지 않도록 &lt;b&gt;요청을 골고루 나누는 역할&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 서버 과부하를 막고, 수평 확장을 통해 전체 처리량을 늘릴 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 장애 감지 및 격리(Health Check)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 서버가 느려지거나 장애가 나면 그 서버로 &lt;b&gt;요청을 보내지 않고 자동으로 제외&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 서비스 전체가 다운되지 않도록 고가용성(HA)을 유지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 대규모 트래픽 환경에서는 &lt;b&gt;로드 밸런서가 전체 시스템의 관문(Gateway)&lt;/b&gt; 역할을 하기 때문에, 어떤 계층(L4/L7), 어떤 알고리즘, 어떤 헬스 체크 정책을 사용하느냐에 따라 시스템의 성능과 안정성이 크게 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순간적으로 트래픽이 폭증하는 상황에서는, 로드 밸런서가 요청을 적절히 분해하지 못하면 서버는 버티고 있어도 전체 서비스가 느려지거나 중단될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ L4 vs L7 Load Balancer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSI 7계층에서 4계층 전송계층 기반인지, 7계층 애플리케이션 계층 기반인지에 따라서 로드밸런서는 2가지로 나뉩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L4 로드 밸런서는 TCP/UDP 수준의 정보(&lt;code&gt;IP, Port&lt;/code&gt;)만 보고 트래픽을 분산합니다. 패킷 내부의 실제 내용을 읽지 않기 때문에 아주 빠르게 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, L7 로드 밸런서는 &lt;code&gt;HTTP 헤더&lt;/code&gt;, &lt;code&gt;URL&lt;/code&gt;, &lt;code&gt;쿠키&lt;/code&gt;, &lt;code&gt;Path&lt;/code&gt;까지 읽어 요청의 의미를 분석하고 클라이언트의 요청을 더더 세분화해서 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;L4 (Transport Layer)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라우팅 기준: IP, Port&lt;/li&gt;
&lt;li&gt;장점: 매우 빠름, 부하 적음&lt;/li&gt;
&lt;li&gt;단점: URL 기반 라우팅 등 고급 기능 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;L7 (Application Layer)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라우팅 기준: HTTP Header, Path, Cookie 등&lt;/li&gt;
&lt;li&gt;장점: 정교한 라우팅, A/B 테스트, 인증 기반 라우팅 가능&lt;/li&gt;
&lt;li&gt;단점: 상대적으로 비용과 처리 부하 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;/static&lt;/code&gt;처럼 정적 파일 요청을 CDN으로 넘기고, &lt;code&gt;/api/order&lt;/code&gt;는 주문 서비스로 분기하는 식의 라우팅은 &lt;b&gt;L7 LB에서만 가능합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 구조가 단순하고 속도가 우선이라면 L4, 정교한 서비스 분기나 인증 처리가 필요하다면 L7을 선택하는 것이 합리적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  로드 밸런싱 알고리즘&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서가 트래픽을 여러 개의 서버로 분산할 때의 알고리즘은 여러 가지가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Round Robin&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 서버 리스트 순서대로 순환 분배합니다. 그냥 순서대로 서버1, 서버2, 서버3, &amp;hellip; 서버1, 서버2,,,.. 이렇게 분배합니다. 별도의 연산은 필요하지 않으니 구현 간단, 성능 빠름이 장점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 서버별 부하 차이를 고려하지 못하기 때문에 모든 서버의 스펙이 동일하거나 비슷할 때 장점을 가지는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Weighted Round Robin&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가중 라운드 로빈은 위의 그냥 라운드로빈이 모든 서버에 가중치가 1로 분배했다면, 다른 가중치를 가지게 되는 경우 더 많은 요청을 받을 수 있도록 하는 방법입니다. 일부 서버의 스펙이 더 좋다면 더 많은 요청을 받도록 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Least Connection&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &amp;lsquo;연결 수&amp;rsquo;가 가장 적은 서버로 라우팅하는 방법으로, 긴 처리 시간을 가진 요청(I/O 작업)에 유리합니다. 하지만 LB가 모든 서버 상태를 추적해서 몇개의 연결 수가 현재 있는지 판단해야 하므로 오버헤드 증가할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ IP Hash&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 IP를 해싱해 특정 서버에 고정하는 방법으로, 세션 Sticky가 필요한 레거시 환경에서 사용합니다. 지역/ISP 편중 시 특정 서버로 쏠림이 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;어떤 경우에 어떤 방법을 쓰는게 좋을까요?&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 웹소켓처럼 연결 유지 시간이 긴 트래픽은 Round Robin보다 Least Connection이 훨씬 안정적입니다. 반대로 사용자 세션을 서버 메모리에 저장하는 레거시 시스템이라면 IP Hash 기반으로 세션 고정을 해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  헬스 체크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서는 서버의 상태를 실시간으로 감지하고, &lt;b&gt;문제가 생긴 서버를 자동으로 트래픽 대상에서 제외합니다.&lt;/b&gt; 이를 Health Check라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;L4 Health Check&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP Port가 열려 있는지만 확인&lt;/li&gt;
&lt;li&gt;빠르고 단순하지만, 애플리케이션이 죽었는데 포트만 열려 있는 경우를 걸러내지 못함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;L7 Health Check&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 요청을 서버로 전송해 상태코드(200), 응답 시간 체크&lt;/li&gt;
&lt;li&gt;/health, /actuator/health 등을 많이 사용&lt;/li&gt;
&lt;li&gt;DB, Redis 등 외부 의존성을 함께 검사하도록 구성 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 애플리케이션 스레드가 모두 막혀도 TCP 포트는 살아 있기 때문에 L4 헬스 체크는 정상이라 판단할 수 있습니다. 이런 문제는 반드시 L7 Health Check에서만 잡아낼 수 있습니다. 현대 시스템에서는 대부분 L7 헬스 체크가 기본입니다.&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;  로드밸런서가 SPOF가 된다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ SPOF(Single Point of Failure)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서 자체가 죽으면 서비스 전체가 중단됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하나가 죽었을 때 서비스 전체가 중단되는 것을 SPOF(Single Point of Failure)라고 하며, 실제 장애 복구 사례에서 가장 자주 언급되는 지점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법 1: LB 이중화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Active-Active 구조&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 개 이상의 로드 밸런서가 &lt;b&gt;동시에 트래픽을 처리&lt;/b&gt;하는 방식&lt;/li&gt;
&lt;li&gt;외부 클라이언트는 DNS 또는 상위 LB를 통해 두 LB 중 하나로 연결됨&lt;/li&gt;
&lt;li&gt;각각이 독립적으로 Health Check를 실행하며, 둘 중 하나가 장애 나면 &lt;b&gt;정상 LB만 계속 요청을 처리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Load Balancer 자체에도 부하가 있으므로, Active-Active는 &lt;b&gt;성능&amp;middot;확장성&amp;middot;가용성 모두 확보하는 구조&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS에서는 ALB/NLB 자체가 여러 AZ로 Active-Active 구성&lt;/li&gt;
&lt;li&gt;On-premise 환경에서는 HAProxy 2대 또는 Nginx 2대를 Active-Active로 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Active-Standby 구조&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Active LB가 모든 트래픽을 처리하고, Standby LB는 &lt;b&gt;대기 상태&lt;/b&gt;로 있으면서 Active LB를 감시&lt;/li&gt;
&lt;li&gt;Active가 장애 나면 Standby가 즉시 역할을 인계(failover)&lt;/li&gt;
&lt;li&gt;비용은 절감되지만, Standby는 평소 트래픽을 처리하지 않아 &lt;b&gt;리소스 낭비 요소&lt;/b&gt;가 있고 전환 시 아주 약간의 지연이 발생할 수 있음&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Keepalived + Nginx 조합으로 Active-Standby 구성&lt;/li&gt;
&lt;li&gt;VRRP 기반으로 Virtual IP를 Standby가 takeover&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법 2: VIP(가상 IP) Floating&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 대의 LB가 하나의 Virtual IP(가상 IP)를 공유하고, 현재 Active LB만 이 IP를 사용합니다. 클라이언트는 Virtual IP만 알고 있기 때문에, Active LB가 어떤 물리 서버인지 알 필요 없습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Active LB가 Virtual IP를 보유하고 있음&lt;/li&gt;
&lt;li&gt;Keepalived/VRRP 프로토콜이 Active LB의 상태를 지속적으로 감시&lt;/li&gt;
&lt;li&gt;Active LB가 장애 나면
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Standby LB가 즉시 Virtual IP를 인계(ARP 갱신)&lt;/li&gt;
&lt;li&gt;클라이언트는 같은 IP로 계속 접속 가능&lt;/li&gt;
&lt;li&gt;서비스는 거의 끊기지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LB 교체 전환(failover)이 매우 빠름&lt;/li&gt;
&lt;li&gt;서비스 접근 IP가 바뀌지 않아 운영이 단순해짐&lt;/li&gt;
&lt;li&gt;내부망 로드 밸런싱에서 특히 많이 쓰임(Nginx, HAProxy)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;VIP는 동일 네트워크 내에서만 부동(floating)이 가능 (Cross-region, Cross-AZ는 불가)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법 3: Multi-AZ 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개념&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라우드(AWS/GCP/Azure)에서는 LB 자체가 멀티 AZ로 구성될 수 있고, LB 뒤의 서버(ECS/EC2/EKS/POD)도 각각 여러 AZ에 분산됩니다.&lt;/li&gt;
&lt;li&gt;AZ 장애는 흔치 않지만&lt;i&gt;(AWS를 믿고 가자,,?)&lt;/i&gt;, 발생하면 &lt;b&gt;전체 서비스가 정지되는 치명적 장애&lt;/b&gt;로 이어질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동작 방식&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LB가 여러 AZ에 걸쳐 네트워크 인터페이스를 생성&lt;/li&gt;
&lt;li&gt;각 AZ에 배치된 서버들을 모든 AZ의 LB가 동시에 Health Check&lt;/li&gt;
&lt;li&gt;특정 AZ가 장애 나면
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;LB가 그 AZ의 모든 서버를 unhealthy로 판단&lt;/li&gt;
&lt;li&gt;트래픽은 자동으로 다른 AZ로 전환&lt;/li&gt;
&lt;li&gt;서비스 전체는 정상 유지&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버뿐 아니라 &lt;b&gt;로드 밸런서 자체의 가용성까지 확보&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;단일 데이터센터 수준 장애에도 생존 가능&lt;/li&gt;
&lt;li&gt;클라우드에서는 사실상 기본(Standard)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 비용 증가 (특히 cross-AZ data transfer)&lt;/li&gt;
&lt;li&gt;인프라 구성이 복잡해질 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>아키텍처+MSA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/461</guid>
      <comments>https://engineerinsight.tistory.com/461#entry461comment</comments>
      <pubDate>Thu, 20 Nov 2025 16:00:28 +0900</pubDate>
    </item>
    <item>
      <title>[아키텍처/Kafka] Kafka로 강한 결합 탈출하기: 회원가입 비동기 처리 미니 프로젝트</title>
      <link>https://engineerinsight.tistory.com/460</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  소스 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/gitchan-Study/kafka-playground&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/gitchan-Study/kafka-playground&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762329497961&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - gitchan-Study/kafka-playground&quot; data-og-description=&quot;Contribute to gitchan-Study/kafka-playground development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/gitchan-Study/kafka-playground&quot; data-og-url=&quot;https://github.com/gitchan-Study/kafka-playground&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/efGdBC/hyZNecvyIn/kjYpfGqh1ErFUEMWQMKeL0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cOw6BZ/hyZNeKmwOP/octAKmJYtnfzAPDYN0wSKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/gitchan-Study/kafka-playground&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/gitchan-Study/kafka-playground&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/efGdBC/hyZNecvyIn/kjYpfGqh1ErFUEMWQMKeL0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cOw6BZ/hyZNeKmwOP/octAKmJYtnfzAPDYN0wSKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - gitchan-Study/kafka-playground&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to gitchan-Study/kafka-playground development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  인트로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스팅에서는 Spring Boot와 Kafka를 사용하여 &quot;회원가입&quot;이라는 하나의 이벤트가 발생했을 때, &quot;쿠폰 발급&quot;과 &quot;이메일 발송&quot;이라는 두 가지 후속 작업이 어떻게 독립적으로 처리될 수 있는지 보여주는 예제 프로젝트에 대해 설명하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미니 프로젝트로 진행한 만큼 카프카를 원래 사용하는 목적이라면 대용량 트래픽을 감당하기 위한 것이 대부분이라 여러 대의 서버로 사용하지만, 개념만 이해를 하기 위해서 로컬에서 구성할 수 있도록 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 카프카 개념이나 설정에 대해서 이해할 수 있도록 최대한 쉽고 간단한 설계로 설명하려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Kafka&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 카프카 쓰는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 방식으로는 회원가입 로직 안에 쿠폰 발급 코드와 이메일 발송 코드가 모두 포함되어야 합니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional
public void register(User user) {
    userRepository.save(user); // 회원 저장
    couponService.issueWelcomeCoupon(user); // 쿠폰 발급
    emailService.sendWelcomeEmail(user); // 이메일 발송
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우, 만약 이메일 시스템에 장애가 발생하면 회원가입 전체 프로세스가 실패할 수 있습니다. 이를 &lt;b&gt;강한 결합(Tightly Coupled)&lt;/b&gt; 이라고 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠폰 서버나 이메일 서버 중 하나라도 장애가 발생하면 전체 회원가입이 실패합니다.&lt;/li&gt;
&lt;li&gt;각 기능이 강하게 결합되어 유지보수가 어렵습니다.&lt;/li&gt;
&lt;li&gt;새로운 후속 기능(예: 알림톡 발송)을 추가하려면 register() 내부를 매번 수정해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 Kafka라는 메시지 브로커를 중간에 두어 이 문제를 해결하는 것을 목표로 합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;[회원가입 API] --&amp;gt; [Kafka Topic: user-registered] --&amp;gt; [CouponConsumer]
                                               \\
                                                --&amp;gt; [EmailConsumer]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입 시스템은 &quot;회원가입이 완료되었다&quot;는 이벤트만 Kafka에 던져주고 자신의 역할을 끝냅니다.&lt;/li&gt;
&lt;li&gt;쿠폰 시스템과 이메일 시스템은 Kafka를 구독하고 있다가, 해당 이벤트가 발생하면 각자 독립적으로 자신의 작업을 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 각 시스템의 의존성이 제거되어 &lt;b&gt;느슨한 결합(Loosely Coupled)&lt;/b&gt; 을 가진 유연하고 안정적인 아키텍처를 만들 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 대용량의 실시간 데이터를 안정적으로 처리하기 위한 &lt;b&gt;분산 이벤트 스트리밍 플랫폼&lt;/b&gt;입니다. 이 프로젝트에서는 여러 시스템이 서로 직접 통신하지 않고, Kafka를 통해 메시지(이벤트)를 주고받도록 하는 &lt;b&gt;메시지 브로커&lt;/b&gt;의 역할로 사용되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Producer&lt;/b&gt;: 메시지를 생산하여 Kafka에 보내는 역할 (이 프로젝트에서는 UserController)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Consumer&lt;/b&gt;: Kafka로부터 메시지를 받아서 소비(처리)하는 역할 (이 프로젝트에서는 CouponConsumer, EmailConsumer)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Topic&lt;/b&gt;: 메시지가 저장되는 공간의 이름 (이 프로젝트에서는 user-registered)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Zookeeper&lt;/b&gt;: Kafka 클러스터의 상태를 관리하고 조정하는 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 설정 방법 (Docker를 이용한 로컬 설정)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 환경에서 Kafka와 Zookeeper를 가장 쉽게 실행하는 방법은 Docker를 사용하는 것입니다. 프로젝트 루트의 docker-compose.yml 파일이 이 역할을 담당합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;version: '3'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.0.1
    container_name: zookeeper
    ports:
      - &quot;2181:2181&quot;
    # ...

  kafka:
    image: confluentinc/cp-kafka:7.0.1
    container_name: kafka
    ports:
      - &quot;9092:9092&quot;
    depends_on:
      - zookeeper
    # ...

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker compose up -d 명령어 하나로 내 컴퓨터에 Kafka와 Zookeeper가 격리된 환경에서 실행됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  프로젝트 설명&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 로컬 실행 환경 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트를 실행하면 다음과 같이 여러 애플리케이션이 각자의 위치와 포트에서 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 실행 위치 포트 번호 역할&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Spring Boot App&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;내 PC (macOS)&lt;/td&gt;
&lt;td&gt;localhost:8080&lt;/td&gt;
&lt;td&gt;API 서버, Kafka Producer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Kafka Broker&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Docker 컨테이너&lt;/td&gt;
&lt;td&gt;localhost:9092&lt;/td&gt;
&lt;td&gt;메시지 중개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Zookeeper&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Docker 컨테이너&lt;/td&gt;
&lt;td&gt;localhost:2181&lt;/td&gt;
&lt;td&gt;Kafka 클러스터 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 회원가입 API 실행 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 회원가입 API를 호출했을 때, 전체 시스템이 동작하는 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;1274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0KWF7/dJMcaksGfSk/rVi24rhCRGRy1sUheNI36K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0KWF7/dJMcaksGfSk/rVi24rhCRGRy1sUheNI36K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0KWF7/dJMcaksGfSk/rVi24rhCRGRy1sUheNI36K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0KWF7%2FdJMcaksGfSk%2FrVi24rhCRGRy1sUheNI36K%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;832&quot; height=&quot;785&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;1274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;요청&lt;/b&gt;: 사용자가 id와 password를 담아 /register API를 호출합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;처리 및 발행&lt;/b&gt;: UserController는 사용자 정보를 내부 Map에 저장하고, UserRegisteredEvent 객체를 생성하여 user-registered 토픽으로 이벤트를 발행(Produce)합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;독립적인 소비&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;coupon-group ID를 가진 CouponConsumer가 토픽으로부터 이벤트를 받아 쿠폰 발급 로직을 수행합니다.&lt;/li&gt;
&lt;li&gt;email-group ID를 가진 EmailConsumer가 &lt;b&gt;동일한&lt;/b&gt; 토픽으로부터 이벤트를 받아 이메일 발송 로직을 수행합니다.&lt;/li&gt;
&lt;li&gt;groupId가 다르기 때문에 두 Consumer는 같은 이벤트를 각각 한 번씩 소비할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://engineerinsight.tistory.com/435&quot;&gt;https://engineerinsight.tistory.com/435&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762329473222&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[아키텍처/Kafka] 비동기 메시징 시스템: 개념 + Consumer Group을 통한 병렬 처리의 원리를 간단히 알&quot; data-og-description=&quot;  비동기 메시징 시스템이란?✅ 먼저, 비동기(Asynchronous)란?동기: 요청을 보내고 응답을 받을 때까지 기다림비동기: 요청을 보내고 응답을 기다리지 않고 다른 일 진행메시징 시스템은 일단 메&quot; data-og-host=&quot;engineerinsight.tistory.com&quot; data-og-source-url=&quot;https://engineerinsight.tistory.com/435&quot; data-og-url=&quot;https://engineerinsight.tistory.com/435&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/rcnqj/hyZM3IOm0R/ZnKbWQPO0dPssUNMJLAUk0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/cASpej/hyZMANJdvg/ZGxJ9qKQsFMGMXDuGjkYSK/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/bB2gPx/hyZMyWEx29/JbyFPBZQ4F23WHw7Uhd9K0/img.png?width=750&amp;amp;height=375&amp;amp;face=0_0_750_375&quot;&gt;&lt;a href=&quot;https://engineerinsight.tistory.com/435&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://engineerinsight.tistory.com/435&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/rcnqj/hyZM3IOm0R/ZnKbWQPO0dPssUNMJLAUk0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/cASpej/hyZMANJdvg/ZGxJ9qKQsFMGMXDuGjkYSK/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/bB2gPx/hyZMyWEx29/JbyFPBZQ4F23WHw7Uhd9K0/img.png?width=750&amp;amp;height=375&amp;amp;face=0_0_750_375');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[아키텍처/Kafka] 비동기 메시징 시스템: 개념 + Consumer Group을 통한 병렬 처리의 원리를 간단히 알&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  비동기 메시징 시스템이란?✅ 먼저, 비동기(Asynchronous)란?동기: 요청을 보내고 응답을 받을 때까지 기다림비동기: 요청을 보내고 응답을 기다리지 않고 다른 일 진행메시징 시스템은 일단 메&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;engineerinsight.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>아키텍처+MSA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/460</guid>
      <comments>https://engineerinsight.tistory.com/460#entry460comment</comments>
      <pubDate>Wed, 5 Nov 2025 17:00:27 +0900</pubDate>
    </item>
    <item>
      <title>[일본어] 조금 늦은 JLPT N3, N2 합격기 (공부기간 3주, 2주 / 찍기 팁)</title>
      <link>https://engineerinsight.tistory.com/459</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  발단&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일본어 공부의 시작이 언제더라,, 19년도 대학교 1학년 때 나고야에서 온 일본인 룸메를 만나면서부터였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;룸메는 워낙 내성적인 성격에 샤이니가 좋다고 한국에 왔지만 이후로 현재까지 나말고는 한국인 친구를 단 한명도 사귀지 못했다고 한다(?)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;룸메가 많이 외로워보여 같이 놀기 시작하면서부터 일본어를 하나씩 접했고 그렇게 야매 공부는 시작되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  JLPT 시작 전 베이스&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 공부하다가 좀 잘해보고 싶어서 학교에서 21년도 여름 계절학기로 일본어(1)을 수강했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단 3주간의 과정이었고&amp;nbsp;그렇게 대략 N4의 수준을 지니게 되었지만, 아직 JLPT를 볼 생각까지는 못했던 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러던중 오빠가 여자친구가 생겼는데 그녀는 고베 출신에 부모님 모두 한국어를 전혀 못하셨다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결혼한다고 상견례 등등의 자리에서도 굉장히 가족간 언어의 고충이 있어서 웃기만 하는 부모님 4명을 보며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 좀더 열심히 공부를 하게 된 것 같다. &lt;i&gt;(결혼식 사회도 냐에게 맡으라고,,, 부담++)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;++ 평상시에 애니는 그렇게 많이 보지는 않는데 짧은 러닝타임과 빠른 전개, 망상씬이 좋아서 일드를 많이 보는 편이다.&amp;nbsp;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  JLPT 공부법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 공부 기간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JLPT &lt;b&gt;N3 공부 기간은 약 3주, N2 공부 기간은 약 2주&lt;/b&gt;였다. 인터넷에서 나보다 촉박한 사람은 많이 못본 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히나 N2 때는 종강(6월24일) 이후로 시험(7월6일)까지 워낙 시간이 촉박해서 5일만에 1700단어를 1회독 하고, 이후에 문제 풀이를 해서 간신히 통과할 수 있었다,,ㅋㅎㅋㅎ&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사실상 JLPT 공부법은 똑같으니 암튼 작성해 보겠슴니다,,&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅ 성적&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3주로 비교적 좀 길게 공부해서 N3는 꽤 안정적으로 합격했다. 특히 독해가 만점인게 와우였음&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그치만 이건 학교 다니면서 병행해서 좀 힘들었던 기억이 있다. 단어는 거의 지하철에서 외웠다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2086&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6YnQO/dJMcaihiZ2E/eQHubKKg2NmvnTXJt9pMc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6YnQO/dJMcaihiZ2E/eQHubKKg2NmvnTXJt9pMc0/img.png&quot; data-alt=&quot;N3&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6YnQO/dJMcaihiZ2E/eQHubKKg2NmvnTXJt9pMc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6YnQO%2FdJMcaihiZ2E%2FeQHubKKg2NmvnTXJt9pMc0%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;2086&quot; height=&quot;856&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2086&quot; data-origin-height=&quot;856&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;N3&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N2는 확실히 반신반의 한 채로 시험장에 간만큼 진짜 간신히 합격했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종강 후 2주간 빡세게 공부했고 턱걸이 합격했다. 아마 컷이 90이던가,,&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2080&quot; data-origin-height=&quot;756&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nZuMz/dJMcab3ylZd/OPYKXtLD1k597mw5hiEaj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nZuMz/dJMcab3ylZd/OPYKXtLD1k597mw5hiEaj1/img.png&quot; data-alt=&quot;N2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nZuMz/dJMcab3ylZd/OPYKXtLD1k597mw5hiEaj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnZuMz%2FdJMcab3ylZd%2FOPYKXtLD1k597mw5hiEaj1%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;2080&quot; height=&quot;756&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2080&quot; data-origin-height=&quot;756&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;N2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅ 단어 암기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 학교 오가는 길에 회독 JLPT라는 어플을 보기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돈 한푼 받지 않았지만 사실 이 포스팅은 이 어플을 칭찬하기 위해 시작된 거라고 봐도 무방할 정도,,ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유료버전은 좀더 다양한 단어가 나오고 광고가 없는데 무료버전도 쓸만하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;광고를 안보는팁은 한번 회독하고 나서 셔플 기능이 있는데 셔플을 안하면 광고를 안볼 수 있다(?)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 광고 봐도 된다. 좀 짜증나는 못하는 게임을 사람 긁는 그런 광고들이 대부분이당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k2qXB/dJMcaeFX0mx/h6ZSiRs3EfWVOJ4JKZYHq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k2qXB/dJMcaeFX0mx/h6ZSiRs3EfWVOJ4JKZYHq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k2qXB/dJMcaeFX0mx/h6ZSiRs3EfWVOJ4JKZYHq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk2qXB%2FdJMcaeFX0mx%2Fh6ZSiRs3EfWVOJ4JKZYHq1%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;379&quot; height=&quot;199&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 어플에서 자신이 신청한 급수를 1회독 하면 사실상 문제 풀이를 위한 준비는 끝났다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_0315.PNG&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1q837/dJMcagcIiVP/CwW2TnSQuec8vP2xDN2eTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1q837/dJMcagcIiVP/CwW2TnSQuec8vP2xDN2eTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1q837/dJMcagcIiVP/CwW2TnSQuec8vP2xDN2eTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1q837%2FdJMcagcIiVP%2FCwW2TnSQuec8vP2xDN2eTK%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;335&quot; height=&quot;725&quot; data-filename=&quot;IMG_0315.PNG&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_0314.PNG&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boYMt3/dJMcagjtRPd/bl7az5GoJzOVNr2iFg77H1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boYMt3/dJMcagjtRPd/bl7az5GoJzOVNr2iFg77H1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boYMt3/dJMcagjtRPd/bl7az5GoJzOVNr2iFg77H1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboYMt3%2FdJMcagjtRPd%2Fbl7az5GoJzOVNr2iFg77H1%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;373&quot; height=&quot;807&quot; data-filename=&quot;IMG_0314.PNG&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;문제 풀이&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음은 문제를 푼다. 내가 풀었던건 유명한 그 다락원 한권으로 끝내기 책인데, 종이도 사각사각하고 전반적으로 모자란 것 없는 책이라고 생각한다. 전부 다 풀지는 못하고 영역별로 한 절반(5개년) 정도 푼 채로 시험장에 들어갔다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  영역별 팁&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt; 언어지식&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히나 어휘 영역은 나오는 단어를 돌려 내는 경향이 있으니 문제 풀이는 필수라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 히라가나-&amp;gt;칸지, 칸지-&amp;gt;히라가나 이건 알면 풀고 모르면 영영 모르니 빠르게 풀고 넘어간다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문법은 정말로 빈칸 고르기 문제들은 공부한 만큼 나오는 것 같다. 모르면 그냥 〜こと 들어간 것들이나 최대한 답안에서 많이 봤던 스타일로 찍는게 정답률이 높다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 긴 문법 지문은 난 그냥 거의 넘긴다는 생각으로 봤다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 너무 촉박해서 풀기 어렵다 (특히 N2의 경우에는 언어지식+독해를 시간을 합쳐서 주기 때문에 더더욱 포기함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;독해&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;독해는 지문이 쏟아지는 느낌을 받지만 나처럼 독해력이 그냥 모국어로도 안좋은 뼛속까지 이과생은 다 읽으려는 생각을 포기하면 오히려 점수가 높아지는 경험을 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;선지를 먼저 읽어보면서 주로 답안은 &quot;일본의 자랑&quot;, &quot;일본이 잘한 일&quot;, &quot;일본 사람들의 의식 수준&quot; 이런 것들이 주로 많이 차지를 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또 가끔 일본인들이 좀 적극적이지 않다던가 너무 스미마센을 많이 한다던가 이런 부분들을 비판하는 글들도 왕왕 있다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;내가 봤던 2025 7월 N2에서는 象 코끼리라는 단어가 내 기억에는 가타카나로 출제되어서 잠깐 이게 무슨 말인지 헤맸었는데 이런 경우에는 진짜 답이 없다,,, 전에 일본인 유튜버가 애기 데리고 동물원 갔던 영상에서 애기가 ゾウ！！！！하면서 소리지르던 게 생각나서 간신히 맞췄는데 이런 경우에는 다른 사람들도 다 틀릴테니 (물론 절평이지만) 일단 안심하고 다른 부분으로 넘어가야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 나는 과반은 맞추겠지 라는 멘탈을 부여잡고 푸는게 중요하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;청해&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이거야말로 평소에 꾸준히가 가장 중요한 영역이 아닐까 싶은데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유튜브에 정말 좋은 채널이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=Dq2O9ftz_Kg&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=Dq2O9ftz_Kg&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=Dq2O9ftz_Kg&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bkeNg5/hyZMJcAwBX/ptYaDO145KpLarkOtrvKT1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/djcGuF/hyZMQ3ODxX/AsKgsf7KKKrpTt1Rk2rFn1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;[ 예문으로 자동암기 ] 일본어 능력시험 JLPT N3 단어 1295 (9시간 전체 재생 풀버전 / 한국어 음성 포&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/Dq2O9ftz_Kg&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 단어, 예문 함께 계속 읽어주는데 나는 베란다에서 식물 가꾸면서 계속 틀어놨었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예문 부분에서 직접 작문해서 목소리 나오기 전에 먼저 말해보면 실력이 정말 많이 늘 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 이걸 계속 듣다보면 청해를 들을 때 무언가 들린다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 말을 시작하기 전에 시험장에서 얼른 선지를 빠르게 스캔하는 연습을 하면 진짜 좋을 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ETC/日本語</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/459</guid>
      <comments>https://engineerinsight.tistory.com/459#entry459comment</comments>
      <pubDate>Fri, 31 Oct 2025 15:30:30 +0900</pubDate>
    </item>
    <item>
      <title>[우테코] E2E 테스트에서 데이터 격리 (템플릿 공유합니다!!!)</title>
      <link>https://engineerinsight.tistory.com/458</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  인트로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 실제로 개발하다 보면 단위 테스트(Unit Test)보다 훨씬 현실적인 &lt;b&gt;End-to-End(E2E) 테스트&lt;/b&gt;를 작성할 일이 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;E2E 테스트는 실제 API를 호출하고, 데이터베이스와의 연동, 인증 절차, 외부 요청 등까지 통합적으로 검증하는 테스트입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ E2E 테스트 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(인수테스트입니다)&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@Test
void 고객이_자신의_리워드를_조회한다() {
    String ownerAccessToken = 카페_사장_회원_가입_요청하고_액세스_토큰_반환(OWNER_CREATE_REQUEST);
    Long savedCafeId = 카페_생성_요청하고_아이디_반환(ownerAccessToken, CAFE_CREATE_REQUEST);

    String customerToken = 가입_고객_회원_가입_요청하고_액세스_토큰_반환(...);
    Long customerId = authTokensGenerator.extractMemberId(customerToken);

    Long couponId = 쿠폰_생성_요청하고_아이디_반환(ownerAccessToken, new CouponCreateRequest(savedCafeId), customerId);

    쿠폰에_스탬프를_적립_요청(ownerAccessToken, customerId, couponId, new StampCreateRequest(10));

    ExtractableResponse&amp;lt;Response&amp;gt; response = 리워드_목록_조회(customerToken);
    VisitorRewardsFindResponse result = response.body().as(VisitorRewardsFindResponse.class);

    assertAll(
            () -&amp;gt; assertThat(response.statusCode()).isEqualTo(OK.value()),
            () -&amp;gt; assertThat(result.getRewards()).isNotEmpty()
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 테스트는 실제로 API를 호출하고 DB에 데이터를 쌓습니다. 문제는 이런 테스트가 &lt;b&gt;DB 상태를 오염시킨다&lt;/b&gt;는 점입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ DB 오염의 문제점: 비결정적 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트가 실행될수록 데이터가 계속 누적되며, 다음 테스트가 기존 데이터에 영향을 받는 비결정적 테스트(Non-deterministic Test)로 변질됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 &amp;lsquo;언제 실행하더라도 같은 결과&amp;rsquo;를 내야 하는데, DB가 오염되면 테스트를 신뢰할 수 없게 됩니다ㅠㅠ&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  테스트 데이터 격리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 모든 테스트는 깨끗한 DB에서 시작해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;End-to-End 테스트에서는 테스트 전후에 항상 DB를 초기화해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &lt;code&gt;@Transactional&lt;/code&gt;을 사용하면 테스트가 끝난 뒤 자동으로 롤백되어 DB가 깨끗해지지만, 이 방식은 &lt;code&gt;@SpringBootTest(webEnvironment = RANDOM_PORT)&lt;/code&gt; 환경에서는 통하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 이 경우에는 실제로 서버를 띄워서 API 요청을 보내기 때문에, 요청이 &lt;b&gt;테스트 메서드의 트랜잭션 경계 밖에서 수행&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드가 아무리 &lt;code&gt;@Transactional&lt;/code&gt;로 감싸져 있어도 HTTP 요청을 통해 실행된 비즈니스 로직은 &lt;b&gt;별도의 트랜잭션에서 커밋&lt;/b&gt;되어 버리기 때문에 단순 롤백으로는 DB를 되돌릴 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 테스트마다 스프링 컨테이너를 통째로 다시 띄우는 방법(&lt;code&gt;@DirtiesContext&lt;/code&gt; 등)도 있긴 하지만, 테스트 한 번 실행할 때마다 애플리케이션 컨텍스트가 새로 만들어지고, 빈 초기화, DI 주입, 서버 부팅까지 전부 다시 일어나기 때문에 너무 비효율적입니당,, 테스트 속도가 &lt;b&gt;수 초 단위로 느려지고&lt;/b&gt;, 대규모 테스트에서는 사실상 불가능에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 테스트 전후에 항상 DB를 직접 초기화해야 합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이 포스팅에서는 쉽게 가져다 쓸 수 있는 초기화 템플릿을 공유하려고 합니당&lt;/i&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  DataCleaner&lt;/h2&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;@Profile(&quot;test&quot;)
@Component
public class DataCleaner {

    private static final String FOREIGN_KEY_CHECK_FORMAT = &quot;SET FOREIGN_KEY_CHECKS %d&quot;;
    private static final String TRUNCATE_FORMAT = &quot;TRUNCATE TABLE %s&quot;;

    private final List&amp;lt;String&amp;gt; tableNames = new ArrayList&amp;lt;&amp;gt;();

    @PersistenceContext
    private EntityManager entityManager;

    @PostConstruct
    public void findDatabaseTableNames() {
        List&amp;lt;Object[]&amp;gt; tableInfos = entityManager.createNativeQuery(&quot;SHOW TABLES&quot;).getResultList();
        for (Object[] tableInfo : tableInfos) {
            tableNames.add((String) tableInfo[0]);
        }
    }

    @Transactional
    public void clear() {
        entityManager.clear();
        truncate();
    }

    private void truncate() {
        entityManager.createNativeQuery(String.format(FOREIGN_KEY_CHECK_FORMAT, 0)).executeUpdate();
        for (String tableName : tableNames) {
            entityManager.createNativeQuery(String.format(TRUNCATE_FORMAT, tableName)).executeUpdate();
        }
        entityManager.createNativeQuery(String.format(FOREIGN_KEY_CHECK_FORMAT, 1)).executeUpdate();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;테이블 이름 로드&lt;/b&gt;&lt;br /&gt;&lt;code&gt;SHOW TABLES&lt;/code&gt;로 현재 DB 내의 모든 테이블 이름을 읽어옵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외래 키 제약 비활성화&lt;/b&gt;각 테이블을 &lt;code&gt;TRUNCATE&lt;/code&gt;로 비웁니다.&lt;br /&gt;&lt;code&gt;SET FOREIGN_KEY_CHECKS = 0&lt;/code&gt;으로 FK 제약을 끄고,&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다시 활성화&lt;/b&gt;&lt;br /&gt;&lt;code&gt;SET FOREIGN_KEY_CHECKS = 1&lt;/code&gt;으로 복구합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &lt;b&gt;모든 데이터가 완전히 초기화된 상태&lt;/b&gt;로 다음 테스트가 시작됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  DataClearExtension&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 매 테스트마다 &lt;code&gt;dataCleaner.clear()&lt;/code&gt;를 수동으로 호출하면 불편하니 자동으로 실행해주는 &lt;b&gt;JUnit 5 Extension&lt;/b&gt;을 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class DataClearExtension implements BeforeEachCallback {

    @Override
    public void beforeEach(ExtensionContext context) {
        DataCleaner dataCleaner = SpringExtension.getApplicationContext(context)
                .getBean(DataCleaner.class);
        dataCleaner.clear();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스를 &lt;code&gt;@ExtendWith&lt;/code&gt;로 등록하면 테스트 시작 전에 항상 &lt;code&gt;DataCleaner.clear()&lt;/code&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;최종적으로 모든 E2E 테스트는 다음과 같은 템플릿을 상속받습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@ExtendWith(DataClearExtension.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class AcceptanceTest {

    @LocalServerPort
    private int port;

    @BeforeEach
    void setup() {
        RestAssured.port = port;
    }

    @AfterEach
    void tearDown() {
        cleaner.clear();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;@ExtendWith(DataClearExtension.class)&lt;/code&gt; 덕분에 각 테스트마다 DB가 자동으로 정리됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  사용 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 단순히 &lt;code&gt;AcceptanceTest&lt;/code&gt;를 상속받기만 하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;class VisitorRewardFindAcceptanceTest extends AcceptanceTest {

    @Test
    void 고객이_자신의_리워드를_조회한다() {
        // 실제 API 호출 및 검증
    }

    @Test
    void 리워드가_없으면_빈_배열을_반환한다() {
        // 또 다른 시나리오
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 테스트가 끝날 때마다 자동으로 데이터가 비워지므로 서로 영향을 주지 않고, 테스트 실행 순서에도 의존하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;936&quot; data-origin-height=&quot;966&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgzED9/dJMcac2r6Tf/T0ii9MuEGPwLXcmXTUIIK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgzED9/dJMcac2r6Tf/T0ii9MuEGPwLXcmXTUIIK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgzED9/dJMcac2r6Tf/T0ii9MuEGPwLXcmXTUIIK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgzED9%2FdJMcac2r6Tf%2FT0ii9MuEGPwLXcmXTUIIK0%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;615&quot; height=&quot;635&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;966&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PROJECT/Stamp Crush</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/458</guid>
      <comments>https://engineerinsight.tistory.com/458#entry458comment</comments>
      <pubDate>Wed, 29 Oct 2025 16:30:10 +0900</pubDate>
    </item>
    <item>
      <title>[AIGOYA LABS] 비동기 이벤트끼리 Race Condition 박멸 기록: 이벤트 직렬 처리부터 Spring 내부에 미니 메시징 큐 구현까지</title>
      <link>https://engineerinsight.tistory.com/457</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  인트로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 개발하다 보면 생각보다 비동기적으로 처리해야 하는 작업들이 많아진다. LLM 호출한다던지, 외부 API 연동, S3에 저장한다던지 등등 외부와 함께 해야하는 작업이 이어질 때, 응답 지연이 길고, 순차적으로 처리하면 전체 서비스가 느려질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우리는 보통 &amp;ldquo;비동기 처리&amp;rdquo;로 이런 작업을 분리한다. 하지만 문제는, 비동기 처리 과정에서 데이터가 섞이거나 순서가 깨지는 상황이 생긴다는 점이다. 동시에 여러 이벤트가 발행될 때 &lt;b&gt;Race Condition&lt;/b&gt;이 발생하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 Spring 환경에서 이벤트 기반 아키텍처를 사용하면서 응답 순서를 보장하기 위해 &lt;b&gt;이벤트를 직렬로 처리하는 방식&lt;/b&gt;을 설계한 경험을 공유하고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제 상황: 비동기 이벤트 처리 시의 순서 불일치 + 스레드풀 제어의 불확실성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 Spring의 &lt;code&gt;@Async&lt;/code&gt;와 &lt;code&gt;@EventListener&lt;/code&gt; 조합으로 다음과 같은 구조를 만들었다:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Async
@EventListener
fun handle(event: NodeCreatedEvent) {
    callLLM() // 비동기 작업 1
    fetchExternalInfo() // 비동기 작업 2
    saveToDB() // 비동기 작업 3
    postProcess() 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;th&gt;상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;t=0ms&lt;/td&gt;
&lt;td&gt;Thread-1&lt;/td&gt;
&lt;td&gt;NodeCreatedEvent(1)&lt;/td&gt;
&lt;td&gt;DB에서 node_id=1 읽음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t=1ms&lt;/td&gt;
&lt;td&gt;Thread-2&lt;/td&gt;
&lt;td&gt;NodeCreatedEvent(1)&lt;/td&gt;
&lt;td&gt;같은 row 다시 읽음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t=50ms&lt;/td&gt;
&lt;td&gt;Thread-1&lt;/td&gt;
&lt;td&gt;LLM 호출 끝나고 결과 A 저장&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t=80ms&lt;/td&gt;
&lt;td&gt;Thread-2&lt;/td&gt;
&lt;td&gt;외부 API 호출 끝나고 결과 B 저장 (A를 덮어씀)&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 &lt;b&gt;데이터 무결성 문제&lt;/b&gt;와 &lt;b&gt;예측할 수 없는 흐름&lt;/b&gt;이 생기기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 보면 간단하지만 실제로는 왜 덮어씌우기가 되는지 발견하기 위해 꽤 힘들었다 ㅎㅎ,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터의 문제 외에도 더 치명적인 문제가 존재하는데 &lt;b&gt;&lt;i&gt;스레드 개수와 실행 시점이 예측 불가능&lt;/i&gt;&lt;/b&gt;하다는 것이다 ㅋㅎㅋㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의 &lt;code&gt;@Async&lt;/code&gt;는 내부적으로 스레드풀(&lt;code&gt;ThreadPoolTaskExecutor&lt;/code&gt;) 을 사용하지만, 명시적으로 설정하지 않으면 &lt;code&gt;SimpleAsyncTaskExecutor&lt;/code&gt;가 기본값이다. 이 경우에는 스레드 수 제한이 없고, 호출마다 새로운 스레드를 생성합니다. &lt;i&gt;(=커넥션 풀 고갈의 공포 ㄷㄷ)&lt;/i&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  해결 전략&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결 전략(1): DB Lock으로 막기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;처음에는 단순하게 생각했다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;&amp;ldquo;같은 리소스를 동시에 수정하려고 하면 DB Lock을 걸면 되지 않을까?&amp;rdquo;&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;@Transactional
public void handleEvent(Event event) {
    var data = repository.findByIdForUpdate(event.getTargetId()); // SELECT ... FOR UPDATE
    data.process(event);
    repository.save(data);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 한 트랜잭션이 끝날 때까지 다른 트랜잭션이 접근하지 못하므로 일시적으로는 Race Condition이 사라진다. 하지만 이 방식에는 한계가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;순서 보장 불가:&lt;/b&gt; 비동기로 발행된 이벤트가 1, 2, 3 순서여도 Lock이 걸리는 타이밍에 따라 2 &amp;rarr; 1 &amp;rarr; 3 순서로 처리될 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 저하:&lt;/b&gt; Lock을 걸고 대기하는 트랜잭션이 많아질수록 DB 커넥션이 묶인다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 유지 시간 증가:&lt;/b&gt; LLM 호출처럼 외부 연동이 포함된 작업은 수 초 단위로 트랜잭션이 열려 있는 상태가 되어버린다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB Lock은 &lt;b&gt;정합성을 강제할 수는 있지만 순서까지 보장하지는 못한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;우리 서비스에서는 앞선 이벤트에서 얻은 메타 데이터를 바탕으로 이후 이벤트에서 llm을 호출해야 하는 경우도 다수 있었기 때문에 이 방법으로는 몇몇 이벤트 처리에서 제대로 된 llm 응답을 받을 수 없다는 문제가 있었다.&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결 전략(2): 이벤트를 직렬 처리 (전용 스레드 1개)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;이벤트를 발행하자마자 실행하지 않고, 하나의 큐(Queue)에 넣고, 전용 스레드 하나가 순서대로 꺼내 처리하자.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Component
public class EventQueueProcessor {

    private final BlockingQueue&amp;lt;Event&amp;gt; queue = new LinkedBlockingQueue&amp;lt;&amp;gt;();
    private final Thread worker;

    public EventQueueProcessor() {
        this.worker = new Thread(this::processEvents);
        this.worker.setName(&quot;event-processor-thread&quot;);
        this.worker.start();
    }

    public void enqueue(Event event) {
        queue.add(event);
    }

    private void processEvents() {
        while (true) {
            try {
                Event event = queue.take(); // 큐에서 하나씩 꺼냄
                handleEvent(event);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void handleEvent(Event event) {
        // 실제 이벤트 처리 로직
        System.out.println(&quot;Processing event: &quot; + event);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서, 이벤트는 모두 큐로 들어간다. 그리고 빈 내에서 초기화 시 지정된 단 하나의 스레드만 이 큐를 소비(consume) 한다. 따라서 같은 리소스에 대해 동시에 접근할 일이 없고, 처리 순서도 큐에 넣은 순서대로 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 순서 보장 + Race Condition 제거를 동시에 달성할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;우리 서비스는 기본적으로 비동기에는 급한 일은 넣고 있지 않았고, 트래픽 자체가 스레드 2개 이상이 들러붙어 처리해야 할 만큼은 아니었기 때문에 이정도로 끝냈지만, 블로그 포스팅에서는 더 확장을 해보려고 한다.&lt;/i&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  확장: 다중 큐와 큐 Manager&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 서비스가 커진다면?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 서비스가 커지고 이벤트 종류가 많아지면 큐가 하나라는 것이 오히려 병목이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, LLM 결과 저장 이벤트, 이메일 발송 이벤트, 알림 전송 이벤트 이 세 가지가 한 큐에 들어가면, 알림 하나 보내려고 10초 걸리는 LLM 처리가 끝날 때까지 기다려야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 큐를 여러 개로 분리하고, 각 큐를 중앙에서 관리하는 &lt;code&gt;QueueManager&lt;/code&gt; 구조로 확장해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public interface EventQueue {
    void enqueue(Event event);
}

@Component
public class SimpleEventQueue implements EventQueue {
    private final BlockingQueue&amp;lt;Event&amp;gt; queue = new LinkedBlockingQueue&amp;lt;&amp;gt;();
    private final ExecutorService workerPool = Executors.newFixedThreadPool(4);

    public SimpleEventQueue() {
        for (int i = 0; i &amp;lt; 4; i++) {
            workerPool.submit(this::process);
        }
    }

    @Override
    public void enqueue(Event event) {
        queue.add(event);
    }

    private void process() {
        while (true) {
            try {
                Event event = queue.take();
                handle(event);
            } catch (Exception e) { e.printStackTrace(); }
        }
    }

    private void handle(Event event) {
        // 이벤트별 처리 로직
    }
}

@Component
public class QueueManager {
    private final List&amp;lt;EventQueue&amp;gt; queues = new ArrayList&amp;lt;&amp;gt;();

    public void register(EventQueue queue) {
        queues.add(queue);
    }

    public EventQueue getQueue(int index) {
        return queues.get(index);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LinkedBlockingQueue&lt;/code&gt;는 자체적으로 동기화(synchronized) 메커니즘이 내장된 Thread-safe 큐로, 여러 스레드가 동시에 &lt;code&gt;put()&lt;/code&gt;, &lt;code&gt;take()&lt;/code&gt;, &lt;code&gt;add()&lt;/code&gt; 등을 호출해도 데이터 손상이나 race condition이 발생하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 활용 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 상황 예시를 들어보겠당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 회원가입할 때 동시에 3가지 비동기 작업을 처리해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LLM을 이용한 환영 메시지 생성&lt;/li&gt;
&lt;li&gt;이메일 발송&lt;/li&gt;
&lt;li&gt;가입 기록 로깅&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 가지는 서로 관련은 있지만, &lt;b&gt;결과 순서를 보장해야 하는 작업도 있고&lt;/b&gt;, &lt;b&gt;서로 다른 큐에서 병렬로 처리 가능한 작업도 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 위의 코드를 이렇게 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 1. 이벤트 정의
public class UserCreatedEvent {
    public final String userId;
    public UserCreatedEvent(String userId) { this.userId = userId; }
}

// 2. 이벤트 처리기 (핸들러)
@Component
public class UserCreatedEventHandler {

    private final QueueManager queueManager;

    public UserCreatedEventHandler(QueueManager queueManager) {
        this.queueManager = queueManager;
    }

    public void handle(UserCreatedEvent event) {
        // 서로 다른 큐에 분배
        queueManager.getQueue(0).enqueue(() -&amp;gt; sendWelcomeEmail(event));
        queueManager.getQueue(1).enqueue(() -&amp;gt; generateWelcomeMessage(event));
        queueManager.getQueue(2).enqueue(() -&amp;gt; saveUserLog(event));
    }

    private void sendWelcomeEmail(UserCreatedEvent event) {
        System.out.println(&quot;  이메일 발송 중... (&quot; + event.userId + &quot;)&quot;);
        sleep(1000);
        System.out.println(&quot;✅ 이메일 발송 완료 (&quot; + event.userId + &quot;)&quot;);
    }

    private void generateWelcomeMessage(UserCreatedEvent event) {
        System.out.println(&quot;  LLM 환영 메시지 생성 중... (&quot; + event.userId + &quot;)&quot;);
        sleep(2000);
        System.out.println(&quot;✅ 메시지 생성 완료 (&quot; + event.userId + &quot;)&quot;);
    }

    private void saveUserLog(UserCreatedEvent event) {
        System.out.println(&quot;  가입 로그 저장 중... (&quot; + event.userId + &quot;)&quot;);
        sleep(500);
        System.out.println(&quot;✅ 로그 저장 완료 (&quot; + event.userId + &quot;)&quot;);
    }

    private void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }
}

// 3. 큐 매니저 구성 (여기가 중요!!!!)
@Component
public class QueueManager {
        // 리스트가 아니라 이메일, LLM, 로그용 이런 이름의 Map으로 관리할 수도 있겠쥬? 
    private final List&amp;lt;SimpleEventQueue&amp;gt; queues = new ArrayList&amp;lt;&amp;gt;();

    public QueueManager() {
        // 큐 3개 생성 (이메일, LLM, 로그용)
        for (int i = 0; i &amp;lt; 3; i++) {
            queues.add(new SimpleEventQueue()); // 여기가 중요!!!!! 
        }
    }

    public SimpleEventQueue getQueue(int index) {
        return queues.get(index);
    }
}

// 4. 큐 자체
public class SimpleEventQueue {
    private final BlockingQueue&amp;lt;Runnable&amp;gt; queue = new LinkedBlockingQueue&amp;lt;&amp;gt;();
    private final ExecutorService workerPool = Executors.newFixedThreadPool(2);

    public SimpleEventQueue() {
        for (int i = 0; i &amp;lt; 2; i++) { // 일하는 스레드 수가 2개라는 뜻 (설정 가능)
            workerPool.submit(this::process);
        }
    }

    public void enqueue(Runnable task) {
        queue.add(task);
    }

    private void process() {
        while (true) {
            try {
                Runnable task = queue.take();
                task.run();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리 관련 시각화를 해보면???&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;사용자: &quot;회원가입 완료&quot;
           │
           ▼
     UserCreatedEvent 발행
           │
           ▼
 ┌──────────────────────────────┐
 │       QueueManager           │
 └──────────────────────────────┘
   │          │          │
   ▼          ▼          ▼
[Queue0]   [Queue1]   [Queue2]
 이메일     LLM생성    로그저장
  │           │           │
  ▼           ▼           ▼
  ▶✅       ▶✅       ▶✅&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 병렬로 처리되지만, 각 큐 내부에서는 직렬로 순서가 유지된다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 메세징 큐&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 사실 Spring 내부에 작은 메시징 시스템을 구현한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 큐는 토픽처럼 독립적으로 동작하며, &lt;code&gt;QueueManager&lt;/code&gt;는 브로커 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 Kafka나 RabbitMQ로 교체할 때도, 이 구조를 그대로 확장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://engineerinsight.tistory.com/435&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://engineerinsight.tistory.com/435&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1761721314555&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[아키텍처/Kafka] 비동기 메시징 시스템: 개념 + Consumer Group을 통한 병렬 처리의 원리를 간단히 알&quot; data-og-description=&quot;  비동기 메시징 시스템이란?✅ 먼저, 비동기(Asynchronous)란?동기: 요청을 보내고 응답을 받을 때까지 기다림비동기: 요청을 보내고 응답을 기다리지 않고 다른 일 진행메시징 시스템은 일단 메&quot; data-og-host=&quot;engineerinsight.tistory.com&quot; data-og-source-url=&quot;https://engineerinsight.tistory.com/435&quot; data-og-url=&quot;https://engineerinsight.tistory.com/435&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bjkypa/hyZMedGbXk/b64w0v4cOkcrBPfjZE18B0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/c4lbOc/hyZMwX8xul/OmdVFSoJS9hGFkXDnvzgmk/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/Rcau5/hyZMxJvTbL/wUNfOsemNTRwIJskuTYQNK/img.png?width=750&amp;amp;height=375&amp;amp;face=0_0_750_375&quot;&gt;&lt;a href=&quot;https://engineerinsight.tistory.com/435&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://engineerinsight.tistory.com/435&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bjkypa/hyZMedGbXk/b64w0v4cOkcrBPfjZE18B0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/c4lbOc/hyZMwX8xul/OmdVFSoJS9hGFkXDnvzgmk/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/Rcau5/hyZMxJvTbL/wUNfOsemNTRwIJskuTYQNK/img.png?width=750&amp;amp;height=375&amp;amp;face=0_0_750_375');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[아키텍처/Kafka] 비동기 메시징 시스템: 개념 + Consumer Group을 통한 병렬 처리의 원리를 간단히 알&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  비동기 메시징 시스템이란?✅ 먼저, 비동기(Asynchronous)란?동기: 요청을 보내고 응답을 받을 때까지 기다림비동기: 요청을 보내고 응답을 기다리지 않고 다른 일 진행메시징 시스템은 일단 메&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;engineerinsight.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/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;742&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgKCY1/dJMcai2FvAT/5kvtPo3hGn7pCx6AzvRHkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgKCY1/dJMcai2FvAT/5kvtPo3hGn7pCx6AzvRHkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgKCY1/dJMcai2FvAT/5kvtPo3hGn7pCx6AzvRHkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgKCY1%2FdJMcai2FvAT%2F5kvtPo3hGn7pCx6AzvRHkk%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;569&quot; height=&quot;544&quot; data-origin-width=&quot;742&quot; data-origin-height=&quot;710&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PROJECT/AIGOYA LABS</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/457</guid>
      <comments>https://engineerinsight.tistory.com/457#entry457comment</comments>
      <pubDate>Wed, 29 Oct 2025 15:35:36 +0900</pubDate>
    </item>
    <item>
      <title>[Python] 얕은복사 vs 깊은복사</title>
      <link>https://engineerinsight.tistory.com/456</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  파이썬 얕은 복사 vs 깊은 복사&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 얕은 복사&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;451&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHxKDK/dJMb9YJ2h3q/M4LUp1DJsTavmsuZJuAju0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHxKDK/dJMb9YJ2h3q/M4LUp1DJsTavmsuZJuAju0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHxKDK/dJMb9YJ2h3q/M4LUp1DJsTavmsuZJuAju0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHxKDK%2FdJMb9YJ2h3q%2FM4LUp1DJsTavmsuZJuAju0%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;665&quot; height=&quot;375&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;451&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬에서 리스트나 딕셔너리 같은 &lt;b&gt;mutable 객체&lt;/b&gt;를 복사할 때, 기본적으로는 &lt;b&gt;객체의 &amp;lsquo;참조 주소&amp;rsquo;만 복사&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 새로운 변수는 생기지만, 내부 데이터는 여전히 원본과 같은 메모리를 가리킵니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;a = [[1, 2], [3, 4]]
b = a.copy()  # 얕은 복사 (shallow copy)

a[0][0] = 99
print(b)  # [[99, 2], [3, 4]]  &amp;larr; 내부 리스트가 같이 바뀜!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 겉보기엔 다른 객체처럼 보여도, &lt;b&gt;내부의 하위 객체(list, dict 등)는 같은 주소를 공유&lt;/b&gt;하기 때문에 내부 데이터를 수정하면 다른 쪽에도 반영됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 깊은 복사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;&amp;ldquo;완전히 새로운 객체를 새로 만들어 복사하는 방식&amp;rdquo;&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깊은 복사는 &lt;b&gt;객체의 모든 계층을 새롭게 복제&lt;/b&gt;합니다. 즉, 내부 리스트나 딕셔너리까지 전부 새로 만들어 다른 주소에 할당합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;import copy

a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)  # 깊은 복사 (deep copy)

a[0][0] = 99
print(b)  # [[1, 2], [3, 4]] &amp;larr; 원본 변경과 무관&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우, &lt;code&gt;a&lt;/code&gt;와 &lt;code&gt;b&lt;/code&gt;는 완전히 독립된 구조를 가지므로 서로 영향을 주지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  활용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 문제 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 개발에서는 다음과 같은 상황에서 &lt;b&gt;얕은 복사 버그&lt;/b&gt;가 자주 터집니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;하이퍼파라미터 딕셔너리 복사&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-python&quot;&gt; params = {&quot;lr&quot;: 0.001, &quot;optimizer&quot;: {&quot;type&quot;: &quot;Adam&quot;, &quot;beta&quot;: [0.9, 0.999]}}
 new_params = params.copy()
 new_params[&quot;optimizer&quot;][&quot;beta&quot;][0] = 0.8  # 원본까지 수정됨!&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 전처리 파이프라인&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;학습용/검증용 데이터셋을 나누면서 &lt;code&gt;.copy()&lt;/code&gt;를 사용해 복사했는데, label 컬럼이나 normalization이 동시에 변함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모델 파라미터 실험 중 checkpoint 비교&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;baseline 모델을 복사해서 다른 하이퍼파라미터로 실험하려는데 weight가 얕게 복사돼 이전 모델까지 손상되는 경우.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 활용 상황&lt;/h3&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;단순 값 복사 (list of int, float 등)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.copy()&lt;/code&gt; 또는 슬라이싱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;중첩 리스트, 딕셔너리&lt;/td&gt;
&lt;td&gt;&lt;code&gt;copy.deepcopy()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;넘파이 배열&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.copy()&lt;/code&gt; (깊은 복사로 동작)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DataFrame 일부 선택&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.copy(deep=True)&lt;/code&gt; 명시 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[예시] Pandas에서의 대표 버그&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;df2 = df1[:10]  # 얕은 복사 (Warning 발생)
df2['col'] = 123  # 원본 df1도 변경될 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;df2 = df1[:10].copy()  # 깊은 복사 &amp;rarr; 안전&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬에서 리스트나 딕셔너리를 복사할 때는 기본적으로 얕은 복사가 일어나기 때문에, 내부 참조를 공유하는 구조에서는 원본까지 변할 수 있습니다. 특히 AI 실험 코드에서 하이퍼파라미터 dict나 모델 상태를 다룰 때는 항상 &lt;code&gt;copy.deepcopy()&lt;/code&gt;로 안전하게 복사하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ETC</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/456</guid>
      <comments>https://engineerinsight.tistory.com/456#entry456comment</comments>
      <pubDate>Mon, 27 Oct 2025 13:00:57 +0900</pubDate>
    </item>
    <item>
      <title>[AI/DL] PyTorch로 딥러닝의 흐름 이해하기</title>
      <link>https://engineerinsight.tistory.com/455</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  인트로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;딥러닝(Deep Learning) 모델을 만들고 학습시키기 위한 오픈소스 라이브러리&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NumPy + GPU + 자동미분 + 신경망 모듈&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;torch&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;기본 수학 연산, Tensor(배열) 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;torch.nn&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;신경망(Neural Network) 모델 구성용 모듈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;torch.optim&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;최적화 알고리즘 (Adam, SGD 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;torch.autograd&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;자동 미분 기능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;torch.utils.data&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;데이터셋 관리, 배치 생성(DataLoader)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;torchvision / torchaudio / torchtext&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;이미지, 오디오, 텍스트용 확장 라이브러리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;torch.distributed&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;분산 학습용 라이브러리 (멀티 GPU, 멀티 서버)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  PyTorch 전체 구조 한눈에 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PyTorch는 여러 하위 모듈로 구성되어 있으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 모듈이 &lt;b&gt;딥러닝 파이프라인의 한 단계를 담당&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;데이터 준비 ─▶ 모델 설계 ─▶ 손실 계산 ─▶ 역전파 ─▶ 최적화 ─▶ 학습 반복
   &amp;uarr;             &amp;uarr;             &amp;uarr;             &amp;uarr;         &amp;uarr;
torch.utils.data torch.nn torch.autograd torch.optim torch&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  torch: 기본 수학 연산, Tensor 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;i&gt;NumPy와 거의 같은 기능*을 제공하지만 *GPU에서 연산 가능&lt;/i&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Tensor 생성, 수학 연산, 브로드캐스팅, 난수 생성 등을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예시&lt;/h3&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;import torch

# Tensor 생성
a = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
b = torch.rand((2, 2))  # 0~1 사이 랜덤 값

# 기본 연산
print(a + b)
print(torch.matmul(a, b))  # 행렬 곱
print(a.mean(), a.std())   # 평균, 표준편차

# GPU 전송
a = a.to(&quot;cuda&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(참고)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;GPU는 CPU가 시킨 계산만 수행하는 보조 연산 장치다.&lt;br /&gt;즉, CPU &amp;rarr; GPU에 연산 명령 전달 &amp;rarr; GPU가 계산 &amp;rarr; 결과를 다시 CPU로 반환 구조다.&lt;br /&gt;GPU는 CPU 없이 혼자 동작할 수 없다.&lt;/i&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  torch.nn: 신경망(Neural Network) 구성 모듈&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모델의 레이어(층)와 구조를 만드는 모듈&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nn.Linear&lt;/code&gt;, &lt;code&gt;nn.Conv2d&lt;/code&gt;, &lt;code&gt;nn.LSTM&lt;/code&gt; 같은 &amp;ldquo;층&amp;rdquo;이 여기에 포함&lt;/li&gt;
&lt;li&gt;모든 모델은 &lt;code&gt;nn.Module&lt;/code&gt;을 상속받아서 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예시&lt;/h3&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;import torch.nn as nn

class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(10, 64)  # 입력 10 &amp;rarr; 은닉 64
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(64, 1)   # 은닉 64 &amp;rarr; 출력 1

    def forward(self, x):
        x = self.relu(self.fc1(x))
        return self.fc2(x)

model = SimpleNet()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CNN, Transformer, Residual Block, Attention 등 모든 구조가 이 안에서 정의된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  torch.optim: 최적화 알고리즘&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모델의 파라미터(가중치)를 업데이트하는 알고리즘을 담당&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Gradient Descent, Adam, RMSProp 등이 여기에 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예시&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import torch.optim as optim

optimizer = optim.Adam(model.parameters(), lr=0.001)

for epoch in range(100):
    optimizer.zero_grad()     # 이전 gradient 초기화
    output = model(x)         # 순전파(forward)
    loss = criterion(output, y)
    loss.backward()           # 역전파(backward)
    optimizer.step()          # 가중치 업데이트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PyTorch는 Optimizer(최적화 알고리즘) 도 전부 구현해서 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 경사하강법(Gradient Descent) 계열 알고리즘을 직접 코딩할 필요 없이 &lt;code&gt;torch.optim&lt;/code&gt; 모듈 안에서 바로 가져다 쓸 수 있다. &lt;i&gt;(매우 편리~)&lt;/i&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  torch.autograd: 자동 미분 기능&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;딥러닝의 핵심인 역전파(backpropagation)&lt;/b&gt; 를 자동으로 수행&lt;/li&gt;
&lt;li&gt;모든 Tensor의 연산 과정을 추적하고, &lt;code&gt;.backward()&lt;/code&gt; 호출 시 미분을 계산&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예시&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;x = torch.tensor(2.0, requires_grad=True)
y = x ** 3 + 4 * x ** 2
y.backward()        # dy/dx 자동 계산
print(x.grad)       # 3x^2 + 8x = 3*(2^2)+8*2 = 28&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 시 &amp;ldquo;오차를 역으로 전달해 가중치를 업데이트&amp;rdquo;하는 과정을 자동화해 PyTorch는 수식으로 미분을 쓸 필요가 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  torch.utils.data: 데이터 관리, 배치 생성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Dataset과 DataLoader 클래스를 통해 데이터를 효율적으로 관리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;데이터를 batch 단위로 나누고, shuffle(섞기), 병렬 로딩 등을 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Dataset&lt;/code&gt;은 &amp;ldquo;데이터가 어디 있고, 어떻게 꺼낼지&amp;rdquo;를 정의하는 클래스로 PyTorch에게 데이터셋이 어떤 형태로 생겼는지 알려주는 설명서 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DataLoader&lt;/code&gt;는 Dataset을 받아서 학습할 때 배치(batch) 단위로 잘라서 공급해준다. 배치 단위로 묶어주고 (batch_size), 순서를 섞어주고 (shuffle=True), 여러 CPU 스레드를 이용해 동시에 로딩(num_workers)까지 해준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예시&lt;/h3&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;from torch.utils.data import Dataset, DataLoader

class MyDataset(Dataset):
    def __init__(self, X, y):
        self.X = X  # 입력 데이터
        self.y = y  # 라벨 (정답)

    def __len__(self):
        return len(self.X)  # 전체 데이터 개수

    def __getitem__(self, idx):
        # 인덱스로 접근했을 때 반환할 한 샘플
        return self.X[idx], self.y[idx]

dataset = MyDataset(x, y)
loader = DataLoader(dataset, batch_size=32, shuffle=True)

for batch_x, batch_y in loader:
    output = model(batch_x)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터셋이 커질수록 batch 단위 학습이 필수다. 이 모듈이 데이터를 효율적으로 GPU로 전달하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 나오는 학습 루프에서는 이런 식으로 Loader를 사용한다&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;for batch_x, batch_y in loader:
    output = model(batch_x)
    loss = criterion(output, batch_y)
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  torchvision, torchaudio, torchtext: 확장 라이브러리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;각 도메인(이미지, 음성, 텍스트)에 특화&lt;/b&gt;된 도구를 제공&lt;/li&gt;
&lt;li&gt;미리 학습된 모델(pretrained models), 데이터셋, 전처리(transform)를 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유명한 모델이나 데이터셋은 torch에서 가져올 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예시&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from torchvision import datasets, transforms, models

# 이미지 전처리
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor()
])

# 데이터셋 불러오기
train_data = datasets.CIFAR10(root=&quot;./data&quot;, train=True, download=True, transform=transform)

# 미리 학습된 모델 불러오기
resnet = models.resnet18(pretrained=True)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;torchvision:&lt;/b&gt; 이미지 분류, 객체 탐지, 세그멘테이션&lt;/li&gt;
&lt;li&gt;&lt;b&gt;torchaudio:&lt;/b&gt; 음성 인식, 음성 합성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;torchtext:&lt;/b&gt; 자연어 처리, 토큰화, 임베딩&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  torch.distributed: 분산 학습&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;여러 GPU나 여러 서버에서 병렬 학습을 수행할 수 있게 하는 모듈&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;대형 모델(LLM, Diffusion 등)을 학습할 때 필수&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예시&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import torch.distributed as dist

dist.init_process_group(backend='nccl')
# 모델 병렬화
model = torch.nn.parallel.DistributedDataParallel(model)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 대의 GPU로는 감당이 안 되는 대규모 모델을 수십 대의 GPU에서 나눠서 학습시킬 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI/딥러닝</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/455</guid>
      <comments>https://engineerinsight.tistory.com/455#entry455comment</comments>
      <pubDate>Fri, 24 Oct 2025 22:00:21 +0900</pubDate>
    </item>
    <item>
      <title>[AI/VLA] 창발성을 테스트할 수 있는 VLA 오픈소스 데이터셋 구축하기(2): LLM 2단계 프롬프트 구조로 현실적인 BDDL 생성하기</title>
      <link>https://engineerinsight.tistory.com/454</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 글에서는 VLA(Vision-Language-Action) 모델의 창발성을 테스트하기 위한 핵심 데이터 표현 언어인 BDDL(Behavior Description Definition Language)을 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 실제로 &lt;b&gt;Object Set으로부터 BDDL을 자동 생성하는 과정&lt;/b&gt;을 다루어 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Object Set으로부터 BDDL을 자동 생성하기]&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  기존: Object Set &amp;rarr; BDDL 직접 생성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 구현 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 시도한 것은 &lt;b&gt;Object Set을 그대로 LLM에 입력해 BDDL을 생성&lt;/b&gt;하는 방식이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력은 다음과 같이 Python 딕셔너리 형태로 구성되어 있고 주어졌습니다. &lt;i&gt;(유일하게 주어진 것은 object list 뿐입니당)&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;living_room&quot;: [&quot;table&quot;, &quot;cup&quot;, &quot;remote&quot;],
  &quot;kitchen&quot;: [&quot;sink&quot;, &quot;sponge&quot;, &quot;cupboard&quot;],
  &quot;bathroom&quot;: [&quot;toothbrush&quot;, &quot;towel&quot;, &quot;soap&quot;]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, key는 방(room) 이름이고, value는 해당 공간 안의 물체(Object) 목록입니다. 먼저 이 데이터를 기반으로 LLM에게 &amp;ldquo;이 장면에서 가능한 BDDL을 만들어라&amp;rdquo;고 요청했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 그럴듯해 보이지만 실제로는 &lt;b&gt;상당히 비현실적인 행동들이 자주 포함&lt;/b&gt;되었습니다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;(:goal
  (and
    (move table to kitchen)
    (place cup on floor)
  )
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;테이블을 옮겨라(move table to kitchen)&amp;rdquo; 같은 이처럼 로봇팔이 수행할 수 없는 명령이 goal에 들어가곤 했습니다. 실제로 로봇팔은 컵이나 수건처럼 작은 물체만 다룰 수 있는데, LLM은 이런 제약을 이해하지 못한 채 &amp;ldquo;그럴듯한&amp;rdquo; 계획을 만들어버린 것입니다. &lt;i&gt;(트랜스포머의 한계,, 흑흑)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 막기 위해 프롬프트 단계에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;로봇팔이 물리적으로 가능한 행동만 포함시켜라. 무거운 가구는 옮기지 마라.&amp;rdquo; 와 같은 제약 문장을 명시적으로 추가했지만, LLM은 여전히 제대로 지키지 않았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 문제점: 자연어 이해 없이 형식만 맞추는 생성 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 핵심은 LLM이 &lt;b&gt;BDDL의 논리 구조만 학습하고, 장면의 실제 제약을 이해하지 못한다는 점&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;(onfloor table floor)&lt;/code&gt; 같은 구문은 만들 수 있지만, &amp;lsquo;테이블은 고정된 가구이므로 옮길 수 없다&amp;rsquo;는 &lt;b&gt;물리적 상식&lt;/b&gt;이 결여되어 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;code&gt;:init&lt;/code&gt; 섹션도 비슷한 문제가 있었습니다. 예를 들어 다음과 같이 전혀 자연스럽지 않은 초기 상태가 등장했습니다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;(:init
  (ontop towel sink)
  (inside cup cupboard)
  (floating toothbrush air)
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 LLM이 형식은 맞추되, 실제 환경에서는 불가능한 배치를 자주 생성했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  개선: LLM 2단계 프롬프트 구조로 자연어로 먼저 변환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 2단계 프롬프트 구조를 도입했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;1단계 (Object Set &amp;rarr; Instruction)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Object Set을 입력으로 주고, LLM이 로봇팔이 수행 가능한 자연어 명령(instruction)을 생성하도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 LLM에 다음과 같은 입력을 주며 가능한 goal을 instruction 형식으로 달라고 요청합니다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;Objects:
{
  &quot;living_room&quot;: [&quot;table&quot;, &quot;cup&quot;],
  &quot;kitchen&quot;: [&quot;sink&quot;, &quot;detergent&quot;]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이런 대답을 얻을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;Clean the cup using detergent, then place it on the table in the living room.&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 모델이 방의 맥락과 물체의 조합을 고려해, 비교적 현실적인 태스크를 제안하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 모델 안에 이런 제약이 내재화된 것은 아니기 때문에 여전히 &amp;ldquo;sink를 들어서 옮기기&amp;rdquo; 같은 말도 안 되는 문장을 가끔 생성했지만, 그 빈도가 기존 방식대로 곧바로 BDDL을 생성할 때에 비해서는 월등히 줄어들었습니다!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 BDDL에 비해서 자연어 형태의 instruction은 가독성이 좋기 때문에 제가 빠르게 걸러낼 수도 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;2단계 (Instruction &amp;rarr; BDDL)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1단계에서 생성된 자연어 지시문을 다시 LLM에 입력으로 넣어, 이를 BDDL 형식으로 변환하도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 위 명령으로부터 다음과 같은 결과를 얻습니다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;(:init
   (onfloor cup floor)
   (onfloor detergent floor)
   (stained cup)
)
(:goal
   (and (not (stained cup))
        (ontop cup table))
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;b&gt;자연어 사고 &amp;rarr; 논리 구조 변환&lt;/b&gt; 단계를 분리하자, 훨씬 현실적이고 논리 일관성 있는 BDDL이 생성되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  추가 개선: init 자연스러움 향상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여전히 초기 상태(&lt;code&gt;:init&lt;/code&gt;)는 단조로운 경향이 있었습니다. LLM은 매번 &amp;ldquo;모든 물체는 floor 위에 있다&amp;rdquo;는 식으로 반복적인 구성을 만들어 냈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 BDDL을 생성하기 전, LLM에게 &amp;ldquo;상황을 간단히 묘사하게 하는&amp;rdquo; 단계를 추가했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 프롬프트를 이렇게 바꿨습니다.&lt;/p&gt;
&lt;pre class=&quot;smalltalk&quot;&gt;&lt;code&gt;Describe a realistic starting scene for this instruction:
&quot;Clean the cup using detergent, then place it on the table in the living room.&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[LLM Output]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;The cup is on the kitchen counter, the detergent is near the sink, and the living room table is empty.&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;b&gt;자연어로 상황을 먼저 상상하게 한 뒤&lt;/b&gt;, 그 출력을 기반으로 &lt;code&gt;:init&lt;/code&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;이 2단계 구조 덕분에 LLM은 BDDL 형식을 잘 따르면서도 로봇이 실제로 수행 가능한 행동만 포함하도록 생성할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 아직 &lt;b&gt;창발성(Emergent Capability)&lt;/b&gt; 을 테스트하기에는 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, LLM이 &amp;ldquo;가능한 행동을 올바르게 기술하는 것&amp;rdquo;은 가능하지만, 아직 &amp;ldquo;배운 행동을 조합하거나 변형하는 능력&amp;rdquo;까지는 도달하지 못했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스팅에서는 이 구조를 기반으로, 일부 Object나 Instruction을 변형해 창발적 행동을 실험하는 과정을 다뤄보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI/VLA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/454</guid>
      <comments>https://engineerinsight.tistory.com/454#entry454comment</comments>
      <pubDate>Thu, 23 Oct 2025 16:30:37 +0900</pubDate>
    </item>
    <item>
      <title>[AI/VLA] 창발성을 테스트할 수 있는 VLA 오픈소스 데이터셋 구축하기(1) (ft. BDDL, LIBERO)</title>
      <link>https://engineerinsight.tistory.com/453</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  인트로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BDDL(Behavior Description Definition Language)은 Embodied AI 연구에서 매우 중요한 자료구조로, 로봇의 행동을 정의하는 상황을 제시하는 언어입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VLA 연구에 있어서 굉장히 핵심적인 데이터셋으로 활용되고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  VLM/VLA란?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ VLM (Vision-Language Model)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;정의&lt;/b&gt;: 이미지(또는 영상)와 텍스트를 동시에 이해하고 처리할 수 있는 모델&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지를 보고 텍스트로 설명 생성 (Image Captioning)&lt;/li&gt;
&lt;li&gt;텍스트 기반 질의응답 (예: &quot;이 그림 속에 고양이가 몇 마리야?&quot;)&lt;/li&gt;
&lt;li&gt;멀티모달 검색 (이미지+텍스트 기반 검색)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;: GPT-4V, &lt;i&gt;CLIP&lt;/i&gt;, BLIP, Flamingo 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;활용 분야&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시각장애인용 이미지 설명&lt;/li&gt;
&lt;li&gt;이미지 기반 QA 챗봇&lt;/li&gt;
&lt;li&gt;제품 이미지 검색/추천&lt;/li&gt;
&lt;li&gt;자율주행, 의료영상 분석 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ VLA (Vision-Language-Action)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;정의&lt;/b&gt;: VLM에 &lt;b&gt;&lt;code&gt;행동(제어) 기능&lt;/code&gt;&lt;/b&gt;이 추가된 모델 &lt;b&gt;(Embodied AI)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순히 보고/말하는 것을 넘어서 &lt;b&gt;실제 행동을 계획하고 실행&lt;/b&gt;하는 능력까지 가짐&lt;/li&gt;
&lt;li&gt;&lt;i&gt;VLM을 보고 이미지 속 상황을 이해한 로봇 팔이 실제로 컵을 옮기는 일 등을 할 수 있도록&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;환경을 보고 상황을 이해한 후, 적절한 행동 결정해 실제 조작 실행&lt;/li&gt;
&lt;li&gt;텍스트 지시, 시각 입력을 기반으로 &lt;b&gt;로봇 조작&lt;/b&gt; 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;: RT-2 (Google DeepMind의 Robot Transformer), SayCan, PaLM-E&lt;/li&gt;
&lt;li&gt;&lt;b&gt;활용 분야&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가정용/산업용 로봇 &lt;i&gt;(책상을 치워줘 라고 말하면, 로봇이 물체 인식 후 치움)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;AR/VR 상호작용&lt;/li&gt;
&lt;li&gt;스마트 팩토리 자동화&lt;/li&gt;
&lt;li&gt;물류, 의료 보조 로봇&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 Embodied AI에 핵심적인 데이터셋으로 사용되는 BDDL에 대해서 이해해 봅시다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  VLM의 한계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ VLM의 한계: 공간적 추론 능력 부족&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 VLM은 꽤나 발전했습니다. 우리가 GPT에 사진을 넣은 후 설명해달라고 하면 꽤나 잘 하듯, 이미 놀라운 성과를 보이고 있기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 사진을 보고는 고양이가 있다는 인식할 수 있지만, 고양이가 옆에 있는 나무와 얼마나 떨어져 있는지, 다른 물체보다 앞에 있는건지 뒤에 있는건지 잘 구분할 수 없는 경우가 많습니다. &lt;i&gt;(공간적 추론 능력이 없는채로 곧장 로봇팔을 돌진할 수는 없겠쥬?)&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ (논문) Spatial VLM: 비전-언어 모델에 공간 추론 능력을 부여하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;CVPR 2024, Google DeepMind &amp;middot; MIT &amp;middot; Stanford&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 논문의 초록을 번역해 보았습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;물체들 사이의 &lt;b&gt;공간적 관계를 이해하고 추론하는 능력&lt;/b&gt;은 VQA(비주얼 질문응답)와 로보틱스에서 아주 중요한 능력입니다. 비전-언어 모델(VLM)은 여러 VQA 벤치마크에서 뛰어난 성능을 보였지만, 여전히 &lt;b&gt;3차원 공간 추론&lt;/b&gt; 능력은 부족합니다. 예를 들어, 두 물체 사이의 거리나 크기 차이를 정량적으로 파악하는 데 약합니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;우리는 이 한계가 모델 구조 때문이 아니라, &lt;b&gt;훈련 데이터에 3D 공간 지식이 부족하기 때문&lt;/b&gt;이라고 가정합니다. 이를 해결하기 위해, 인터넷 규모의 공간 추론 데이터를 이용해 VLM을 학습시키는 방법을 제안합니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;hellip;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 논문은 AI 모델이 모자라서가 아닌, 3D 공간추론에 대한 데이터셋을 더 제공해주면 이 문제가 해결될 것이라고 제안합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  (연구) LIBERO&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://libero-project.github.io/main.html&quot;&gt;LIBERO 바로가기&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 목표: 지식의 전이 (Transfer Learning)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LIBERO는 이렇게 공간 추론 능력까지 학습한 모델을 테스트하기 위한 데이터셋을 제시합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LIBERO의 핵심은 &lt;b&gt;Transfer Evaluation&lt;/b&gt;입니다. 즉 로봇이 이미 배운 작업을 얼마나 잘 응용할 수 있는가를 측정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서 &amp;ldquo;Put the cup on the table.&amp;rdquo;를 학습했다면, 새로운 태스크 &amp;ldquo;Put the cup on the shelf.&amp;rdquo;도 마찬가지로 동일하게 수행할 수 있어야 합니다. &lt;i&gt;(object가 바뀐 경우)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 로봇이 기존의 &amp;lsquo;컵을 집는 법&amp;rsquo;을 기억하고, 새로운 &amp;lsquo;선반 위치&amp;rsquo;를 인식해 올바른 행동으로 전이하는지를 평가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 LIBERO는 지속학습(Lifelong Learning), 전이학습(Transfer Learning), 멀티태스크 학습(Multi-task Learning) 연구의 공통 벤치마크로 활용됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ LIBERO 데이터셋&lt;/h3&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;1174&quot; data-origin-height=&quot;812&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doii4s/dJMb9LxejYw/Ct8UweCl29ruFQHJyzgobK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doii4s/dJMb9LxejYw/Ct8UweCl29ruFQHJyzgobK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doii4s/dJMb9LxejYw/Ct8UweCl29ruFQHJyzgobK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdoii4s%2FdJMb9LxejYw%2FCt8UweCl29ruFQHJyzgobK%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;656&quot; height=&quot;454&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;812&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LIBERO는 단일 데이터셋이 아니라, 여러 난이도와 주제를 가진 시리즈로 구성되어 있습니다.&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;LIBERO-Spatial&lt;/b&gt; : 공간적 위치 이해 중심으로 테스트 (ex. 위&amp;middot;아래&amp;middot;옆)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LIBERO-Object&lt;/b&gt; : 객체를 바꿔도 잘 동작하나? (ex. 컵을 들어라 vs 접시를 들어라)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LIBERO-Physical&lt;/b&gt; : 물리적 조작을 중심으로 테스트 (ex. 밀기, 당기기, 잡기)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LIBERO-Long&lt;/b&gt; : 여러 단계를 거치는 장기 과제 (ex. 컵을 씻어서 선반에 올리기)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 태스크는 &lt;b&gt;자연어 설명 + 로봇 행동 궤적(trajectory)&lt;/b&gt; + &lt;b&gt;시각 관찰 데이터&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;&lt;b&gt;자연어 지시 (Instruction)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;사람이 로봇에게 주는 명령&lt;/td&gt;
&lt;td&gt;&amp;ldquo;Put the mug on the shelf.&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시각 입력 (Visual Input)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;로봇이 보는 RGB-D 이미지&lt;/td&gt;
&lt;td&gt;카메라 시점의 장면 이미지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;행동(Action)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;로봇의 실제 제어 명령&lt;/td&gt;
&lt;td&gt;그리퍼 이동, 회전, 파지 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Emergent Capability (EC)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;emergent capability는 작은 모델에서는 전혀 없던 능력이, &lt;b&gt;모델의 크기나 복잡성이 일정 수준을 넘었을 때 갑자기 나타나는 현상&lt;/b&gt;을 말한다. 예를 들어 작은 언어모델은 단순한 문장 예측만 가능하지만, 일정 규모 이상이 되면 논리적 추론이나 언어 전이 같은 고차 사고를 스스로 수행할 수 있다. 이런 능력은 사람이 직접 설계하거나 지도하지 않아도 데이터와 모델 구조만으로 &amp;lsquo;자연스럽게&amp;rsquo; 발현된다는 점에서 의미가 크다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ VLA에서의 EC&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 개념을 vla(vision-language-action) 모델에 적용하면, 단순히 보는 것과 말하는 것을 넘어서 보고 판단하고 &lt;b&gt;&lt;i&gt;행동하는&lt;/i&gt;&lt;/b&gt; 능력으로 확장된다. 예를 들어 로봇이 컵을 인식하고, 그것을 옮기라는 명령을 받았을 때, 학습하지 않은 상황에서도 스스로 최적의 경로를 판단하거나 새로운 행동 시퀀스를 만들어내는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, vla의 emergent capability는 개별 인식&amp;middot;언어&amp;middot;행동 모듈이 결합되면서, 시스템 전체가 자율적인 의사결정과 문제 해결 능력을 보이기 시작하는 현상으로 이해할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  BDDL&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BDDL은 Embodied AI 연구에서 실험 환경과 행동 목표를 기술하기 위한 데이터 포맷으로, LIBERO&amp;middot;BEHAVIOR 등 VLA 데이터셋에서 표준적으로 사용됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ BDDL의 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BDDL의 구조는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;BDDL
├── PDDL
│   ├── Scene
│   │   ├── Objects
│   │   └── Background
│   ├── Init
│   └── Goal
└── Instruction
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ BDDL 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 BDDL 문법으로 작성한 하나의 예시이다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;(define 
    (problem cleaning_the_pool_0)
    (:domain igibson)

    (:objects
         pool.n.01_1 - pool.n.01
        floor.n.01_1 - floor.n.01
        scrub_brush.n.01_1 - scrub_brush.n.01
        shelf.n.01_1 - shelf.n.01
        detergent.n.02_1 - detergent.n.02
        sink.n.01_1 - sink.n.01
        agent.n.01_1 - agent.n.01
    )

    (:init 
        (onfloor pool.n.01_1 floor.n.01_1) 
        (stained pool.n.01_1) 
        (onfloor scrub_brush.n.01_1 floor.n.01_1) 
        (onfloor detergent.n.02_1 floor.n.01_1) 
        (inroom shelf.n.01_1 garage) 
        (inroom floor.n.01_1 garage) 
        (inroom sink.n.01_1 storage_room)
        (onfloor agent.n.01_1 floor.n.01_1)
    )

    (:goal 
        (and 
            (onfloor ?pool.n.01_1 ?floor.n.01_1) 
            (not 
                (stained ?pool.n.01_1)
            ) 
            (ontop ?scrub_brush.n.01_1 ?shelf.n.01_1) 
            (onfloor ?detergent.n.02_1 ?floor.n.01_1)
        )
    )
)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;:objects&lt;/code&gt;와 &lt;code&gt;:init&lt;/code&gt; 섹션은 &lt;b&gt;에이전트가 시작하는 초기 상태&lt;/b&gt;를 정의함.&amp;ldquo;이 물체가 어떤 공간에 있고, 어떤 상태에 있는지&amp;rdquo;를 명시함.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;inroom&lt;/code&gt;은 &lt;b&gt;객체가 어떤 방(공간)에 존재해야 하는지&lt;/b&gt;를 나타냄.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ontop&lt;/code&gt;, &lt;code&gt;inside&lt;/code&gt; 등은 &lt;b&gt;작은 물체의 배치 위치(위치 관계)&lt;/b&gt; 를 나타냄.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;예를 들어 &lt;code&gt;(onfloor scrub_brush floor)&lt;/code&gt;나 &lt;code&gt;(inroom sink storage_room)&lt;/code&gt; 같은 구문은&lt;/li&gt;
&lt;li&gt;이렇게 정의된 상태는 BDDL 내부에서 &lt;b&gt;시뮬레이터 샘플링 기능&lt;/b&gt;으로 전달되어, 실제 3D 물리 장면에 객체들을 배치할 때 사용됨. &lt;i&gt;(여기에는 Mujoco가 사용됨)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:goal&lt;/code&gt; 섹션은 &lt;b&gt;활동이 성공하기 위해 만족해야 할 조건&lt;/b&gt;을 정의함.&lt;br /&gt;예를 들어 &lt;code&gt;(not (stained pool))&lt;/code&gt; 는 pool의 얼룩이 없어져야 한다를 의미하는데, &lt;i&gt;이렇게 써두면 로봇이 수영쟝을 겁나 닦을까?를 테스트하는 것입니다&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;BDDL은 이런 목표 상태만 정의하고, &lt;b&gt;어떤 과정을 통해 도달할지는 명시하지 않음&lt;/b&gt;. 즉, &amp;ldquo;행동 시퀀스&amp;rdquo;가 아니라 최종 상태 조건만 규정하는 &lt;b&gt;과정 비의존적(process-agnostic)&lt;/b&gt; 구조라는 뜻입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI/VLA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/453</guid>
      <comments>https://engineerinsight.tistory.com/453#entry453comment</comments>
      <pubDate>Thu, 23 Oct 2025 16:00:02 +0900</pubDate>
    </item>
    <item>
      <title>[컴퓨터그래픽스] 3D 그래픽스에서의 좌표 변환: Local &amp;rarr; World &amp;rarr; View &amp;rarr; Clip &amp;rarr; Screen</title>
      <link>https://engineerinsight.tistory.com/452</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;3D 그래픽스에서 물체가 화면에 보이기까지는 여러 단계의 좌표계 변환을 거칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내가 만든 물체의 좌표가 카메라 기준으로 바뀌고, 최종적으로 &amp;ldquo;모니터 픽셀 위치&amp;rdquo;로 매핑되는 과정입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(이 모든 과정은 행렬 연산으로 이루어집니당)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 Local &amp;rarr; World &amp;rarr; View &amp;rarr; Clip &amp;rarr; Screen 순서로 이 과정을 정리해봤습니다.&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;  3D 그래픽스 좌표 변환 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Model Matrix: Local Space &amp;rarr; World Space&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Local Space&lt;/b&gt;는 물체 자체의 기준 좌표계입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 큐브를 원점(0,0,0)에 만들었을 때, 이건 &amp;ldquo;세상 속의 큐브&amp;rdquo;가 아니라 &amp;ldquo;큐브 자신 입장에서의 좌표&amp;rdquo;예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;Model Matrix&lt;/code&gt;를 적용하면 이 큐브를 &lt;b&gt;세상(World)&lt;/b&gt; 안의 적절한 위치, 방향, 크기로 옮길 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;mat4.translate(modelMatrix, modelMatrix, [x, y, z]);
mat4.rotateZ(modelMatrix, modelMatrix, angle);
mat4.scale(modelMatrix, modelMatrix, [sx, sy, sz]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 이렇게 &lt;code&gt;translate &amp;rarr; rotate &amp;rarr; scale&lt;/code&gt; 순서로 작성하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행렬은 &lt;b&gt;곱셈 순서가 반대(S * R * T)&lt;/b&gt; 로 적용된다는 점을 꼭 기억해야 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행렬 곱셈은 교환법칙이 성립하지 않습니다 (not commutative).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, TRS &amp;ne; SRT 입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ View Matrix: World Space &amp;rarr; View Space&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;World Space&lt;/b&gt;는 장면(Scene) 전체의 공통 좌표계입니다. 여러 개의 오브젝트가 한 공간 안에 놓이는 단계입니당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우리가 보는 화면은 &amp;ldquo;세상&amp;rdquo;이 아니라 &amp;ldquo;카메라가 바라보는 시점(view)&amp;rdquo;이기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 카메라 위치 기준으로 좌표계를 바꿔줘야 합니다. 그게 바로 &lt;code&gt;View Matrix&lt;/code&gt; 입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;mat4.lookAt(viewMatrix, eye, center, up);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;eye&lt;/code&gt;: 카메라 위치 (예: &lt;code&gt;[0, 0, 5]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;center&lt;/code&gt;: 카메라가 바라보는 대상 (예: &lt;code&gt;[0, 0, 0]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;up&lt;/code&gt;: 카메라의 위쪽 방향 (보통 &lt;code&gt;[0, 1, 0]&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;lookAt&lt;/code&gt; 내부적으로는 많은 계산이 일어납니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카메라 위치(COP)를 원점으로 이동&lt;/li&gt;
&lt;li&gt;Z축 방향을 반전시켜 화면 쪽으로 바라보게 만듦&lt;/li&gt;
&lt;li&gt;카메라 기준 프레임을 새로 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 행렬 곱은 프레임 변경 &amp;times; Z축 반전 &amp;times; 카메라 원점 이동 순으로 일어납니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Projection Matrix: View Space &amp;rarr; Clip Space&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 카메라 기준으로 본 장면을 &amp;ldquo;화면 좌표&amp;rdquo;로 압축하는 단계입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Projection Matrix&lt;/code&gt;는 &lt;b&gt;3D 좌표를 2D 평면으로 투영&lt;/b&gt;하는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원근 투영 (Perspective Projection)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;mat4.perspective(out, fov, aspect, near, far);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;fov&lt;/code&gt;: 시야각 (예: &lt;code&gt;Math.PI / 4&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aspect&lt;/code&gt;: 화면 비율 (예: &lt;code&gt;canvas.width / canvas.height&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;near&lt;/code&gt;, &lt;code&gt;far&lt;/code&gt;: 시야 깊이 범위&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀리 있는 물체는 작게, 가까운 물체는 크게 표현되어 현실감이 살아납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;직교 투영 (Orthographic Projection)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;mat4.ortho(out, left, right, bottom, top, near, far);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비율이 일정해서 &lt;b&gt;원근감이 없습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;CAD나 UI 렌더링, 미니맵 등에 자주 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Viewport Matrix: Clip Space &amp;rarr; Screen Space&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;투영까지 끝나면 이제 GPU가 그 좌표를 화면에 실제 픽셀로 옮기는 단계입니다. 화면에 어디까지 등장하고 잘려얗라지를 이 단계에서 결정하게 되고, 최종적인 화면에서 렌더링될 픽셀 좌표까지 결정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 나오는 결과가 &lt;code&gt;gl_Position&lt;/code&gt; 입니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값은 &amp;ldquo;&lt;b&gt;Clip Space&lt;/b&gt;&amp;rdquo;라고 불리는 중간 좌표계에 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 GPU는 내부적으로 &lt;code&gt;w&lt;/code&gt;로 나누어 &lt;b&gt;NDC(Normalized Device Coordinates)&lt;/b&gt; 로 변환합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NDC는 [-1, 1] 범위로 정규화된 좌표계입니다.&lt;/li&gt;
&lt;li&gt;x, y, z가 이 범위를 벗어나면 클리핑(clipping)되어 화면에 안 나옵니다. &lt;i&gt;(1.234325 이런건 렌더링 안됩니당)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;[-1, 1]&lt;/code&gt; &amp;rarr; 화면 왼쪽부터 오른쪽, 아래에서 위까지의 픽셀 공간으로 변환됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 작성한 3D 모델이 GPU의 렌더 파이프라인을 거쳐 &amp;ldquo;화면의 픽셀 위치(Screen Space)&amp;rdquo; 로 매핑&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  3D 그래픽스 좌표 변환 요약&lt;/h2&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;Local &amp;rarr; World&lt;/td&gt;
&lt;td&gt;Model Matrix&lt;/td&gt;
&lt;td&gt;물체 기준에서 세계 좌표로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;World &amp;rarr; View&lt;/td&gt;
&lt;td&gt;View Matrix&lt;/td&gt;
&lt;td&gt;카메라 기준 좌표로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;View &amp;rarr; Clip&lt;/td&gt;
&lt;td&gt;Projection Matrix&lt;/td&gt;
&lt;td&gt;3D를 2D로 투영&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clip &amp;rarr; Screen&lt;/td&gt;
&lt;td&gt;Viewport Matrix&lt;/td&gt;
&lt;td&gt;GPU가 NDC를 픽셀로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/컴퓨터그래픽스</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/452</guid>
      <comments>https://engineerinsight.tistory.com/452#entry452comment</comments>
      <pubDate>Thu, 23 Oct 2025 15:30:40 +0900</pubDate>
    </item>
    <item>
      <title>[컴퓨터그래픽스] 3D 조명: Local/Global Illumination, Phong 반사 모델</title>
      <link>https://engineerinsight.tistory.com/451</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  조명(Lighting)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3D 그래픽스에서 &quot;조명&quot;은 단순히 화면을 밝게 만드는 게 아닙니다. 입체감을 만들고, 소재의 질감을 표현하고, 장면의 분위기를 결정짓는 핵심 요소입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 &lt;b&gt;광원의 위치, 표면의 재질, 관찰자의 시점&lt;/b&gt;이 결합되어 물체의 입체감과 사실감을 결정짓습니다.&lt;br /&gt;즉, 조명은 &amp;ldquo;렌더링의 질&amp;rdquo;을 좌우하는 핵심 요소입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스팅에서는 Local/Global 조명, Phong 모델, 셰이딩 기법에 대해서 다루려고 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Local vs Global Illumination&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Local Illumination (직접 조명)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조명과 물체 간의 &lt;b&gt;직접적인 상호작용만&lt;/b&gt; 고려합니다.&lt;/li&gt;
&lt;li&gt;반사, 투과, 그림자 등의 &lt;b&gt;간접 조명은 무시&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;예. &lt;b&gt;Phong 모델&lt;/b&gt;, &lt;b&gt;Cook-Torrance 모델&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Global Illumination (간접 조명 포함)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조명뿐 아니라 &lt;b&gt;다른 표면에서 반사된 빛&lt;/b&gt;도 고려합니다.&lt;/li&gt;
&lt;li&gt;보다 &lt;b&gt;현실적이지만 계산량이 많음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예. &lt;b&gt;Ray tracing&lt;/b&gt;, &lt;b&gt;Radiosity&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Local은 손전등처럼 직접 비추는 빛만, Global은 벽에서 반사돼 다시 들어오는 빛까지 생각하는 방식입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Phong 반사 모델&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Phong 모델은 현실적인 조명을 간단한 수학으로 근사한 대표적인 로컬 조명 모델입니다. 세 가지 성분을 더해서 최종 색상을 만듭니다&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;655&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNh9RP/dJMb9gRsEjc/cCYkifCxAgVn6OS8vtQ5gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNh9RP/dJMb9gRsEjc/cCYkifCxAgVn6OS8vtQ5gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNh9RP/dJMb9gRsEjc/cCYkifCxAgVn6OS8vtQ5gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNh9RP%2FdJMb9gRsEjc%2FcCYkifCxAgVn6OS8vtQ5gk%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;655&quot; height=&quot;182&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Ambient (환경광)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주변에 퍼져 있는 일관된 빛&lt;/li&gt;
&lt;li&gt;물체가 &lt;b&gt;어디에 있든 항상 일정한 밝기&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;I_ambient = k_a * I_a&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;k&lt;/code&gt;는 가중치&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Diffuse (난반사광)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;물체 표면이 거칠어서 빛을 여러 방향으로 퍼뜨리는 효과&lt;/li&gt;
&lt;li&gt;&lt;b&gt;입사각이 작을수록 밝게 보임&lt;/b&gt; (램버트 법칙)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;I_diffuse = k_d * I_l * max(0, N &amp;middot; L)&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;N&lt;/code&gt;: 표면의 법선 벡터&lt;/li&gt;
&lt;li&gt;&lt;code&gt;L&lt;/code&gt;: 빛의 방향&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Specular (정반사광)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;표면이 반들반들할 때 생기는 하이라이트&lt;/li&gt;
&lt;li&gt;관찰자 방향에 따라 밝기가 달라짐&lt;/li&gt;
&lt;li&gt;계산 식:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;R&lt;/code&gt;: 반사 벡터&lt;/li&gt;
&lt;li&gt;&lt;code&gt;V&lt;/code&gt;: 시선 벡터&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt;: shininess 계수 (클수록 날카롭고 작은 하이라이트)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;I_specular = k_s * I_l * (R &amp;middot; V)^n&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세가지를 모두 합하면 Phong Reflection 값이 된다!!!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Specular Reflection에서 n의 영향&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt;이 클수록 하이라이트가 작고 날카로워집니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt;이 작으면 넓게 퍼지고 흐릿한 하이라이트가 나타납니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 금속처럼 반들거리는 물체는 &lt;code&gt;n&lt;/code&gt;이 큽니당&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 셰이딩 기법: Gouraud vs Phong&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셰이딩(Shading)은 조명 모델을 &lt;b&gt;다각형에 적용하는 방식&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;&lt;b&gt;Gouraud&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;정점에서 색상을 계산하고, 픽셀에 &lt;b&gt;보간&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;빠름, 부드럽지만 하이라이트 표현 약함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Phong&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;정점에서 법선 벡터를 보간하여 픽셀마다 조명 계산&lt;/td&gt;
&lt;td&gt;느리지만 &lt;b&gt;정밀한 하이라이트&lt;/b&gt; 표현 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ Gouraud: 색상을 보간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ Phong: 법선 벡터를 보간&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Flat vs Smooth Shading&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Flat Shading&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;삼각형 하나마다 &lt;b&gt;하나의 법선 벡터&lt;/b&gt; 사용 &amp;rarr; 각지게 표현&lt;/li&gt;
&lt;li&gt;각 면마다 다른 색상 &amp;rarr; 다소 투박 &lt;i&gt;(완성도 겁나 낮아보임)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Smooth Shading&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;정점에서의 법선&lt;/b&gt;을 보간하여 &lt;b&gt;부드럽게 연결&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;곡면 같은 느낌을 줄 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebGL에서는 보통 Phong + Smooth 조합을 많이 씁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조명에 대해서 구체적으로 계산할 때 근사하는 식도 있습니다만 이건 개발을 위한 개념 이해에는 그닥 필요 없다고 생각해 스킵하겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/컴퓨터그래픽스</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/451</guid>
      <comments>https://engineerinsight.tistory.com/451#entry451comment</comments>
      <pubDate>Thu, 23 Oct 2025 15:00:15 +0900</pubDate>
    </item>
    <item>
      <title>[컴퓨터그래픽스] Three.js를 사용해 3D 게임을 만들어보자!</title>
      <link>https://engineerinsight.tistory.com/450</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  3D 게임 보러가기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2940&quot; data-origin-height=&quot;1840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqZDXd/dJMb9WFrfl9/kjnHDfdGncIQaNnQUNm1R1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqZDXd/dJMb9WFrfl9/kjnHDfdGncIQaNnQUNm1R1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqZDXd/dJMb9WFrfl9/kjnHDfdGncIQaNnQUNm1R1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqZDXd%2FdJMb9WFrfl9%2FkjnHDfdGncIQaNnQUNm1R1%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;566&quot; height=&quot;354&quot; data-origin-width=&quot;2940&quot; data-origin-height=&quot;1840&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://computer-graphics-yonsei.github.io/TermProject/garden&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://computer-graphics-yonsei.github.io/TermProject/garden&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1761196151253&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;https://computer-graphics-yonsei.github.io/TermProject/garden&quot; data-og-description=&quot;꽃에 물을 주세요! 이동: 목표지점 클릭 및 WASD 물 주기: 영역 내부 클릭 및 Space 카메라 조정: Orbit Control Watered: 0 / 325&quot; data-og-host=&quot;computer-graphics-yonsei.github.io&quot; data-og-source-url=&quot;https://computer-graphics-yonsei.github.io/TermProject/garden&quot; data-og-url=&quot;https://computer-graphics-yonsei.github.io/TermProject/garden&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://computer-graphics-yonsei.github.io/TermProject/garden&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://computer-graphics-yonsei.github.io/TermProject/garden&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;https://computer-graphics-yonsei.github.io/TermProject/garden&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;꽃에 물을 주세요! 이동: 목표지점 클릭 및 WASD 물 주기: 영역 내부 클릭 및 Space 카메라 조정: Orbit Control Watered: 0 / 325&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;computer-graphics-yonsei.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 레포지토리: &lt;a href=&quot;https://github.com/computer-graphics-yonsei/TermProject&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/computer-graphics-yonsei/TermProject&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1761196167382&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - computer-graphics-yonsei/TermProject&quot; data-og-description=&quot;Contribute to computer-graphics-yonsei/TermProject development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/computer-graphics-yonsei/TermProject&quot; data-og-url=&quot;https://github.com/computer-graphics-yonsei/TermProject&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/z0hZo/hyZMn0Z8G2/D7NqO3m22rx85okpHAbk31/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/b16iyO/hyZMhzI5D4/80l9OYDe2MW4aSIeLIDKK0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/computer-graphics-yonsei/TermProject&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/computer-graphics-yonsei/TermProject&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/z0hZo/hyZMn0Z8G2/D7NqO3m22rx85okpHAbk31/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/b16iyO/hyZMhzI5D4/80l9OYDe2MW4aSIeLIDKK0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - computer-graphics-yonsei/TermProject&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to computer-graphics-yonsei/TermProject development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Three.js&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Three.js는 웹 브라우저 안에서 고성능 3D 그래픽을 구현할 수 있도록 도와주는 &lt;b&gt;JavaScript 기반의 고수준(high-level) 라이브러리&lt;/b&gt;입니다.&lt;br /&gt;WebGL은 저수준 API로, 버텍스 셰이더와 프래그먼트 셰이더를 직접 작성해야 하며, 카메라&amp;middot;조명&amp;middot;모델 관리도 모두 수작업으로 처리해야 합니다.&lt;br /&gt;Three.js는 이러한 복잡한 단계를 고수준의 객체 지향 구조(Scene, Camera, Mesh 등)로 추상화하여 생산성을 비약적으로 높여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Three.js의 구성 요소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Three.js로 3D 장면(scene)을 구성하려면 일반적으로 다음과 같은 요소들이 필요합니다.&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;Scene&lt;/b&gt;: 모든 3D 객체들이 들어갈 컨테이너
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;THREE.Scene()&lt;/code&gt; 객체를 생성&lt;/li&gt;
&lt;li&gt;이 안에 &lt;b&gt;Mesh, Light, Camera&lt;/b&gt; 등 모든 객체를 &lt;code&gt;scene.add()&lt;/code&gt;를 통해 추가해야 렌더링됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Camera&lt;/b&gt;: 장면을 바라보는 시점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;new THREE.PerspectiveCamera(fov, aspect, near, far)&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;fov&lt;/code&gt;: 시야각(Field of View), 일반적으로 50~75도 사이.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aspect&lt;/code&gt;: 화면 종횡비 (보통 &lt;code&gt;window.innerWidth / window.innerHeight&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;near&lt;/code&gt;: 카메라 앞에서 어느 거리까지 볼 것인지 (near clipping plane)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;far&lt;/code&gt;: 어느 거리까지 그릴 것인지 (far clipping plane)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Renderer&lt;/b&gt;: Scene + Camera 조합을 통해 브라우저에 그려주는 역할 (&lt;code&gt;WebGLRenderer&lt;/code&gt;를 주로 사용)&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-jsx&quot;&gt;  const renderer = new THREE.WebGLRenderer();
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement); // canvas를 HTML에 추가&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Geometry와 Material&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Geometry는 Three.js에서 자주 쓰이는 도형들을 쉽게 만들 수 있게 해주는 도구 클래스&lt;/b&gt;&lt;br /&gt;개발자는 별도로 정점 데이터를 설계할 필요 없이, 이 생성자만 호출하면 곧바로 3D 모델을 씬에 추가할 수 있게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Geometry&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;CylinderGeometry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;원기둥&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BoxGeometry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;큐브&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SphereGeometry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;구&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IcosahedronGeometry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;정12면체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ConvexGeometry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3D 점 배열로 convex shape 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LatheGeometry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2D 곡선을 회전시켜 만든 회전체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OctahedronGeometry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;정8면체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ParametricGeometry.klein&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Klein bottle (수학적 곡면)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TetrahedronGeometry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;정4면체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TorusGeometry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;도넛 모양&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TorusKnotGeometry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;매듭 형태의 도넛&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;- &lt;b&gt;Material&lt;/b&gt;은 광원에 어떻게 반응할지를 정의합니다.&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;- &lt;code&gt;MeshPhongMaterial&lt;/code&gt;: ambient + diffuse + specular 모두 포함&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;- &lt;code&gt;MeshLambertMaterial&lt;/code&gt;: ambient + diffuse만 포함&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;const material = new THREE.MeshPhongMaterial({
  color: 0xffaa00,
  shininess: 100,
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 조명 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Three.js에서는 여러 광원을 지원합니다.&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;AmbientLight&lt;/b&gt;: 전체 장면에 고르게 퍼지는 빛&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PointLight&lt;/b&gt;: 특정 지점에서 퍼지는 빛&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DirectionalLight&lt;/b&gt;: 태양처럼 병렬로 쏘는 빛&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SpotLight&lt;/b&gt;: 특정 방향으로 원뿔형으로 퍼지는 빛&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const light = new THREE.PointLight(0xffffff, 1, 100);
light.position.set(10, 10, 10);
scene.add(light);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Scene 그래프와 계층 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Three.js의 scene은 실제로 &lt;b&gt;Tree 구조&lt;/b&gt;로 되어 있으며, 각 객체(Object3D, Mesh, Light 등)는 서로 parent-child 관계를 가질 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/berLxB/dJMb9WFrfmn/2yesCZlzdag9dWg2Uy8ukK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/berLxB/dJMb9WFrfmn/2yesCZlzdag9dWg2Uy8ukK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/berLxB/dJMb9WFrfmn/2yesCZlzdag9dWg2Uy8ukK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FberLxB%2FdJMb9WFrfmn%2F2yesCZlzdag9dWg2Uy8ukK%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;347&quot; height=&quot;347&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const group = new THREE.Group();
group.add(mesh1);
group.add(mesh2);
scene.add(group);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 계층적인 애니메이션이나 복잡한 모델의 구성도 쉽게 처리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 카메라 종류와 시점 설정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;PerspectiveCamera&lt;/b&gt;: 원근감 있는 시야 (3D 게임, VR에 적합)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OrthographicCamera&lt;/b&gt;: 평면적인 시야 (UI, 설계도, CAD에 적합)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
camera.position.z = 5;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 렌더링과 애니메이션 루프&lt;/h3&gt;
&lt;pre class=&quot;openscad&quot;&gt;&lt;code&gt;function animate() {
  requestAnimationFrame(animate);
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  renderer.render(scene, camera);
}
animate();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Three.js에서는 &lt;code&gt;requestAnimationFrame()&lt;/code&gt;을 사용하여 60FPS 기반의 애니메이션 루프를 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 텍스처와 맵핑&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Three.js에서는 다음과 같은 다양한 텍스처 매핑을 지원합니다:&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;기본 텍스처 (diffuse map)&lt;/b&gt;: 표면 색상&lt;/li&gt;
&lt;li&gt;&lt;b&gt;범프 맵 (bump map)&lt;/b&gt;: 높낮이 효과&lt;/li&gt;
&lt;li&gt;&lt;b&gt;노멀 맵 (normal map)&lt;/b&gt;: 정밀한 광원 효과&lt;/li&gt;
&lt;li&gt;&lt;b&gt;환경 맵 (env map)&lt;/b&gt;: 반사 효과&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;const texture = new THREE.TextureLoader().load('brick.jpg');
const material = new THREE.MeshStandardMaterial({ map: texture });&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 객체 선택 및 이벤트 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Raycaster를 활용하면 마우스 클릭이나 hover 이벤트를 통해 3D 객체와 상호작용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// 마우스 이벤트 등록 후
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 외부 모델 및 애니메이션 로딩&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Three.js는 &lt;code&gt;.fbx&lt;/code&gt;, &lt;code&gt;.glTF&lt;/code&gt; 등의 포맷을 지원하며, &lt;code&gt;FBXLoader&lt;/code&gt;, &lt;code&gt;GLTFLoader&lt;/code&gt; 등을 통해 외부 모델과 애니메이션을 로드할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const loader = new FBXLoader();
loader.load('character.fbx', (object) =&amp;gt; {
  scene.add(object);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애니메이션은 &lt;code&gt;AnimationMixer&lt;/code&gt;를 통해 재생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/컴퓨터그래픽스</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/450</guid>
      <comments>https://engineerinsight.tistory.com/450#entry450comment</comments>
      <pubDate>Thu, 23 Oct 2025 14:30:17 +0900</pubDate>
    </item>
    <item>
      <title>[AI/RAG] RAG 기반 희귀품종 무화과 LLM 챗봇 프로젝트(3): AI Agent를 통한 pure-RAG 챗봇 완성!</title>
      <link>https://engineerinsight.tistory.com/449</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;관련 레포: &lt;a href=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/tree/pure-RAG&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/AiResearch2025/FigVarietyRAGChat/tree/pure-RAG&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764230087497&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - AiResearch2025/FigVarietyRAGChat: RAG 기반 희귀품종 무화과 챗봇 프로젝트&quot; data-og-description=&quot;RAG 기반 희귀품종 무화과 챗봇 프로젝트. Contribute to AiResearch2025/FigVarietyRAGChat development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/tree/pure-RAG&quot; data-og-url=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cO88kY/hyZOoNiEqJ/jUIYaqKc3LiPGuWJOns42k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bcuXwU/hyZOwdu3lk/a91Yim8rqGBnnjydxbRP40/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/tree/pure-RAG&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/tree/pure-RAG&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cO88kY/hyZOoNiEqJ/jUIYaqKc3LiPGuWJOns42k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bcuXwU/hyZOwdu3lk/a91Yim8rqGBnnjydxbRP40/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - AiResearch2025/FigVarietyRAGChat: RAG 기반 희귀품종 무화과 챗봇 프로젝트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;RAG 기반 희귀품종 무화과 챗봇 프로젝트. Contribute to AiResearch2025/FigVarietyRAGChat development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  AI Agent&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ AI Agent가 해야 할 일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Agent는 사용자의 질문이 들어오면 곧바로 그 질문을 LLM에 돌리기 전에 우선 질문을 아래와 같이 분류하고, 각 분류에 따라서 우리가 정해놓은 로직에 따라서 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;사용자가 특정 품종 이름을 알고있는가?&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그 품종이 실제 존재하는(또는 RAG 데이터에 존재하는) 품종인가?&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;현재 질문이 RAG 검색으로 해결 가능한가, 아니면 추가 질의가 필요한가?&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; &lt;/b&gt; AI Agent의 질문 분류 및 처리&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1386&quot; data-origin-height=&quot;1156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bohlMx/dJMb9Ogpo2d/bV8oPrCBf7EP7DjoRbdmv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bohlMx/dJMb9Ogpo2d/bV8oPrCBf7EP7DjoRbdmv0/img.png&quot; data-alt=&quot;목표&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bohlMx/dJMb9Ogpo2d/bV8oPrCBf7EP7DjoRbdmv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbohlMx%2FdJMb9Ogpo2d%2FbV8oPrCBf7EP7DjoRbdmv0%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;732&quot; height=&quot;611&quot; data-origin-width=&quot;1386&quot; data-origin-height=&quot;1156&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;목표&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ CHITCHAT&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설명: &quot;안녕&quot;, &quot;고마워&quot; 등과 같이 간단한 인사나 감사 표현이 포함된 일상적인 대화입니다.&lt;/li&gt;
&lt;li&gt;코드 로직: chitchat_keywords = [&quot;안녕&quot;, &quot;고마워&quot;, &quot;감사&quot;, &quot;땡큐&quot;] 키워드가 있는지 확인합니다.&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;안녕?&quot;&lt;/li&gt;
&lt;li&gt;&quot;고마워&quot;&lt;/li&gt;
&lt;li&gt;&quot;답변 감사합니다&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 질문에 &quot;고마워&quot;, &quot;감사&quot;, &quot;땡큐&quot;가 포함되어 있다면, &quot;천만에요! 더 궁금한 점이 있으신가요?&quot;라는&lt;br /&gt;정해진 답변을 반환합니다.&lt;/li&gt;
&lt;li&gt;그 외의 경우(예: &quot;안녕&quot;)는 &quot;안녕하세요! 무화과에 대해 무엇이든 물어보세요.&quot;라는 인사말을 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ UNKNOWN_VARIETY_IMAGE_QUERY&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설명: &quot;사진&quot;, &quot;이미지&quot; 등의 키워드를 포함하여 이미지 속 무화과 품종을 물어보는 질문입니다.&lt;/li&gt;
&lt;li&gt;코드 로직: image_keywords = [&quot;사진&quot;, &quot;이미지&quot;] 키워드가 있는지 확인합니다.&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;이 사진 속 무화과는 무슨 품종이야?&quot;&lt;/li&gt;
&lt;li&gt;&quot;이미지를 보여줄테니 품종을 알려줘&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;처리: &quot;현재는 이미지를 분석하여 품종을 식별하는 기능이 지원되지&lt;br /&gt;않습니다.&quot;라는 고정된 메시지를 반환합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추후에 LLM을 통해 이미지의 내용을 질문자의 쿼리에 포함하는 방식으로 개선 예정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ KNOWN_VARIETY_TYPO&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설명: 시스템에 미리 정의된, 사용자가 자주 틀리는 품종명 오타가 포함된 질문입니다.&lt;/li&gt;
&lt;li&gt;코드 로직: known_typos = [&quot;ciccio vero&quot;] 리스트에 포함된 오타가 있는지 확인합니다. (현재는 &quot;Ciccio Vero&quot;만 해당)&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;Ciccio Vero는 키우기 쉬운가요?&quot; (&amp;rarr; 'Ciccio Nero'의 오타로 인식)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;처리: 사용자에게 올바른 품종명을 되묻는 질문을 생성하여 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ KNOWN_VARIETY_NEEDS_CLARIFICATION&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설명: 특정 품종 이름이 언급되었지만, &quot;어때?&quot;, &quot;좋아?&quot; 와 같이 질문이 너무 광범위하여 구체적인 답변을 주기 어려운 경우입니다. 에이전트는 더 구체적인 질문을 하도록 유도합니다.&lt;/li&gt;
&lt;li&gt;코드 로직: clarification_keywords = [&quot;어때&quot;, &quot;좋아&quot;, &quot;키울만&quot;, &quot;괜찮&quot;] 키워드가 있는지 확인합니다.&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;브런즈윅 품종 어때?&quot;&lt;/li&gt;
&lt;li&gt;&quot;하디 시카고 키울만 한가요?&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;처리: data 딕셔너리에서 품종명(variety)을 가져온 뒤, &quot;어떤 점이&lt;br /&gt;궁금하신가요? 맛, 생산성, 나무 크기 등 구체적인 기준을 알려주시면&lt;br /&gt;더 자세히 답변해 드릴 수 있습니다.&quot;라며 더 구체적인 정보를&lt;br /&gt;요구하는 응답을 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ KNOWN_VARIETY_EXISTS&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설명: 특정 품종 이름이 명확하게 포함된 구체적인 질문입니다. 에이전트는 이 품종에 대한 정보를 찾아 답변합니다.&lt;/li&gt;
&lt;li&gt;코드 로직: 위의 TYPO나 NEEDS_CLARIFICATION에 해당하지 않으면서, 알려진 품종명(또는 별명)이 질문에 포함된 경우입니다.&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;브런즈윅의 내한성은 어떤가요?&quot;&lt;/li&gt;
&lt;li&gt;&quot;스트로베리 베르테(SV) 품종의 특징 알려줘&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;처리
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;data 딕셔너리에서 전체 질문(query)과 품종명(variety)을&lt;br /&gt;가져옵니다.&lt;/li&gt;
&lt;li&gt;벡터 데이터베이스에서 사용자의 전체&lt;br /&gt;질문과 가장 유사한 정보 3개를 검색합니다. (&lt;code&gt;self.vector_db.similarity_search(data['query'], k=3)&lt;/code&gt;를 호출)&lt;/li&gt;
&lt;li&gt;검색 결과가 없으면 &quot;관련 정보를 찾지 못했습니다.&quot;라는&lt;br /&gt;메시지를 반환합니다.&lt;/li&gt;
&lt;li&gt;결과가 있으면, 각 결과의 내용을 합쳐 가장 관련성 높은 정보를 모아 컨텍스트를 구성하고, RAG를 통해 좋은 품질의 답변을 제공합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ UNKNOWN_VARIETY_GENERAL_QUERY&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설명: 특정 품종명이 언급되지 않은, 일반적인 무화과 관련 질문입니다. 에이전트는 질문의 키워드를 바탕으로 가장 관련성 높은 품종 정보를 추천해 줍니다.&lt;/li&gt;
&lt;li&gt;코드 로직: 위 5가지 분류에 모두 해당하지 않는 모든 질문입니다.&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;달콤한 품종 추천해줘&quot;&lt;/li&gt;
&lt;li&gt;&quot;추위에 강한 무화과는 뭐가 있어?&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;처리
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;특징(features) 벡터 데이터베이스에서 질문 내용과&lt;br /&gt;가장 유사한 정보 2개를 검색합니다.&lt;/li&gt;
&lt;li&gt;검색 결과가 없으면 &quot;관련 품종을 찾지 못했습니다.&quot;라는&lt;br /&gt;메시지를 반환합니다.&lt;/li&gt;
&lt;li&gt;결과가 있으면, 이를 바탕으로 RAG를 통해 높은 품질의 답변을 제공합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fig Agent는 사용자의 질문을 6가지 카테고리로 분류하여 각 상황에 맞는 최적의 답변을 제공하도록 설계했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  질문 처리 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1698&quot; data-origin-height=&quot;1194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bT2lLA/dJMb9Mpmgqn/MieDeVkdHzttDLoXKtOkeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bT2lLA/dJMb9Mpmgqn/MieDeVkdHzttDLoXKtOkeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bT2lLA/dJMb9Mpmgqn/MieDeVkdHzttDLoXKtOkeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbT2lLA%2FdJMb9Mpmgqn%2FMieDeVkdHzttDLoXKtOkeK%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;684&quot; height=&quot;481&quot; data-origin-width=&quot;1698&quot; data-origin-height=&quot;1194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내한성과 같이 feature_db에 포함되지 않았던 내용은 솔직히 정보가 없다고 응답했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 정보를 충분히 구할 수 있는 당도 기반 품종 추천 역시 정확하게 응답했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  AI Agent 전체 코드&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# === Fig Variety AI Agent (v4.0 - Direct Gemini SDK) ===

import os
from enum import Enum, auto
from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings
import google.generativeai as genai
from dotenv import load_dotenv

# .env 파일에서 환경 변수를 로드합니다.
load_dotenv()

class QueryCategory(Enum):
    &quot;&quot;&quot;사용자 질문의 의도를 나타내는 분류 Enum&quot;&quot;&quot;
    KNOWN_VARIETY_EXISTS = auto()
    KNOWN_VARIETY_TYPO = auto()
    KNOWN_VARIETY_NEEDS_CLARIFICATION = auto()
    UNKNOWN_VARIETY_GENERAL_QUERY = auto()
    UNKNOWN_VARIETY_IMAGE_QUERY = auto()
    CHITCHAT = auto()

class FigAgent:
    &quot;&quot;&quot;
    사용자 질문을 분류하고 RAG 파이프라인을 관리하는 AI 에이전트 (Gemini SDK 직접 호출 버전)
    &quot;&quot;&quot;
    def __init__(self, varieties_db_path=&quot;./vector_db&quot;, features_db_path=&quot;./features_db&quot;, model_name=&quot;intfloat/multilingual-e5-base&quot;):
        print(&quot;  Fig Agent (Direct Gemini SDK)를 초기화하는 중입니다...&quot;)

        self.embedding = HuggingFaceEmbeddings(model_name=model_name)

        try:
            self.varieties_db = FAISS.load_local(varieties_db_path, self.embedding, allow_dangerous_deserialization=True)
            self.features_db = FAISS.load_local(features_db_path, self.embedding, allow_dangerous_deserialization=True)
        except Exception as e:
            print(f&quot;DB 로드 실패: {e}&quot;)
            raise

        # --- LLM 설정 (google-generativeai SDK 직접 사용) ---
        google_api_key = os.getenv(&quot;GEMINI_API_KEY&quot;)
        if not google_api_key:
            raise ValueError(&quot;GEMINI_API_KEY 환경 변수를 찾을 수 없습니다. .env 파일을 확인해주세요.&quot;)

        genai.configure(api_key=google_api_key)
        self.llm = genai.GenerativeModel('gemini-2.5-flash') 

        # --- Retriever 설정 (LangChain 사용) ---
        self.retriever = self.features_db.as_retriever(search_kwargs={'k': 3})

        # --- 별명 및 오타 처리 시스템 ---
        self.variety_aliases = {
            &quot;Brunswick&quot;: [&quot;brunswick&quot;, &quot;브런즈윅&quot;],
            &quot;Ciccio_Nero&quot;: [&quot;ciccio nero&quot;, &quot;씨씨오 네로&quot;, &quot;ciccio vero&quot;],
            &quot;Coll_De_Dama_Rimada&quot;: [&quot;coll de dama rimada&quot;, &quot;콜 드 다마 리마다&quot;],
            &quot;Hardy_Chicago&quot;: [&quot;hardy chicago&quot;, &quot;하디 시카고&quot;, &quot;시카고&quot;],
            &quot;Horaishi&quot;: [&quot;horaishi&quot;, &quot;호래시&quot;, &quot;봉래시&quot;],
            &quot;Strawberry_Verte&quot;: [&quot;strawberry verte&quot;, &quot;스트로베리 베르테&quot;, &quot;sv&quot;]
        }
        self.known_typos = [&quot;ciccio vero&quot;]

        self.sorted_aliases = []
        for canonical, alias_list in self.variety_aliases.items():
            for alias in alias_list:
                self.sorted_aliases.append((alias, canonical))
        self.sorted_aliases.sort(key=lambda x: len(x[0]), reverse=True)

        print(&quot;✅ Agent 초기화 완료.&quot;)

    def _generate_rag_response(self, query: str) -&amp;gt; str:
        &quot;&quot;&quot;RAG 파이프라인을 수동으로 실행하여 답변을 생성합니다.&quot;&quot;&quot;
        print(f&quot;    '{query}'에 대한 관련 문서 검색 중...&quot;)
        docs = self.retriever.get_relevant_documents(query)

        if not docs:
            return &quot;관련 정보를 찾지 못했습니다. 질문을 조금 더 구체적으로 해주시겠어요?&quot;

        # 검색된 문서 내용을 하나의 컨텍스트 문자열로 합칩니다.
        context = &quot;\n&quot;.join([doc.page_content for doc in docs])

        prompt_template = f&quot;&quot;&quot;
        당신은 무화과 품종 전문가입니다. 사용자의 질문에 대해 아래의 '검색된 정보'를 바탕으로 친절하고 명확하게 답변해주세요.
        답변은 반드시 한국어로 작성해야 합니다. 정보가 부족하여 답변할 수 없는 경우, &quot;정보가 부족하여 답변하기 어렵습니다.&quot;라고 솔직하게 말해주세요.

        [검색된 정보]
        {context}

        [사용자 질문]
        {query}

        [전문가 답변]
        &quot;&quot;&quot;

        print(&quot;    LLM이 답변 생성 중...&quot;)
        try:
            response = self.llm.generate_content(prompt_template)
            return response.text
        except Exception as e:
            print(f&quot;  ❗️ LLM 호출 중 오류 발생: {e}&quot;)
            return &quot;답변을 생성하는 중에 오류가 발생했습니다.&quot;

    def _extract_variety_from_query(self, query: str) -&amp;gt; tuple[str, str] | None:
        &quot;&quot;&quot;질문에서 (매칭된 별명, 표준 품종명) 튜플을 추출합니다.&quot;&quot;&quot;
        query_lower = query.lower()
        for alias, canonical in self.sorted_aliases:
            if alias in query_lower:
                return (alias, canonical)
        return None

    def _classify_query(self, query: str) -&amp;gt; tuple[QueryCategory, dict]:
        &quot;&quot;&quot;질문을 분석하여 카테고리와 관련 데이터를 반환합니다.&quot;&quot;&quot;
        chitchat_keywords = [&quot;안녕&quot;, &quot;고마워&quot;, &quot;감사&quot;, &quot;땡큐&quot;]
        if any(keyword in query for keyword in chitchat_keywords):
            return QueryCategory.CHITCHAT, {}

        image_keywords = [&quot;사진&quot;, &quot;이미지&quot;, &quot;이 무화과&quot;]
        if any(keyword in query for keyword in image_keywords):
            return QueryCategory.UNKNOWN_VARIETY_IMAGE_QUERY, {}

        variety_info = self._extract_variety_from_query(query)
        if variety_info:
            matched_alias, canonical_name = variety_info
            canonical_name_display = canonical_name.replace('_', ' ')

            if matched_alias in self.known_typos:
                return QueryCategory.KNOWN_VARIETY_TYPO, {&quot;suggestion&quot;: canonical_name_display}

            clarification_keywords = [&quot;어때&quot;, &quot;좋아&quot;, &quot;키울만&quot;, &quot;괜찮&quot;]
            if any(keyword in query for keyword in clarification_keywords):
                return QueryCategory.KNOWN_VARIETY_NEEDS_CLARIFICATION, {&quot;variety&quot;: canonical_name_display}

            return QueryCategory.KNOWN_VARIETY_EXISTS, {&quot;query&quot;: query, &quot;variety&quot;: canonical_name_display}

        return QueryCategory.UNKNOWN_VARIETY_GENERAL_QUERY, {&quot;query&quot;: query}

    def handle_query(self, query: str) -&amp;gt; str:
        &quot;&quot;&quot;질문을 받아 분류하고, 각 케이스에 맞는 응답을 생성합니다.&quot;&quot;&quot;
        category, data = self._classify_query(query)

        print(f&quot;  Agent 분류 결과: {category.name}&quot;)

        if category == QueryCategory.CHITCHAT:
            return &quot;천만에요! 더 궁금한 점이 있으신가요?&quot; if any(k in query for k in [&quot;고마워&quot;, &quot;감사&quot;, &quot;땡큐&quot;]) else &quot;안녕하세요! 무화과에 대해 무엇이든 물어보세요.&quot;

        elif category == QueryCategory.UNKNOWN_VARIETY_IMAGE_QUERY:
            return &quot;현재는 이미지를 분석하여 품종을 식별하는 기능이 지원되지 않습니다.&quot;

        elif category == QueryCategory.KNOWN_VARIETY_TYPO:
            return f&quot;혹시 '{data['suggestion']}' 품종을 말씀하신 건가요?&quot;

        elif category == QueryCategory.KNOWN_VARIETY_NEEDS_CLARIFICATION:
            return f&quot;'{data['variety']}' 품종에 대해 어떤 점이 궁금하신가요? 맛, 생산성, 나무 크기 등 구체적인 기준을 알려주시면 더 자세히 답변해 드릴 수 있습니다.&quot;

        elif category == QueryCategory.KNOWN_VARIETY_EXISTS:
            print(f&quot;  ▶️ '{data['variety']}'에 대한 정보 검색 및 답변 생성 시작...&quot;)
            return self._generate_rag_response(data['query'])

        elif category == QueryCategory.UNKNOWN_VARIETY_GENERAL_QUERY:
            print(&quot;  ▶️ 특징 DB에서 추천 품종 검색 및 답변 생성 시작...&quot;)
            return self._generate_rag_response(data['query'])

        else:
            return &quot;죄송합니다. 질문을 이해하지 못했습니다.&quot;

# === Agent 실행 예시 ===
if __name__ == '__main__':
    try:
        agent = FigAgent()

        queries_to_test = [
            &quot;안녕?&quot;,
            &quot;브런즈윅의 내한성은 어떤가요?&quot;,
            &quot;Ciccio Vero는 키우기 쉬운가요?&quot;,
            &quot;브런즈윅은 키울만 한가요?&quot;,
            &quot;달콤한 품종 추천해줘&quot;,
            &quot;이 사진 속 무화과는 무슨 품종이야?&quot;,
            &quot;고마워&quot;
        ]

        for q in queries_to_test:
            print(f&quot;\n  사용자 질문: {q}&quot;)
            response = agent.handle_query(q)
            print(f&quot;  Agent 응답: {response}&quot;)

    except Exception as e:
        print(f&quot;Agent 실행 중 오류 발생: {e}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  Client로 대화형 즐겨보기(?)&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;(.venv) (base) ➜  FigVarietyRAGChat git:(fig_agent) ✗ python fig_client.py
  Fig Agent (Direct Gemini SDK)를 초기화하는 중입니다...
✅ Agent 초기화 완료.

--- 무화과 품종 챗봇 ---
안녕하세요! 무화과에 대해 궁금한 점을 물어보세요.
(종료하시려면 'exit' 또는 'quit'을 입력하세요)

  나: 안녕?? 난 깃짱이야 ㅎㅎ
  Agent 분류 결과: CHITCHAT
  Agent: 안녕하세요! 무화과에 대해 무엇이든 물어보세요.

  나: 내가 키우는 무화과가 있는데 진짜 열매가 겁나  크거든?? 이게 뭔지 알겠어?
  Agent 분류 결과: UNKNOWN_VARIETY_GENERAL_QUERY
  ▶️ 특징 DB에서 추천 품종 검색 및 답변 생성 시작...
    '내가 키우는 무화과가 있는데 진짜 열매가 겁나 크거든?? 이게 뭔지 알겠어?'에 대한 관련 문서 검색 중...
/Users/gitchan/Projects/FigVarietyRAGChat/fig_agent.py:71: LangChainDeprecationWarning: The method `BaseRetriever.get_relevant_documents` was deprecated in langchain-core 0.1.46 and will be removed in 1.0. Use :meth:`~invoke` instead.
  docs = self.retriever.get_relevant_documents(query)
    LLM이 답변 생성 중...
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
E0000 00:00:1761035138.036196 11995233 alts_credentials.cc:93] ALTS creds ignored. Not running on GCP and untrusted ALTS is not enabled.
  Agent: 안녕하세요! 무화과 품종 전문가입니다. 열매가 '겁나게 크다'는 말씀에 어떤 품종인지 궁금하시군요.

제가 가진 정보에 따르면, **브런즈윅(Brunswick)** 품종이 **대형 ~ 초대형**의 열매 크기를 자랑합니다. 따라서 고객님의 무화과가 아주 큰 열매를 맺는다면 브런즈윅 품종일 가능성이 높습니다.

  나:  브레바 먹을 수 있는 품종??
  Agent 분류 결과: UNKNOWN_VARIETY_GENERAL_QUERY
  ▶️ 특징 DB에서 추천 품종 검색 및 답변 생성 시작...
    '브레바 먹을 수 있는 품종??'에 대한 관련 문서 검색 중...
    LLM이 답변 생성 중...
  Agent: 안녕하세요! 무화과 품종 전문가입니다. 고객님의 질문에 대해 검색된 정보를 바탕으로 답변해 드리겠습니다.

브레바(Breba)를 생산하여 드실 수 있는 품종으로는 **브런즈윅(Brunswick)**이 있습니다. 브런즈윅은 풍부한 단맛(브라운 슈가 타입)을 자랑하는 품종이기도 합니다.

콜 드 다마 리마다(Col de Dame Rimada)의 경우, 브레바 생산 여부가 &quot;X 또는 불명확&quot;으로 표시되어 있어, 현재 정보만으로는 브레바 생산 품종으로 추천해 드리기 어렵습니다.

  나:  한국이랑 가까운 나라에서 유래한 무화과는?
  Agent 분류 결과: UNKNOWN_VARIETY_GENERAL_QUERY
  ▶️ 특징 DB에서 추천 품종 검색 및 답변 생성 시작...
    '한국이랑 가까운 나라에서 유래한 무화과는?'에 대한 관련 문서 검색 중...
    LLM이 답변 생성 중...
  Agent: 제공된 정보에 따르면, 한국과 가까운 나라에서 유래한 무화과 품종은 없습니다. 검색된 정보에는 이탈리아, 미국, 포르투갈에서 유래한 품종들만 명시되어 있어, 사용자님의 질문에 직접적으로 답변하기는 어렵습니다.

  나: ㅋㅋ 그러면 미국에서 유래한거는?
  Agent 분류 결과: UNKNOWN_VARIETY_GENERAL_QUERY
  ▶️ 특징 DB에서 추천 품종 검색 및 답변 생성 시작...
    'ㅋㅋ 그러면 미국에서 유래한거는?'에 대한 관련 문서 검색 중...
    LLM이 답변 생성 중...
  Agent: 미국에서 유래한 무화과 품종은 &quot;하디 시카고&quot;입니다.

  나: exit
  Agent: 이용해주셔서 감사합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겁나,, 기특하자나,,?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG을 활용하니 LLM만 있었다면 모른다고 절대 안할텐데 모르는건 모른다고 딱 강제할 수도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도감을 다 먹이면 정말 쓸만 하겠어요 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMYDZg/dJMb8WrUUxD/3kSCVbu2ZXhH6KMzORH6C0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMYDZg/dJMb8WrUUxD/3kSCVbu2ZXhH6KMzORH6C0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMYDZg/dJMb8WrUUxD/3kSCVbu2ZXhH6KMzORH6C0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMYDZg%2FdJMb8WrUUxD%2F3kSCVbu2ZXhH6KMzORH6C0%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;606&quot; height=&quot;446&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  앞으로는?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ LLM Configuration&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Fig Agent는 Gemini SDK의 기본 설정을 사용하고 있어, 응답이 다소 장황한 경우가 있다. 물론 RAG를 통해서 데이터를 제공할 수 없는 경우에는 백엔드에서 자동으로 모른다는 답을 내놓기에 할루시네이션을 억제하고는 있지만, LLM 수준에서 이를 조절하기 위해서는 Config 기반 제어를 추가할 수 있다.&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;Temperature(창의성) 제어&lt;/b&gt;: 단답형 정보 중심 질문에서는 temperature=0로 낮추어 일관된 사실 위주로, 품종 추천이나 조언형 질문에서는 temperature=0.8 정도로 높여 다양성을 확보한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Top-K / Top-P 설정&lt;/b&gt;: top_p나 top_k를 조정해, LLM이 불필요한 맥락을 덜 생성하고 더 압축된 답변을 하도록 유도할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;max_output_tokens 제한&lt;/b&gt;: 응답 길이를 300자 내외로 제한해, 실제 사용자 경험에서 피로감을 줄일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 최신 데이터 우선 검색 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 RAG 검색은 FAISS 기반의 벡터 유사도만 사용하고 있어, &lt;b&gt;최신 데이터 반영이 어렵다는 한계&lt;/b&gt;가 있다. 동일한 내용이 있을 때 최신 데이터를 더 우선하기 위해서는 아래와 같은 방법을 사용해볼 수 있다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;타임스탬프 기반 가중치 추가&lt;/b&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;search_kwargs={'k':3, 'filter': lambda doc: doc.metadata['timestamp'] &amp;gt; threshold}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;문서 저장 시 timestamp 메타데이터를 함께 저장하고, 검색 시 최신 데이터에 더 높은 가중치를 준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주기적 재임베딩 파이프라인&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;자동 크롤링 또는 사용자가 추가 입력한 품종 데이터를 주기적으로 재임베딩하여 최신 벡터 DB로 갱신한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 멀티 에이전트(Multi-Agent) 구조로 확장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Fig Agent는 단일 파이프라인에서 모든 질문을 분류하고 처리하고 있다. 하지만 RAG가 확장되면 좀더 역할에 특화한 LLM 구조를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Agent 역할 예시]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Classifier Agent&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;질문을 6가지 카테고리로 분류&lt;/td&gt;
&lt;td&gt;현재 FigAgent의 _classify_query 역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Retriever Agent&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;variety_db, feature_db 중 어디서 검색할지 결정&lt;/td&gt;
&lt;td&gt;품종명 중심인지, 특징 중심인지 판단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Answering Agent&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;검색된 결과 기반으로 LLM에게 프롬프트 구성 및 응답 생성&lt;/td&gt;
&lt;td&gt;temperature 조절 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Feedback Agent&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;사용자의 후속 질문(clarification)을 학습해 RAG 개선&lt;/td&gt;
&lt;td&gt;&amp;ldquo;좀 더 구체적으로 알려줘&amp;rdquo; &amp;rarr; context 강화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 LangGraph 등의 스타일로 구성하면, 각 에이전트가 병렬/직렬로 협력하며 &lt;b&gt;질문 분류, 문서 검색, 답변 생성, 피드백 반영&lt;/b&gt;까지 완전히 자동화된 챗봇 시스템으로 발전할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG를 직접 구현해 보면서 느낀건 복잡하다,,?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://engineerinsight.tistory.com/467&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;랭체인/랭그래프&lt;/a&gt; 바탕으로 프로젝트를 개선시켜보겠다 향후에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PROJECT/RAG 기반 희귀품종 무화과 챗봇</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/449</guid>
      <comments>https://engineerinsight.tistory.com/449#entry449comment</comments>
      <pubDate>Tue, 21 Oct 2025 17:45:04 +0900</pubDate>
    </item>
    <item>
      <title>[AI/LLM] LLM Fine-tuning 완벽 정리: LoRA부터 파인튜닝 vs RAG까지</title>
      <link>https://engineerinsight.tistory.com/447</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  LLM Fine-tuning이란?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM(Fine-tuning)은 이미 사전 학습된 대규모 언어모델(Pre-trained LLM)을 특정 목적에 맞게 추가로 학습시키는 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해, GPT나 LLaMA 같은 모델이 이미 &amp;lsquo;언어의 일반적인 패턴&amp;rsquo;을 충분히 배웠다면, Fine-tuning은 여기에 도메인 지식, 기업 데이터 등을 추가해 모델이 더 내가 원하는 답을 정확하게 하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 GPT가 &amp;ldquo;일반적인 언어 전반&amp;rdquo;을 안다면, Fine-tuning된 모델은 &amp;ldquo;법률 문서 요약&amp;rdquo;이나 &amp;ldquo;우리 제품의 정보를 안 상태로 자동 고객 응대&amp;rdquo; 같은 특정 작업을 훨씬 더 잘하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fine-tuning은 거대한 언어모델을 다시 처음부터 가르치는 것이 아닙니다. 이미 똑똑한 모델에게 &amp;ldquo;우리 회사의 방식&amp;rdquo;이나 &amp;ldquo;특정 업무 스타일&amp;rdquo;을 익히게 하는 마무리 훈련에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 GPU 리소스 등을 고려해야 하며, 최근에는 RAG(Retrieval-Augmented Generation)와 병행해 추가적인 학습 없이도 도메인 지식을 확장하는 방법도 많이 쓰입니다. &lt;i&gt;(맨 뒷 부분에서 비교해보겠습니다)&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Fine-tuning이 필요한 이유&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;특정 도메인 적응
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반 모델은 의료, 법률, 제조처럼 전문 용어가 많은 영역에서 성능이 떨어질 수 있습니다.&lt;/li&gt;
&lt;li&gt;Fine-tuning으로 그 분야의 텍스트를 학습시키면 맥락을 더 잘 이해하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;말투/일관성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 기업 챗봇이라면 &amp;ldquo;친절하고 정중한 말투&amp;rdquo;로 답하도록 일관성을 부여할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모델의 행동 제어
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반 모델은 &amp;ldquo;추론형 답변&amp;rdquo;을 하거나 &amp;ldquo;창의적인 생성&amp;rdquo;을 하지만, Fine-tuning을 통해 &amp;ldquo;항상 JSON으로 대답&amp;rdquo;처럼 출력 형식과 행동을 제어할 수도 있습니다. &lt;i&gt;(백엔드에서 LLM 호출값을 활용할 때 유용하겠쥬?)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 파인튜닝 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fine-tuning은 전체 파라미터를 학습시키는 Full Fine-tuning과, 일부만 조정하는 Parameter-efficient Fine-tuning(PEFT)으로 나뉩니다.&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;&lt;b&gt;Full Fine-tuning&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;모든 파라미터 업데이트 &amp;rarr; 비용, 데이터 많음&lt;/td&gt;
&lt;td&gt;대기업 모델 커스터마이징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;LoRA (Low-Rank Adaptation)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;pre-trained model 파라미터는 모두 그대로!!!&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모델 일부에 작은 가중치 행렬을 추가&lt;/td&gt;
&lt;td&gt;효율적이고 경량화된 Fine-tuning&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Prefix / Adapter Tuning&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;입력 앞뒤에 특수 토큰을 추가&lt;/td&gt;
&lt;td&gt;빠른 도메인 적응, 재사용 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 벌써 과거가 되어버린 GPT-3만 하더라도 파라미터가 175 Billion개인 것을 생각해보면 Full Fine Tuning은 조금 망설여볼 필요가 있습니다. 또 정말 냉정하게 내가 파인튜닝한 모델이 과연 현재 GPT의 일반적인 성능보다 좋을까도 잘 생각해볼 필요가 있습니다 &lt;i&gt;(비싼 돈 들여 튜닝했는데 성능이 더 떨어질 수도 있습니당)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 &lt;b&gt;LoRA&lt;/b&gt;가 가장 널리 사용됩니다. GPU 메모리 사용이 적고, 기존 모델을 그대로 활용할 수 있기 때문입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Fine Tuning 데이터 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fine-tuning의 성패는 &lt;b&gt;데이터 품질&lt;/b&gt;에 달려 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 아래 세 가지 형태로 데이터를 준비합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Instruction + Input + Output&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: &amp;ldquo;아래 문장을 요약해줘.&amp;rdquo; &amp;rarr; &amp;ldquo;오늘 회의에서는 AI 모델 최적화가 논의됨.&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문답 데이터(QA 형태)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: &amp;ldquo;RAG란 무엇인가요?&amp;rdquo; &amp;rarr; &amp;ldquo;LLM이 외부 지식을 불러와 활용하는 구조입니다.&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대화형 데이터(Chat 형태)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: 사용자와 모델 간 다턴 대화를 저장하여 학습&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  LoRA(Low-Rank Adaptation)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoRA(Low-Rank Adaptation)는 LLM 전체를 다시 학습하지 않고, 기존 모델의 일부 가중치만 추가로 학습시키는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM의 파라미터는 수십억 개이기 때문에, 전체를 학습하려면 GPU 메모리와 시간이 엄청나게 듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoRA는 기존 pre trained model의 거대한 가중치 W는 그대로 두고, 아주 작은 보정 행렬 &amp;Delta;W만 학습하도록 만들어졌습니다. 즉, 모델은 그대로 두되, LoRA가 추가로 만든 조정 레이어만 훈련합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;기존 LLM (pre-trained model)
      │
      │ (기존 파라미터는 freeze)
      &amp;darr;
  [LoRA 모듈 추가]
      │
      ├── 작은 선형 레이어 A, B (low-rank 행렬)
      ├── &amp;Delta;W = A * B 형태로 기존 가중치 보정
      &amp;darr;
  Fine-tuning 수행&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 코드 예시&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from transformers import AutoTokenizer, AutoModelForCausalLM, Trainer, TrainingArguments
from peft import LoraConfig, get_peft_model
from datasets import load_dataset

# 1. 모델과 토크나이저 불러오기
model_name = &quot;meta-llama/Llama-2-7b-hf&quot;
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# 2. LoRA 설정
lora_config = LoraConfig(
    r=8,                  # rank 크기 (작을수록 경량)
    lora_alpha=16,        # scaling factor
    lora_dropout=0.05,    # dropout 비율
    target_modules=[&quot;q_proj&quot;, &quot;v_proj&quot;]  # 주로 Attention 레이어에 적용
)

# 3. LoRA 적용
model = get_peft_model(model, lora_config)

# 4. 데이터 로드
dataset = load_dataset(&quot;json&quot;, data_files={&quot;train&quot;: &quot;train.jsonl&quot;, &quot;eval&quot;: &quot;eval.jsonl&quot;})

# 5. 토크나이징 함수
def tokenize_function(example):
    text = f&quot;{example['instruction']}\n{example['input']}\n{example['output']}&quot;
    return tokenizer(text, truncation=True)

tokenized_datasets = dataset.map(tokenize_function, batched=True)

# 6. 학습 설정
training_args = TrainingArguments(
    output_dir=&quot;./lora-llm&quot;,
    per_device_train_batch_size=2,
    num_train_epochs=3,
    logging_steps=10,
    save_strategy=&quot;epoch&quot;,
    fp16=True,  # GPU 메모리 절약
)

# 7. Trainer 실행
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_datasets[&quot;train&quot;],
    eval_dataset=tokenized_datasets[&quot;eval&quot;],
)

trainer.train()&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Fine-Tuning vs RAG&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fine-tuning과 RAG 모두 LLM에게 더 내가 원하는 답변을 커스텀하기 위한 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 LLM(특히 GPT-2, GPT-3 시절)에는 컨텍스트 윈도우(context window) 한계가 컸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 몇백~몇천 토큰밖에 못 봤기 때문에, RAG로 긴 문서를 붙이면 정보가 잘리는 문제가 자주 생겼습니다. &lt;i&gt;(긴 글을 프롬프트로 보내면 앞부분을 잊어버려서 앞뒤가 잘 안맞는 답변이 생성되는것)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 외부에서 정보를 주입하기보다는 모델 자체에 지식을 내재화하는 것이 대세였고, 그래서 2021~2022년쯤은 LoRA, Adapter, Prefix 같은 Parameter-efficient Fine-tuning(PEFT) 연구가 많이 진행되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 LLM이 길고 복잡한 문맥을 이해할 수 있게 되면서 (GPT-4o, Claude 3, Gemini 1.5 등은 200k~1M 토큰까지 처리합니다.) 예전엔 문서 전체를 넣을 수 없었지만, 이제는 보고서 한 권 통째로 넣는 것도 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 임베딩 품질이 좋아지고, re-ranking, graph-RAG, multi-hop retrieval 같은 기법이 나오면서&lt;br /&gt;검색 결과가 문맥에 맞지 않는다는 문제도 줄었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 모델이 커질수록 재학습이 너무 비싸졌습니다. GPU 1~2대로도 가능하던 시대에서 H100 클러스터 몇 대가 필요하게 되어서 파인튜닝이 부담이 점점 커지기 시작했습니다. 반면, RAG는 데이터만 바꾸면 즉시 적용 가능하기 때문에 최근에는 지식을 모델 안에 넣기보단, 밖에 두는 기업이 많아진 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두가지를 함께 사용하더라도 좋은 결과를 충분히 얻을 수 있습니다.&lt;br /&gt;(Fine Tuning은 가볍게, RAG를 통해 답변 보완)&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;b&gt;Fine-tuning&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;RAG (Retrieval-Augmented Generation)&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;모델 자체를 다시 학습시켜 지식을 &amp;ldquo;내재화&amp;rdquo;&lt;/td&gt;
&lt;td&gt;외부 지식베이스를 &amp;ldquo;조회&amp;rdquo;하여 실시간으로 반영&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;데이터 저장 위치&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;모델 파라미터 내부 (Internal Memory)&lt;/td&gt;
&lt;td&gt;외부 DB / 벡터스토어 (External Memory)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;응답 시 동작&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;질문 &amp;rarr; 모델이 자체 기억을 바탕으로 생성&lt;/td&gt;
&lt;td&gt;질문 &amp;rarr; 벡터 검색 &amp;rarr; 관련 문서 불러와 함께 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;업데이트 방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;재학습 필요 (비용&amp;uarr;)&lt;/td&gt;
&lt;td&gt;데이터베이스만 업데이트 (빠름)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;대표 활용&lt;/b&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;b&gt;예시&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;의학 논문 요약 모델&amp;rdquo;, &amp;ldquo;고객 응대 말투 모델&amp;rdquo;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;사내 위키 기반 질의응답&amp;rdquo;, &amp;ldquo;뉴스 기반 챗봇&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Fine-tuning의 장단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모델이 완전히 새롭게 도메인에 적응
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답 톤, 표현, 추론 스타일 모두 학습 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RAG보다 일관된 답변과 자연스러운 문체&lt;/li&gt;
&lt;li&gt;추론 속도가 빠름 (외부 검색 과정 없음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재학습 필요: GPU, 시간, 비용 부담&lt;/li&gt;
&lt;li&gt;데이터 저작권, 라이선스 이슈 존재&lt;/li&gt;
&lt;li&gt;최신 정보 반영이 어려움 &lt;i&gt;(매번 재학습해야 함)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;말투나 스타일, 태스크 포맷을 통일해야 할 때
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예. &amp;ldquo;AI 상담봇&amp;rdquo;, &amp;ldquo;요약/번역/리라이팅 전용 모델&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ RAG의 장단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;학습 없이도 지식 확장 가능 (데이터만 추가)&lt;/li&gt;
&lt;li&gt;&lt;i&gt;최신 정보 반영이 즉시 가능&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;법적 리스크 적음 (모델 파라미터 변경 X)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색 품질에 따라 답변 품질이 좌우됨&lt;/li&gt;
&lt;li&gt;문맥 결합(Context Fusion)이 어렵고 불안정할 수 있음&lt;/li&gt;
&lt;li&gt;답변 톤이나 표현 방식은 제어가 약함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;사내 문서, 논문, 뉴스, 제품 매뉴얼&amp;rdquo; 등 지속적으로 갱신되는 데이터 기반 Q&amp;amp;A
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예. 사내 지식 챗봇, 법률 질의응답, 연구 논문 검색 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI/LLM</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/447</guid>
      <comments>https://engineerinsight.tistory.com/447#entry447comment</comments>
      <pubDate>Tue, 21 Oct 2025 17:00:28 +0900</pubDate>
    </item>
    <item>
      <title>[AI/RAG] RAG 기반 희귀품종 무화과 LLM 챗봇 프로젝트(2): 벡터DB 구축 (ft. 무화과 도감)</title>
      <link>https://engineerinsight.tistory.com/448</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;관련 PR: &lt;a href=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/pull/1&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/AiResearch2025/FigVarietyRAGChat/pull/1&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1761030974399&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;feat: vector DB (query by variety / feature) by gitchannn &amp;middot; Pull Request #1 &amp;middot; AiResearch2025/FigVarietyRAGChat&quot; data-og-description=&quot;&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/pull/1&quot; data-og-url=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/pull/1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c6sRtt/hyZL0yWoFD/kkvmmwuT50JXb8VRLlEtQ0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cFid5L/hyZLY8Wg3Y/XzFiGPkOCgDbJYlyKHccr0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/pull/1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/pull/1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c6sRtt/hyZL0yWoFD/kkvmmwuT50JXb8VRLlEtQ0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cFid5L/hyZLY8Wg3Y/XzFiGPkOCgDbJYlyKHccr0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;feat: vector DB (query by variety / feature) by gitchannn &amp;middot; Pull Request #1 &amp;middot; AiResearch2025/FigVarietyRAGChat&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 레포: &lt;a href=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/tree/pure-RAG&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/AiResearch2025/FigVarietyRAGChat/tree/pure-RAG&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764230128691&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - AiResearch2025/FigVarietyRAGChat: RAG 기반 희귀품종 무화과 챗봇 프로젝트&quot; data-og-description=&quot;RAG 기반 희귀품종 무화과 챗봇 프로젝트. Contribute to AiResearch2025/FigVarietyRAGChat development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/tree/pure-RAG&quot; data-og-url=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cO88kY/hyZOoNiEqJ/jUIYaqKc3LiPGuWJOns42k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bcuXwU/hyZOwdu3lk/a91Yim8rqGBnnjydxbRP40/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/tree/pure-RAG&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/tree/pure-RAG&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cO88kY/hyZOoNiEqJ/jUIYaqKc3LiPGuWJOns42k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bcuXwU/hyZOwdu3lk/a91Yim8rqGBnnjydxbRP40/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - AiResearch2025/FigVarietyRAGChat: RAG 기반 희귀품종 무화과 챗봇 프로젝트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;RAG 기반 희귀품종 무화과 챗봇 프로젝트. Contribute to AiResearch2025/FigVarietyRAGChat development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  데이터 수집 from 무화과 도감&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 RAG의 근간이 될 텍스트 데이터를 모았습니다. 무화과 품종 관련 자료를 모으기 위해서 무화과도감의 데이터를 이용했습니다.&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;454&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fsyw1/dJMb8Vs0GU9/ZNztGgFwdt0GGnFBnkeLe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fsyw1/dJMb8Vs0GU9/ZNztGgFwdt0GGnFBnkeLe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fsyw1/dJMb8Vs0GU9/ZNztGgFwdt0GGnFBnkeLe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFsyw1%2FdJMb8Vs0GU9%2FZNztGgFwdt0GGnFBnkeLe1%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;360&quot; height=&quot;262&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무화과도감을 LLM이 해석한 자연어를 각각은 txt 파일에 넣었습니다. 좀더 와닿도록 하나만 예를 들어서 보여드리자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Strawberry_Verte.txt&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;스트로베리 베르테(Strawberry Verte, SV)는 Marius Nedelcu에 의해 2011~2012년에 프랑스에서 국내로 도입된 품종이다.
국내에서는 &amp;lsquo;SV&amp;rsquo;, &amp;lsquo;SV_unk&amp;rsquo; 등의 이름으로 불리며, 달콤한 베리 향과 진한 붉은 과육으로 유명하다.

이 품종은 미국 동호인들 사이에서 인기를 얻으며 퍼져 나갔으며,
녹색 과피와 붉은 과육의 강한 대비로 인해 시각적인 매력이 매우 뛰어나다.
잘 익은 과실은 시원한 산미와 달콤한 향이 균형을 이루며, 생식용과 상업용 모두 적합한 품종으로 평가된다.

열매는 소형에서 중형 크기이며, Fig의 평균 과중은 32~56g, Breba는 70~118g이다.
열매의 형태는 물방울형이며 과피는 연녹색에서 완숙 시 연한 녹황색으로 변한다.
과육은 진한 붉은색(딸기색에 가까움)으로, 베리 타임의 매우 진한 단맛과 크리미한 식감이 특징이다.
보존성은 높고, 꼭지는 매우 튼튼하다.

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 최대한 해당 품종에 대한 정보를 최대한 자세히 정리했습니당&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  벡터 DB 구축 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터DB를 설계할 때 있어서 크게는 2가지 종류의 쿼리를 고민해 보았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 확실한 무화과 품종 이름을 알고 물어보는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;BNR이 정말 무화과의 황제 급으로 달아? 몇 브릭스야?&lt;br /&gt;햇빛이 더 많이 필요한건 CDDB vs 하디시카고? (두가지 비교)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터DB에는 품종 이름만 임베딩하고, 이후에 이 품종이 사용자의 질문에서 언급된 것과 얼마나 유사한가를 찾는 역할만 하도록 했습니다. &lt;i&gt;(오탈자, 약어, 유사명 등을 처리하는 lookup index로 활용)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서 AI Agent가 &amp;ldquo;Ciccio Nero&amp;rdquo;가 언급되었다고 판단하면, 벡터DB에서 품종명 유사도 검색 &amp;rarr; 가장 가까운 품종명 key 반환 &amp;rarr; 해당 품종 txt 파일 로드 &amp;rarr; RAG context 구성, 이 순서로 진행됩니다.&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;Brunswick&lt;/td&gt;
&lt;td&gt;&amp;ldquo;Brunswick fig variety&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ciccio Nero&lt;/td&gt;
&lt;td&gt;&amp;ldquo;Ciccio Nero fig variety&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardy Chicago&lt;/td&gt;
&lt;td&gt;&amp;ldquo;Hardy Chicago fig variety&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 그냥 txt 파일 내용 전체를 함께 임베딩하는 방안을 생각해 보았지만, chunk로 끊어서 문장을 저장하다보면 문장 내에 서로 상충되는 단어들이 종종 존재하기도 하고 가장 치명적으로 느꼈던 부분은, 하나의 품종을 설명할 때 다른 품종과의 비교를 하는 부분에 있어서 이런 부분이 나중에 오동작의 원인이 될 것 같았기 때문입니다.&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;1182&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXUnVK/dJMb9WFqUMO/UnmK9jcDQYjItNCvyC6to1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXUnVK/dJMb9WFqUMO/UnmK9jcDQYjItNCvyC6to1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXUnVK/dJMb9WFqUMO/UnmK9jcDQYjItNCvyC6to1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXUnVK%2FdJMb9WFqUMO%2FUnmK9jcDQYjItNCvyC6to1%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;755&quot; height=&quot;130&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bP71yW/dJMb81msv0O/geE7T8RIH64jZIIr7rDgk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bP71yW/dJMb81msv0O/geE7T8RIH64jZIIr7rDgk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bP71yW/dJMb81msv0O/geE7T8RIH64jZIIr7rDgk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbP71yW%2FdJMb81msv0O%2FgeE7T8RIH64jZIIr7rDgk0%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;647&quot; height=&quot;356&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크로 저런 부분 500자가 함께 저장된다면,, 요즘 vector db는 그래도 성능이 괜찮다고는 하지만, 무화과는 워낙에 품종별로 아주 미묘한 차이가 있기 때문에 세밀히 분간하기 어렵다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat/commit/17e2861bf7e40ff298f8d789f41d7726c09949d1#diff-98ec44c0b2cc002cc7c87c822c5dafe81a3adbb3ab01d18245984b492342c6a7&quot;&gt;(채택되지는 못했지만 청크로 저장하는 코드)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 품종 이름만을 vector db에 저장했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjMThC/dJMb9PzCSxN/JilrXC3oFLuWyYKMouAFNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjMThC/dJMb9PzCSxN/JilrXC3oFLuWyYKMouAFNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjMThC/dJMb9PzCSxN/JilrXC3oFLuWyYKMouAFNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjMThC%2FdJMb9PzCSxN%2FJilrXC3oFLuWyYKMouAFNk%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;582&quot; height=&quot;293&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# === 벡터DB 생성 ===

import os
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings

# === 설정 ===
DATA_DIR = &quot;./varieties&quot;
DB_PATH = &quot;./vector_db&quot;
MODEL_NAME = &quot;intfloat/multilingual-e5-base&quot;

# === 임베딩 모델 로드 ===
embedding = HuggingFaceEmbeddings(model_name=MODEL_NAME)

# === 데이터 생성 (파일 이름 기반) ===
texts = []
metadatas = []
for fname in os.listdir(DATA_DIR):
    if fname.endswith(&quot;.txt&quot;):
        # 파일 이름에서 확장자를 제거하고 &quot; fig variety&quot;를 추가합니다.
        variety_name = os.path.splitext(fname)[0]
        text = f&quot;{variety_name.replace('_', ' ')} fig variety&quot;
        texts.append(text)
        metadatas.append({&quot;source&quot;: fname})

print(&quot;  벡터 DB에 저장될 텍스트:&quot;)
for t in texts:
    print(f&quot;- {t}&quot;)

# === FAISS 인덱스 생성 ===
vectorstore = FAISS.from_texts(texts, embedding=embedding, metadatas=metadatas)

# === 저장 ===
os.makedirs(DB_PATH, exist_ok=True)
vectorstore.save_local(DB_PATH)

print(f&quot;\n✅ 벡터DB 저장 완료: {DB_PATH}&quot;)

# === 저장된 모든 데이터 확인 ===
print(&quot;\n  벡터DB에 저장된 모든 단어 확인:&quot;)
# FAISS 인덱스에서 모든 문서를 가져오는 직접적인 방법은 없지만,
# 인덱스의 모든 벡터를 검색하여 내용을 확인할 수 있습니다.
# FAISS는 ID를 0부터 순차적으로 부여하므로, index_to_docstore_id를 통해 접근합니다.
ids = list(vectorstore.index_to_docstore_id.values())
retrieved_docs = vectorstore.docstore._dict
for i, doc_id in enumerate(ids):
    content = retrieved_docs[doc_id].page_content
    print(f&quot;  {i+1}. {content} (Source: {retrieved_docs[doc_id].metadata['source']})&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이런 질문들에는 가볍게 오탈자나 약자를 포함해서 품종을 구분할 수 있다.&lt;/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;934&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YPLM1/dJMb8Ywu40h/GXtcTJCgwZKB3IXuiy3sVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YPLM1/dJMb8Ywu40h/GXtcTJCgwZKB3IXuiy3sVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YPLM1/dJMb8Ywu40h/GXtcTJCgwZKB3IXuiy3sVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYPLM1%2FdJMb8Ywu40h%2FGXtcTJCgwZKB3IXuiy3sVk%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;624&quot; height=&quot;437&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 품종은 모르고 특성 가지고 질문하는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;1. 당도 높은 무화과들 추천좀 &amp;rarr; bnr, cddb 등 응답&lt;br /&gt;2. 산도 있는 무화과는?&lt;br /&gt;3. 잎이 예쁜, 인테리어 효과가 좋은 무화과 품종은?&lt;br /&gt;4. 제주도 돌담에서 월동 가능한 무화과는?&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에 대처하기 위해서는 더 정확한 쿼리를 위해서 무화과의 특성에 따라 조금 파일을 더 추가해서 임베딩하기로 했다. &lt;i&gt;(이 파일들은 gemini cli가 10초만에 varieties 파일을 순회하더니 아주 야무지게 만들어줬다)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;438&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1Gv6g/dJMb9NV6yPS/kPlz1uOreNfa94mGRIYDs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1Gv6g/dJMb9NV6yPS/kPlz1uOreNfa94mGRIYDs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1Gv6g/dJMb9NV6yPS/kPlz1uOreNfa94mGRIYDs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1Gv6g%2FdJMb9NV6yPS%2FkPlz1uOreNfa94mGRIYDs1%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;355&quot; height=&quot;266&quot; data-origin-width=&quot;438&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;맛.txt&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;풍부한 단맛 (브라운 슈가 타입): 브런즈윅
달콤한 베리 타입: 씨씨오 네로
강렬한 베리 향과 부드러운 단맛: 콜 드 다마 리마다
진한 단맛과 무화과 향: 하디 시카고
적당한 단맛과 약간의 산미: 호래시
달콤한 베리 향과 시원한 산미: 스트로베리 베르테&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Breba_생산.txt&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;ldif&quot;&gt;&lt;code&gt;O: 브런즈윅, 씨씨오 네로, 하디 시카고, 호래시, 스트로베리 베르테
X 또는 불명확: 콜 드 다마 리마다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;맛.txt&amp;rdquo;, &amp;ldquo;월동.txt&amp;rdquo;, &amp;ldquo;잎.txt&amp;rdquo; 같은 속성 파일들은 속성별 세컨더리 벡터DB로 다루려고 한다. (기존의 varieties를 정리한 벡터 DB와 분리)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 저대로 저장하게 되면 너무 다양한 정보가 뭉쳐있다는 단점이 이어지고, 한줄씩 끊어서 저장하기 애매하게 여러 종이 한 줄에 있는 경우도 많아, 아래와 같이 하나의 종 - feature를 문장으로 만들어 &lt;code&gt;features_db&lt;/code&gt;에 저장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;956&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t1MBm/dJMb9WSX9r0/JdkXtBCizAVqdvo2hgpEkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t1MBm/dJMb9WSX9r0/JdkXtBCizAVqdvo2hgpEkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t1MBm/dJMb9WSX9r0/JdkXtBCizAVqdvo2hgpEkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft1MBm%2FdJMb9WSX9r0%2FJdkXtBCizAVqdvo2hgpEkk%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;667&quot; height=&quot;425&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;956&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# === features 벡터DB 생성 ===

import os
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings

# === 설정 ===
FEATURES_DIR = &quot;./features&quot;  # 특징 파일이 있는 디렉토리
DB_PATH = &quot;./features_db&quot;   # 새로운 벡터DB를 저장할 경로
MODEL_NAME = &quot;intfloat/multilingual-e5-base&quot;

# === 임베딩 모델 로드 ===
embedding = HuggingFaceEmbeddings(model_name=MODEL_NAME)

# === 데이터 생성 (특징 파일 기반) ===
texts = []
metadatas = []

print(&quot;  특징 파일을 파싱하여 문장을 생성합니다...&quot;)

# ./result 디렉토리의 모든 .txt 파일을 순회합니다.
for fname in os.listdir(FEATURES_DIR):
    if not fname.endswith(&quot;.txt&quot;):
        continue

    feature_category = os.path.splitext(fname)[0].replace('_', ' ')
    file_path = os.path.join(FEATURES_DIR, fname)

    with open(file_path, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
        for line in f:
            line = line.strip()
            if not line or &quot;:&quot; not in line:
                continue

            # &quot;특징 값: 품종1, 품종2, ...&quot; 형식으로 분리합니다.
            feature_value, varieties_str = line.split(&quot;:&quot;, 1)
            feature_value = feature_value.strip()
            varieties = [v.strip() for v in varieties_str.split(&quot;,&quot;)]

            # 각 품종에 대해 문장을 생성합니다.
            for variety in varieties:
                if not variety: continue
                # 예: &quot;브런즈윅 품종의 나무 수형은(는) 개장형입니다.&quot;
                text = f'&quot;{variety}&quot; 품종의 &quot;{feature_category}&quot;은(는) &quot;{feature_value}&quot;입니다.'
                texts.append(text)
                # 메타데이터에는 원본 파일명과 품종명을 저장하여 나중에 참조할 수 있도록 합니다.
                metadatas.append({&quot;source_file&quot;: fname, &quot;variety&quot;: variety})
                print(f&quot;  - 생성: {text}&quot;)

# === FAISS 인덱스 생성 ===
if texts:
    vectorstore = FAISS.from_texts(texts, embedding=embedding, metadatas=metadatas)

    # === 저장 ===
    os.makedirs(DB_PATH, exist_ok=True)
    vectorstore.save_local(DB_PATH)

    print(f&quot;\n✅ 특징 벡터DB 저장 완료: {DB_PATH}&quot;)

    # === 저장된 모든 데이터 확인 (옵션) ===
    print(&quot;\n  특징 벡터DB에 저장된 모든 문장 확인:&quot;)
    ids = list(vectorstore.index_to_docstore_id.values())
    retrieved_docs = vectorstore.docstore._dict
    for i, doc_id in enumerate(ids):
        content = retrieved_docs[doc_id].page_content
        metadata = retrieved_docs[doc_id].metadata
        print(f&quot;  {i+1}. {content} (Source: {metadata['source_file']}, Variety: {metadata['variety']})&quot;)
else:
    print(&quot;\n❌ 처리할 텍스트가 없어 벡터DB를 생성하지 않았습니다.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;1282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/benqaT/dJMb9LqrXM1/jmof02SoKZKsKgDOiKWLg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/benqaT/dJMb9LqrXM1/jmof02SoKZKsKgDOiKWLg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/benqaT/dJMb9LqrXM1/jmof02SoKZKsKgDOiKWLg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbenqaT%2FdJMb9LqrXM1%2Fjmof02SoKZKsKgDOiKWLg1%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;1422&quot; height=&quot;1282&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;1282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xrLvH/dJMb9LqrXNf/ekh22ZnvjbVKlRfCH1egJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xrLvH/dJMb9LqrXNf/ekh22ZnvjbVKlRfCH1egJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xrLvH/dJMb9LqrXNf/ekh22ZnvjbVKlRfCH1egJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxrLvH%2FdJMb9LqrXNf%2Fekh22ZnvjbVKlRfCH1egJ1%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;1570&quot; height=&quot;338&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;/figure&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;이제 데이터베이스는 우리가 하려고 하는 목표 챗봇에 맞게 구성한 것 같다. 이제는 품종을 물어보면서 정보를 물어보더라도, 특성을 물어보면서 품종 추천을 요청해도 다 대응하는 만능 벡터 DB가 있으니 문제없다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 이제 AI Agent를 구성해서 더 원활한 사용자 경험을 제공하도록 해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PROJECT/RAG 기반 희귀품종 무화과 챗봇</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/448</guid>
      <comments>https://engineerinsight.tistory.com/448#entry448comment</comments>
      <pubDate>Tue, 21 Oct 2025 16:30:58 +0900</pubDate>
    </item>
    <item>
      <title>[AIGOYA LABS] LLM 코드 생성 파이프라인 설계: 중복 코드 방지를 위한 RAG, 코드 품질을 위한 LLM Evaluation</title>
      <link>https://engineerinsight.tistory.com/446</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  인트로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학부 시절, AI 개발 과정이 지나치게 복잡하고 비효율적이라는 점에서 불편함을 느꼈다. 이를 해결하기 위해 AI 개발 생산성 플랫폼 &amp;lsquo;AIGOYA&amp;rsquo;를 직접 기획&amp;middot;개발했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AIGOYA에서는 사용자가 쉽게 AI 개발을 시작할 수 있도록, 모듈 단위의 코드 블록(프리셋모듈) 을 미리 만들어놔서 사용자가 Drag&amp;amp;Drop으로 가볍게 가져다 사용할 수 있는 기능을 제공했다. 베타 사용자의 테스트 기간이 오기까지 수천 개의 프리셋모듈을 만들던 당시 발생한 이슈에 대해 이번 포스팅에서는 정리해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 AI 연구실 대학원생들이 직접 제작한 코드 모듈 우리 팀 사람들이 검토하고 제공할 수 있는 시스템을 만들었지만, 베타 사용자 유입 전까지 충분한 양을 확보하기에는 코드가 턱없이 부족하고 일정이 촉박했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표로서 일정 준수와 품질 확보를 동시에 달성해야 했던 나에게 결국 남은 선택지는 하나였다.&amp;nbsp;&lt;br /&gt;사람의&amp;nbsp;손으로는&amp;nbsp;불가능한&amp;nbsp;규모였기에,&amp;nbsp;나는&amp;nbsp;LLM으로&amp;nbsp;코드를&amp;nbsp;&amp;lsquo;직접&amp;nbsp;만들어내는&amp;nbsp;시스템&amp;rsquo;을&amp;nbsp;설계하기로&amp;nbsp;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 이 과정에서 사용한 LLM 기반 코드 생성 파이프라인 설계, RAG를 활용한 중복 검증 방식, 그리고 생성된 코드의 품질 평가(Evaluation) 절차에 대해 구체적으로 다루려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  LLM 기반 코드 생성 파이프라인 설계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761024732768&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ 노드 카테고리 정의 ]
             │
             ▼
     (Tag, Title 생성)
[ LLM 1차 생성 요청 ]
             │
             ▼
     (유사도 검색 / 중복 검증)
[ 벡터 DB 비교 (RAG) ]
     ├───────────────▶ 중복 &amp;ge; 3개 &amp;rarr; [ 스킵 ]
     │
     ▼
     (중복 적음 &amp;rarr; 신규 생성 진행)
[ 코드 5개 생성 (LLM 2차) ]
             │
             ▼
     (스타일&amp;middot;간결성&amp;middot;주석 평가)
[ 평가 LLM (Evaluation) ]
             │
             ▼
     (최고 점수 선택)
[ 최종 코드 선택 및 DB 저장 ]
             │
             ▼
[ 프리셋 모듈 제공 / 서비스 반영 ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 코드 카테고리 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 개발에 사용될 수 있는 모든 코드의 기본 단위를 노드(Node) 로 정의했다.&lt;br /&gt;각 노드는 &amp;lsquo;제목&amp;rsquo;, &amp;lsquo;설명&amp;rsquo;, &amp;lsquo;코드&amp;rsquo;, &amp;lsquo;태그&amp;rsquo;, &amp;lsquo;데이터 유형&amp;rsquo;, &amp;lsquo;작업 분야&amp;rsquo;, &amp;lsquo;수행 작업&amp;rsquo;으로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 유형(Data Type): Image&lt;/li&gt;
&lt;li&gt;작업 분야(Task): Image Classification&lt;/li&gt;
&lt;li&gt;수행 작업(Category): TRAINING&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 노드를 구조화하면, 나중에 자동화 시스템이 정확한 맥락을 인식하고 코드를 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 우리 팀원이 정리했던 분류이다. &lt;i&gt;(결국 구현도 비슷하게 흘러갔다)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1476&quot; data-origin-height=&quot;1232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nksx2/dJMb9OHs9Vr/SyqfGEraFHHIW3yaCh9QtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nksx2/dJMb9OHs9Vr/SyqfGEraFHHIW3yaCh9QtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nksx2/dJMb9OHs9Vr/SyqfGEraFHHIW3yaCh9QtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnksx2%2FdJMb9OHs9Vr%2FSyqfGEraFHHIW3yaCh9QtK%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;725&quot; height=&quot;605&quot; data-origin-width=&quot;1476&quot; data-origin-height=&quot;1232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 태그 기반 노드 생성 요청&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드에서는 카테고리 중 하나를 선택해 새로운 코드를 만들 때, &lt;b&gt;태그(tag)&lt;/b&gt; 와 &lt;b&gt;제목(title)&lt;/b&gt; 을 기준으로 자동 생성 요청을 보낸다. 이건 그냥 골고루 만들기 위해서 위의 노드 구성요소를 순회하면서 처음부터 끝까지 1개씩 만들고 이 과정을 쉬지 않고 일정 시간 간격을 두고 LLM을 호출하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Image Classification을 가지고 1차로 LLM을 호출하면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Tag: [&quot;ResNet training&quot;, &amp;ldquo;image&amp;rdquo;, &amp;ldquo;image classification&amp;rdquo;], Title: Tag: &quot;ResNet image training&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머 이런 Tag, Title을 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AIGOYA에서는 경량화된 정보로만 소통하기 위해서 이런 과정에서는 Tag, Title만 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 코드 벡터 DB 중복 검사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 태그로 생성 요청을 보내기 전에, 백엔드는 &lt;b&gt;기존 코드 벡터 데이터베이스&lt;/b&gt;에서 유사한 코드가 이미 존재하는지 확인한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가지고 있는 모든 프리셋모듈의 태그 및 제목을 임베딩&lt;/li&gt;
&lt;li&gt;1차 LLM 호출로 생성된 Tag, Title을 벡터 유사도 기반으로 검색&lt;/li&gt;
&lt;li&gt;3개 이상 비슷한 코드가 있으면 중복으로 판단 &amp;rarr; 생성 스킵하고 해당 Tag, Title은 폐기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 통해 코드 중복을 방지하고, 불필요한 2차 LLM 호출 비용을 줄였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ LLM을 통한 코드 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유사한 코드가 충분하지 않다면, 해당 태그와 제목을 기반으로 &lt;b&gt;LLM에 코드 생성을 요청한다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 번에 5개의 후보 코드를 생성하도록 요청 &lt;i&gt;(품질 좋은 1개를 고르기 위해 일단 5개를 만들라고 지시했다)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;모델에 &lt;b&gt;AIGOYA의 코드 스타일 가이드&lt;/b&gt;(주석 형식, 함수 구조, 변수 네이밍 등)를 시스템 프롬프트로 제공&lt;/li&gt;
&lt;li&gt;결과적으로 코드와 설명이 일관된 포맷으로 반환되도록 유도&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정은 완전한 자동화 파이프라인으로, 사람이 개입하지 않아도 코드를 대량으로 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 일정 시간 간격을 두고 이 파이프라인을 실행하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 5. 코드 평가(Evaluation)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 코드 5개는 다시 &lt;b&gt;평가용 LLM&lt;/b&gt;을 통해 검증된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에게 다음 네 가지 기준으로 1~10점을 부여하도록 프롬프트를 설계했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[평가 기준]&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AIGOYA 코드 스타일에 부합하는가&lt;/li&gt;
&lt;li&gt;코드가 충분히 간결한가&lt;/li&gt;
&lt;li&gt;범용성이 있는가&lt;/li&gt;
&lt;li&gt;주석 설명이 명확한가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평가 결과 점수가 가장 높은 코드를 최종적으로 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 비용문제로 한번만에 평가를 했지만 만약에 더 좋은 코드를 선택할 확률을 높이고 싶을 경우, 같은 평가를 여러 번 반복해 평균 점수를 낼 수도 있다. 비용과 품질 간의 트레이드오프로, 목적에 따라 조정 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  결과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 통해 수천 건의 고품질 코드 프리셋을 자동으로 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 초반 사용자들은 서비스 첫날부터 다양한 AI 모델 코드를 탐색하고 실행할 수 있었고, 직접 작성한 코드가 아니더라도 충분한 사용자 경험을 주어서 좋은 피드백을 받을 수 있었고 &lt;i&gt;(결과적으로 정부지원사업 최종 보고도 아주 성공적이었다!)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 첫날부터 사용자는 실제 AI 모델을 실행하며 &amp;lsquo;진짜 개발자 경험&amp;rsquo;을 느낄 수 있었다.&lt;br /&gt;그리고 나는 그때 처음으로, AI가 사람의 시간을 진짜로 절약해줄 수 있구나 하는 걸 실감했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PROJECT/AIGOYA LABS</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/446</guid>
      <comments>https://engineerinsight.tistory.com/446#entry446comment</comments>
      <pubDate>Tue, 21 Oct 2025 15:00:43 +0900</pubDate>
    </item>
    <item>
      <title>[AI/DL] FP8부터 H100까지, AI 학습용 GPU와 기술용어 완벽 정리</title>
      <link>https://engineerinsight.tistory.com/445</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  AI 학습에 GPU를 사용하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPU(Graphics Processing Unit, 그래픽카드)는 원래 그래픽 렌더링을 빠르게 처리하기 위해 만들어진 장치로, 화면의 수많은 픽셀을 동시에 계산하는 병렬 연산에 특화되어 있습니다. CPU는 복잡한 제어와 논리를 수행하지만, 대량의 단순 연산에는 비효율적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딥러닝은 대규모 행렬 곱셈으로 구성되어 있고, GPU는 수천 개의 코어로 이런 병렬 연산을 한 번에 처리할 수 있어 학습 속도가 크게 향상됩니다. 2000년대 중반 NVIDIA가 GPU를 일반 연산에도 사용할 수 있게 하는 CUDA 기술을 공개하면서, GPU는 그래픽 전용이 아닌 범용 병렬 연산 장치로 진화했습니다. 그 결과 오늘날 대부분의 AI 모델 학습과 추론은 GPU 위에서 수행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  용어 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;정밀도(Precision): FP32, FP16, BF16, FP8&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;FP32 (32-bit)&lt;/b&gt; 는 &lt;code&gt;높은 정밀도&lt;/code&gt;를 갖지만 연산이 느리고 메모리를 많이 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FP16 (16-bit)&lt;/b&gt; 은 정밀도를 낮추는 대신 속도와 효율이 크게 향상된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BF16 (BFloat16)&lt;/b&gt; 은 FP16과 같은 16비트지만 FP32의 지수 범위를 유지해 안정적이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FP8 (8-bit)&lt;/b&gt; 은 &lt;b&gt;초저정밀&lt;/b&gt; 연산 포맷으로, FP16보다 메모리 사용량이 절반이며 연산 속도는 2배 이상 &lt;code&gt;빠르다&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딥러닝 학습에서는 완벽한 실수 계산보다 효율이 중요하다. &lt;i&gt;(실제로 양자화를 통해서 큰 성능에 손실 없이 모델의 크기를 줄이는 기법도 있으니껭)&lt;/i&gt; FP32에서 FP16, FP8로의 전환은 단순한 경량화가 아니라 &lt;b&gt;대규모 모델 학습을 가능하게 한 하드웨어 진화의 핵심&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;H100부터는 FP8을 Transformer Engine이 자동 전환하며 사용해 안정성과 효율을 모두 확보한다.&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;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;b&gt;FP32&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;32bit&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;b&gt;FP16&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;16bit&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;b&gt;BF16&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;16bit&lt;/td&gt;
&lt;td&gt;FP32 수준의 안정성&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;b&gt;FP8&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;8bit&lt;/td&gt;
&lt;td&gt;매우 빠름, 메모리 절약&lt;/td&gt;
&lt;td&gt;정밀도 낮음&lt;/td&gt;
&lt;td&gt;H100 이후 LLM 학습/추론&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;Mixed Precision Training&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서로 다른 정밀도(FP32, FP16, BF16)를 혼합&lt;/b&gt;해 사용하는 학습 기법&lt;/li&gt;
&lt;li&gt;중요한 연산(LayerNorm, Gradient 계산)은 FP32로 나머지는 FP16으로 수행해 메모리 절약과 속도 향상을 동시에 얻는다.&lt;/li&gt;
&lt;li&gt;대부분의 대형 모델 학습 프레임워크(PyTorch, DeepSpeed, Megatron)는 이미 기본적으로 이 방식을 적용한다. &lt;i&gt;(사용하고 있는 기술들도 알고보면 소프트웨어적으로 Mixed Precision을 자동 적용해 내가 따로 설정하지 않아도 대부분 알아서 처리되고 있을 수도)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;Tensor Core&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GPU 내부에 존재하는 행렬 연산 전용 연산 유닛&lt;/li&gt;
&lt;li&gt;기존 CUDA Core가 한 번에 하나의 곱셈을 처리했다면, Tensor Core는 4&amp;times;4 행렬 곱을 한 사이클에 연산한다.&lt;/li&gt;
&lt;li&gt;GPU가 단순히 코어 수로만 빠른 것이 아니라, &lt;b&gt;딥러닝 전용 회로&lt;/b&gt; 수준의 가속 구조를 갖고 있기 때문에 AI에 강하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;CUDA (Compute Unified Device Architecture)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NVIDIA가 개발한 GPU 병렬 프로그래밍 프레임워크&lt;/li&gt;
&lt;li&gt;GPU를 그래픽 렌더링용이 아닌 일반 수학 연산 장치로 사용할 수 있게 만든 기술&lt;/li&gt;
&lt;li&gt;CUDA의 등장을 통해서 GPU가 딥러닝 엔진으로 유명해지기 시작했고, 현재 거의 모든 딥러닝 프레임워크(PyTorch, TensorFlow)가 CUDA 위에서 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;HBM (High Bandwidth Memory)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GPU에 탑재되는 초고속 메모리&lt;/li&gt;
&lt;li&gt;CPU의 DDR 메모리보다 대역폭이 수십 배 빠르다.&lt;/li&gt;
&lt;li&gt;예. H100의 HBM3는 초당 3.3TB의 데이터를 전송할 수 있다.&lt;/li&gt;
&lt;li&gt;LLM 학습의 병목은 연산량보다 데이터 전송 속도인데, HBM은 이 병목을 제거해 대규모 모델의 실시간 학습을 가능하게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;VRAM (Video RAM)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GPU의 연산용 메모리로, 딥러닝에서는 모델 파라미터와 텐서를 저장하는 공간이다.&lt;/li&gt;
&lt;li&gt;VRAM보다 모델이 크면 학습이 불가능하므로, 연구자들은 Gradient Checkpointing, ZeRO 등으로 VRAM 사용을 최소화한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;NVLink / NVSwitch&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔비디아에서 만든 기술로, 여러개의 그래픽카드(GPU)를 하나의 거대한 GPU로 동작하게 하기 위해서 사용한다&lt;/li&gt;
&lt;li&gt;NVLink는 여러 GPU를 고속으로 연결하는 링크&lt;/li&gt;
&lt;li&gt;NVSwitch는 여러 NVLink를 중앙에서 스위칭하는 장치&lt;/li&gt;
&lt;li&gt;GPT, Claude, Gemini 등 초대형 모델은 이 구조 위에서 학습된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;MIG (Multi-Instance GPU)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 GPU를 여러 개의 논리 GPU로 분할하는 기능 (A100부터 지원)&lt;/li&gt;
&lt;li&gt;GPU 자원을 효율적으로 나누어 사용하기 위한 구조&lt;/li&gt;
&lt;li&gt;연구소나 클라우드 환경에서 여러 사용자가 동시에 A100 한 장을 공유할 때 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;Data Parallel / Model Parallel / Pipeline Parallel&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Data Parallel:&lt;/b&gt; 각 GPU가 서로 다른 데이터를 학습&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Model Parallel:&lt;/b&gt; 모델을 여러 GPU에 나누어 학습&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pipeline Parallel:&lt;/b&gt; 모델의 레이어 단위를 여러 GPU에 순차적으로 배치&lt;/li&gt;
&lt;li&gt;LLM 학습은 단일 GPU로 불가능하고, 이 세 가지 병렬화 전략이 결합된 &lt;b&gt;Hybrid Parallel&lt;/b&gt; 구조가 실제 대규모 분산 학습에 사용된당&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;Transformer Engine&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;H100 GPU에 탑재된 FP8/FP16 자동 전환 연산 엔진&lt;/li&gt;
&lt;li&gt;Transformer 구조의 연산 패턴에 맞게 실시간으로 정밀도를 조정&lt;/li&gt;
&lt;li&gt;FP8의 속도 + FP16의 안정성 사이에 트레이드오프를 알아서 고려해 최적의 성능을 낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;FlashAttention&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Transformer의 Self-Attention을 GPU 친화적으로 재구현한 알고리즘&lt;/li&gt;
&lt;li&gt;메모리 사용량을 줄이고 속도를 크게 향상시켜, 대부분의 최신 LLM이 채택하고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;ZeRO (Zero Redundancy Optimizer)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대규모 모델 학습 시 &lt;b&gt;중복 메모리를 제거&lt;/b&gt;하는 분산 최적화 기술&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  GPU 종류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 시장에서 GPU는 크게 세 가지 계열로 나뉩니다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 1. GeForce 계열 &amp;ndash; 소비자용 그래픽카드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반 게이머와 크리에이터를 위한 &lt;b&gt;소비자용 GPU 라인업&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예. &lt;b&gt;RTX 3080, 4080, 4090, 5090&lt;/b&gt; 등 &lt;i&gt;(3080을 2장 붙여서 사용하는 경우가 많다고 한다)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;VRAM은 보통 12~24GB 수준으로, &lt;b&gt;개인 개발자나 연구자들이 소규모 모델 학습&lt;/b&gt;에 많이 사용&lt;/li&gt;
&lt;li&gt;그래픽용으로 설계되었지만 CUDA나 PyTorch를 이용하면 &lt;b&gt;딥러닝 학습/추론&lt;/b&gt;도 가능&lt;/li&gt;
&lt;li&gt;가격대는 약 &lt;b&gt;100만~500만 원&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;개인용으로는 현실적인 선택지 &lt;i&gt;(이 뒤로는 조금 천상계 가격이기 땜시)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 2. RTX / Quadro / RTX A 시리즈 &amp;ndash; 전문가용 워크스테이션 GPU&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디자인, 렌더링, AI 연구를 위한 &lt;b&gt;전문용 GPU 라인&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예. &lt;b&gt;RTX A5000, A6000&lt;/b&gt; 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ECC 메모리&lt;/b&gt;, &lt;b&gt;드라이버 안정성&lt;/b&gt;, &lt;b&gt;정밀도(FP32/FP64)&lt;/b&gt; 가 보장되어 장시간 연산에도 안정적&lt;/li&gt;
&lt;li&gt;VRAM은 24~48GB 수준으로, 중형 모델 학습이나 연구용 서버에서 많이 쓰임&lt;/li&gt;
&lt;li&gt;가격대는 &lt;b&gt;천만 원 안팎&lt;/b&gt;, 학술 연구나 기업용 워크스테이션에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 3. Tesla / A100 / H100 계열 &amp;ndash; 데이터센터&amp;middot;AI 전용 GPU&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대규모 모델 학습용으로 설계된 &lt;b&gt;서버급 GPU&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예. &lt;b&gt;V100, A100, H100&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수십 GB의 VRAM(40~94GB)&lt;/b&gt; 과 &lt;b&gt;NVLink 인터커넥트&lt;/b&gt;로 여러 GPU를 묶어 거대한 학습 클러스터를 구성할 수 있음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NVLink / NVSwitch: 여러 GPU를 하나의 클러스터처럼 묶는 고속 통신 기술&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GPT, Claude, Gemini 같은 &lt;b&gt;초대형 LLM 학습 인프라의 중심&lt;/b&gt;에 있는 장비&lt;/li&gt;
&lt;li&gt;가격은 GPU 한 장당 &lt;b&gt;2천만~6천만 원 이상&lt;/b&gt;으로, 개인이 구매하기는 현실적으로 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 GPU VRAM 용량이 제한되어 있기 때문에, 대규모 모델은 여러 GPU를 묶어서 학습합니다. NVIDIA는 NVLink/NVSwitch 기술을 제공하고, GPU 간 통신 속도를 높여 여러개를 묶어서도 마치 하나의 거대한 GPU처럼 작동하게 합니다.&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;1618&quot; data-origin-height=&quot;188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k06MZ/dJMb9MiBcrb/3AYQ17jpoHWnNWL2vgJFSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k06MZ/dJMb9MiBcrb/3AYQ17jpoHWnNWL2vgJFSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k06MZ/dJMb9MiBcrb/3AYQ17jpoHWnNWL2vgJFSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk06MZ%2FdJMb9MiBcrb%2F3AYQ17jpoHWnNWL2vgJFSK%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;817&quot; height=&quot;95&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;188&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나도 가지진 못했지만 학부시절 과제할 때 학교 대여를 통해 원격이었지만 A100을 잠시 스쳐지나간(?) 적이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI/딥러닝</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/445</guid>
      <comments>https://engineerinsight.tistory.com/445#entry445comment</comments>
      <pubDate>Tue, 21 Oct 2025 14:30:11 +0900</pubDate>
    </item>
    <item>
      <title>[AI/LLM] 거대한 LLM에서 가벼운 sLLM으로: LLM 경량화 필요성과 방법</title>
      <link>https://engineerinsight.tistory.com/444</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/AiResearch2025/FigVarietyRAGChat&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1761020513964&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - AiResearch2025/FigVarietyRAGChat: RAG 기반 희귀품종 무화과 챗봇 프로젝트&quot; data-og-description=&quot;RAG 기반 희귀품종 무화과 챗봇 프로젝트. Contribute to AiResearch2025/FigVarietyRAGChat development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat&quot; data-og-url=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cE8cee/hyZL9CzUHy/gzjCSEFM5Od5gPK2fK0kMK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cEdGdB/hyZL4Vy3lq/K5oh5sXztyrW3fAiYLKdO1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/AiResearch2025/FigVarietyRAGChat&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cE8cee/hyZL9CzUHy/gzjCSEFM5Od5gPK2fK0kMK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cEdGdB/hyZL4Vy3lq/K5oh5sXztyrW3fAiYLKdO1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - AiResearch2025/FigVarietyRAGChat: RAG 기반 희귀품종 무화과 챗봇 프로젝트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;RAG 기반 희귀품종 무화과 챗봇 프로젝트. Contribute to AiResearch2025/FigVarietyRAGChat development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  sLLM이 왜 필요할까?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 인트로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 몇 년간 인공지능은 LLM(Large Language Model) 덕분에 폭발적으로 발전했다.&lt;br /&gt;ChatGPT, Claude, Gemini와 같은 모델들이 대표적이다.&lt;br /&gt;이들은 방대한 데이터를 학습해 사람처럼 언어를 이해하고 문장을 생성할 수 있다.&lt;br /&gt;하지만, 이런 &amp;lsquo;거대함&amp;rsquo;은 동시에 여러 가지 현실적인 한계를 낳는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ LLM의 한계 (너무 큼)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 언어의 문맥, 의미, 추론 구조 등을 수학적으로 모델링하기 위해 수십억 개에서 많게는 수천억 개의 파라미터(parameter) 를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파라미터 하나하나가 모델이 문장을 이해하고 생성할 때 사용되는 &amp;ldquo;지식 단위&amp;rdquo;이다.&lt;br /&gt;즉, 모델이 커질수록 더 많은 언어적 패턴과 개념을 표현할 수 있지만,&lt;br /&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;GPT-2&lt;/td&gt;
&lt;td&gt;1.5억 (0.15B)&lt;/td&gt;
&lt;td&gt;약 0.5GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-3&lt;/td&gt;
&lt;td&gt;1,750억 (175B)&lt;/td&gt;
&lt;td&gt;약 350GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4&lt;/td&gt;
&lt;td&gt;수천억~1조 이상 추정&lt;/td&gt;
&lt;td&gt;TB 단위 이상&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연산 비용이 너무 큼
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 문장 만드려면 수십억 번의 matrix 연산이 필요하다.&lt;/li&gt;
&lt;li&gt;고성능 GPU나 TPU가 여러 대 필요한데 대부분의 회사나 개인은 이 비용을 맞출 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;메모리, 저장 공간이 너무 많이 필요함&lt;/li&gt;
&lt;li&gt;높은 전력 소모&lt;/li&gt;
&lt;li&gt;지연 시간
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에서는 빠르지만, 기기에서 직접 실행하면 응답 속도가 느려진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;높은 비용&lt;/li&gt;
&lt;li&gt;보안과 프라이버시 이슈
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부 문서나 개인정보를 모델에 입력하기 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ sLLM의 장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 이유로 최근 작지만 똑똑한 sLLM이 주목받고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&amp;ldquo;대규모 모델의 지능을 유지하면서, 훨씬 가볍고 빠르게 동작하는 모델을 만들자.&amp;rdquo;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(1) 기업 내부용 sLLM&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 기업들은 LLM을 자체 서버나 폐쇄망에서 운영하고 싶어 한다. 하지만 거대한 LLM은 GPU 수십 장 없이는 구동조차 어렵다.&lt;br /&gt;그래서 등장한 것이, LLaMA, Mistral, Phi 시리즈처럼 오픈소스 기반의 경량화 모델이다. 기업들은 이를 fine-tuning하여 내부 문서 검색, 이메일 요약, 코드 분석 등 다양한 업무에 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(2) 온디바이스(On-device) sLLM&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에는 스마트폰과 노트북에서도 AI 모델을 직접 돌리는 온디바이스 AI가 주목받고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, Apple의 Neural Engine (뉴럴 코어), Qualcomm의 Hexagon NPU, 삼성의 Exynos NPU와 같은 이런 전용 AI 칩은 클라우드 없이도 기기 안에서 바로 추론을 수행할 수 있다.&lt;br /&gt;다만 LLM을 그대로 올리기엔 너무 커서, 경량화된 sLLM이 필수적이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  LLM을 sLLM으로 줄이는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM의 지능을 최대한 보존하면서 크기를 최대한 줄여야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해서 대표적으로 4가지 방법이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 지식 증류 (Knowledge Distillation)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;581&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4mNOT/dJMb9LjGxxj/j628M6iOkm730dxmVmNCdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4mNOT/dJMb9LjGxxj/j628M6iOkm730dxmVmNCdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4mNOT/dJMb9LjGxxj/j628M6iOkm730dxmVmNCdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4mNOT%2FdJMb9LjGxxj%2Fj628M6iOkm730dxmVmNCdk%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;571&quot; height=&quot;237&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;581&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 직관적인 방법이다. 큰 모델을 &amp;lsquo;선생님(Teacher)&amp;rsquo;, 작은 모델을 &amp;lsquo;학생(Student)&amp;rsquo;이라고 생각하면 된다.&lt;br /&gt;크고 비싼 Teacher 모델이 가지고 있는 지식(knowledge)을 작은 student 모델에게 전수(transfer) 하는 방법론이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, Teacher가 &amp;ldquo;이 문장에서 다음 단어는 70% 확률로 X야&amp;rdquo;라고 예측하면 Student는 그 확률 분포를 따라 학습한다. 이렇게 하면 작은 모델이 데이터뿐 아니라 생각하는 방식까지 배울 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 자체를 모르더라도 API 콜만 할 수 있다면 작은 모델을 학습시킬 수 있다. 그리고 teacher, student 의 결과값의 분포를 비슷하게 만드는 것을 목표로 한다. (분포의 차이를 재는 KL Divergence를 최소화하는 방향으로 학습)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예. DistilBERT (BERT보다 40% 작지만 97% 성능 유지)&lt;br /&gt;gpt-4o-mini (gpt-4o보다 빠르지만 성능 차이도 크지 않음)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@yugank.aman/knowledge-distillation-for-llms-techniques-and-applications-e23a17093adf&quot;&gt;https://medium.com/@yugank.aman/knowledge-distillation-for-llms-techniques-and-applications-e23a17093adf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=167285&amp;amp;boardType=techBlog&quot;&gt;https://devocean.sk.com/blog/techBoardDetail.do?ID=167285&amp;amp;boardType=techBlog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 양자화 (Quantization)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델의 숫자 표현을 단순화하는 방법이다. 일반적으로 딥러닝 모델은 모든 가중치를 16비트나 32비트 부동소수(float32) 로 저장하는데, 양자화를 하면 가중치를 8비트, 4비트 정수(Int8, Int4) 로 바꾸어 모델 크기와 메모리 사용량이 급격히 줄어든다. 물론 세부적인 표현력이나 특정 정보의 처리 능력에서 저하가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국어 모델을 양자화할 때 더 조심할 점은, LLM 양자화가 영어나 프랑스어보다 한국어/일본어와 같은 언어에 더 큰 영향을 미친다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예. QLoRA, GPTQ, bitsandbytes 4bit quantization&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ariz1623.tistory.com/354&quot;&gt;https://ariz1623.tistory.com/354&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 프루닝 (Pruning)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cP4EMR/dJMb9NBNNHn/HyR54buNiCXIDDnxhUHYA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cP4EMR/dJMb9NBNNHn/HyR54buNiCXIDDnxhUHYA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cP4EMR/dJMb9NBNNHn/HyR54buNiCXIDDnxhUHYA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcP4EMR%2FdJMb9NBNNHn%2FHyR54buNiCXIDDnxhUHYA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;498&quot; height=&quot;219&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 안을 들여다보면, 어떤 뉴런이나 연결은 거의 쓰이지 않는다. 이런 부분을 &amp;ldquo;가지치기&amp;rdquo;하듯 제거하는 과정이 프루닝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요도가 낮은 연결을 잘라내고, 남은 연결들만 다시 미세조정(fine-tuning)하면 성능은 거의 유지하면서 크기는 크게 줄어든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 대규모 언어 모델에서 특정 레이어를 제거해도 성능이 유지되거나, 최소한의 파인튜닝으로 복원될 수 있다고 하는 논문 역시 발표되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예. SparseGPT, Movement Pruning 등&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.deepchecks.com/llm-pruning-and-distillation-importance/&quot;&gt;https://www.deepchecks.com/llm-pruning-and-distillation-importance/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://discuss.pytorch.kr/t/llm-layer-pruning-qlora/3951&quot;&gt;https://discuss.pytorch.kr/t/llm-layer-pruning-qlora/3951&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ LoRA (Low-Rank Adaptation)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hwk1P/dJMb9LqrFCP/i4ZeS8IFabJowZGw8VJIpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hwk1P/dJMb9LqrFCP/i4ZeS8IFabJowZGw8VJIpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hwk1P/dJMb9LqrFCP/i4ZeS8IFabJowZGw8VJIpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHwk1P%2FdJMb9LqrFCP%2Fi4ZeS8IFabJowZGw8VJIpK%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;373&quot; height=&quot;270&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoRA는 필요한 부분만 살짝 학습하는 효율적인 Fine-tuning 기법이다.&lt;br /&gt;기존 모델 전체를 새로 훈련하지 않고, 가중치 행렬 일부를 작은 두 행렬로 분해해 그 부분만 학습한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그래서 기존 모델을 건드리지 않으면서도 새로운 태스크에 빠르게 적응할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예. LoRA, QLoRA, AdaLoRA 등&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://da2so.tistory.com/79&quot;&gt;https://da2so.tistory.com/79&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI/LLM</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/444</guid>
      <comments>https://engineerinsight.tistory.com/444#entry444comment</comments>
      <pubDate>Mon, 20 Oct 2025 17:00:44 +0900</pubDate>
    </item>
    <item>
      <title>[AI/RAG] RAG 기반 희귀품종 무화과 LLM 챗봇 프로젝트(1): 프로젝트의 시작과 구상 (AI Agent, RAG 적용)</title>
      <link>https://engineerinsight.tistory.com/443</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  프로젝트의 시작,,&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국내에서 유통되는 무화과의 대부분(무려 &lt;b&gt;95% 이상&lt;/b&gt;)은 &lt;b&gt;&amp;lsquo;도핀(Dauphin)&amp;rsquo;&lt;/b&gt; 품종이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도핀은 저장성과 유통성이 좋아 상업적으로는 효율적이지만, 식감이 단단하고 당도가 낮아 &lt;b&gt;집에서 직접 키워 먹는 무화과의 풍미와는 거리가 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 최근에는 더 부드럽고 달콤한 품종을 직접 키워보려는 사람들이 많아졌고, 나 역시 &amp;lsquo;즐거운 무화과 생활&amp;rsquo;이라는 네이버 카페를 통해서 품종 무화과를 접했고 나뭇가지 하나일 뿐인 삽수부터 뿌리를 내리고, 열매를 맺는 과정까지를 즐기는 사람이당.&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;1170&quot; data-origin-height=&quot;1387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sZvYv/dJMb9aqabUL/HdYMpmTa4PNtEKqGhy6281/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sZvYv/dJMb9aqabUL/HdYMpmTa4PNtEKqGhy6281/img.png&quot; data-alt=&quot;삽수(나뭇가지)에서 뿌리가 나왔다 (=발근)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sZvYv/dJMb9aqabUL/HdYMpmTa4PNtEKqGhy6281/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsZvYv%2FdJMb9aqabUL%2FHdYMpmTa4PNtEKqGhy6281%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;343&quot; height=&quot;407&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1387&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;삽수(나뭇가지)에서 뿌리가 나왔다 (=발근)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;869&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRR4Ss/dJMb9PM9ruy/iFaePsDa0F6p6f1Q8dX2OK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRR4Ss/dJMb9PM9ruy/iFaePsDa0F6p6f1Q8dX2OK/img.png&quot; data-alt=&quot;이렇게 많은 삽수들을 통해서 나무를 얻었다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRR4Ss/dJMb9PM9ruy/iFaePsDa0F6p6f1Q8dX2OK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRR4Ss%2FdJMb9PM9ruy%2FiFaePsDa0F6p6f1Q8dX2OK%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;412&quot; height=&quot;306&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;869&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이렇게 많은 삽수들을 통해서 나무를 얻었다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 품종 이름이나 특징이 생소한 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키우면서 느낀 답답함은, 품종마다 삽목(나뭇가지에서 뿌리를 내리는 과정)에 걸리는 시간이나 뿌리가 난 직후에 식물의 예민도, 햇빛을 선호하는 정도, 물을 선호하는 정도가 제각각인데 이 정보를 찾기가 매우 힘들다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무화과 키우기가 취미인 사람들은 굉장히 소수이기 때문에 이러한 희귀 품종들은 검색을 통해서도 제대로 나오지 않으며 무화과 카페 내에서도 영어로 된 해외 도감에 의존하는 경우가 많다. 예를 들어 SV, LSUT, Cicco Nero, BNR, RDB, Hardy Chicago처럼 해외 품종들은 이게 우리 기후에서 잘 자라는지, 혹은 어떤 맛과 숙기(익는 시기)를 가지는지, 인터넷 검색만으로는 정리된 정보를 얻기 어렵다. 또 인터넷 거래를 통해 비싸게 거래되는 삽수의 경우에도 판매자가 판매하는 품종이 제대로 된 품종인지 알기도 어렵다. 삽수를 구매하거나 선물받게 되면 보통 페인트 마커로 (주로 악필로) 종의 이름이 영어 약자로 적힌 경우가 굉장히 많은데, 이 경우까지도 어떤 품종인지 사진을 통해 알아내고 해당 품종을 키울 때의 주의사항에 대해서 자세히 알려줄 수 있으면 좋겠다고 생각했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBpnON/dJMb9QrKOER/aL8URecODmKglKxLbjQL3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBpnON/dJMb9QrKOER/aL8URecODmKglKxLbjQL3K/img.png&quot; data-alt=&quot;네임펜으로 이름을 적어서 받은 선물&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBpnON/dJMb9QrKOER/aL8URecODmKglKxLbjQL3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBpnON%2FdJMb9QrKOER%2FaL8URecODmKglKxLbjQL3K%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;390&quot; height=&quot;510&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1530&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;네임펜으로 이름을 적어서 받은 선물&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;854&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qJs1C/dJMb9XEk08Q/KYpwJM37cZKLvBHYBdc1UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qJs1C/dJMb9XEk08Q/KYpwJM37cZKLvBHYBdc1UK/img.png&quot; data-alt=&quot;페인트마커로 이름을 적어서 받은 선물&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qJs1C/dJMb9XEk08Q/KYpwJM37cZKLvBHYBdc1UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqJs1C%2FdJMb9XEk08Q%2FKYpwJM37cZKLvBHYBdc1UK%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;455&quot; height=&quot;332&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;854&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;페인트마커로 이름을 적어서 받은 선물&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 해결하기 위해 이번 프로젝트에서는 &lt;b&gt;&amp;ldquo;무화과 품종에 대해 자유롭게 물어볼 수 있는 AI 챗봇&amp;rdquo;&lt;/b&gt;, 즉 &lt;b&gt;LLM + RAG 기반의 무화과 품종 질의응답 시스템&lt;/b&gt;을 직접 구축해보려고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  프로젝트 구상&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ AI Agent&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG 시스템 위에 &lt;b&gt;AI Agent 계층&lt;/b&gt;을 추가해, 사용자의 질문 의도를 분류하고 적절한 처리 흐름을 선택하는 구조를 설계한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 질문을 입력하면, 먼저 LLM이 단순히 답을 내기 전에 &lt;b&gt;&amp;ldquo;이 질문의 유형이 무엇인지&amp;rdquo;를 판단&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[핵심 분류 기준]&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;사용자가 특정 품종 이름을 알고있는가?&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그 품종이 실제 존재하는(또는 RAG 데이터에 존재하는) 품종인가?&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;현재 질문이 RAG 검색으로 해결 가능한가, 아니면 추가 질의가 필요한가?&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 Agent는 두 갈래의 주요 행동 방식을 갖는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ AI Agent 동작 흐름&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;사용자 입력 &amp;rarr; Agent (질문 분석 &amp;amp; 분기) &amp;rarr; RAG Search or Clarifying Q &amp;rarr; LLM 응답&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;품종 언급 여부 판단&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력문 분석 단계에서 품종명(&lt;code&gt;Dauphin&lt;/code&gt;, &lt;code&gt;Brown Turkey&lt;/code&gt;, &lt;code&gt;Violette de Bordeaux&lt;/code&gt; 등)이 포함되어 있는지 탐지 &lt;i&gt;(무화과 덕후들의 경우 Hardy Chicago를 HC와 같이 약자로 사용하는 경우가 많기 때문에 이 경우도 특별히 고려해야 한다)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;품종명 데이터는 사전에 수집한 품종 리스트(혹은 벡터DB의 key index)에서 식별 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Case A] 품종을 알고 있는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(1) 품종 존재 여부 확인&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력된 품종이 RAG 데이터셋에 존재하지 않거나 유사 오탈자인 경우,&lt;br /&gt;LLM이 &amp;ldquo;혹시 이 품종 이름이 &amp;lsquo;Ciccio Vero&amp;rsquo;이 아니라 &amp;lsquo;Ciccio Nero&amp;rsquo;을 말씀하신 건가요?&amp;rdquo; 처럼 정정 제안&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(2) 품종이 정확히 존재한다면&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;질문의 내용이 RAG 기반 검색으로 처리 가능한지 판단&lt;br /&gt;(예. &amp;ldquo;BNR은 당도가 높나요?&amp;rdquo;, &amp;ldquo;브런즈윅은 제주도 돌담 바로 옆에서 월동이 가능한가요?&amp;rdquo; 등)&lt;/li&gt;
&lt;li&gt;질문의 정보만으로도 응답이 가능하면 RAG retrieval 수행해 context 기반 답변 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(3) 추가 정보가 필요한 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;질문이 모호하거나 &amp;ldquo;어떤 품종이 제일 맛있나요?&amp;rdquo;처럼 비교적 문맥이 부족한 경우, Agent가 보충 질문을 던진다.&lt;br /&gt;(예. &amp;ldquo;맛 기준이 당도인가요, 식감인가요?&amp;rdquo;, &amp;ldquo;지역은 어디 기준인가요?&amp;rdquo;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Case B] 품종을 모르는 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;질문이 &amp;ldquo;달콤한 무화과 품종 추천해줘&amp;rdquo;, &amp;ldquo;무화과 중에서 단단하지 않은 게 뭐야?&amp;rdquo; 같은 일반적인 형태라면 품종명을 직접 모르는 상태로 판단
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Agent가 사용자 요구를 품종 검색 쿼리 형태로 변환 (&amp;ldquo;달콤한 품종&amp;rdquo; &amp;rarr; sweetness score 높은 품종 리스트 요청)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;질문이 (사진과 함께) &amp;ldquo;이 무화과의 품종은 뭐야?&amp;rdquo; 라고 물어보면 품종을 모르는 상태로 판단하나, 사진을 통해서 품종을 유추하도록 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PROJECT/RAG 기반 희귀품종 무화과 챗봇</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/443</guid>
      <comments>https://engineerinsight.tistory.com/443#entry443comment</comments>
      <pubDate>Mon, 20 Oct 2025 12:00:49 +0900</pubDate>
    </item>
    <item>
      <title>[AI/DL] 딥러닝의 발전(2): RNN에서 Transformer까지 &amp;mdash; 순차 데이터와 Self-Attention 이해하기</title>
      <link>https://engineerinsight.tistory.com/442</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;자연어 처리(NLP)나 시계열 데이터는 입력의 순서가 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RNN, LSTM, Transformer는 이런 순차 데이터를 다루기 위해 만들어진 대표적인 구조들입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  RNN (Recurrent Neural Network)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이전 정보를 기억하면서 순서가 있는 데이터를 처리하는 신경망&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순차 데이터 처리를 위해 등장함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;549&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dliTql/dJMb9PTUPDy/y3Zl06x20ElukCQwaK5VPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dliTql/dJMb9PTUPDy/y3Zl06x20ElukCQwaK5VPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dliTql/dJMb9PTUPDy/y3Zl06x20ElukCQwaK5VPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdliTql%2FdJMb9PTUPDy%2Fy3Zl06x20ElukCQwaK5VPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;464&quot; height=&quot;257&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;549&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYhodV/dJMb9PTUPDD/t0PgZK0kxM54BTpEkIpOuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYhodV/dJMb9PTUPDD/t0PgZK0kxM54BTpEkIpOuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYhodV/dJMb9PTUPDD/t0PgZK0kxM54BTpEkIpOuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYhodV%2FdJMb9PTUPDD%2Ft0PgZK0kxM54BTpEkIpOuK%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;295&quot; height=&quot;47&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;90&quot;/&gt;&lt;/span&gt;&lt;/figure&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;x(t)&lt;/td&gt;
&lt;td&gt;지금 단어의 벡터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;h(t-1)&lt;/td&gt;
&lt;td&gt;이전까지의 기억&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;w(x), w(t)&lt;/td&gt;
&lt;td&gt;학습 가능한 가중치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;h(t)&lt;/td&gt;
&lt;td&gt;새로운 기억(은닉 상태)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tanh&lt;/td&gt;
&lt;td&gt;활성화 함수 &amp;rarr; 기억 압축&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 시점까지의 기억과 현재의 입력을 합쳐서 지금까지의 문맥의 요약을 만든 것을 은닉 상태(hidden state)라고 합니다. 즉, 새로운 기억을 만들 때 이때까지의 문맥을 반영하기 때문에 일반적인 NN과 달리 인접한 직전 정보까지의 내용을 반영할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RNN에는 치명적인 단점이 있는데, 본질적으로 순차적인 구조라는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 순서대로 데이터를 처리하기 때문에 이전 시점의 계산 결과가 나와야만 다음 시점의 계산을 시작할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 문장 &lt;code&gt;&quot;I love deep learning&quot;&lt;/code&gt;을 처리한다고 하면&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;t=1: &quot;I&quot; &amp;rarr; h₁ 계산
t=2: &quot;love&quot; &amp;rarr; h₂ = f(h₁, x₂)
t=3: &quot;deep&quot; &amp;rarr; h₃ = f(h₂, x₃)
t=4: &quot;learning&quot; &amp;rarr; h₄ = f(h₃, x₄)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;h₃&lt;/code&gt;를 계산하려면 반드시 &lt;code&gt;h₂&lt;/code&gt;가 먼저 나와야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;이전 단계가 끝나야 다음 단계로 넘어갈 수 있어서 GPU 병렬화 불가능&lt;/b&gt;합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  LSTM &lt;b&gt;(Long Short-Term Memory)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RNN이 오래 기억을 못 하는 문제를 해결한 개선형 RNN&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RNN은 이전 정보를 계속 곱해가면서 전달하기 때문에 시간이 길어지면 기울기(gradient)가 사라져서 오래된 정보가 증발합니다. (= 기울기 소실 문제, Vanishing Gradient Problem)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 문장이 조금만 길어져도 앞부분 내용을 잊어버리는 문제가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해서 LSTM은 기억을 직접 관리하기 위해서 3개의 Gate를 도입합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요한 건 오래 두고, 쓸데없는 것은 잊어버리는 전략입니다&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Forget Gate&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;어떤 정보는 &lt;b&gt;잊을지&lt;/b&gt; 결정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Input Gate&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;어떤 새 정보를 &lt;b&gt;기억할지&lt;/b&gt; 결정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Output Gate&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;어떤 정보를 &lt;b&gt;출력할지&lt;/b&gt; 결정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 3개의 게이트가 하나의 &amp;ldquo;기억 셀(cell state)&amp;rdquo;을 조절하면서 장기 기억을 유지시켜줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LSTM은 구조적으로 RNN의 확장판이기 때문에, 순차적 특성(sequential dependency) 은 여전히 그대로 존재합니다.&lt;br /&gt;즉, LSTM은 RNN의 단점을 기억 유지 측면에서 보완했을 뿐, 병렬 처리 불가능 문제는 여전히 남아 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ RNN vs LSTM&lt;/h3&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;RNN&lt;/th&gt;
&lt;th&gt;LSTM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기억 구조&lt;/td&gt;
&lt;td&gt;단일 hidden state&lt;/td&gt;
&lt;td&gt;hidden + cell state&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;/tr&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;긴 문장, 번역, 음성 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;Transformer (2017)&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;문장 안의 모든 단어가 서로를 직접 참고할 수 있게 만든 모델&amp;rdquo;&lt;br /&gt;&amp;mdash; 2017년 Google 논문 《Attention is All You Need》 발표&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transformer는 RNN을 완전히 대체한 모델로, RNN처럼 단어를 한 단어씩 순서대로 읽지 않고&lt;br /&gt;&lt;b&gt;문장 전체를 병렬로 한 번에 처리하면서 단어 간 관계를 Attention으로 계산&lt;/b&gt;합니다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Self-Attention&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각 단어가 문장 안의 다른 단어와 얼마나 관련이 있는지 점수를 계산&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어,&lt;br /&gt;The cat sat on the mat.&lt;br /&gt;이런 문장이 있다면 &amp;ldquo;cat&amp;rdquo;은 &amp;ldquo;sat&amp;rdquo;과 강하게 연결되고, &amp;ldquo;the&amp;rdquo;와는 약하게 연결됩니다.&lt;br /&gt;이 연결 강도(weight) 를 계산하는 게 바로 Attention입니당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산하는 방식은 다음과 같습니다!&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;각 단어를 벡터로 바꿈 (embedding)&lt;/li&gt;
&lt;li&gt;각 단어마다 세 가지 벡터를 계산&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Q (Query)&lt;/b&gt; : 내가 어떤 정보를 찾고 싶은지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;K (Key)&lt;/b&gt; : 내가 어떤 정보를 가지고 있는지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;V (Value)&lt;/b&gt; : 실제 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 모든 단어 쌍에 대해 연관성 점수 계산&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqFEXA/dJMb9PM9qPQ/qsS9IGwQHKtPkewu8YS8zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqFEXA/dJMb9PM9qPQ/qsS9IGwQHKtPkewu8YS8zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqFEXA/dJMb9PM9qPQ/qsS9IGwQHKtPkewu8YS8zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqFEXA%2FdJMb9PM9qPQ%2FqsS9IGwQHKtPkewu8YS8zk%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;494&quot; height=&quot;92&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 이 점수를 가중치로 해서 문장 전체 정보를 종합&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡하니 한번 예시를 통해 직접 해보겠습니당&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Self-Attention 계산 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Gitchan is hungry all the time&lt;/i&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;입력: 단어 임베딩&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transformer는 텍스트를 숫자 벡터로 변환해서 처리합니다. (=임베딩)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신경망은 숫자 이외에 String은 처리할 수 없기 때문에 신경망을 통과시키기 위해서는 임베딩을 통해 각각의 텍스트(토큰)를 숫자 벡터로 변환하는 과정이 필수적입니다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;&quot;Gitchan&quot; &amp;rarr; [0.1, 0.4, 0.6, ...]
&quot;is&quot;      &amp;rarr; [0.3, 0.2, 0.1, ...]
&quot;hungry&quot;  &amp;rarr; [0.7, 0.9, 0.4, ...]
&quot;all&quot;     &amp;rarr; [0.2, 0.1, 0.5, ...]
&quot;the&quot;     &amp;rarr; [0.05, 0.3, 0.2, ...]
&quot;time&quot;    &amp;rarr; [0.6, 0.8, 0.9, ...]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Q, K, V 계산&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단어 벡터(embedding)에 세 개의 가중치 행렬을 곱해서 &lt;b&gt;Query(Q), Key(K), Value(V)&lt;/b&gt; 를 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Q = X * W_Q
K = X * W_K
V = X * W_V&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Q(Query)&lt;/b&gt; : 이 단어가 &amp;ldquo;무엇을 찾고 싶은가?&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;K(Key)&lt;/b&gt; : 이 단어가 &amp;ldquo;어떤 정보를 가지고 있는가?&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;V(Value)&lt;/b&gt; : 이 단어의 &amp;ldquo;실제 내용 정보&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산했을 때 이렇게 나왔다고 해 봅시당&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;&quot;Gitchan&quot; &amp;rarr; Q=[0.9,0.3], K=[0.8,0.2], V=[0.7,0.4]
&quot;is&quot;      &amp;rarr; Q=[0.2,0.6], K=[0.1,0.8], V=[0.3,0.5]
&quot;hungry&quot;  &amp;rarr; Q=[0.5,0.9], K=[0.4,0.9], V=[0.6,0.7]
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 유사도 계산 (QKᵀ)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단어의 &lt;b&gt;Q와 다른 단어들의 K를 내적(dot product)&lt;/b&gt; 해서 두 단어가 서로 얼마나 관련 있는지 점수를 계산합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &amp;ldquo;Gitchan&amp;rdquo;의 Q와 나머지 단어들의 K 비교&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;Q&amp;middot;Kᵀ (유사도)&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;is&lt;/td&gt;
&lt;td&gt;0.9&amp;times;0.1 + 0.3&amp;times;0.8 = 0.33&lt;/td&gt;
&lt;td&gt;약간 관련&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hungry&lt;/td&gt;
&lt;td&gt;0.9&amp;times;0.4 + 0.3&amp;times;0.9 = 0.63&lt;/td&gt;
&lt;td&gt;관련 큼&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;time&lt;/td&gt;
&lt;td&gt;0.9&amp;times;0.6 + 0.3&amp;times;0.9 = 0.81&lt;/td&gt;
&lt;td&gt;매우 관련&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;Gitchan&amp;rdquo;은 &amp;ldquo;hungry&amp;rdquo;나 &amp;ldquo;time&amp;rdquo;과 의미적으로 더 강한 연결을 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Softmax로 확률화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Softmax 함수를 활용하면 값을 확률로 변환할 수 있습니다. 위에서 계산한 점수들을 softmax에 넣어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가중치(Attention weight)로 바꿉니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[0.33, 0.63, 0.81] &amp;rarr; Softmax &amp;rarr; [0.20, 0.35, 0.45]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;Gitchan&amp;rdquo;은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;is&amp;rdquo; 정보 20%&lt;/li&gt;
&lt;li&gt;&amp;ldquo;hungry&amp;rdquo; 정보 35%&lt;/li&gt;
&lt;li&gt;&amp;ldquo;time&amp;rdquo; 정보 45%&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도로 참고한다는 의미입니다. &lt;i&gt;(softmax 함수 값의 합은 항상 1입니다)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. V 정보 가중평균&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 각 단어의 &lt;b&gt;V 값(정보 벡터)&lt;/b&gt; 를 위 가중치로 가중 평균을 내면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HhmFd/dJMb9MJEuxW/ccGopoIWs7eeqhP9AV4820/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HhmFd/dJMb9MJEuxW/ccGopoIWs7eeqhP9AV4820/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HhmFd/dJMb9MJEuxW/ccGopoIWs7eeqhP9AV4820/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHhmFd%2FdJMb9MJEuxW%2FccGopoIWs7eeqhP9AV4820%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;451&quot; height=&quot;84&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;Gitchan&amp;rdquo;이라는 단어의 &lt;b&gt;최종 표현은&lt;/b&gt; 문장 전체 중 &amp;ldquo;hungry&amp;rdquo;와 &amp;ldquo;time&amp;rdquo;에 더 초점을 맞춰 만들어집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ GPU 병렬 연산 가능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 언급했던 RNN은 본질적으로 순차적인 구조이기 때문에 병렬 처리가 불가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;b&gt;Transformer는 Self-Attention을 사용&lt;/b&gt;하면서 완전히 구조가 바뀝니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 단어의 Q, K, V를 &lt;b&gt;한 번에 계산&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;QKᵀ&lt;/code&gt;로 유사도를 구할 때, &lt;b&gt;행렬 곱(Matrix Multiplication)&lt;/b&gt; 형태로 모든 단어 쌍의 관계를 동시에 계산합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, RNN처럼 &amp;ldquo;이전 단어의 결과를 기다리지 않아도&amp;rdquo; 됩니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Q = X * W_Q
K = X * W_K
V = X * W_V
Attention = softmax(QKᵀ / &amp;radic;d) * V&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 연산은 &lt;b&gt;모두 행렬 단위 계산&lt;/b&gt;이므로, GPU의 장점인 &lt;b&gt;벡터화(vectorization)&lt;/b&gt; 와 &lt;b&gt;병렬 행렬곱 연산&lt;/b&gt;을 100% 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Transformer는 문장 전체를 동시에 처리할 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RNN 대비 10~100배 이상 빠른 학습 속도&lt;/b&gt;를 보입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문장 내 &lt;b&gt;멀리 떨어진 단어 관계&lt;/b&gt;도 포착 가능&lt;/li&gt;
&lt;li&gt;GPU 병렬 연산 가능 &amp;rarr; 학습 속도 향상&lt;/li&gt;
&lt;/ul&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;b&gt;RNN/LSTM&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;Transformer&lt;/b&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;모든 단어 간 관계 파악 (Attention)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;속도&lt;/td&gt;
&lt;td&gt;느림&lt;/td&gt;
&lt;td&gt;빠름 (GPU 병렬화 최적)&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;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대표 모델&lt;/td&gt;
&lt;td&gt;LSTM, GRU&lt;/td&gt;
&lt;td&gt;GPT, BERT, ViT 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://wikidocs.net/31379&quot;&gt;https://wikidocs.net/31379&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI/딥러닝</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/442</guid>
      <comments>https://engineerinsight.tistory.com/442#entry442comment</comments>
      <pubDate>Mon, 20 Oct 2025 08:00:08 +0900</pubDate>
    </item>
    <item>
      <title>[AI/DL] 딥러닝의 발전(1): AlexNet, ResNet, YOLO (2012~2016)</title>
      <link>https://engineerinsight.tistory.com/441</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;AlexNet (2012)&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 이미지 인식에서 CNN의 가능성을 처음으로 증명한 모델로&lt;br /&gt;지금의 CNN, YOLO, ResNet 등 모든 비전 모델의 출발점이 되었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 활성화 함수 ReLU 등장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존까지는 활성화 함수로 sigmoid, tanh와 같은 함수를 사용했는데, 이 함수들은 미분하면 0으로 수렴하기 때문에 층이 조금만 깊어도 기울기 소실이 심해, 층을 깊이 쌓을 수 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RELU가 등장하면서 층을 깊게 쌓을 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IBmFz/dJMb9Nht4ND/YCqzOELPhPwuGvnUN4ZPT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IBmFz/dJMb9Nht4ND/YCqzOELPhPwuGvnUN4ZPT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IBmFz/dJMb9Nht4ND/YCqzOELPhPwuGvnUN4ZPT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIBmFz%2FdJMb9Nht4ND%2FYCqzOELPhPwuGvnUN4ZPT0%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;275&quot; height=&quot;205&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RELU 함수는 결과값이 양수일 때는 미분 값이 그대로 1, 음수일 때는 0으로 기울기가 사라지지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;물론 20층 정도까지는 가능하지만 50층 이상 쌓으면 결국 기울기 값이 작아지는 효과가 발생했습니다&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;한 번이라도 입력이 음수여서 0으로 꺼지면, 그 뉴런은 그 이후로 영원히 gradient가 0이 됩니다. 이걸 Dead ReLU problem 이라고 합니당&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1924&quot; data-origin-height=&quot;634&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eGgTa3/dJMb9gRrC79/zh8Tn0S4Z8Ajb2LG1HVxM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eGgTa3/dJMb9gRrC79/zh8Tn0S4Z8Ajb2LG1HVxM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eGgTa3/dJMb9gRrC79/zh8Tn0S4Z8Ajb2LG1HVxM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeGgTa3%2FdJMb9gRrC79%2Fzh8Tn0S4Z8Ajb2LG1HVxM0%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;705&quot; height=&quot;232&quot; data-origin-width=&quot;1924&quot; data-origin-height=&quot;634&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 특징&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1233&quot; data-origin-height=&quot;503&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqKctx/dJMb9Miz3vA/fyVeNIjIWnHK6beOBxk3ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqKctx/dJMb9Miz3vA/fyVeNIjIWnHK6beOBxk3ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqKctx/dJMb9Miz3vA/fyVeNIjIWnHK6beOBxk3ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqKctx%2FdJMb9Miz3vA%2FfyVeNIjIWnHK6beOBxk3ak%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;576&quot; height=&quot;235&quot; data-origin-width=&quot;1233&quot; data-origin-height=&quot;503&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2012년 당시 &lt;b&gt;이미지넷(ImageNet)&lt;/b&gt; 대회에서 압도적인 1등 (오류율 16%)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CNN (Convolutional Neural Network)&lt;/b&gt; 구조를 사용&lt;/li&gt;
&lt;li&gt;GPU 연산을 활용해서 대규모 이미지 학습을 가능하게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 핵심 구조&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;[Conv &amp;rarr; ReLU &amp;rarr; Pool] &amp;times; 여러 층 &amp;rarr; Fully Connected &amp;rarr; Softmax&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Conv Layer&lt;/b&gt;: 이미지의 국소적인 특징 추출 (가장자리, 패턴 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ReLU&lt;/b&gt;: 음수 제거, 학습 안정화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pooling&lt;/b&gt;: 크기 줄이기, 위치 변화에 덜 민감하게&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FC Layer&lt;/b&gt;: 추출한 특징을 종합해 최종 분류&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;VGGNet (2014)&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 구조가 아닌, 간단한 구조를 깊은 층으로 쌓음&lt;br /&gt;네트워크를 깊게 쌓으면 성능이 좋아진다는 것을 보여, 이후 딥러닝(Deep Learning)이 진짜로 &amp;lsquo;딥&amp;rsquo;해지기 시작했음&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 필터 크기를 &lt;b&gt;3&amp;times;3&lt;/b&gt;로 통일&lt;/li&gt;
&lt;li&gt;층(Depth)을 16~19개까지 깊게 쌓음&lt;/li&gt;
&lt;li&gt;규칙적이고 단순한 구조로 &lt;b&gt;CNN의 표준 설계법&lt;/b&gt;이 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;ResNet (2015)&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;딥러닝이 너무 깊으면 오히려 성능이 떨어지는데, 그걸 해결한 모델.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 문제: 기울기 소실&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;층이 깊어질수록 &lt;b&gt;기울기 소실(Gradient Vanishing)&lt;/b&gt; 발생
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;역전파가 초기 층으로 갈수록 기울기가 0에 수렴하면서, 가중치가 업데이트되지 않아 학습이 중단되는 현상이 발생함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결: &lt;b&gt;Residual Connection (잔차 연결)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QWGR8/dJMb9PM8EXe/LKKv01CjbN2DIkBTPX5FZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QWGR8/dJMb9PM8EXe/LKKv01CjbN2DIkBTPX5FZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QWGR8/dJMb9PM8EXe/LKKv01CjbN2DIkBTPX5FZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQWGR8%2FdJMb9PM8EXe%2FLKKv01CjbN2DIkBTPX5FZK%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;404&quot; height=&quot;166&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfABVB/dJMb9MCShCc/8KYbEY7irU4uKsLKdLrxY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfABVB/dJMb9MCShCc/8KYbEY7irU4uKsLKdLrxY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfABVB/dJMb9MCShCc/8KYbEY7irU4uKsLKdLrxY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfABVB%2FdJMb9MCShCc%2F8KYbEY7irU4uKsLKdLrxY1%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;410&quot; height=&quot;231&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 입력을 다음 층에 바로 더해줌.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JuoOw/dJMb9hphrqS/53VD7QZccGSGlKvTR7vmkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JuoOw/dJMb9hphrqS/53VD7QZccGSGlKvTR7vmkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JuoOw/dJMb9hphrqS/53VD7QZccGSGlKvTR7vmkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJuoOw%2FdJMb9hphrqS%2F53VD7QZccGSGlKvTR7vmkK%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;313&quot; height=&quot;75&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eyoI2a/dJMb9Miz3v2/FqpvTFRBWKKlyOX5KRmMak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eyoI2a/dJMb9Miz3v2/FqpvTFRBWKKlyOX5KRmMak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eyoI2a/dJMb9Miz3v2/FqpvTFRBWKKlyOX5KRmMak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeyoI2a%2FdJMb9Miz3v2%2FFqpvTFRBWKKlyOX5KRmMak%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;318&quot; height=&quot;105&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cH1OjI/dJMb84DtqbK/JCADHu182DkWYklwrWJKJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cH1OjI/dJMb84DtqbK/JCADHu182DkWYklwrWJKJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cH1OjI/dJMb84DtqbK/JCADHu182DkWYklwrWJKJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcH1OjI%2FdJMb84DtqbK%2FJCADHu182DkWYklwrWJKJ0%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;367&quot; height=&quot;148&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기울기가 직접 1을 타고 전달되는 경로가 생겨 F(x)가 잘못되거나 Gradient가 죽더라도 x 경로를 통해서 gradient가 다음 층으로 흘러갈 수 있게 되었습니다. (기울기 소실 문제 해결!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단순한 수식 덕분에 &lt;b&gt;152층&lt;/b&gt;짜리 네트워크도 안정적으로 학습됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResNet 이후 모든 현대 딥러닝 모델들은 &amp;ldquo;&lt;b&gt;Skip Connection&lt;/b&gt;&amp;rdquo;을 기본 구조로 탑재되어 있습니다. (CNN, Transformer, LLM 전부 이 개념을 반영)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;YOLO (2016~2024)&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;You Only Look Once~&lt;br /&gt;이미지 한 장을 한 번에 보고, 물체를 실시간으로 탐지한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 기존 객체탐지: R-CNN&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CID2K/dJMb9QyvLOG/d7bTgeLkPvJM6dZK2sjz8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CID2K/dJMb9QyvLOG/d7bTgeLkPvJM6dZK2sjz8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CID2K/dJMb9QyvLOG/d7bTgeLkPvJM6dZK2sjz8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCID2K%2FdJMb9QyvLOG%2Fd7bTgeLkPvJM6dZK2sjz8k%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;703&quot; height=&quot;359&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Selective Search&lt;/b&gt;로 이미지 안에서 &amp;ldquo;여기가 물체일 것 같다&amp;rdquo;는 후보영역(region proposals) 수천 개 추출
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;region proposal, feature extraction, classification 등의 작업을 별도로 수행해야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;각 영역을 &lt;b&gt;CNN&lt;/b&gt;에 넣어 &lt;b&gt;클래스 분류 + 위치 보정&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;느리지만 정확도는 높음&lt;/li&gt;
&lt;/ul&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;b&gt;R-CNN (2014)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;후보영역마다 CNN 돌림 &amp;rarr; 너무 느림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Fast R-CNN (2015)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;한 번만 CNN 돌리고 feature map에서 영역 추출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Faster R-CNN (2015)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;Region Proposal Network (RPN)&amp;rdquo; 추가 &amp;rarr; 후보영역 자동 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ YOLO의 핵심 아이디어&lt;/h3&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;851&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blGur6/dJMb9Miz3wy/g82kAEP2TmLqA2QjlY9NN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blGur6/dJMb9Miz3wy/g82kAEP2TmLqA2QjlY9NN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blGur6/dJMb9Miz3wy/g82kAEP2TmLqA2QjlY9NN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblGur6%2FdJMb9Miz3wy%2Fg82kAEP2TmLqA2QjlY9NN0%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;404&quot; height=&quot;269&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;851&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지를 &lt;b&gt;S&amp;times;S 격자&lt;/b&gt;로 나눔&lt;/li&gt;
&lt;li&gt;각 셀에서
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bounding box 좌표 (x, y, w, h)&lt;/li&gt;
&lt;li&gt;Confidence (물체 존재 확률)&lt;/li&gt;
&lt;li&gt;Class probability (무슨 물체인지)&lt;/li&gt;
&lt;li&gt;을 한 번에 예측&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전부 한 번의 forward-pass로 계산됨&lt;/li&gt;
&lt;li&gt;속도는 수십~수백 배 빨라짐 &lt;b&gt;&lt;i&gt;(실시간 가능)&lt;/i&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;초기 YOLO는 정확도는 다소 낮았지만, 최근 버전(YOLOv5~v8)은 속도와 정확도 모두 개선되어 실용성이 매우 높음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이미지를 한 번만 본다는 의미는, 기존의 R-CNN 계열은 이미지를 여러 장으로 분할해 여러 번 CNN에 통과시키는 작업을 했는데, YOLO는 원본 이미지를 CNN에 그대로 통과시키는 1번의 과정만을 거친다는 것이다.&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지를 그대로 CNN에 통과시킴.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;속도가 빨라져서 실시간 객체 탐지 가능&lt;/li&gt;
&lt;li&gt;주변 정보까지 학습해 background error가 상대적으로 적음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;자율주행, CCTV, 로보틱스 등 &lt;b&gt;실시간 비전 시스템의 표준으로 활용&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://viso.ai/deep-learning/alexnet/&quot;&gt;https://viso.ai/deep-learning/alexnet/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://studyopedia.com/deep-learning/recurrent-neural-networks/&quot;&gt;https://studyopedia.com/deep-learning/recurrent-neural-networks/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dotiromoook.tistory.com/24&quot;&gt;https://dotiromoook.tistory.com/24&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI/딥러닝</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/441</guid>
      <comments>https://engineerinsight.tistory.com/441#entry441comment</comments>
      <pubDate>Fri, 17 Oct 2025 18:00:25 +0900</pubDate>
    </item>
    <item>
      <title>[AI/DL] 딥러닝 기본기 총정리: 퍼셉트론&amp;middot;손실함수&amp;middot;옵티마이저</title>
      <link>https://engineerinsight.tistory.com/440</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;퍼셉트론(Perceptron)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딥러닝의 시작은 &amp;ldquo;&lt;b&gt;하나의 뉴런을 수학적으로 흉내 낸 것&lt;/b&gt;&amp;rdquo;으로 이것을 &lt;b&gt;퍼셉트론(Perceptron)&lt;/b&gt; 이라고 부릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퍼셉트론은 입력을 받아 &amp;rarr; 가중치를 곱하고 &amp;rarr; 합친 뒤 &amp;rarr; 결과를 출력하는 단순한 모델이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기본 단위가 여러 개 연결되면, 우리가 알고 있는 &lt;b&gt;신경망(Neural Network)&lt;/b&gt; 이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Perceptron&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 퍼셉트론은 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;370&quot; data-origin-height=&quot;98&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lNzb9/dJMb9YiV3dJ/0kElfnIrY5BPJ0Rru8F5C0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lNzb9/dJMb9YiV3dJ/0kElfnIrY5BPJ0Rru8F5C0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lNzb9/dJMb9YiV3dJ/0kElfnIrY5BPJ0Rru8F5C0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlNzb9%2FdJMb9YiV3dJ%2F0kElfnIrY5BPJ0Rru8F5C0%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;279&quot; height=&quot;74&quot; data-origin-width=&quot;370&quot; data-origin-height=&quot;98&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;x&lt;/b&gt; : 입력 (예: 픽셀 값, 데이터 피처)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;W&lt;/b&gt; : 가중치 matrix (입력마다 중요도를 조절, 직접 정하는게 아니라 학습을 통해 자동으로 조절됨!!!)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;b&lt;/b&gt; : 바이어스 (기울기 조정용 상수)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;f(&amp;middot;)&lt;/b&gt; : 활성화 함수 (activation function, 예: sigmoid, ReLU 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력이 많을수록, 퍼셉트론이 &lt;b&gt;여러 특징(feature)&lt;/b&gt; 을 조합해서 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[한계] 퍼셉트론은 &lt;b&gt;선형 분리 가능한 데이터만 학습 가능&lt;/b&gt;하다는 한계가 있습니다. 즉 저 1차 식 wx+b를 바탕으로 위인지 아래인지 둘중 하나로만 가를 수 있기 때문에 선형 경계만 표현할 수 있습니다 (= 즉, 단일 퍼셉트론으로는 XOR 문제처럼 &lt;b&gt;비선형 경계&lt;/b&gt;를 표현할 수 없습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 한계를 극복하기 위해 &lt;b&gt;다층 퍼셉트론(Multi-Layer Perceptron, MLP)&lt;/b&gt; 이 등장합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Multi Layer Perceptron (MLP)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퍼셉트론이 여러 개 모이면 &lt;b&gt;다층 퍼셉트론(MLP: Multi-Layer Perceptron)&lt;/b&gt; 이 됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Input Layer  &amp;rarr;  Hidden Layer(s)  &amp;rarr;  Output Layer&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;입력층(Input layer)&lt;/b&gt; : 데이터가 처음 들어오는 곳&lt;/li&gt;
&lt;li&gt;&lt;b&gt;은닉층(Hidden layer)&lt;/b&gt; : 가중치&amp;middot;활성화 함수를 통해 특징을 추출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;출력층(Output layer)&lt;/b&gt; : 최종 예측 결과 (예. &amp;ldquo;고양이&amp;rdquo;, &amp;ldquo;강아지&amp;rdquo;, 1.25와 같은 연속적인 숫자)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MLP는 은닉층(hidden layer)에 &lt;b&gt;비선형 활성화 함수&lt;/b&gt;를 추가하여 복잡한 패턴을 학습합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딥러닝이 &amp;ldquo;Deep&amp;rdquo;한 이유는 바로 &lt;b&gt;은닉층이 깊게 여러 층 쌓여 있기 때문입니당&lt;/b&gt;&lt;br /&gt;각 층이 데이터의 더 &lt;b&gt;복잡한 패턴을 학습&lt;/b&gt;하면서, 모델의 표현력이 강해집니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;손실 함수, 역전파(Backpropagation), 옵티마이저&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신경망은 &amp;ldquo;정답을 얼마나 잘 맞추는가&amp;rdquo;를 계속 확인하면서 가중치를 조금씩 조정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 얼마나 잘 맞추는가의 판단에 손실함수가 사용됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 손실 함수 (Loss Function)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델의 &amp;ldquo;예측이 얼마나 틀렸는지&amp;rdquo;를 수치로 계산하는 함수예요.&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;494&quot; data-origin-height=&quot;108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZRvzY/dJMb9Nu0W3c/T3yQqlI6teihlX0nvk546K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZRvzY/dJMb9Nu0W3c/T3yQqlI6teihlX0nvk546K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZRvzY/dJMb9Nu0W3c/T3yQqlI6teihlX0nvk546K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZRvzY%2FdJMb9Nu0W3c%2FT3yQqlI6teihlX0nvk546K%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;288&quot; height=&quot;63&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;108&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 가지 손실함수가 있지만 그중에서도 특히 2가지가 유명한데, MSE와 CEL입니다.&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;MSE (Mean Squared Error)&lt;/b&gt; : 회귀 문제에 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cross Entropy Loss&lt;/b&gt; : 분류 문제에 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분류 문제에서는 Cross Entropy Loss를 주로 사용하며,&lt;br /&gt;이때 모델의 출력층은 Softmax 함수를 통해 각 클래스 확률을 산출합니다.&lt;br /&gt;Cross Entropy는 이 확률 분포와 정답(one-hot 벡터)의 차이를 계산합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 역전파 (Backpropagation)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손실이 생겼다면, &lt;b&gt;그 손실이 어디서 얼마나 생겼는지를 추적해서 가중치를 수정&lt;/b&gt;해야 하는데, 그걸 수학적으로 처리하는 과정이 &lt;b&gt;역전파(Backpropagation)&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력층에서 생긴 오차를 거꾸로 전파(backpropagate)하면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 가중치가 얼마나 잘못되었는지 계산하는 과정&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 미분(&amp;part;)을 이용해서 &lt;b&gt;기울기(gradient)&lt;/b&gt; 를 구하고, 그걸로 어느 방향으로 가중치를 바꿔야 손실이 줄어드는지 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급했던 거처럼 W(가중치) 는 사람이 직접 지정하지 않고, 손실함수&amp;ndash;역전파&amp;ndash;옵티마이저 과정을 통해 자동으로 학습됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 옵티마이저 (Optimizer)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역전파로 구한 기울기를 이용해서, &lt;b&gt;가중치를 실제로 업데이트하는 알고리즘&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 건 &lt;b&gt;경사하강법(Gradient Descent)&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czu77c/dJMb82Tb4Dy/hIS8fK9k1nNh8s7k8UCo71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czu77c/dJMb82Tb4Dy/hIS8fK9k1nNh8s7k8UCo71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czu77c/dJMb82Tb4Dy/hIS8fK9k1nNh8s7k8UCo71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fczu77c%2FdJMb82Tb4Dy%2FhIS8fK9k1nNh8s7k8UCo71%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;236&quot; height=&quot;80&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&amp;eta; (Eta)&lt;/b&gt; : 학습률(learning rate)로, 얼마나 크게 한 번에 움직일지 결정하는 파라미터
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;크게 잡으면 빨리 최적점에 닿을 수도 있지만 영원히 닿지 못하고 횡보할 수도 있습니다.&lt;/li&gt;
&lt;li&gt;작게 잡으면 정말 느리게 최적점에 가기 때문에 적절한 값을 설정해주어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&amp;part;L/&amp;part;W&lt;/b&gt; : 손실에 대한 가중치의 기울기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 개발에서 모델이 학습을 통해 자동 조정하는 값(W, b) 들을 파라미터(parameter) 라고 합니다.&lt;br /&gt;반면, 개발자가 직접 설정하는 값(learning rate, epoch, batch size, optimizer 등) 은 하이퍼파라미터(hyperparameter) 라고 구분합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저 관련해서 많은 하이퍼 파라미터들이 등장합니다. 우선 위에서 나온 학습률도 직접 정해야 하고, 기울기는 계산에 따라 정해지지만 이때 사용되는 옵티마이저의 종류도 직접 정해야 합니다. Optimizer는 기울기를 얼마나, 어떻게 반영할지를 다르게 설계한 버전들이 많습니당&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Optimizer&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;b&gt;SGD&lt;/b&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;b&gt;Adam&lt;/b&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;b&gt;AdamW&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Weight Decay를 분리, 과적합 방지&lt;/td&gt;
&lt;td&gt;고성능 모델 파인튜닝&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;- &lt;b&gt;RMSProp, Adagrad&lt;/b&gt; : 특정 상황에 강한 변형들&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  DL 개발의 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DL 개발의 큰흐름은 아래와 같습니다. Epoch 수 만큼 반복해서 학습을 하게 됩니다. &lt;i&gt;(파라미터에 따라 중간에 멈출지 등등을 세밀하게 조정할 수 있습니다)&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;[입력 데이터]
      &amp;darr;
[신경망 Forward] &amp;rarr; 예측
      &amp;darr;
[손실 함수 계산]
      &amp;darr;
[Backpropagation: 기울기 계산]
      &amp;darr;
[Optimizer로 가중치 업데이트]
      &amp;darr;
[다음 Epoch 학습 반복]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Perceptron in torch&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;Forward Perceptron&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;# 모델 정의
import torch
import torch.nn as nn

class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        # 입력 10 &amp;rarr; 은닉 32 &amp;rarr; 출력 2 (분류 문제 가정)
        self.fc1 = nn.Linear(10, 32)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(32, 2)

    def forward(self, x):
        x = self.fc1(x)   # Wx + b
        x = self.relu(x)  # 활성화 함수 f()
        x = self.fc2(x)   # Output layer
        return x

model = MLP()&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;fc1&lt;/code&gt;, &lt;code&gt;fc2&lt;/code&gt; &amp;rarr; &lt;b&gt;퍼셉트론 / MLP의 층(layer)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReLU()&lt;/code&gt; &amp;rarr; &lt;b&gt;활성화 함수&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forward()&lt;/code&gt; &amp;rarr; &lt;b&gt;신경망 Forward 연산 (Wx+b &amp;rarr; f &amp;rarr; 출력)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;손실 함수 (Loss Function)&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;criterion = nn.CrossEntropyLoss()  # 분류 문제용 손실 함수&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;criterion&lt;/code&gt; = &lt;b&gt;손실 함수 L(y, ŷ)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;모델의 예측값과 정답(label)의 차이&amp;rdquo;를 계산하는 역할&lt;/li&gt;
&lt;li&gt;회귀라면 &lt;code&gt;nn.MSELoss()&lt;/code&gt; 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;역전파 (Backpropagation)&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;loss = criterion(outputs, labels)  # 손실 계산
loss.backward()                    # 역전파 수행 (&amp;part;L/&amp;part;W 계산)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;loss.backward()&lt;/code&gt; &amp;rarr; &lt;b&gt;Backpropagation 실행 부분&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;내부적으로 PyTorch가 &lt;b&gt;자동 미분(autograd)&lt;/b&gt; 으로 기울기를 계산합니다.&lt;/li&gt;
&lt;li&gt;여기서 계산된 기울기는 각 파라미터(&lt;code&gt;W&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;)에 저장됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;옵티마이저 (Optimizer)&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

optimizer.zero_grad()  # 이전 기울기 초기화
loss.backward()        # 새로운 기울기 계산
optimizer.step()       # 가중치 업데이트&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;optimizer&lt;/code&gt; &amp;rarr; &lt;b&gt;경사하강법 알고리즘 (SGD, Adam 등)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zero_grad()&lt;/code&gt; &amp;rarr; 이전 iteration의 gradient를 초기화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;step()&lt;/code&gt; &amp;rarr; 가중치 업데이트 (( W = W - \eta \cdot &amp;part;L/&amp;part;W ))&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;학습 루프&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;for epoch in range(num_epochs):
    for inputs, labels in dataloader:
        outputs = model(inputs)          # [1] Forward
        loss = criterion(outputs, labels) # [2] 손실 계산
        optimizer.zero_grad()            # [3] 기울기 초기화
        loss.backward()                  # [4] 역전파
        optimizer.step()                 # [5] 옵티마이저 업데이트

    print(f&quot;Epoch {epoch+1}, Loss: {loss.item():.4f}&quot;)
&lt;/code&gt;&lt;/pre&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;&lt;code&gt;model(inputs)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Forward&lt;/td&gt;
&lt;td&gt;신경망 계산 (Wx+b &amp;rarr; f)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;criterion(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Loss Function&lt;/td&gt;
&lt;td&gt;예측 오차 계산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loss.backward()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Backpropagation&lt;/td&gt;
&lt;td&gt;기울기 계산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;optimizer.step()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optimizer&lt;/td&gt;
&lt;td&gt;가중치 갱신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;반복(&lt;code&gt;for epoch&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;학습 루프&lt;/td&gt;
&lt;td&gt;여러 번 학습 (Epoch) 반복&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI/딥러닝</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/440</guid>
      <comments>https://engineerinsight.tistory.com/440#entry440comment</comments>
      <pubDate>Fri, 17 Oct 2025 16:46:03 +0900</pubDate>
    </item>
    <item>
      <title>[아키텍처/Kafka] 메세지 전송 보장 방식(At most once, At least once, Exactly once): 특징과 EOS 구현 방법</title>
      <link>https://engineerinsight.tistory.com/439</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  인트로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka와 같은 메시징 시스템에서는 메시지가 &lt;b&gt;언제, 어떻게 소비되는지&lt;/b&gt;를 보장하는 수준이 중요합니다. &lt;i&gt;&lt;del&gt;(데이터베이스의 트랜잭션 격리 수준과 비슷한 포지션이랄까&amp;hellip;?)&lt;/del&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 지연, 장애, 재시도 같은 상황에서 메시지가 유실되거나 중복 처리될 수 있기 때문에, 전송 보장 방식(Delivery Semantics)을 우리 서비스의 특성에 맞게 잘 설정해야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  메시지 전송 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 간단하게나마 카프카가 어떻게 메세지를 애플리케이션으로부터 받는지에 대해 알아보겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka에서 &amp;ldquo;메시지 전송&amp;rdquo;&lt;br /&gt;= &lt;b&gt;사건(Event)이 애플리케이션에서 발생했을 때,&lt;br /&gt;애플리케이션 코드가 Kafka Producer API를 호출해서 Broker에 메시지를 보내는 것&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;애플리케이션에서 사건 발생&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: 주문 서비스에서 &amp;ldquo;주문 생성됨&amp;rdquo; 이벤트 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;애플리케이션이 Producer 코드 실행&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션은 &lt;b&gt;Kafka Producer 클라이언트 라이브러리&lt;/b&gt;를 사용해서 메시지를 Kafka Broker로 전송&lt;/li&gt;
&lt;li&gt;메시지에는 &lt;b&gt;Topic 이름, Key&lt;i&gt;(선택)&lt;/i&gt;, Value&lt;/b&gt; 등이 포함됨&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-java&quot;&gt;ProducerRecord&amp;lt;String, String&amp;gt; record =
  new ProducerRecord&amp;lt;&amp;gt;(&quot;order-topic&quot;, &quot;orderId=12345&quot;, &quot;주문 생성됨&quot;); // topic, key, value
producer.send(record);&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Broker 저장&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지는 지정된 Topic의 Partition에 순차적으로 저장됨&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*acks&lt;/code&gt; 설정에 따라, Leader/Follower가 확인 응답*&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Consumer가 꺼내 처리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consumer Group이 해당 Topic을 구독 중이라면, 메시지를 가져오고 보장 방식 설정에 따라 메세지 처리 전/후에 Offset 을 커밋한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  메시지 전송 보장 방식&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ At most once (최대 한 번)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지가 &lt;b&gt;0번 또는 1번 전달&lt;/b&gt;되는 모델&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중복은 절대 발생하지 않지만, 메시지가 유실될 수 있음&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동작 방식&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Consumer가 메시지를 가져오면 바로 Offset 커밋 &lt;i&gt;(&amp;ldquo;읽었다&amp;rdquo; 표시)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;처리 도중 장애 발생한다면 메시지는 이미 읽은 걸로 표시됐으니 &lt;b&gt;재처리 불가&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점: 빠름, 중복 없음&lt;/li&gt;
&lt;li&gt;단점: 장애 시 데이터 손실 발생 가능&lt;/li&gt;
&lt;li&gt;활용 사례: &lt;b&gt;로그 수집, 모니터링&lt;/b&gt; &lt;i&gt;(조금 빠져도 큰 문제 없는 경우)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ At least once (최소 한 번)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지가 &lt;b&gt;최소 1번 이상 전달&lt;/b&gt;되는 모델&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유실은 없지만, 중복 처리될 수 있음&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동작 방식&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Consumer가 메시지를 가져옴&lt;/li&gt;
&lt;li&gt;처리를 끝낸 뒤에 Offset을 커밋&lt;/li&gt;
&lt;li&gt;만약 처리 후 커밋 전에 장애가 나면 같은 메시지를 다시 읽어와서 &lt;b&gt;중복 처리&lt;/b&gt;함&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점: 데이터 손실 없음&lt;/li&gt;
&lt;li&gt;단점: 중복 처리 가능하기 때문에 애플리케이션에서 &lt;b&gt;Idempotency(멱등성) 처리&lt;/b&gt; 필요&lt;/li&gt;
&lt;li&gt;활용 사례: &lt;b&gt;결제, 포인트 적립&lt;/b&gt; &lt;i&gt;(중복 적립을 막기 위해 DB에서 unique key 등으로 보정)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Exactly once (정확히 한 번)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지가 &lt;b&gt;유실도 없고, 중복도 없음 &amp;rArr; &lt;code&gt;단 한 번만&lt;/code&gt; 처리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;i&gt;가장 이상적&lt;/i&gt;&lt;/b&gt;인 모델이지만 구현이 가장 복잡함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점: 유실이나 중복 없는 완벽한 처리&lt;/li&gt;
&lt;li&gt;단점: 복잡도와 성능 비용 &amp;uarr;&lt;/li&gt;
&lt;li&gt;활용 사례: &lt;b&gt;금융, 송금, 재고 관리&lt;/b&gt; &lt;i&gt;(중복/유실이 치명적인 경우)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;&lt;i&gt;카프카에서는 Exactly Once를 어떻게 구현하나?&lt;/i&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최대한 예시를 들어 설명해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카의 Exactly-once 옵션은 크게 2가지 트랜잭션을 조율하는 과정입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Producer 애플리케이션의 로컬 트랜잭션&lt;/li&gt;
&lt;li&gt;Consumer 애플리케이션의 로컬 트랜잭션&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;Producer 측 (Idempotent Producer)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;문제: 네트워크 장애나 재시도로 인해 같은 메시지가 &lt;b&gt;중복 전송&lt;/b&gt;될 수 있음.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 애플리케이션 서버&lt;i&gt;(=Producer)&lt;/i&gt;가 &lt;code&gt;&quot;주문 생성됨(orderId=12345)&quot;&lt;/code&gt; 메시지를 보냈지만 네트워크 지연으로 인해 Producer가 &lt;b&gt;응답(ACK)을 못 받았다면&lt;/b&gt; Producer는 &quot;메시지가 실패했나?&quot; 하고 &lt;b&gt;같은 메시지를 재전송하게 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Broker는 그대로 두 번 저장하게 되고, 그러면 결제 서비서&lt;i&gt;(=Consumer)&lt;/i&gt;가 두 번 읽게 되어, &lt;b&gt;&lt;i&gt;중복 결제 발생 위험&lt;/i&gt;&lt;/b&gt;이 발생합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;해결: &lt;b&gt;Idempotent Producer&lt;/b&gt; 사용 (enable.idempotence=true)&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Producer가 메시지를 보낼 때 &lt;code&gt;(PID=111, seq=1, orderId=12345)&lt;/code&gt; 라는 메타데이터를 함께 전송합니다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;PID&lt;/b&gt;: Producer가 브로커와 연결될 때 카프카가 부여하는 고유 ID (프로듀서별로 고정됨)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sequence number&lt;/b&gt;: Producer가 각 Partition에 메시지를 보낼 때마다 자동 증가하는 번호 &lt;i&gt;(auto-increment처럼 관리)&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정상적으로 저장된 메시지는 seq가 증가하지만, ACK 못 받아서 재전송하는 경우는 seq를 유지한 채로 보내게 됩니당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Broker는 메세지를 저장하기 전에 항상 &lt;code&gt;(PID, seq)&lt;/code&gt; 조합이 &lt;b&gt;중복되는지 확인&lt;/b&gt;합니다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약에 Producer가 두번 메세지를 전송하게 된다면 두 번째로 온 건 &lt;b&gt;중복이니까 무시&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;결과: 정확성 보장&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka에 &lt;code&gt;&quot;orderId=12345&quot;&lt;/code&gt; 메시지가 한 번만 저장되고, Consumer(결제 서비스)도 한 번만 처리할 수 있습니다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 2. Transactional Producer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;문제: Producer가 자신의 로컬 DB에 처리를 했는데 &lt;b&gt;메세지를 전송하는 과정에서 에러&lt;/b&gt;가 난다면, 데이터 불일치가 발생할 수 있음.&lt;/i&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Producer가 자신의 비즈니스 로직을 처리하면서 DB에 변경을 함.&lt;/li&gt;
&lt;li&gt;Producer가 Kafka에 메세지를 전송함&lt;/li&gt;
&lt;li&gt;Kafka는 브로커에 메세지를 저장함.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 만약에 2번까지는 해서 Producer DB에는 변화가 생겼는데, 3번 전에 장애가 발생했다면???&lt;br /&gt;데이터 불일치(inconsistency)가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;해결: *&lt;/i&gt;Transactional Producer***&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;(메세지 쓰기 + Producer 서버 내 DB 커밋)&lt;/code&gt;을 하나의 트랜잭션으로 묶어야만 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 시작&lt;/b&gt;Producer가 트랜잭션 안에서 메세지를 보내겠다고 선언합니다&lt;br /&gt;&lt;i&gt;(애플리케이션 코드에서 직접 설정해야 합니다)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-java&quot;&gt; Properties props = new Properties();
 props.put(&quot;bootstrap.servers&quot;, &quot;localhost:9092&quot;);
 props.put(&quot;key.serializer&quot;, &quot;org.apache.kafka.common.serialization.StringSerializer&quot;);
 props.put(&quot;value.serializer&quot;, &quot;org.apache.kafka.common.serialization.StringSerializer&quot;);

 // 트랜잭션 활성화
 props.put(&quot;enable.idempotence&quot;, &quot;true&quot;);
 props.put(&quot;transactional.id&quot;, &quot;order-tx-1&quot;); // 반드시 고유해야 함&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-java&quot;&gt; producer.initTransactions();
 producer.beginTransaction();&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Producer의 메시지 전송&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: 주문 서비스에서 DB insert 후 &lt;code&gt;&quot;주문 생성됨&quot;&lt;/code&gt; 이벤트 Kafka 전송한다면 DB 트랜잭션과 Kafka 메시지 전송이 &lt;b&gt;원자적으로 묶여야&lt;/b&gt; 함&lt;/li&gt;
&lt;li&gt;메시지가 브로커에 기록되지만, 아직 Producer의 트랜잭션이 &lt;code&gt;commit&lt;/code&gt;될지 &lt;code&gt;abort&lt;/code&gt;될지 모르기 때문에 &lt;b&gt;Consumer에 노출하지 않습니다.&lt;/b&gt; &lt;i&gt;(= prepare 상태)&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consumer는 &quot;확정된 데이터만 본다&quot;라는 보장(원자성)을 위해 잠시 숨겨둡니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Producer의 DB 트랜잭션 커밋 시점에 맞춰서 같이 Kafka &lt;code&gt;commitTransaction()&lt;/code&gt; 호출하면 브로커가 메시지를 확정(commit) 처리하고, 이제부터는 Consumer가 읽을 수 있습니당
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약에 DB에서 rollback 되면 Kafka도 &lt;code&gt;abortTransaction()&lt;/code&gt; 호출해 메시지는 폐기됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 3. Transactional Consumer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;문제: Consumer가 메시지를 처리한 뒤 &lt;b&gt;커밋 전에 장애&lt;/b&gt;가 나면, 다시 같은 메시지를 읽어 중복 처리될 수 있음&lt;/i&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;consumer가 메세지를 읽음&lt;/li&gt;
&lt;li&gt;consumer가 자신의 처리를 하고 DB에 저장 (예를 들어 주문했다는 정보를 Insert)&lt;/li&gt;
&lt;li&gt;Kafka에 offset 커밋 &lt;i&gt;(이 메세지는 다 읽었다는 표시)&lt;/i&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 만약에 2번까지는 해서 서비 DB에는 변화가 생겼는데, 3번 전에 장애가 발생했다면???&lt;br /&gt;데이터 불일치(inconsistency)가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;해결: &lt;b&gt;Transactional Consumer&lt;/b&gt; (read-process-write 패턴에서 EOS 지원)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Offset을 +1 해서 &quot;여기까지 읽었다&quot;는 표시를 하는 주체는 Consumer 입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 일반적인 &lt;code&gt;commitSync()&lt;/code&gt;나 &lt;code&gt;commitAsync()&lt;/code&gt;로 단독으로 커밋하는 게 아니라,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Consumer가 메시지를 처리함&lt;/li&gt;
&lt;li&gt;그 &lt;b&gt;offset 커밋 작업을 &lt;i&gt;새로운&lt;/i&gt; Producer 트랜잭션 안에 포함&lt;/b&gt;시켜서 (&lt;code&gt;sendOffsetsToTransaction&lt;/code&gt;) Broker에 전달합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;*Consumer가 &quot;offset 저장&quot;이라는 이벤트를 Kafka에 보내는 순간, 그 행위는 Producer처럼 메시지를 보내는 것과 똑같이 동작&lt;/b&gt;합니다.*&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소의 세팅에서는 Consumer는 &quot;읽기 전용&quot;으로, 토픽에서 메시지만 가져오는 존재이지만, 빡빡한 exactly once 세팅을 구현하기 위해서는 처리 이후에 Producer로 offset 커밋 메세지를 브로커에 보내야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서 offset=101의 메세지를 처리했다면, &quot;offset=101까지 읽음&quot;이라는 특별한 메시지를 &lt;code&gt;__consumer_offsets&lt;/code&gt; 토픽에 써 넣는 Producer 역할을 하게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 구현방법을 정리하자면!&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Producer 실패 케이스&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 메시지를 여러 번 보내는 문제 &amp;rArr; &lt;code&gt;Idempotent Producer&lt;/code&gt; (PID + seq)로 해결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;반쪽짜리 쓰기*&lt;/b&gt;(DB는 커밋됐는데 Kafka는 실패 or 반대)* &amp;rArr; &lt;code&gt;Transactional Producer&lt;/code&gt;로 DB 트랜잭션과 Kafka 쓰기를 묶어서 해결
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;DB 저장 + 메세지 발행&lt;/code&gt;&lt;/b&gt;이 원자적으로 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Consumer 실패 케이스&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지를 처리했지만 offset 커밋 전에 죽으면 같은 메시지를 또 읽어서 중복 처리 위험&lt;/li&gt;
&lt;li&gt;Consumer가 &lt;b&gt;offset 커밋 자체를 Kafka 트랜잭션 안에 포함&lt;/b&gt; &lt;i&gt;(이때 offset 기록은 특별한 Producer 행위처럼 동작)&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;메시지 처리 + offset 커밋&lt;/code&gt;&lt;/b&gt;이 원자적으로 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  정리&lt;/h2&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;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;At most once&lt;/td&gt;
&lt;td&gt;최대 1번 (중복 X, 유실 O)&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;At least once&lt;/td&gt;
&lt;td&gt;최소 1번 (중복 O, 유실 X)&lt;/td&gt;
&lt;td&gt;유실 없음&lt;/td&gt;
&lt;td&gt;중복 가능 &amp;rarr; 멱등성 필요&lt;/td&gt;
&lt;td&gt;결제, 포인트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exactly once&lt;/td&gt;
&lt;td&gt;정확히 1번 (중복 X, 유실 X)&lt;/td&gt;
&lt;td&gt;완벽&lt;/td&gt;
&lt;td&gt;복잡도/성능 비용 &amp;uarr;&lt;/td&gt;
&lt;td&gt;금융, 재고&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>아키텍처+MSA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/439</guid>
      <comments>https://engineerinsight.tistory.com/439#entry439comment</comments>
      <pubDate>Wed, 17 Sep 2025 20:00:13 +0900</pubDate>
    </item>
    <item>
      <title>[아키텍처/Kafka] acks(Acknowledgement) 설정: acks=0,1,all</title>
      <link>https://engineerinsight.tistory.com/438</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  인트로&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Acknowledgement(acks)가 필요한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka에서 메시지는 이렇게 흘러갑니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Producer &amp;rarr; Broker(Leader) &amp;rarr; Follower(Replication) &amp;rarr; Consumer&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Broker = 메시지를 직접 들고 있는 Kafka 서버 1대&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Producer가 메시지를 보내면 Broker가 &lt;b&gt;Topic의 Partition&lt;/b&gt;에 저장합니다.&lt;/li&gt;
&lt;li&gt;Consumer는 Broker로부터 메세지를 가져가서 처리합니다&lt;/li&gt;
&lt;li&gt;여러 Broker가 모이면 &lt;b&gt;Kafka Cluster&lt;/b&gt;가 되고, 여기서 Partition들을 서로 나눠서 저장하거나 복제(Leader/Follower)도 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer (애플리케이션) 입장에서는 &amp;ldquo;내가 보낸 메시지가 진짜 카프카 서버(=브로커)에 안전하게 저장됐을까?&amp;rdquo;를 알고 싶습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 메시지가 네트워크 중간에 날아가면?&lt;/li&gt;
&lt;li&gt;만약 Broker Leader가 디스크에 기록하기도 전에 죽어버리면?&lt;/li&gt;
&lt;li&gt;만약 아직 Follower에 복제되지 않았는데 Leader가 장애 나면?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이런 상황에서 &amp;ldquo;어느 수준까지 저장됐을 때 성공으로 인정할지&amp;rdquo;를 정해야 하고 그게 바로 &lt;code&gt;acks&lt;/code&gt; 설정입니당&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ acks 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;acks = Producer가 Broker로부터 받는 &amp;ldquo;확인 응답(Acknowledgement)&amp;rdquo; 수준&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;acks=0&lt;/code&gt; &amp;rarr; 확인 필요 없음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;acks=1&lt;/code&gt; &amp;rarr; Leader Broker까지만 확인받기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;acks=all&lt;/code&gt; &amp;rarr; Leader + Follower(동기화된 모든 Replica)까지 저장 확인해야 성공으로 정하겠음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  acks 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 1. acks=0 (응답 안 받음, Fire-and-Forget)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Producer는 메시지 전송 후 확인 안 기다림&lt;/li&gt;
&lt;li&gt;Broker가 받았는지, 디스크에 썼는지 알 수 없음&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;del&gt;네트워크에서 UDP와 같이 그냥 잘 갔는지 확인 안함&lt;/del&gt;&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;: 제일 빠름&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;: 메시지 유실 위험 높음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예: 로그, 모니터링 등 일부 손실 허용 가능한 경우&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 2. acks=1 (Leader 확인, 기본값)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Leader Broker가 메시지를 &lt;b&gt;디스크에 저장&lt;/b&gt;하면 OK 응답&lt;/li&gt;
&lt;li&gt;Follower 복제는 아직 안 됐을 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;: 속도와 안정성의 균형&lt;i&gt;(이 적절한 느낌)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;: Leader가 저장 직후 죽으면, Follower에는 아직 데이터가 없을 수 있음 &lt;i&gt;(데이터 유실 가능)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예&lt;/b&gt;: 대부분의 서비스 이벤트 처리&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 3. acks=all (모든 ISR 확인)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Leader + In-Sync Replica(ISR, 최신 동기화된 Follower)까지 저장이 끝나야 OK&lt;/li&gt;
&lt;li&gt;&lt;b&gt;(= 복제까지 확인 후 성공 처리)*&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;: 가장 안전 &lt;i&gt;(리더 장애 시에도 데이터 복구 가능)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;: 속도 느려짐 &lt;i&gt;(팔로워 복제 기다려야 함)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예&lt;/b&gt;: 결제, 금융, 재고 등 절대 유실되면 안 되는 경우&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  acks / 전송 보장 방식의 관계&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;acks=0 &amp;rarr; 보통 &lt;b&gt;At most once&lt;/b&gt; (빠르지만 유실 가능)&lt;/li&gt;
&lt;li&gt;acks=1 &amp;rarr; &lt;b&gt;At least once&lt;/b&gt; (유실은 거의 없지만 중복 가능)&lt;/li&gt;
&lt;li&gt;acks=all &amp;rarr; &lt;b&gt;At least once ~ Exactly once&lt;/b&gt; (Idempotent Producer/트랜잭션을 추가해야 Exactly once 달성)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>아키텍처+MSA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/438</guid>
      <comments>https://engineerinsight.tistory.com/438#entry438comment</comments>
      <pubDate>Wed, 17 Sep 2025 17:00:31 +0900</pubDate>
    </item>
    <item>
      <title>[아키텍처/Kafka] 메세지 복제(Replication)와 브로커의 Leader-Follower 구조</title>
      <link>https://engineerinsight.tistory.com/437</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  메세지 복제(Replication)와 브로커의 Leader-Follower 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Replication (복제)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 데이터를 &lt;b&gt;여러 Broker에 복제(Replication)&lt;/b&gt; 해서 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 다중화하기 때문에 Broker 하나가 장애가 나도 다른 Broker에 복제된 데이터를 사용할 수 있습니다. 이 복제 개수는 &lt;b&gt;Replication Factor&lt;/b&gt;로 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Replication Factor = 3이라면 하나의 Partition 데이터가 총 3개의 Broker에 저장됩니다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Leader-Follower 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 Partition은 &lt;b&gt;Leader&lt;/b&gt;와 &lt;b&gt;Follower&lt;/b&gt;라는 두 가지 역할로 나뉘어 관리됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Leader&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기/쓰기는 Leader가 담당합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Producer가 데이터를 쓸 때 반드시 Leader Partition에 기록합니다.&lt;/li&gt;
&lt;li&gt;Consumer가 데이터를 읽을 때도 Leader Partition에서 읽습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;i&gt;카프카 서비스는 리더가 죽지 않으면 리더에서만 모두 일어납니다&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Follower&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;리더가 죽었을 때를 위한 대비용 복제본입니다&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;Leader에 기록된 데이터를 그대로 복제만 하고 평상시에는 직접 읽기/쓰기를 처리하지 않습니다.&lt;/li&gt;
&lt;li&gt;Leader가 죽으면 Follower 중 하나가 새로운 Leader로 선출되어 서비스가 중단되지 않도록 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 동작 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Replication Factor가 3이고, &lt;code&gt;order-topic&lt;/code&gt;의 Partition 0이 있다고 가정해봅시다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Broker1: Leader Partition 0&lt;/li&gt;
&lt;li&gt;Broker2: Follower Partition 0&lt;/li&gt;
&lt;li&gt;Broker3: Follower Partition 0&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Producer 메세지 전송&lt;/b&gt;: Producer가 메시지를 전송합니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Leader 기록&lt;/b&gt;: Broker1(Leader)에 먼저 저장됩니다. (디스크 기록)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Followers 동기화&lt;/b&gt;: Broker2, Broker3은 Broker1의 데이터를 그대로 따라 적습니다.&lt;/li&gt;
&lt;li&gt;만약 Broker1이 장애로 다운되면 Kafka는 Broker2나 Broker3 중 하나를 새 Leader로 선출합니다.&lt;/li&gt;
&lt;li&gt;서비스는 중단되지 않고 그대로 이어집니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 포스팅에서 설명했던 acks를 더 명확하게 이해해 보자면,&lt;br /&gt;&lt;code&gt;acks=0&lt;/code&gt;: step 1 직후 OK 반환&lt;br /&gt;&lt;code&gt;acks=1&lt;/code&gt;: step 2 직후 OK 반환&lt;br /&gt;&lt;code&gt;acks=all&lt;/code&gt;: step 3 이후 OK 반환 &lt;i&gt;(&lt;b&gt;min.insync.replicas&lt;/b&gt; 수만큼 Followers가 복제 완료한 뒤)&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ 데이터베이스와 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 데이터베이스의 &lt;b&gt;Master-Slave 복제&lt;/b&gt;와 비슷합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Master = Leader (쓰기/읽기 담당)&lt;/li&gt;
&lt;li&gt;Slave = Follower (복제본 유지, 필요시 Master 승격)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>아키텍처+MSA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/437</guid>
      <comments>https://engineerinsight.tistory.com/437#entry437comment</comments>
      <pubDate>Wed, 17 Sep 2025 14:00:42 +0900</pubDate>
    </item>
    <item>
      <title>[아키텍처/Kafka] Kafka만의 특징: 분산 로그 저장, Queue vs Topic (기존 메세징 시스템과 차이)</title>
      <link>https://engineerinsight.tistory.com/436</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Kafka의 분산 로그 저장 시스템(Distributed Commit Log)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 카프카의 가장 뚜렷한 차별점입니다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 로그(Log)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka는 메시지를 단순히 &quot;큐에 넣었다가 소비되면 삭제&quot;하는 게 아니라,&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디스크에 순차적으로 로그 형태로 저장(Commit Log)&lt;/b&gt; 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Partition 0
 ├─ Offset 0 : 주문1
 ├─ Offset 1 : 주문2
 ├─ Offset 2 : 주문3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 메시지는 &lt;b&gt;Offset&lt;/b&gt;이라는 번호와 함께 디스크에 기록되어, 메시지를 읽어도 바로 사라지지 않고, &lt;b&gt;Retention 설정(예: 7일, 30일)&lt;/b&gt; 동안 보존됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 나중에 새로 온 Consumer도 과거 데이터를 다시 읽을 수 있습니다. (Replaying 가능)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 디스크에 순차 쓰기(append only log)라서 초당 수백만 건 처리 가능하여 대규모 처리에도 유리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 분산(Distributed)&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파티션별 다른 서버 저장 (= 샤딩)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 브로커에 모든 데이터를 저장하면 위험하니, &lt;b&gt;Partition 단위로 쪼개 여러 Broker에 분산 저장&lt;/b&gt;합니다. &lt;i&gt;(물론 파티션의 개수가 서버 개수보다 많게 되면 같은 서버에 저장하게 될 수도 있겠지만 최대한 나누려고 합니다)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, order-topic을 생성했다면 들어오는 메시지들은 라운드 로빈(round-robin) 방식이나 Key 해싱을 통해 Partition에 분산 저장됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Leader/Follower 구조 (= DB 다중화)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partition은 Leader/Follower 구조로 &lt;b&gt;Replication&lt;/b&gt; 되어 장애에도 안전합니다. 데이터를 복제해서 저장해 장애 복구와 안전성을 확보할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  기존 메시징 시스템 vs Kafka (= Queue vs Topic)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka와 기존 메세징 시스템은 서비스 간 데이터를 중개하기 위해 Producer/Consumer 구조를 사용한다는 점에서는 비슷하지만 아래와 같은 차이가 있습니당&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 기존 메시징 시스템 (RabbitMQ, ActiveMQ, IBM MQ 등)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;본질: &lt;b&gt;메시지 큐(Message Queue)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;특징
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지를 소비(consume)하면 &lt;b&gt;큐에서 제거&lt;/b&gt;됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Queue 모델&lt;/b&gt; 중심으로, 생산자(Producer)가 넣고, 소비자(Consumer)가 꺼내는 구조&lt;/li&gt;
&lt;li&gt;보통 &lt;b&gt;Point-to-Point&lt;/b&gt; (1개의 메시지는 1번만 소비됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;장점: 단순하고 이해하기 쉬움, 전통적으로 많이 사용됨&lt;/li&gt;
&lt;li&gt;단점: 대규모 데이터 스트리밍이나 장기 보관에는 적합하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Kafka&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;본질: &lt;b&gt;메시지 브로커 + 분산 로그 저장 시스템&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;특징
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지를 &lt;b&gt;디스크에 Commit Log 형식으로 저장&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;메시지를 소비해도 바로 삭제되지 않고 &lt;b&gt;설정한 기간 동안 보관&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;i&gt;하나의 메시지를 여러 Consumer Group이 독립적으로 여러 번 소비 가능&lt;/i&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Pub/Sub 모델과 Queue 모델을 모두 지원하는 유연성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;장점: &lt;b&gt;대규모 실시간 데이터 처리, 확장성, 내구성&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;단점: 초기 진입장벽이 높음, 운영 복잡성 존재&lt;/li&gt;
&lt;/ul&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;전통 MQ (RabbitMQ 등)&lt;/th&gt;
&lt;th&gt;Kafka&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;저장 방식&lt;/td&gt;
&lt;td&gt;Queue (읽으면 삭제)&lt;/td&gt;
&lt;td&gt;Commit Log (읽어도 일정 기간 보관)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 모델&lt;/td&gt;
&lt;td&gt;큐 중심&lt;/td&gt;
&lt;td&gt;로그(Partition + Offset) 중심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재처리 가능 여부&lt;/td&gt;
&lt;td&gt;어려움 &lt;i&gt;(Replyaing은 안됨)&lt;/i&gt;&lt;/td&gt;
&lt;td&gt;가능 &lt;i&gt;(Replaying 가능)&lt;/i&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;매우 뛰어남 (Partition 기반 분산 저장)&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;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>아키텍처+MSA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/436</guid>
      <comments>https://engineerinsight.tistory.com/436#entry436comment</comments>
      <pubDate>Wed, 17 Sep 2025 10:00:46 +0900</pubDate>
    </item>
    <item>
      <title>[아키텍처/Kafka] 비동기 메시징 시스템: 개념 + Consumer Group을 통한 병렬 처리의 원리를 간단히 알아보자!</title>
      <link>https://engineerinsight.tistory.com/435</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  비동기 메시징 시스템이란?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 먼저, 비동기(Asynchronous)란?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동기: 요청을 보내고 &lt;b&gt;응답을 받을 때까지 기다림&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;비동기: 요청을 보내고 &lt;b&gt;응답을 기다리지 않고 다른 일 진행&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시징 시스템은 &lt;b&gt;&lt;i&gt;일단 메시지를 보내 두면, 상대가 나중에 처리해도 되도록&lt;/i&gt;&lt;/b&gt; 하는 구조입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 메시징 시스템이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 간에 데이터를 직접 주고받는 대신, &lt;b&gt;중간에 &lt;code&gt;메시지 브로커&lt;/code&gt;&lt;/b&gt;라는 우편함을 두는 방식입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;발신자(Producer)는 메시지를 우편함에 넣고 바로 다른 일 하러 가는 역할&lt;/li&gt;
&lt;li&gt;수신자(Consumer)는 필요할 때 우편함을 열어 메시지를 읽어 처리하는 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 만약에 수신자 서버가 제대로 동작하지 않는다 하더라도, 메세지 브로커에 할 일을 쌓아두었다가 추후에 서비스가 다시 정상 동작할 때 일을 처리할 수 있으므로 안정적으로 시스템을 운영할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 비동기 메세징 시스템의 효능(?)&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;서비스 간 결합도 낮춤 = 느슨한 연결(Loosely Coupled)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A 서비스가 B 서비스에 직접 HTTP 요청을 보내면, B가 죽으면 A의 요청도 실패하면서 오류가 발생함. &lt;i&gt;(서로 강하게 결합됨)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;메시징을 쓰면 A는 그냥 메시지를 큐에 넣고 끝이고, B가 살아날 때 처리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정성(장애 격리)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한쪽이 터져도 전체 시스템이 같이 죽지 않음&lt;/li&gt;
&lt;li&gt;메시지가 중간에 안전하게 저장되기 때문에 복구 가능하고, 추후 처리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성(Scalability)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소비자(Consumer)를 여러 개 늘리면 동시에 메시지를 병렬 처리 가능하기 때문에 대량 트래픽 처리에 유리함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비동기 처리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;꼭 지금 처리할 필요 없는 작업(예: 이메일 발송, 로그 저장, 알림 전송)을 나중에 처리하기 때문에 사용자 응답 속도 빨라짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 사용 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 시스템을 구현한다고 했을 때,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결제 성공 &amp;rarr; 메시지 발행&lt;/li&gt;
&lt;li&gt;메일 서비스가 메시지를 보고 &amp;ldquo;영수증 발송&amp;rdquo;&lt;/li&gt;
&lt;li&gt;포인트 서비스가 메시지를 보고 &amp;ldquo;포인트 적립&amp;rdquo;&lt;/li&gt;
&lt;li&gt;재고 서비스가 메시지를 보고 &amp;ldquo;재고 차감&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 작업들을 다 메세징으로 구현하면 독립적으로 구현할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 전통적인 메시징 시스템&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RabbitMQ, ActiveMQ, IBM MQ&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;원래부터 &quot;메시지 큐(Message Queue)&quot; 역할을 위해 만들어졌음&lt;/li&gt;
&lt;li&gt;특징: &lt;b&gt;메시지를 한 번 소비하면 사라짐&lt;/b&gt; (Queue 성격)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 카프카(Kafka)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음에는 LinkedIn이 &quot;실시간 로그 수집/처리&quot;용으로 만든 시스템&lt;/li&gt;
&lt;li&gt;메시징 시스템이면서 동시에 &lt;b&gt;분산 로그 저장 시스템&lt;/b&gt;이라고도 불림.&lt;/li&gt;
&lt;li&gt;RabbitMQ 같은 큐 방식과 다르게, &lt;b&gt;메시지를 지우지 않고 일정 기간 저장&lt;/b&gt;해 둡니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 여러 가지 메세징 시스템이 있지만, 대용량 처리에 특화되고, 분산/확장성이 용이하며 메세지를 로그 파일로 디스크에 저장해 복구가 유연한 &amp;lsquo;카프카&amp;rsquo;에 대해서 공부해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;먼저 용어부터 알아봅시당&lt;/i&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  카프카에서 사용되는 용어&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MuGEL/btsQAsFJuZp/pcjxHaKvxkYmLeYqZGXEjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MuGEL/btsQAsFJuZp/pcjxHaKvxkYmLeYqZGXEjK/img.png&quot; data-alt=&quot;이미지 출처:&amp;amp;nbsp; https://medium.com/@cobch7/kafka-producer-and-consumer-f1f6390994fc&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MuGEL/btsQAsFJuZp/pcjxHaKvxkYmLeYqZGXEjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMuGEL%2FbtsQAsFJuZp%2FpcjxHaKvxkYmLeYqZGXEjK%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;668&quot; height=&quot;334&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 출처:&amp;nbsp; https://medium.com/@cobch7/kafka-producer-and-consumer-f1f6390994fc&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Producer (생산자)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터를 보내는 쪽&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 주문 서비스가 &quot;주문 생성됨&quot; 이벤트를 메시지로 발행하면 Producer 역할을 하게 됩니다. Producer는 데이터를 &lt;b&gt;Topic&lt;/b&gt;이라는 곳에 써 넣습니다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;[Producer] &amp;rarr; &quot;주문 완료&quot; &amp;rarr; [Kafka Topic]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Consumer (소비자)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터를 받아서 처리하는 쪽&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메일 서비스가 &quot;주문 생성됨&quot; 메시지를 보고 영수증 메일을 보내는 경우, Consumer 역할을 하게 됩니다. Consumer는 Topic에서 메시지를 꺼내 읽습니다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;[Kafka Topic] &amp;rarr; &quot;주문 완료&quot; &amp;rarr; [Consumer]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Broker (브로커)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;카프카 서버 자체&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 대의 서버에 모든 메시지를 보관하면 위험하니까, 보통 여러 대의 Broker를 띄워서 &lt;b&gt;클러스터&lt;/b&gt;를 만듭니다. 클러스터 안에서 &lt;b&gt;토픽을 분산 저장&lt;/b&gt;하고, 메시지를 안전하게 보관합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Kafka Cluster = Broker1 + Broker2 + Broker3 ...&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Zookeeper (또는 KRaft)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 카프카는 &lt;b&gt;Zookeeper&lt;/b&gt;라는 별도 시스템을 통해 &lt;b&gt;&lt;code&gt;클러스터 상태를 관리&lt;/code&gt;&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;브로커 관리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 대의 Kafka Broker가 있을 때, 누가 클러스터에 참여했는지 기록&lt;/li&gt;
&lt;li&gt;새로운 Broker가 들어오거나 나가면 상태 갱신&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리더 선출&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 Partition에는 Leader Broker가 있고, 나머지는 Follower&lt;/li&gt;
&lt;li&gt;장애가 나면 Zookeeper가 새로운 Leader를 뽑음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메타데이터 저장&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 Topic이 있고, Partition이 몇 개인지&lt;/li&gt;
&lt;li&gt;Consumer Group의 Offset 정보 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Zookeeper를 별도로 운영하니 운영이 복잡해지고, 이중 관리 문제 + 성능의 병목이 되면서 &lt;b&gt;KRaft Controller라는 프로세스 역할을 하는 Broker&lt;/b&gt;가 있는 KRaft 모드가 생겼습니다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내장 합의 알고리즘(Raft) 도입으로, 최근 버전부터는 &lt;b&gt;KRaft 모드&lt;/b&gt;로 Zookeeper 없이 동작 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Topic&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시지를 넣어두는 큰 상자&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 메시지는 반드시 특정 Topic에 저장됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: 주문 이벤트는 &lt;code&gt;order-topic&lt;/code&gt;, 결제 이벤트는 &lt;code&gt;payment-topic&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Partition&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 Topic을 여러 개 조각(Partition)으로 나눌 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partition을 여러 개로 나누면 메시지를 분산 저장하고 &lt;b&gt;&lt;i&gt;동시에 병렬 처리&lt;/i&gt;&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: &lt;code&gt;order-topic&lt;/code&gt;을 3개의 Partition으로 나눔 &amp;rarr; 주문 데이터가 나눠져 저장됨&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;order-topic
 ├─ Partition 0 : 주문1, 주문4, 주문7 ...
 ├─ Partition 1 : 주문2, 주문5, 주문8 ...
 └─ Partition 2 : 주문3, 주문6, 주문9 ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;각 Partition은 같은 Consumer Group 내에서는 딱 1개의 Consumer만 처리하도록 해서 중복 처리를 막습니당&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Offset&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Partition 안에서 메시지의 고유한 번호&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer는 Offset을 기준으로 메시지를 읽고, &amp;ldquo;어디까지 읽었는지&amp;rdquo; 기록해 둡니다. Offset은 Consumer가 스스로 기억할 수도 있고, &lt;b&gt;Kafka가 내부적으로 저장&lt;/b&gt;해 줄 수도 있습니다. (보통은 &lt;code&gt;__consumer_offsets&lt;/code&gt; 토픽에 저장)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: Partition 0에서 &lt;code&gt;Offset=5&lt;/code&gt;까지 읽었다면, 다음에는 &lt;code&gt;Offset=6&lt;/code&gt;부터 읽음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Consumer Group&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer들을 묶어서 하나의 그룹으로 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Partition은 한 그룹 안에서 &lt;b&gt;단 한 Consumer만&lt;/b&gt; 처리합니다. 덕분에 메시지가 중복 처리되지 않고, 동시에 여러 Consumer가 나눠서 일을 할 수 있습니다.&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;1400&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpwP9J/btsQCPe4wdV/0I0w0GzLtxURKKKrK7xtJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpwP9J/btsQCPe4wdV/0I0w0GzLtxURKKKrK7xtJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpwP9J/btsQCPe4wdV/0I0w0GzLtxURKKKrK7xtJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpwP9J%2FbtsQCPe4wdV%2F0I0w0GzLtxURKKKrK7xtJ1%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;682&quot; height=&quot;411&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;s&gt;(위 그림에서는 1:1이지만)&lt;/s&gt;&lt;/i&gt; 전체적인 카프카 구조에서 본다면, &lt;b&gt;토픽(Topic) &amp;harr; 컨슈머 그룹(Consumer Group)&lt;/b&gt; 관계는 &lt;b&gt;다&lt;/b&gt;&lt;b&gt;&amp;nbsp;: 다&lt;/b&gt; 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 하나의&amp;nbsp;토픽을&amp;nbsp;여러&amp;nbsp;Consumer&amp;nbsp;Group이&amp;nbsp;읽을&amp;nbsp;수&amp;nbsp;있음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 order-topic에 「주문완료」 라는 메세지가 발행되면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;email service consumer group &amp;rarr; 영수증 발송&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;point service consumer group &amp;rarr; 포인트 적립&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;inventory service consumer group &amp;rarr; 재고 차감&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 같은 메세지가 각 그룹마다 한번씩 독립적으로 전달되도록 할 수 있습니다&lt;i&gt; = Pub/Sub 모델&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 한 Consumer Group은 여러 토픽을 구독할 수도 있음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로, Consumer Group 하나가 order-topic, payment-topic 두 개 토픽을 동시에 구독할 수도 있습니다.&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;  Consumer Group을 통한 병렬 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 기본 아이디어&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Consumer Group&lt;/b&gt; = 여러 개의 Consumer(소비자)를 &lt;b&gt;하나의 그룹&lt;/b&gt;으로 묶은 것&lt;/li&gt;
&lt;li&gt;Kafka는 한 &lt;b&gt;Partition&lt;/b&gt;을 동시에 여러 Consumer가 읽을 수 없게 막아두는 대신 &lt;b&gt;Partition 단위로 나눠서 Consumer들에게 배분&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rArr; 병렬 처리 가능&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예시 1: Topic에 Partition이 3개, Consumer Group 안에 Consumer 3명인 경우&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;order-topic (Partition 3개)
 ├─ Partition 0 &amp;rarr; Consumer A
 ├─ Partition 1 &amp;rarr; Consumer B
 └─ Partition 2 &amp;rarr; Consumer C&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 Consumer가 하나의 Partition을 맡아서 읽음 &lt;i&gt;(효율 100%)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;동시에 3개의 메시지를 병렬로 처리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예시 2: Topic에 Partition이 3개, Consumer Group 안에 Consumer 2명인 경우&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;order-topic (Partition 3개)
 ├─ Partition 0 &amp;rarr; Consumer A
 ├─ Partition 1 &amp;rarr; Consumer A
 └─ Partition 2 &amp;rarr; Consumer B&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consumer A가 Partition 0,1 두 개 맡고&lt;/li&gt;
&lt;li&gt;Consumer B가 Partition 2를 맡음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여전히 메시지들은 &lt;b&gt;중복 없이&lt;/b&gt; 나눠서 처리되지만, 균등 분배는 아님&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예시 3: Topic에 Partition이 3개, Consumer Group 안에 Consumer가 4명인 경우&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;order-topic (Partition 3개)
 ├─ Partition 0 &amp;rarr; Consumer A
 ├─ Partition 1 &amp;rarr; Consumer B
 └─ Partition 2 &amp;rarr; Consumer C
Consumer D &amp;rarr; 할당받을 Partition 없음 (Idle 상태)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Partition 수(3) &amp;lt; Consumer 수(4)면, 남는 Consumer는 놀고 있게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Partition 수 &amp;ge; Consumer 수일 때,&lt;/b&gt; 병렬 처리 극대화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Partition 수 &amp;lt; Consumer 수일 때&lt;/b&gt; , 일부 Consumer는 할당 못 받고 놀게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예시 4: Topic 1개에 Consumer Group이 2개인 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;똑같이 &lt;code&gt;order-topic&lt;/code&gt;에 &quot;주문 완료&quot; 메시지가 들어온다고 했을 때, 이번에는 &lt;code&gt;order-service-group&lt;/code&gt;과 &lt;code&gt;email-service-group&lt;/code&gt; 두 개 그룹이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 그룹은 서로 독립적이므로 &lt;b&gt;같은 메시지를 각각 한 번씩 소비하게 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Producer &amp;rarr; order-topic

Group 1: order-service-group
Partition 0 &amp;rarr; Consumer A
Partition 1 &amp;rarr; Consumer B

Group 2: email-service-group
Partition 0 &amp;rarr; Consumer X
Partition 1 &amp;rarr; Consumer Y&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 &lt;b&gt;Pub/Sub 모델&lt;/b&gt;처럼, 같은 메시지가 두 그룹에 모두 전달됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;order-service&lt;/code&gt;는 주문 DB 업데이트&lt;/li&gt;
&lt;li&gt;&lt;code&gt;email-service&lt;/code&gt;는 영수증 메일 발송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;즉 한번의 publish로 두 번의 처리가 가능해집니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  Kafka 효율적 활용 전략 (뽕 뽑기)&lt;/h2&gt;
&lt;h3 data-end=&quot;172&quot; data-start=&quot;143&quot; data-ke-size=&quot;size23&quot;&gt;✅ 하나의 토픽을 여러 파티션으로 나누기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;345&quot; data-start=&quot;173&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;224&quot; data-start=&quot;173&quot;&gt;&lt;b&gt;이유&lt;/b&gt;: 파티션 수 &amp;ge; Consumer 수일 때, 병렬 처리를 극대화할 수 있음&lt;/li&gt;
&lt;li data-end=&quot;312&quot; data-start=&quot;225&quot;&gt;예: Consumer Group에 Consumer 5개가 있으면, 최소 5개 이상의 Partition을 두어야 &lt;b&gt;각자 일을 나눠 가질 수 있음&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;312&quot; data-start=&quot;225&quot;&gt;만약에 Partition이 5개 미만이면 Consumer 하나는 놀게 되기 때문에 비효율적&amp;nbsp;&lt;/li&gt;
&lt;li data-end=&quot;345&quot; data-start=&quot;313&quot;&gt;파티션 = &quot;작업 분산 단위&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;345&quot; data-start=&quot;313&quot;&gt;주의사항: 파티션을 &lt;b&gt;너무 많이 만들면&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 관리 비용 증가 (메타데이터 관리, 리밸런스 비용 커짐).&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;345&quot; data-start=&quot;313&quot;&gt;보통 트래픽량과 Consumer 수를 고려해서 적당한 파티션 수를 정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;481&quot; data-start=&quot;459&quot; data-ke-size=&quot;size23&quot;&gt;✅ 토픽을 기능별로 분리하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;653&quot; data-start=&quot;482&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;545&quot; data-start=&quot;482&quot;&gt;&lt;b&gt;이유&lt;/b&gt;: 관심사가 다른 메시지를 한 토픽에 섞으면 Consumer가 불필요한 데이터까지 다 읽어야 함&lt;/li&gt;
&lt;li data-end=&quot;653&quot; data-start=&quot;546&quot;&gt;예
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;653&quot; data-start=&quot;555&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;584&quot; data-start=&quot;555&quot;&gt;order-topic &amp;rarr; 주문 관련 이벤트&lt;/li&gt;
&lt;li data-end=&quot;618&quot; data-start=&quot;587&quot;&gt;payment-topic &amp;rarr; 결제 관련 이벤트&lt;/li&gt;
&lt;li data-end=&quot;653&quot; data-start=&quot;621&quot;&gt;email-topic &amp;rarr; 알림/메일 관련 이벤트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;703&quot; data-start=&quot;655&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Consumer는 필요한 토픽만 구독&lt;/b&gt;해서 처리 효율 &amp;uarr;&lt;/p&gt;
&lt;p data-end=&quot;703&quot; data-start=&quot;655&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;759&quot; data-start=&quot;710&quot; data-ke-size=&quot;size23&quot;&gt;✅ Consumer Group이 여러 토픽을 구독하게 하는건 트레이드오프 고려하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;981&quot; data-start=&quot;760&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;882&quot; data-start=&quot;760&quot;&gt;&lt;b&gt;하나의 Consumer가 여러 작업&lt;/b&gt; (토픽 여러 개 구독)
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;882&quot; data-start=&quot;804&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;837&quot; data-start=&quot;804&quot;&gt;장점: 단일 서비스에서 여러 역할 가능해서 운영이 단순해진다&amp;nbsp;&lt;/li&gt;
&lt;li data-end=&quot;882&quot; data-start=&quot;840&quot;&gt;단점: 한 Consumer 장애 시 여러 기능이 동시에 멈출 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;981&quot; data-start=&quot;884&quot;&gt;&lt;b&gt;Consumer를 역할별로 분리&lt;/b&gt; (각 서비스 전용 Consumer Group을 만드는 경우)
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;981&quot; data-start=&quot;939&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;962&quot; data-start=&quot;939&quot;&gt;장점: 독립적 확장/장애 격리 가능&lt;/li&gt;
&lt;li data-end=&quot;981&quot; data-start=&quot;965&quot;&gt;단점: 운영 복잡도 &amp;uarr; (브로커가 많이 필요해질 수 있음)&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@cobch7/kafka-producer-and-consumer-f1f6390994fc&quot;&gt;https://medium.com/@cobch7/kafka-producer-and-consumer-f1f6390994fc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>아키텍처+MSA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/435</guid>
      <comments>https://engineerinsight.tistory.com/435#entry435comment</comments>
      <pubDate>Tue, 16 Sep 2025 22:00:24 +0900</pubDate>
    </item>
    <item>
      <title>[Spring/AOP] AOP의 거의 모든 것: 개념, 내부 구현 원리, 활용(트랜잭션, 로깅, 보안), 주의사항(self-invocation 등등)</title>
      <link>https://engineerinsight.tistory.com/434</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  AOP (관점 지향 프로그래밍)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP는 &lt;b&gt;핵심 비즈니스 로직에서 공통 관심사를 분리&lt;/b&gt;하기 위한 프로그래밍 패러다임입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 모듈에서 반복되는 부가 기능(공통 관심사)을 따로 빼서 관리하고, 필요한 지점에 끼워 넣는 기법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들자면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핵심 관심사: 주문 생성, 결제 처리&lt;/li&gt;
&lt;li&gt;공통 관심사: 로깅, 트랜잭션 관리, 보안 검사&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 필요성&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;중복 코드 제거&lt;/b&gt;: 로깅, 보안, 예외처리 같은 코드가 여러 클래스에 흩어져 있지 않음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;관심사 분리(Separation of Concerns)&lt;/b&gt;: 핵심 로직과 부가 로직을 분리해 가독성과 유지보수성을 높임&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변경 용이성&lt;/b&gt;: 공통 기능을 한 곳에서만 수정하면 전체에 반영&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 핵심 용어 (Terms)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Aspect (관점)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공통 관심사를 모듈화한 단위 (ex: 트랜잭션 관리, 로깅)&lt;/li&gt;
&lt;li&gt;예: 모든 팀이 출퇴근 기록을 해야 한다고 하면, 각 팀 코드(영업, 개발, 회계)에 중복해서 쓰는 것이 아니라 &amp;lsquo;출퇴근 관리 부서&amp;rsquo;를 따로 둬서 처리하는 것과 같습니당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Join Point (결합 지점)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AOP가 &lt;b&gt;&lt;i&gt;적용될 수 있는&lt;/i&gt;&lt;/b&gt; 모든 지점 (메서드 호출, 예외 발생 등)&lt;/li&gt;
&lt;li&gt;예: 직원이 회사에 출근할 때, 회의실 들어갈 때, 문서 결재할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pointcut (선택 지점)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Join Point 중 &lt;b&gt;실제로 Aspect를 적용할 지점&lt;/b&gt;을 표현 (조건식)&lt;/li&gt;
&lt;li&gt;예: &lt;code&gt;execution(* com.example.service.*.*(..))&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;예: 모든 행동 중에서 &lt;b&gt;출근할 때만&lt;/b&gt; 출퇴근 체크기를 찍는다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Advice (실행 동작)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제로 끼워 넣을 부가기능 (메서드 실행 전/후/예외 시 동작 등)&lt;/li&gt;
&lt;li&gt;종류
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Before&lt;/code&gt;: 메서드 실행 전 &lt;i&gt;(예: 출근 전 신분증 확인)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AfterReturning&lt;/code&gt;: 메서드 성공 후 &lt;i&gt;(예: 퇴근 후 정상 기록)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AfterThrowing&lt;/code&gt;: 예외 발생 시 &lt;i&gt;(예: 출근 중 사고 발생 시)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@After&lt;/code&gt;: 메서드 종료 시(성공/실패 무관) &lt;i&gt;(예: 퇴근 시간 기록은 무조건 남김)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Around&lt;/code&gt;: 메서드 실행 전후 전체 제어 &lt;i&gt;(예: 출근부터 퇴근까지 전부 관리)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;예: 지문 인식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Weaving (위빙)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 코드에 Aspect를 엮어서 넣는 과정&lt;/li&gt;
&lt;li&gt;시점: 컴파일 타임, 로드 타임, 런타임&lt;/li&gt;
&lt;li&gt;스프링은 주로 런타임 위빙 (프록시 기반), 프로그램이 돌면서 프록시를 씌우는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Proxy (프록시 객체)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 AOP는 내부적으로 대상 객체를 감싸는 프록시를 만들어서 Aspect를 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ Join Point vs Pointcut?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Join Point&lt;/b&gt;: AOP가 적용될 &lt;i&gt;수 있는 모든 지점&lt;/i&gt; (후보군)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pointcut&lt;/b&gt;: Join Point 중 &lt;i&gt;실제로 AOP를 적용하겠다고 선택한 지점&lt;/i&gt; (실행군)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행은 항상 Pointcut에서 일어나지만, 그 바탕에는 &amp;ldquo;전체 후보군&amp;rdquo;을 정의한 Join Point 개념이 필요함&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프록시 기반 AOP&lt;/b&gt;: JDK 동적 프록시&lt;i&gt;(인터페이스 있는 경우)&lt;i&gt;나 CGLIB 프록시(&lt;/i&gt;인터페이스 없는 경우, 클래스 상속받은 가짜 자식을 만들어서 프록시 처리)&lt;/i&gt; 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;런타임 위빙&lt;/b&gt;: 실행 시점에 프록시 객체를 만들어 Aspect 적용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메서드 실행 단위의 Join Point만 지원&lt;/b&gt;: Spring AOP는 메서드 호출에만 적용 가능 (AspectJ는 더 풍부한 Join Point 지원)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Advice 실행 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;@Around&lt;/code&gt; 어드바이스가 붙은 경우, 대상 메서드의 실행 전, 후에 Advice가 실행됩니다. &lt;i&gt;(API 성능 모니터링, 트랜잭션 관리 등에 활용됨)&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Around(&quot;execution(* com.example..*(..))&quot;)
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    // 1. 메서드 실행 전 로직
    System.out.println(&quot;Before: &quot; + joinPoint.getSignature());

    // 2. 실제 대상 메서드 실행
    Object result = joinPoint.proceed();

    // 3. 메서드 실행 후 로직
    System.out.println(&quot;After: &quot; + result);

    return result;
}&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;Client &amp;rarr; Proxy &amp;rarr; [Before Advice] &amp;rarr; Target Method &amp;rarr; [After Advice] &amp;rarr; Return&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Spring AOP 내부 구현 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AOP는 &lt;b&gt;프록시 기반&lt;/b&gt;입니다. 즉, 원래 객체를 그대로 쓰지 않고 &lt;b&gt;대리 객체(Proxy)를 앞에 세워서&lt;/b&gt; 부가기능을 끼워 넣습니다. 그리고 이 프록시 객체가 스프링 IoC 컨테이너 내부에서 관리됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 프록시 객체 생성 과정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;BeanPostProcessor 등록&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 컨테이너 초기화 단계에서 &lt;code&gt;AnnotationAwareAspectJAutoProxyCreator&lt;/code&gt; 같은 특별한 &lt;code&gt;BeanPostProcessor&lt;/code&gt;가 등록됩니다.&lt;/li&gt;
&lt;li&gt;이 후처리기가 &lt;b&gt;AOP 어드바이스가 필요한 빈을 감지&lt;/b&gt;하고, 프록시를 만들어줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빈 생성 시점에 프록시로 대체&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링이 빈을 만들 때, 해당 빈이 AOP 대상(Pointcut 조건과 매칭)이라면, 원본 객체 대신 &lt;b&gt;프록시 객체&lt;/b&gt;를 컨테이너에 등록합니다.&lt;/li&gt;
&lt;li&gt;이후 다른 빈에서 &lt;code&gt;@Autowired&lt;/code&gt;로 주입받는 것은 원본이 아니라 프록시입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메서드 호출 가로채기&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 객체가 호출을 가로채서, &lt;b&gt;Advice 실행 &amp;rarr; 원본 메서드 실행 &amp;rarr; Advice 후처리&lt;/b&gt; 순서로 실행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 프록시 객체 종류: JDK Dynamic Proxy vs CGLIB&lt;/h3&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;기본적으로 인터페이스 있으면 JDK 프록시, 없으면 CGLIB&lt;/b&gt;을 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;JDK Dynamic Proxy&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;인터페이스가 있으면&lt;/i&gt; 인터페이스 기반으로 프록시 생성&lt;/li&gt;
&lt;li&gt;원리: &lt;code&gt;java.lang.reflect.Proxy&lt;/code&gt; + &lt;code&gt;InvocationHandler&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CGLIB Proxy&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;인터페이스가 없으면&lt;/i&gt; 클래스 상속 기반으로 프록시 생성 &lt;i&gt;(빈 클라스 앞에 private 같은거 붙이면 상속이 안되기 때문에 스프링부트에서 에러를 발생시키는 이유입니다)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;원리: &lt;code&gt;net.sf.cglib.proxy.Enhancer&lt;/code&gt; + &lt;code&gt;MethodInterceptor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;최종 클래스(&lt;code&gt;final&lt;/code&gt;)나 &lt;code&gt;final&lt;/code&gt; 메서드에는 적용 불가&lt;/li&gt;
&lt;li&gt;(Spring Boot 2.x+에서는 항상 CGLIB 활성화 옵션을 켜두는 경우가 많음)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 프록시 객체는 원래 메서드를 감싸버려~&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 로깅을 하려고 AOP를 사용했다면, 스프링은 이 &amp;ldquo;프록시 클래스&amp;rdquo;를 자동으로 만들어주고, 컨테이너에 주입해줍니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;public class MemberServiceProxy implements MemberService {

    private final MemberService target;

    public MemberServiceProxy(MemberService target) {
        this.target = target;
    }

    @Override
    public void join() {
        System.out.println(&quot;로그 시작&quot;);
        target.join(); // 실제 메서드 실행
        System.out.println(&quot;로그 끝&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  스프링에서 활용되는 AOP&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP는 &amp;ldquo;내가 직접 Aspect를 만든다&amp;rdquo;는 것보다는 &amp;ldquo;스프링이 제공하는 기능들이 다 AOP 위에 돌아간다&amp;rdquo;는 걸 이해하는 게 중요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 1. 트랜잭션 관리 (@Transactional)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스 작업은 &lt;b&gt;중간에 끊기면 안 되고, 전부 성공하거나 전부 실패해야&lt;/b&gt; 합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어, 계좌 이체를 한다면 &amp;ldquo;A 계좌에서 돈 빼기&amp;rdquo;와 &amp;ldquo;B 계좌에 돈 넣기&amp;rdquo;가 반드시 함께 성공해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Transactional&lt;/code&gt;을 메서드에 붙이면, 스프링이 &lt;b&gt;프록시를 만들어서 메서드 실행 전후에 트랜잭션을 시작하고, 끝내고, 예외 시 롤백&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 아래와 같은 빈을 작성했다고 합시다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Service
public class AccountService {
    @Transactional
    public void transferMoney(String from, String to, int amount) {
        withdraw(from, amount);   // A 계좌 출금
        deposit(to, amount);      // B 계좌 입금
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 위 클래스를 그대로 쓰지 않고, &lt;b&gt;프록시 클래스&lt;/b&gt;를 만들어서 컨테이너에 등록합니다. 프록시는 내부에 &lt;b&gt;트랜잭션 처리 로직&lt;/b&gt;을 넣고, 원래 &lt;code&gt;AccountService&lt;/code&gt;를 위임 호출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 클라이언트는 &lt;code&gt;accountService.transferMoney()&lt;/code&gt;를 호출하더라도 사실상 호출되는 건 &lt;b&gt;&lt;code&gt;AccountServiceProxy.transferMoney()&lt;/code&gt;&lt;/b&gt;가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;// 프록시로 감싸진 버전 (실제 개발자가 보는 건 아님, 개념 설명용)
public class AccountServiceProxy extends AccountService {

    private final AccountService target; // 실제 원본 객체
    private final PlatformTransactionManager txManager;

    public AccountServiceProxy(AccountService target, PlatformTransactionManager txManager) {
        this.target = target;
        this.txManager = txManager;
    }

    @Override
    public void transferMoney(String from, String to, int amount) {
        // 트랜잭션 시작
        TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());
        try {
            // 실제 원본 객체의 메서드 실행
            target.transferMoney(from, to, amount);

            // 정상 &amp;rarr; 커밋
            txManager.commit(status);
        } catch (RuntimeException e) {
            // 예외 &amp;rarr; 롤백
            txManager.rollback(status);
            throw e;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 개발자는 그냥 비즈니스 로직만 작성하면 되고, 트랜잭션 시작/커밋/롤백은 AOP가 알아서 처리합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프록시가 먼저 트랜잭션 매니저에게 &amp;ldquo;트랜잭션 시작&amp;rdquo; 요청&lt;/li&gt;
&lt;li&gt;실제 비즈니스 메서드 실행&lt;/li&gt;
&lt;li&gt;정상 종료 &amp;rarr; 트랜잭션 커밋&lt;/li&gt;
&lt;li&gt;예외 발생 &amp;rarr; 트랜잭션 롤백&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 2. 보안 처리 (@Secured, @PreAuthorize)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 기능은 &lt;b&gt;관리자만 실행할 수 있어야&lt;/b&gt; 하거나, &lt;b&gt;로그인한 사용자만 접근 가능&lt;/b&gt;해야 합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;권한 체크를 서비스 로직마다 직접 넣으면 코드가 지저분해집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Secured(&quot;ROLE_ADMIN&quot;)&lt;/code&gt; 같은 어노테이션을 붙이면, &lt;b&gt;메서드 실행 전에 권한을 검사&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;권한이 없으면 로직 자체가 실행되지 않고, 바로 예외 발생 &amp;rarr; 접근 차단.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
public class AdminService {
    @Secured(&quot;ROLE_ADMIN&quot;)
    public void deleteUser(Long userId) {
        // 관리자만 가능한 작업
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 &lt;code&gt;AdminService&lt;/code&gt;를 빈으로 등록할 때, 바로 쓰지 않고 &lt;b&gt;프록시 클래스를 만들어서&lt;/b&gt; 컨테이너에 등록합니다. (아래는 개념 설명용 의사코드임)&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;public class AdminServiceProxy extends AdminService {

    private final AdminService target; // 원본 AdminService
    private final SecurityInterceptor securityInterceptor;

    public AdminServiceProxy(AdminService target, SecurityInterceptor securityInterceptor) {
        this.target = target;
        this.securityInterceptor = securityInterceptor;
    }

    @Override
    public void deleteUser(Long userId) {
        // 1. 권한 확인 (ROLE_ADMIN이 있는지 검사)
        if (!securityInterceptor.hasRole(&quot;ROLE_ADMIN&quot;)) {
            throw new AccessDeniedException(&quot;권한 없음&quot;);
        }

        // 2. 권한 통과 &amp;rarr; 실제 메서드 실행
        target.deleteUser(userId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 3. 캐싱 (@Cacheable)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조회 쿼리나 외부 API 호출처럼 &lt;b&gt;자주 요청되지만 결과가 자주 바뀌지 않는 데이터&lt;/b&gt;가 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매번 DB나 API를 호출하면 느려지고 비용이 큽니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Cacheable&lt;/code&gt;을 붙이면, &lt;b&gt;메서드 실행 전에 캐시에 값이 있는지 확인&lt;/b&gt;합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;있으면 DB를 건드리지 않고 캐시 값을 반환.&lt;/li&gt;
&lt;li&gt;없으면 실제 메서드를 실행하고 결과를 캐시에 저장.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
public class ProductService {
    @Cacheable(&quot;products&quot;)
    public Product getProduct(Long id) {
        return productRepository.findById(id).get();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시로 감으면 이런 느낌이겠죠..?&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;public class ProductServiceProxy extends ProductService {

    private final ProductService target; // 실제 ProductService
    private final CacheManager cacheManager;

    public ProductServiceProxy(ProductService target, CacheManager cacheManager) {
        this.target = target;
        this.cacheManager = cacheManager;
    }

    @Override
    public Product getProduct(Long id) {
        Cache cache = cacheManager.getCache(&quot;products&quot;);

        // 1. 캐시에 값 있는지 확인
        Product cached = cache.get(id, Product.class);
        if (cached != null) {
            System.out.println(&quot;캐시에서 조회: &quot; + id);
            return cached;
        }

        // 2. 없으면 실제 메서드 실행
        Product result = target.getProduct(id);

        // 3. 실행 결과 캐시에 저장
        cache.put(id, result);
        System.out.println(&quot;DB 조회 후 캐시 저장: &quot; + id);

        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 4. 로깅 / 모니터링 (실행 시간 측정)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실무에서는 &amp;ldquo;이 API가 몇 초 걸리는지&amp;rdquo;, &amp;ldquo;에러가 어디서 나는지&amp;rdquo; 같은 로깅/모니터링이 중요합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 모든 메서드마다 &lt;code&gt;System.out.println()&lt;/code&gt;이나 &lt;code&gt;logger.info()&lt;/code&gt;를 넣는 건 비효율적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Around&lt;/code&gt; 어드바이스를 사용하면, 메서드 실행 전후 시간을 측정하고 로그를 남길 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Aspect
@Component
public class LoggingAspect {

    @Around(&quot;execution(* com.example.service.*.*(..))&quot;)
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();

        Object result = joinPoint.proceed(); // 실제 메서드 실행

        long end = System.currentTimeMillis();
        System.out.println(joinPoint.getSignature() + &quot; 실행 시간: &quot; + (end - start) + &quot;ms&quot;);

        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구현을 하는데 만약에 @Around 어드바이스 안에서 proceed()를 안 부르면 원본 메서드가 실행되지 않기 때문에 주의해야 합니다(ㅋㅋ..)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Spring AOP 주의사항!!!&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;프록시 기반 한계 (Self-invocation 문제)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 AOP는 &lt;b&gt;프록시 객체&lt;/b&gt;를 통해 동작합니다.&lt;/li&gt;
&lt;li&gt;그런데 &lt;b&gt;자기 자신 안에서 자기 메서드를 호출하면 프록시를 거치지 않습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
public class OrderService {
    @Transactional
    public void outer() {
        inner(); // 같은 클래스 메서드 호출 &amp;rarr; 프록시 안 거침 &amp;rarr; 트랜잭션 적용 안 됨
    }

    @Transactional
    public void inner() {
        // 트랜잭션 적용 안 됨
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 적용하고 싶다면 구조를 분리해서 별도 빈으로 나누거나, &lt;code&gt;AopContext.currentProxy()&lt;/code&gt; 활용해야 하는데 이 경우는 복잡도가 증가하니 비추합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도 빈으로 나누는 편이 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;final 클래스 / final 메서드&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시는 &lt;b&gt;상속(CGLIB)&lt;/b&gt; 이나 &lt;b&gt;인터페이스(JDK Dynamic Proxy)&lt;/b&gt; 기반으로 만들어지는데, &lt;code&gt;final&lt;/code&gt; 클래스나 &lt;code&gt;final&lt;/code&gt; 메서드는 오버라이드가 불가능해 AOP 적용이 안 됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;final class PaymentService { ... } // 프록시 못 씌움&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;private 메서드에는 적용 불가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 &lt;b&gt;public 메서드&lt;/b&gt;에만 적용하는 것이 원칙이기 땜시 private 메서드는 프록시 호출 경로에서 가려지기 때문에 Advice를 걸 수 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;체크 예외 vs 언체크 예외 (@Transactional 롤백 규칙)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Transactional&lt;/code&gt; 기본 동작의 경우에&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;RuntimeException&lt;/code&gt;(언체크) &amp;rarr; 롤백&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Checked Exception&lt;/code&gt; &amp;rarr; 롤백 안 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헷갈려서 잘못 설정하면 데이터 정합성 문제 발생 가능할 수 있기 때문에 필요시 &lt;code&gt;rollbackFor&lt;/code&gt; 속성으로 명시해야 함.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;프록시 객체와 원본 객체의 타입 혼동&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시는 원본 객체를 감싸고 있지만 &lt;b&gt;실제로는 다른 클래스&lt;/b&gt;입니다. 특히 JDK Dynamic Proxy는 인터페이스 타입으로만 캐스팅 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;// JDK Dynamic Proxy 사용 시
OrderServiceImpl order = (OrderServiceImpl) context.getBean(&quot;orderService&quot;);
// ❌ ClassCastException 발생 (프록시는 인터페이스 기반이라 Impl로 캐스팅 불가)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>언어+프레임워크/Spring</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/434</guid>
      <comments>https://engineerinsight.tistory.com/434#entry434comment</comments>
      <pubDate>Tue, 16 Sep 2025 20:00:22 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] 패킷이 이동하는 과정, 포장부터 개봉까지: 브라우저에 깃짱코딩.com을 입력하면 무슨 일이 일어나나요?</title>
      <link>https://engineerinsight.tistory.com/433</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;브라우저에 깃짱코딩.com을 입력하면 무슨 일이 일어나나요?&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&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;665&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Frl00/btsQybQfljI/eoA5Jf1kjQZWkkB65EEifk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Frl00/btsQybQfljI/eoA5Jf1kjQZWkkB65EEifk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Frl00/btsQybQfljI/eoA5Jf1kjQZWkkB65EEifk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFrl00%2FbtsQybQfljI%2FeoA5Jf1kjQZWkkB65EEifk%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;520&quot; height=&quot;374&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  송신지에서 일어나는 일&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CsHpA/btsQyohDT5P/jmALuGDMoshCxh8lKSKp41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CsHpA/btsQyohDT5P/jmALuGDMoshCxh8lKSKp41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CsHpA/btsQyohDT5P/jmALuGDMoshCxh8lKSKp41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCsHpA%2FbtsQyohDT5P%2FjmALuGDMoshCxh8lKSKp41%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;303&quot; height=&quot;275&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 1. 응용 계층: 사용자의 요청 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 주소창에 &lt;code&gt;깃짱코딩.com&lt;/code&gt;을 입력했다고 가정해 봅시다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.1 DNS 요청 (= IP 주소 알아내기)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 주소창에 &lt;code&gt;깃짱코딩.com&lt;/code&gt;을 입력하면, 먼저 DNS 캐시를 확인하고, 없는 경우 DNS 서버에 질의를 하는 식으로 흐름이 이어집니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;캐시 확인&lt;/b&gt;: 브라우저의 DNS 캐시, OS 캐시 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;호스트 파일 확인&lt;/b&gt;: &lt;code&gt;/etc/hosts&lt;/code&gt;(리눅스/맥) 혹은 &lt;code&gt;hosts&lt;/code&gt; 파일(윈도우)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로컬 DNS 리졸버로 요청&lt;/b&gt;&lt;br /&gt;캐시나&amp;nbsp;hosts&amp;nbsp;파일&amp;nbsp;어디에도&amp;nbsp;없다면,&amp;nbsp;브라우저는&amp;nbsp;OS&amp;nbsp;네트워크&amp;nbsp;설정에&amp;nbsp;지정된&amp;nbsp;DNS&amp;nbsp;서버(로컬&amp;nbsp;리졸버)에&amp;nbsp;질의합니다.&lt;br /&gt;이 DNS 서버는 보통 인터넷 서비스 제공자(ISP)가 제공하기 때문에 단순히 OS가 알려주는 DNS 서버 주소로 바로 패킷을 내게 됩니다. &lt;i&gt;사용자가 직접 설정한 경우에는 구글 퍼블릭 DNS(&lt;code&gt;8.8.8.8&lt;/code&gt;)나 클라우드플레어 DNS(&lt;code&gt;1.1.1.1&lt;/code&gt;) 같은 곳으로 보낼 수도 있습니다.&lt;br /&gt;이때 실제 패킷 레벨에서는&lt;/i&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;응용 계층&lt;/b&gt;: DNS 메시지 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전송 계층&lt;/b&gt;: UDP 헤더 붙임 (출발지 포트=랜덤, 목적지 포트=53)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;DNS는 짧고 단순한 요청/응답 구조라서 연결 설정이 필요 없는 UDP가 효율적이라 UDP 사용&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 계층&lt;/b&gt;: IP 헤더 붙임 (출발지=내 PC IP, 목적지=DNS 서버 IP)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;링크 계층&lt;/b&gt;: MAC 주소 붙여서 라우터로 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DNS 트리 탐색 by 로컬 리졸버&lt;br /&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Root 서버&lt;/b&gt; &lt;i&gt;(맨 꼭대기)&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;.com 도메인은 어디에 있어요?&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Root 서버는 최상위 도메인(TLD) 서버(.com, .net, .org 등)의 IP 주소를 알려줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TLD(Top-Level Domain) 서버&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TLD 서버 = &lt;code&gt;.com&lt;/code&gt;, &lt;code&gt;.net&lt;/code&gt;, &lt;code&gt;.org&lt;/code&gt;, &lt;code&gt;.kr&lt;/code&gt; 같은 최상위 도메인을 담당&lt;/li&gt;
&lt;li&gt;&amp;ldquo;깃짱코딩.com은 어디에 있어요?&amp;rdquo;&lt;/li&gt;
&lt;li&gt;TLD 서버는 해당 도메인의 &lt;b&gt;권한 있는 네임서버(Authoritative DNS)&lt;/b&gt; 주소를 알려줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;권한 있는 네임서버(Authoritative DNS)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Authoritative DNS = 특정 도메인에 대한 &lt;b&gt;최종 정보 보관소&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;깃짱코딩.com &amp;rarr; 203.0.113.50&lt;/code&gt; 같은 실제 IP 주소를 직접 저장하고 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;ldquo;깃짱코딩.com의 실제 IP 주소가 뭐예요?&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Authoritative 서버가 최종 IP 주소를 응답합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답 반환&lt;/b&gt; &lt;i&gt;(= IP 주소 획득!)&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬 리졸버는 받은 IP 주소를 자기 메모리 캐시에 저장합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보통 DNS 응답에는 TTL이 있어서 이 캐시는 일정 시간 동안만 캐시 유효&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;로컬 리졸버 &amp;rarr; 내 PC로 응답을 전달합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;내 PC의 OS DNS 캐시가 이 결과를 저장해, 다음부터는 빠르게 해당 사이트의 IP 주소를 알아낼 수 있습니다.&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;내 PC의 브라우저는 이 IP 주소를 사용해 &lt;b&gt;TCP 3-way handshake&lt;/b&gt;를 시작하고, 실제 HTTP 요청을 보냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;603&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zXpdw/btsQxIU686R/vYgyQdUh4uX6aAB8Pl2SzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zXpdw/btsQxIU686R/vYgyQdUh4uX6aAB8Pl2SzK/img.png&quot; data-alt=&quot;DNS 트리 탐색&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zXpdw/btsQxIU686R/vYgyQdUh4uX6aAB8Pl2SzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzXpdw%2FbtsQxIU686R%2FvYgyQdUh4uX6aAB8Pl2SzK%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;432&quot; height=&quot;337&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;603&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DNS 트리 탐색&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.2 TCP 3-way handshake (= 연결 시작)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 도메인의 IP를 DNS로 알아낸 후, TCP 3-way handshake를 통해 연결을 성립합니다. HTTP 요청을 미리 생성하더라도, 연결이 성립되지 않으면 전송할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 3-way handshake도 결국 TCP 패킷(세그먼트)입니다만, 이때는 본격적인 응용 계층 데이터(HTTP 요청, 파일 데이터 등)가 실리지 않고, &lt;b&gt;헤더의 제어 정보만 채워져 있는 경량 패킷&lt;/b&gt;입니다. &lt;i&gt;(페이로드 없음)&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SYN 패킷 (클라이언트 &amp;rarr; 서버)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP 헤더 안에 &lt;code&gt;SYN=1, ACK=0, SEQ=1000&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;SYN=1&lt;/code&gt; 의미 = 연결을 새로 시작하고 싶다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ACK=0&lt;/code&gt; 의미 = 아직 상대방의 ISN을 받은 게 없으니 확인할 게 없음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SEQ=1000&lt;/code&gt; 의미 = 클라이언트가 이번 연결을 위해 무작위로 정한 시작점으로, 이후 데이터 바이트를 보낼 때 이 숫자부터 하나씩 증가시킴.&lt;/li&gt;
&lt;li&gt;Payload는 비어 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 패킷을 받은 서버는 TCP 헤더에서 &lt;code&gt;SYN=1&lt;/code&gt;을 확인하면 새로운 연결 요청이라는걸 눈치채고 &lt;b&gt;LISTEN 상태&lt;/b&gt;에서 &lt;b&gt;SYN-RECEIVED 상태&lt;/b&gt;로 전이&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SYN+ACK 패킷 (서버 &amp;rarr; 클라이언트)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP 헤더 안에 &lt;code&gt;SYN=1, ACK=1, SEQ=3000, ACK=1001&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;SYN = 1&lt;/code&gt; 의미 = 서버도 이제 연결을 시작하겠다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시에 내 ISN(Initial Sequence Number)을 시퀀스 번호 필드에 싣는다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ACK = 1&lt;/code&gt; 의미 = 클라이언트가 보낸 SYN 잘 받았음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이게 켜져 있어야 클라이언트에게 시퀀스 번호를 확인했다는 걸 알린다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SEQ = 3000&lt;/code&gt; 의미 = 서버가 이번 연결을 위해 새로 정한 자기 ISN(초기 시퀀스 번호)로, 이후 서버가 데이터를 보낼 때는 3000부터 시작해 바이트 단위로 증가함&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ACK = 1001&lt;/code&gt; 의미 = 클라이언트가 보낸 &lt;code&gt;ISN=1000&lt;/code&gt;을 받았으니, 이제 다음 번호 1001부터 보내달라
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1001은 클라이언트가 보낸 시퀀스 번호 + 1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ACK 패킷 (클라이언트 &amp;rarr; 서버)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP 헤더 안에 &lt;code&gt;SYN=0, ACK=1, SEQ=1001, ACK=3001&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;SYN=0&lt;/code&gt; 의미 = 이제 더 이상 새 연결을 시작하는 건 아님 &lt;i&gt;(데이터 교환 단계로 진입)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ACK=1&lt;/code&gt; 의미 = 서버가 보낸 SYN을 잘 받았다 (서버 ISN 확인 완료)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SEQ=1001&lt;/code&gt; 의미 = 클라이언트 ISN(1000)에서 +1
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제부터 1001부터 실제 데이터 전송 시작&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ACK=3001&lt;/code&gt; 의미 = 서버가 보낸 &lt;code&gt;ISN=3000&lt;/code&gt;을 잘 받았으니, 이제 다음 번호 3001부터 보내달라&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 패킷을 받은 서버는 TCP 헤더에서 &lt;code&gt;ACK=1&lt;/code&gt;을 확인하고, &lt;b&gt;ESTABLISHED 상태&lt;/b&gt;로 전이 &amp;rarr; 이제 양쪽 모두 연결이 성립되어 데이터 전송 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.3 HTTP 요청 생성&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IP 주소를 알게 되면, 브라우저는 HTTP 요청을 생성합니다.&lt;/li&gt;
&lt;li&gt;요청 데이터(payload): HTML 페이지 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 응용 계층은 우리가 실제로 보내고 싶은 &quot;내용물&quot;을 만들고, 이건 실제 패킷의 Payload가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 만드는 실제 요청 메시지 = 페이로드&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;GET / HTTP/1.1
Host: 깃짱코딩.com
User-Agent: Chrome/128.0
Accept: text/html
Connection: keep-alive&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 2. 전송 계층: 포트 번호와 제어 정보 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응용 계층에서 만들어진 데이터(예: HTTP 요청, DNS 쿼리)가 바로 네트워크로 흘러가지 않습니다. &lt;b&gt;전송 계층(Transport Layer)&lt;/b&gt; 에서 &amp;ldquo;이 데이터가 정확히 어떤 프로그램으로 가야 하는지, 어떻게 순서를 보장하고, 오류가 나면 어떻게 복구할지&amp;rdquo;를 정의해 주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 데이터(payload)는 전송 계층으로 내려갑니다.&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;DNS 요청&lt;/b&gt;은 &lt;b&gt;UDP&lt;/b&gt;를 사용 &lt;i&gt;(보통 53번 포트)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP 요청&lt;/b&gt;은 &lt;b&gt;TCP&lt;/b&gt;를 사용 &lt;i&gt;(보통 80번 HTTP / 443번 HTTPS 포트)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전송 계층은 &amp;ldquo;어느 애플리케이션으로 가야 하는지&amp;rdquo;를 구분하기 위해 &lt;b&gt;포트 번호&lt;/b&gt;를 붙이고,&lt;br /&gt;신뢰성 보장을 위해 &lt;b&gt;시퀀스 번호, ACK&lt;/b&gt; 같은 정보를 넣습니다. &lt;i&gt;(이건 TCP의 경우만)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mpLp4/btsQwLEZZ9e/tYE5AkhnoHzsXlTXHz9Fj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mpLp4/btsQwLEZZ9e/tYE5AkhnoHzsXlTXHz9Fj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mpLp4/btsQwLEZZ9e/tYE5AkhnoHzsXlTXHz9Fj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmpLp4%2FbtsQwLEZZ9e%2FtYE5AkhnoHzsXlTXHz9Fj1%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;493&quot; height=&quot;289&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP를 예시로 보면,&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;출발지 포트(Source Port)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내 PC에서 요청을 보낸 &lt;b&gt;애플리케이션을 식별&lt;/b&gt;하기 위해 사용합니다.&lt;/li&gt;
&lt;li&gt;브라우저가 HTTP 요청을 보낼 때는 보통 49152~65535 범위의 &lt;b&gt;임시 포트(에페메럴 포트)&lt;/b&gt; 를 할당받습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목적지 포트(Destination Port)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목적지 서버의 &lt;b&gt;어떤 서비스에 접속할 것인지&lt;/b&gt; 지정합니다.&lt;/li&gt;
&lt;li&gt;예
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;80번: HTTP&lt;/li&gt;
&lt;li&gt;443번: HTTPS&lt;/li&gt;
&lt;li&gt;53번: DNS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;제어 정보
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;시퀀스 번호(Sequence Number)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;송신 측에서 보낸 데이터 바이트 스트림의 순서를 나타냅니다.&lt;/li&gt;
&lt;li&gt;네트워크는 패킷이 순서대로 도착한다는 보장이 없기 때문에, 수신 측은 시퀀스 번호를 보고 원래 순서대로 조립할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ACK(Acknowledgment Number)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수신 측이 &amp;ldquo;어디까지 잘 받았는지&amp;rdquo; 송신 측에 알려주는 번호입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;처음 보낼 때는 의미가 없고 연결 절차나 실제 수신 이후부터 유효하게 채워집니다.&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;예: 시퀀스 번호 1001~1500까지 받았다면, ACK=1501을 보냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TCP Flags&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SYN&lt;/b&gt;: 연결 시작(3-way handshake에서 사용)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ACK&lt;/b&gt;: 데이터 수신 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FIN&lt;/b&gt;: 연결 종료 요청&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RST&lt;/b&gt;: 비정상 연결 강제 종료&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PSH&lt;/b&gt;: 데이터를 즉시 전달하도록 지시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;URG&lt;/b&gt;: 긴급 데이터 존재&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TCP는 단순 데이터 전달뿐 아니라, 연결을 관리하는 여러 제어 플래그를 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 3. 인터넷 계층: IP 주소 붙이기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷 계층은 &amp;ldquo;이 패킷이 어디서 어디로 가야 하는가&amp;rdquo;를 지정합니다. 전송 계층에서 만들어진 &lt;b&gt;TCP 세그먼트&lt;/b&gt; 또는 &lt;b&gt;UDP 데이터그램&lt;/b&gt;을 받아, 네트워크 상에서 &lt;b&gt;출발지 &amp;rarr; 목적지&lt;/b&gt; 까지 정확히 전달하는 것이 목적입니다. 즉, &lt;b&gt;IP 주소 기반의 논리적 주소 지정 + 라우팅&lt;/b&gt;을 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서는 전송 계층에서 포장된 TCP 세그먼트에 &lt;b&gt;IP 헤더&lt;/b&gt;가 추가됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;IPv4 헤더 주요 필드 (20바이트 고정 + 옵션 최대 40바이트)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;309&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJPSB4/btsQxFKSqdW/rTQBwIOwFHdaaPOHco0XKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJPSB4/btsQxFKSqdW/rTQBwIOwFHdaaPOHco0XKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJPSB4/btsQxFKSqdW/rTQBwIOwFHdaaPOHco0XKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJPSB4%2FbtsQxFKSqdW%2FrTQBwIOwFHdaaPOHco0XKk%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;277&quot; height=&quot;235&quot; data-origin-width=&quot;309&quot; data-origin-height=&quot;262&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;출발지 IP (Source IP)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패킷을 보낸 내 컴퓨터의 논리적 주소&lt;/li&gt;
&lt;li&gt;예: &lt;code&gt;192.168.0.10&lt;/code&gt; (사설 IP), &lt;code&gt;203.0.113.50&lt;/code&gt; (공인 IP)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목적지 IP (Destination IP)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패킷을 받아야 하는 상대방의 논리적 주소&lt;/li&gt;
&lt;li&gt;예: &lt;code&gt;203.0.113.50&lt;/code&gt; (깃짱코딩 웹 서버)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TTL (Time To Live)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패킷이 네트워크에서 무한 루프에 빠지지 않도록 &lt;b&gt;최대 홉 수&lt;/b&gt;를 제한&lt;/li&gt;
&lt;li&gt;TTL 값은 라우터를 지날 때마다 1씩 줄어들고, 0이 되면 &lt;b&gt;목적지에 도달하지 못했더라도 해당 패킷은 폐기&lt;/b&gt;됩니다.&lt;/li&gt;
&lt;li&gt;기본값: 64, 128, 255 등 운영체제별로 다름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로토콜 번호 (Protocol)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IP 패킷 안에 어떤 상위 계층 데이터가 들어있는지 알려줌&lt;/li&gt;
&lt;li&gt;예: &lt;code&gt;6&lt;/code&gt; = TCP, &lt;code&gt;17&lt;/code&gt; = UDP, &lt;code&gt;1&lt;/code&gt; = ICMP&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버전 (Version)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IPv4인지 IPv6인지 표시 (IPv4 = 4, IPv6 = 6)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;헤더 길이 (IHL, Internet Header Length)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IP 헤더가 몇 바이트인지 지정 (IPv4 기본값 = 20바이트)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전체 길이 (Total Length)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;헤더 + 데이터(payload) 전체 크기 (최대 65,535바이트)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;식별자, 플래그, 프래그먼트 오프셋&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패킷이 너무 크면 쪼개서(fragmentation) 보낼 수 있는데, 이때 조각들을 재조립하기 위해 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;헤더 체크섬 (Header Checksum)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IP 헤더 자체의 오류 검출용 값&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 4. 링크 계층: 물리 네트워크에 맞는 주소 붙이기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IP 계층&lt;/b&gt;까지 끝나면, 패킷은 &quot;출발지/목적지 IP&quot;라는 &lt;b&gt;논리적 주소&lt;/b&gt;를 가지게 됩니다. 하지만 네트워크 카드(NIC)는 &lt;b&gt;물리적인 주소(MAC 주소)&lt;/b&gt; 를 기반으로 동작합니다. 따라서 실제 전송 직전, &lt;b&gt;MAC 주소를 담은 링크 계층 헤더&lt;/b&gt;가 추가되어야 합니다. 이 과정을 흔히 &amp;ldquo;프레임(Frame)으로 캡슐화한다&amp;rdquo;라고 부릅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MAC 주소란?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NIC(Network Interface Card, 네트워크 카드)가 출고될 때 제조사에서 박아 넣은 &lt;b&gt;48비트 고유 식별자&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예: &lt;code&gt;00:1A:2B:3C:4D:5E&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;이더넷, 와이파이 모두 기본 단위는 MAC 주소 기반 통신&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;링크 계층 헤더 주요 필드&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;출발지 MAC 주소 (Source MAC)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내 PC의 NIC 고유 주소&lt;/li&gt;
&lt;li&gt;&quot;이 프레임은 내가 보낸 거다&quot;라는 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목적지 MAC 주소 (Destination MAC)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보통은 내 PC가 직접 붙은 &lt;b&gt;게이트웨이(라우터)의 MAC 주소&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;왜냐면 목적지가 인터넷 어딘가라면, 일단 라우터를 통해야 하니까&lt;/li&gt;
&lt;li&gt;만약 목적지가 같은 LAN 안에 있는 경우라면 상대방 기기의 MAC 주소&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타입 (EtherType)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 프레임 안에 들어있는 상위 계층 프로토콜이 뭔지 표시&lt;/li&gt;
&lt;li&gt;예: &lt;code&gt;0x0800&lt;/code&gt; = IPv4, &lt;code&gt;0x86DD&lt;/code&gt; = IPv6, &lt;code&gt;0x0806&lt;/code&gt; = ARP&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 5. 네트워크를 통한 전달&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 완성된 프레임이 네트워크를 타고 흘러갑니다. 이때 패킷의 IP 주소는 유지되지만, MAC 주소는 라우터를 지나갈 때마다 새롭게 바뀝니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;내 PC &amp;rArr; 공유기/라우터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 PC 에서 출발합니다.&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;출발지 MAC&lt;/b&gt; = 내 PC NIC&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목적지 MAC&lt;/b&gt; = 공유기(게이트웨이) MAC&lt;/li&gt;
&lt;li&gt;&lt;b&gt;출발지 IP&lt;/b&gt; = 내 PC IP&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목적지 IP&lt;/b&gt; = 깃짱코딩 서버 IP&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유기가 받으면 &lt;b&gt;링크 계층 헤더(MAC 주소)만 벗기고, IP 헤더부터는 그대로 두게 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;공유기 &amp;rArr; ISP 라우터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유기는 IP 헤더를 보고 이 패킷은 깃짱코딩 서버로 가야 한다고 판단하고, 다음 홉(Next Hop, ISP 라우터)의 MAC 주소를 ARP로 알아냅니다. &lt;i&gt;(이건 다른 포스팅에서 자세히 다루겠습니다)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷은 이제 새 Ethernet 헤더를 입혀서 다시 보내집니다. &lt;i&gt;(IP 헤더는 그대로 유지)&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;출발지 MAC&lt;/b&gt; = 공유기 MAC&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목적지 MAC&lt;/b&gt; = ISP 라우터 MAC&lt;/li&gt;
&lt;li&gt;&lt;b&gt;출발지 IP&lt;/b&gt; = 내 PC IP&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목적지 IP&lt;/b&gt; = 깃짱코딩 서버 IP&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;*여기서 ISP 라우터는 사실 하나의 장비가 아니라, 통신사(Internet Service Provider)가 운영하는 여러 라우터들로, &amp;lsquo;공유기 다음부터 만나는 통신사 네트워크 장비들&amp;rsquo;을 모두 포함합니다.&lt;/i&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ISP Backbone을 통과 (여러 라우터 경유)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 라우터는 IP 헤더는 건드리지 않고, 자신과 다음 홉 사이의 링크 계층 헤더만 다시 씌웁니다. 즉, 라우터마다 &quot;MAC 주소 쌍&quot;만 계속 바뀌는 구조로 IP 주소는 출발지/목적지 그대로 유지됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;목적지 서버에 도착&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 라우터가 서버 네트워크 카드(NIC)까지 전달합니다. 목적지 서버에 도달하기 직전의 패킷 상태입니다.&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;출발지 MAC&lt;/b&gt; = 마지막 라우터 MAC&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목적지 MAC&lt;/b&gt; = 깃짱코딩 서버 NIC MAC&lt;/li&gt;
&lt;li&gt;&lt;b&gt;출발지 IP&lt;/b&gt; = 내 PC IP&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목적지 IP&lt;/b&gt; = 깃짱코딩 서버 IP&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 &lt;b&gt;라우팅 테이블&lt;/b&gt;과 &lt;b&gt;ARP(Address Resolution Protocol)&lt;/b&gt;가 사용됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ARP: IP 주소에 대응하는 MAC 주소를 찾음&lt;/li&gt;
&lt;li&gt;라우팅: 최적 경로를 따라 목적지로 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  수신지에서 일어나는 일&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsfERb/btsQvX61q5N/kkYPfr7YGA27u7n00Q4sf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsfERb/btsQvX61q5N/kkYPfr7YGA27u7n00Q4sf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsfERb/btsQvX61q5N/kkYPfr7YGA27u7n00Q4sf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsfERb%2FbtsQvX61q5N%2FkkYPfr7YGA27u7n00Q4sf1%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;416&quot; height=&quot;320&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 1. 포장 해체 (역캡슐화)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적지 서버(깃짱코딩 서버)에 도착하면, 역순으로 헤더를 벗깁니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;링크 계층&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MAC 주소 확인 (내 서버 맞는지 확인)&lt;/li&gt;
&lt;li&gt;Ethernet 헤더 제거 후 IP 계층으로 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인터넷 계층 (IP)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목적지 IP 확인 (내 서버 맞는지 확인)&lt;/li&gt;
&lt;li&gt;TTL 감소 및 유효성 체크&lt;/li&gt;
&lt;li&gt;IP 헤더 제거 후 TCP 계층으로 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전송 계층 (TCP)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목적지 포트 확인&lt;/li&gt;
&lt;li&gt;시퀀스 번호/ACK 확인 (데이터 순서와 신뢰성 보장)&lt;/li&gt;
&lt;li&gt;TCP 헤더 제거 후 응용 계층으로 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응용 계층 (HTTP 서버)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 서버(Nginx, Apache, Spring Boot 등)가 요청을 해석&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET / HTTP/1.1&lt;/code&gt; 요청에 대해 HTML 페이지를 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 2. 응답 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃짱코딩 서버는 HTML 데이터를 다시 포장합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응용 계층: HTML 페이지&lt;/li&gt;
&lt;li&gt;전송 계층: TCP 헤더 붙임 (포트 80 &amp;rarr; 출발지, 50000번대 &amp;rarr; 목적지)&lt;/li&gt;
&lt;li&gt;인터넷 계층: 서버 IP &amp;rarr; 출발지 IP&lt;/li&gt;
&lt;li&gt;링크 계층: 서버 MAC &amp;rarr; 라우터 MAC&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 역방향으로 클라이언트에게 전달됩니다. 그러면 이제까지 길게 설명한 부분을 역방향으로 똑같이 진행하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 브라우저는 응답을 받으면 다시 헤더를 하나씩 벗기고, 최종적으로 HTML 내용을 화면에 렌더링합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/네트워크</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/433</guid>
      <comments>https://engineerinsight.tistory.com/433#entry433comment</comments>
      <pubDate>Sat, 13 Sep 2025 12:00:35 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] TCP/IP 헤더와 패킷의 구조</title>
      <link>https://engineerinsight.tistory.com/432</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  TCP/IP 헤더와 패킷&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP/IP 네트워크를 이해하기 위해서는 &lt;b&gt;패킷(packet)&lt;/b&gt; 이라는 단위와, 이를 구성하는 &lt;b&gt;헤더(header)&lt;/b&gt; 구조를 먼저 살펴봐야 네트워크에서 실제 데이터가 어떻게 나누어지고 전송되는지를 알 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 패킷(Packet)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;네트워크에서 전송되는 데이터의 최소 단위&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 데이터(payload)만 존재하지 않고, 그 앞에는 다양한 제어 정보를 담은 &lt;b&gt;헤더(header)&lt;/b&gt; 가 붙습니다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;[ 링크 계층 헤더 | 네트워크 계층 헤더 | 전송 계층 헤더 | 데이터(payload) ]&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Payload (데이터)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우리가 실제로 보내려는 내용물&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP라면 &lt;code&gt;GET /index.html HTTP/1.1&lt;/code&gt; 같은 요청 메시지&lt;/li&gt;
&lt;li&gt;이메일이라면 &lt;code&gt;Subject: Hello&lt;/code&gt; 같은 본문&lt;/li&gt;
&lt;li&gt;파일 전송이라면 파일 조각 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Header (&lt;i&gt;메타&lt;/i&gt;데이터)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가 잘 전달되도록 붙여진 &lt;b&gt;주소 + 제어 정보&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;누가 보냈는지, 어디로 가는지, 어떤 방식으로 전송하는지, 오류가 있는지 등을 담음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TCP/IP 계층별 메타데이터&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;링크 계층 헤더&lt;/b&gt;: 출발지/목적지 &lt;b&gt;&lt;code&gt;MAC 주소&lt;/code&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IP 헤더&lt;/b&gt;: 출발지/목적지 &lt;b&gt;&lt;code&gt;IP 주소&lt;/code&gt;&lt;/b&gt;, TTL, 프로토콜 번호&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TCP 헤더&lt;/b&gt;: 출발지/목적지 &lt;b&gt;&lt;code&gt;포트 번호&lt;/code&gt;&lt;/b&gt;, 시퀀스 번호, ACK, 플래그(SYN, ACK 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UDP 헤더&lt;/b&gt;: 출발지/목적지 &lt;b&gt;&lt;code&gt;포트 번호&lt;/code&gt;&lt;/b&gt;, 길이, 체크섬&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 데이터를 전송할 때 &lt;b&gt;캡슐화(encapsulation)&lt;/b&gt; 과정이 발생하여, 상위 계층의 데이터가 하위 계층의 헤더와 합쳐져 내려가는 구조입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ TCP/IP 계층과 헤더 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP/IP는 4계층 모델로 흔히 구분합니다. 각 계층에서 붙는 헤더를 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;링크 계층 (Ethernet, Wi-Fi 등)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;헤더: &lt;b&gt;&lt;code&gt;MAC 주소&lt;/code&gt;&lt;/b&gt;(출발지/목적지), 타입, CRC 등&lt;/li&gt;
&lt;li&gt;예시: Ethernet 프레임 헤더 (14바이트)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 계층 (IP)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대표 프로토콜: IPv4, IPv6&lt;/li&gt;
&lt;li&gt;헤더: 출발지/목적지 &lt;b&gt;&lt;code&gt;IP 주소&lt;/code&gt;&lt;/b&gt;, TTL, 프로토콜 번호, 헤더 체크섬 등&lt;/li&gt;
&lt;li&gt;IPv4 기본 헤더 크기: 20바이트 (옵션 포함 시 최대 60바이트)&lt;/li&gt;
&lt;li&gt;IPv6 기본 헤더 크기: 40바이트 (확장 헤더 구조 존재)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전송 계층 (TCP/UDP)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TCP 헤더 (20~60바이트)&lt;/b&gt; &lt;i&gt;&amp;rArr; 더 많은 정보&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;출발지/목적지 &lt;b&gt;&lt;code&gt;포트&lt;/code&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;시퀀스 번호(Sequence Number)&lt;/li&gt;
&lt;li&gt;확인 응답 번호(Acknowledgment Number)&lt;/li&gt;
&lt;li&gt;플래그 (SYN, ACK, FIN 등)&lt;/li&gt;
&lt;li&gt;윈도우 크기(Window Size)&lt;/li&gt;
&lt;li&gt;체크섬(Checksum)&lt;/li&gt;
&lt;li&gt;옵션 (MSS, 윈도우 스케일 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UDP 헤더 (&lt;code&gt;8바이트&lt;/code&gt; 고정)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;출발지/목적지 &lt;b&gt;&lt;code&gt;포트&lt;/code&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;길이 (Length)&lt;/li&gt;
&lt;li&gt;체크섬 (Checksum)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응용 계층 (HTTP, DNS, SMTP 등)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;헤더: 프로토콜별 정의&lt;/li&gt;
&lt;li&gt;예: &lt;b&gt;&lt;code&gt;HTTP 헤더&lt;/code&gt;&lt;/b&gt;에는 Host, User-Agent, Content-Type, Cookie 등이 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ IPv4 패킷 구조 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 웹 브라우저에서 &lt;code&gt;http://example.com&lt;/code&gt;에 접속할 때 패킷은 다음과 같은 구조를 가집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Ethernet 헤더&lt;/b&gt;: 목적지/출발지 MAC 주소&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IPv4 헤더&lt;/b&gt;: 목적지 IP = 93.184.216.34, 출발지 IP = 내 PC의 IP&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TCP 헤더&lt;/b&gt;: 목적지 포트 = 80, 출발지 포트 = 임의 포트(에페메럴 포트)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP 요청 데이터&lt;/b&gt;: &lt;code&gt;GET / HTTP/1.1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ TCP 헤더 필드 상세&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 연결 지향적이며 신뢰성을 보장합니다. 따라서 헤더에 다양한 제어 정보가 포함됩니다.&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;Source Port&lt;/td&gt;
&lt;td&gt;출발지 포트 번호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destination Port&lt;/td&gt;
&lt;td&gt;목적지 포트 번호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequence Number&lt;/td&gt;
&lt;td&gt;송신 바이트 스트림에서의 &lt;b&gt;&lt;i&gt;순서&lt;/i&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Acknowledgment Number&lt;/td&gt;
&lt;td&gt;수신 확인 번호 (ACK)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Offset&lt;/td&gt;
&lt;td&gt;헤더 길이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flags&lt;/td&gt;
&lt;td&gt;SYN, ACK, FIN, RST, PSH, URG 등 제어 비트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Window Size&lt;/td&gt;
&lt;td&gt;수신 버퍼 크기 (흐름 제어)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checksum&lt;/td&gt;
&lt;td&gt;오류 검출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Urgent Pointer&lt;/td&gt;
&lt;td&gt;긴급 데이터 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Options&lt;/td&gt;
&lt;td&gt;MSS, 윈도우 스케일, 타임스탬프 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;0      4       8              16            24            32
+------+-------+---------------+---------------------------+
|   Source Port   |   Destination Port                     |
+---------------------------------------------------------+
|                 Sequence Number                         |
+---------------------------------------------------------+
|              Acknowledgment Number                      |
+--------+------+--------+-------------------+------------+
| DataOff| Res. | Flags  |   Window Size                  |
+--------+------+--------+--------------------------------+
|   Checksum     |  Urgent Pointer                        |
+---------------------------------------------------------+
|   Options (if any)                                      |
+---------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ UDP 헤더 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UDP는 단순하고 가벼운 프로토콜입니다. 헤더는 &lt;b&gt;단 8바이트&lt;/b&gt;로 구성됩니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;0      8      16     24     32
+------+-------+------+------+
| Source Port  | Destination Port |
+--------------+-----------------+
| Length       | Checksum        |
+--------------+-----------------+
| Data (Payload)                 |
+--------------------------------&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/네트워크</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/432</guid>
      <comments>https://engineerinsight.tistory.com/432#entry432comment</comments>
      <pubDate>Sat, 13 Sep 2025 10:00:37 +0900</pubDate>
    </item>
    <item>
      <title>[Spring/DI] 스프링 컨테이너: BeanFactory vs ApplicationContext</title>
      <link>https://engineerinsight.tistory.com/431</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  BeanFactory&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 초창기(2000년대 초반)부터 존재하던 &lt;b&gt;가장 기본적인 IoC 컨테이너&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할은 단순했습니다: &lt;b&gt;&lt;i&gt;객체(빈) 생성, 의존성 주입(DI)&lt;/i&gt;&lt;/b&gt;&lt;br /&gt;즉, 스프링의 &amp;ldquo;컨테이너 철학&amp;rdquo;을 처음 구현한 게 바로 &lt;code&gt;BeanFactory&lt;/code&gt;입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 빈을 만드는 시점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청할 때! 즉, &lt;code&gt;getBean()&lt;/code&gt;을 호출해야 그제서야 객체를 생성합니다. (Lazy Loading)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 기능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 만들고, 관리하는 단순한 기능 뿐 &lt;i&gt;(Spring MVC, @Transactional, @EventListener 대부분 없음)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BeanFactory&lt;/code&gt;는 &lt;b&gt;필요한 시점에만 객체를 만들어주는 단순 기계&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;BeanFactory factory = new XmlBeanFactory(new ClassPathResource(&quot;beans.xml&quot;));
MyService service = (MyService) factory.getBean(&quot;myService&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  ApplicationContext&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 지나면서 &amp;ldquo;빈만 관리하는 것&amp;rdquo;으로는 부족했습니다. 엔터프라이즈 애플리케이션에서는 &lt;b&gt;다국어 처리, 이벤트 시스템, AOP, 트랜잭션 관리&lt;/b&gt; 같은 부가기능이 필요했기 때문에, &lt;code&gt;BeanFactory&lt;/code&gt;를 확장해서 &lt;b&gt;ApplicationContext&lt;/b&gt;가 등장했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BeanFactory가 비유하자면 그냥 자판기라면, ApplicationContext는 풀옵션 편의점입니다. 단순히 객체만 관리하는 게 아니라, &lt;b&gt;애플리케이션 전반에서 필요한 기능&lt;/b&gt;들을 함께 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 빈을 만드는 시점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너가 뜨자마자! 실행 시점에 대부분의 객체를 미리 만들어둡니다. (Eager Loading)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;빈 설정이나 의존성 주입에 문제가 있으면 애플리케이션이 아예 로딩되지 않고 바로 실패&lt;/b&gt;합니다. 런타임 중간에 갑자기 &amp;ldquo;빈 못 만듦&amp;rdquo; 오류가 나는 것보다 애플리케이션 시작할 때 바로 에러를 잡는 게 안정적이기 때문입니다.&lt;/li&gt;
&lt;li&gt;이론적으로는 BeanFactory를 직접 쓰면 Lazy Loading만 되니까, 문제가 있는 빈도 요청하지 않으면 생성되지 않고 애플리케이션은 떠 있을 수 있습니다. 하지만 기능이 너무 부족해서 직접 사용하지는 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 기능&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다국어 지원 (메시지 국제화)&lt;/li&gt;
&lt;li&gt;이벤트 발행/리스너 등록&lt;/li&gt;
&lt;li&gt;AOP, 트랜잭션 관리&lt;/li&gt;
&lt;li&gt;스프링 MVC와의 통합&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ApplicationContext context =
    new ClassPathXmlApplicationContext(&quot;beans.xml&quot;);

MyService service = context.getBean(&quot;myService&quot;, MyService.class);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 설정 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 우리가 직접 &amp;ldquo;ApplicationContext를 쓰겠다&amp;rdquo;고 설정하지는 않습니다. Spring 자체가 &lt;b&gt;기본 컨테이너로 ApplicationContext를 사용&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 &lt;code&gt;SpringApplication.run()&lt;/code&gt;을 실행하면 자동으로 &lt;code&gt;ApplicationContext&lt;/code&gt;가 생성됩니다. 이때 &lt;b&gt;Profile&lt;/b&gt;, &lt;b&gt;AutoConfiguration&lt;/b&gt; 같은 기능이 모두 ApplicationContext 위에서 동작합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 다양한 구현체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext는 인터페이스이기 때문에 상황에 따라 다양한 구현체가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ClassPathXmlApplicationContext&lt;/code&gt;: XML 설정 기반&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FileSystemXmlApplicationContext&lt;/code&gt;: 파일 시스템 경로 기반 XML 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AnnotationConfigApplicationContext&lt;/code&gt;: &lt;code&gt;@Configuration&lt;/code&gt; 자바 설정 기반&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GenericWebApplicationContext&lt;/code&gt;, &lt;code&gt;ServletWebServerApplicationContext&lt;/code&gt;: 웹 애플리케이션 전용 (Spring Boot가 사용하는 기본 컨텍스트)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 그러면 빈 Lazy Loading은 못 쓰나?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능은 합니다. 전부 BeanFactory로 바꿀 필요 없고, &lt;b&gt;특정 빈만 지연 로딩&lt;/b&gt;으로 바꾸는 게 일반적인 방법입니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
@Lazy
public class MyService {
    // 이 빈은 요청될 때까지 생성 안 됨
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 무거운 객체 (대용량 캐시, 외부 API 클라이언트)는 &lt;code&gt;@Lazy&lt;/code&gt;로 늦춰서 애플리케이션 기동 속도를 개선할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 설정 파일에서 전체 빈에 대해 Lazy Loading을 걸 수도 있습니다&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@Lazy
public class AppConfig {
    // 모든 빈이 lazy init으로 동작
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPK0PU/btsQwTvXotH/dPakQGZ4PEOvzuPCcCqDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPK0PU%2FbtsQwTvXotH%2FdPakQGZ4PEOvzuPCcCqDik%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;382&quot; height=&quot;382&quot; data-filename=&quot;ChatGPT Image 2025년 9월 12일 오후 02_28_18.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>언어+프레임워크/Spring</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/431</guid>
      <comments>https://engineerinsight.tistory.com/431#entry431comment</comments>
      <pubDate>Fri, 12 Sep 2025 21:00:39 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] CORS &amp;amp; SOP: Preflight (OPTIONS) 요청은 매번 보내나요?</title>
      <link>https://engineerinsight.tistory.com/430</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  SOP (Same-Origin Policy)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 브라우저에는 기본적으로 Same-Origin Policy(동일 출처 정책, SOP)라는 제약이 걸려 있습니다. 출처(Origin)는 &lt;code&gt;프로토콜 + 도메인 + 포트&lt;/code&gt; 조합으로 결정되고, 브라우저는 이 조합이 완전히 같을 때만 자유롭게 요청, 응답을 통한 데이터 접근을 허용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 제약은 보안을 위해 꼭 필요합니다. 만약 이 정책이 없다면, 악성 웹사이트에 접속했을 때 그 사이트의 자바스크립트가 내가 로그인해 둔 은행 사이트의 쿠키나 계좌 정보를 그대로 읽어버릴 수도 있습니다. 따라서 SOP는 &lt;b&gt;브라우저 단에서 다른 출처 간 데이터 접근을 차단하는 안전장치&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOP가 막는 것들을 정리해 보자면,&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 1. DOM 접근 차단&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 만든 사이트에서 &lt;code&gt;&amp;lt;iframe src=&quot;https://bank.com&quot;&amp;gt;&lt;/code&gt;을 넣었다고 해본다면, SOP 때문에 내 자바스크립트가 &lt;code&gt;iframe&lt;/code&gt; 안쪽(=은행 사이트 DOM 구조)에 직접 접근하지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이게 가능했다면? &amp;rarr; 악성 사이트가 은행 페이지를 불러서 사용자 계좌 정보를 몰래 읽을 수 있었을 테지만, SOP 덕분에 &lt;b&gt;다른 출처의 화면 안쪽 내용은 볼 수 없습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;cf. iframe은 웹페이지 안에 또 다른 웹페이지를 &amp;ldquo;창&amp;rdquo;처럼 끼워 넣는 태그입니다. 신문에 붙어 있는 광고 전단처럼, 하나의 HTML 안에 다른 사이트를 그대로 불러올 수 있습니다.&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 2. 쿠키 / LocalStorage 차단&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 사용자가 은행(&lt;code&gt;bank.com&lt;/code&gt;)에 로그인한 상태라면, 그 쿠키나 LocalStorage 안에 세션 토큰이 저장돼 있습니다. 공격자가 만든 사이트(&lt;code&gt;evil.com&lt;/code&gt;)에서 JS로 &lt;code&gt;document.cookie&lt;/code&gt;를 실행한다고 해도 &lt;code&gt;bank.com&lt;/code&gt;의 쿠키는 읽히지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 3. AJAX 응답 읽기 차단&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 다른 출처 API로 요청을 &lt;b&gt;보내는 것 자체는 허용&lt;/b&gt;합니다. 예를 들어, &lt;code&gt;frontend.com&lt;/code&gt;에서 &lt;code&gt;api.bank.com&lt;/code&gt;으로 AJAX 요청을 보낼 수는 있어요. 하지만 응답을 브라우저가 가로막기 때문에, JS 코드에서 응답 본문을 읽을 수 없습니다. 그래서 &amp;ldquo;요청은 날아가지만, 응답은 SOP 때문에 볼 수 없다&amp;rdquo;는 특징이 있습니다. 즉, &lt;b&gt;데이터 읽기는 막히지만, 요청 자체는 가능합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;cf. AJAX란, 웹페이지가 새로고침 없이(부분 갱신) 서버와 데이터를 주고받게 해주는 기술입니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 서비스에서는 프론트엔드와 백엔드가 다른 도메인에 있는 경우가 흔합니다. 예를 들어, &lt;code&gt;https://frontend.com&lt;/code&gt; 웹앱이 &lt;code&gt;https://api.backend.com&lt;/code&gt; API를 호출해야 하는 상황이 있습니다. 이렇게 되면 프론트엔드와 백엔드의 출처가 다르게 되고, SOP 때문에 브라우저가 기본적으로 응답을 막습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 나온 것이 CORS(Cross-Origin Resource Sharing)입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  CORS (Cross-Origin Resource Sharing)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS는 &lt;b&gt;보안을 강화하는 기능이 아니라, 예외를 허용하는 기능&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOP가 원래 브라우저 보안의 핵심이고, CORS는 개발 편의상 &lt;i&gt;&amp;ldquo;이 출처만 열어줄게&amp;rdquo;&lt;/i&gt; 하는 장치입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ CORS의 동작 원리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 응답 헤더에 특정 출처를 허용한다고 명시&lt;/li&gt;
&lt;li&gt;브라우저는 이를 확인한 뒤 교차 출처 요청을 허용&lt;/li&gt;
&lt;li&gt;주요 헤더
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;: 허용할 출처&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt;: 허용할 HTTP 메서드&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt;: 허용할 커스텀 헤더&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Allow-Credentials&lt;/code&gt;: 브라우저가 &lt;b&gt;쿠키&amp;middot;Authorization 헤더&lt;/b&gt; 같은 자격 증명을 요청에 포함할 수 있는지 여부
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Allow-Credentials: true&lt;/code&gt; 는 &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; 와 &lt;b&gt;동시에 사용할 수 없음&lt;br /&gt;&amp;rArr; &amp;ldquo;누구나 + 인증정보까지&amp;rdquo; 조합이면 보안상 치명적이기 때문&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Expose-Headers&lt;/code&gt;: 기본적으로 브라우저는 응답 헤더 중 일부만 자바스크립트에 보여주는데, 이 옵션을 통해 필요한 커스텀 헤더를 JS에서 읽을 수 있게 열어줍니다.&lt;br /&gt;예: &lt;code&gt;Access-Control-Expose-Headers: X-Total-Count, X-Request-Id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Max-Age&lt;/code&gt;: Preflight(OPTIONS) 요청 결과를 브라우저가 &lt;b&gt;얼마나 오래 캐싱할지&lt;/b&gt; 지정합니다. 잘 설정하면 매번 OPTIONS를 보내지 않아도 돼서 성능이 좋아집니다.&lt;br /&gt;예: &lt;code&gt;Access-Control-Max-Age: 3600&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Preflight Request (사전 요청)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 민감한 요청은 그냥 실행하지 않고, 먼저 &lt;code&gt;OPTIONS&lt;/code&gt; 요청을 보내 서버에 &amp;ldquo;이 요청 보내도 되나요?&amp;rdquo;라고 묻습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저 &amp;rarr; 서버: Preflight(&lt;code&gt;OPTIONS&lt;/code&gt;) 요청 전송&lt;/li&gt;
&lt;li&gt;서버 &amp;rarr; 브라우저: CORS 관련 허용 헤더 응답&lt;/li&gt;
&lt;li&gt;브라우저 &amp;rarr; 서버: 실제 요청 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 통해 보안을 해치지 않으면서도 필요한 경우 교차 출처 요청을 안전하게 수행할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ Preflight (OPTIONS) 요청은 매번 보내나요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 입장에서 보면, &lt;b&gt;Preflight는 일종의 안전 확인 절차&lt;/b&gt;입니다. &amp;ldquo;이 요청을 그냥 보내면 혹시 보안 사고가 나지 않을까?&amp;rdquo; 하고 걱정되는 경우에만 &lt;code&gt;OPTIONS&lt;/code&gt;로 먼저 물어보게 됩니다. 따라서 단순한 요청에는 &lt;code&gt;OPTIONS&lt;/code&gt; 요청을 보내지 않게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단순한 요청&lt;/b&gt; (예: GET으로 이미지 가져오기, 폼 데이터 전송 등)은 웹에서 아주 오래 전부터 안전하게 쓰여왔습니다. 이런 건 굳이 매번 서버한테 허락을 구하지 않아도 괜찮다고 판단합니다. 그래서 바로 본 요청을 보냅니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: GET/POST/HEAD 중 &lt;code&gt;Content-Type&lt;/code&gt;이 &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;, &lt;code&gt;multipart/form-data&lt;/code&gt;, &lt;code&gt;text/plain&lt;/code&gt; 중 하나일 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;반대로 &lt;b&gt;조금 특수하거나 민감한 요청&lt;/b&gt; (예: JSON POST, Authorization 헤더, PUT/DELETE 같은 메서드)은 보안적으로 민감할 수 있습니다. 그래서 브라우저가 직접 서버에 먼저 &amp;ldquo;이런 요청 보내도 되나요?&amp;rdquo;라고 묻는 과정을 거칩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ Preflight 캐싱&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Preflight(OPTIONS) 요청은 &lt;b&gt;매번 보내지 않고 브라우저가 캐시&lt;/b&gt;할 수 있습니다.&lt;/li&gt;
&lt;li&gt;서버 응답 헤더 &lt;code&gt;Access-Control-Max-Age&lt;/code&gt; 값만큼 캐시되며, 그 시간 동안은 OPTIONS 요청을 생략합니다.&lt;/li&gt;
&lt;li&gt;즉, &amp;ldquo;OPTIONS가 무조건 매번 나가는 게 아니라, 서버 설정에 따라 줄일 수 있다&amp;rdquo;는 점이 중요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchX0BJ%2FbtsALWnWIgA%2FXMkMDnLKRussvcLggSXEq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;367&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/네트워크</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/430</guid>
      <comments>https://engineerinsight.tistory.com/430#entry430comment</comments>
      <pubDate>Fri, 12 Sep 2025 17:00:51 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] 웹 보안 공격들(XSS, CSRF, SQLi, DDos 등등): 공격 방법과 방어 전략</title>
      <link>https://engineerinsight.tistory.com/429</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  주요 웹 보안 공격 기법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ XSS (Cross-Site Scripting)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사이트에 스크립트를 삽입해 공격하는 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 게시판에 악성 자바스크립트 코드를 심어두면, 다른 사용자가 그 글을 열었을 때 브라우저가 그대로 실행해버립니다. 이때 공격자는 사용자의 &lt;b&gt;세션 정보나 입력 값&lt;/b&gt;을 직접적으로 훔칠 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;탈취할 수 있는 것&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;세션 쿠키&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예: &lt;code&gt;document.cookie&lt;/code&gt;를 읽어서 공격자 서버로 전송 (단, HttpOnly 옵션을 붙이면 탈취할 수 없음)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로컬 스토리지 / 세션 스토리지 값&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;JWT 토큰 같은 인증 정보가 저장되어 있다면 노출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 입력 값&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;폼(form)에 입력한 계좌번호, 비밀번호, 카드 정보 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DOM 조작을 통한 피싱&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;가짜 입력창을 띄워 ID/비밀번호 입력 유도&lt;/li&gt;
&lt;li&gt;&lt;b&gt;브라우저에서 접근 가능한 API 응답 데이터&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예: AJAX 응답(JSON)을 가로채서 탈취&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;즉, **XSS는 브라우저 안에서 돌아가는 모든 정보**를 공격자가 가로챌 수 있습니다.&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;방어 전략&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠키를 &lt;b&gt;HttpOnly&lt;/b&gt;로 설정해 JS에서 접근 불가하게 만들기 &lt;i&gt;(cf. CSRF에는 큰 효과 없음)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;입력값 철저히 검증(escape, sanitize)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 입력한 &lt;code&gt;&amp;lt;script&amp;gt;...&amp;lt;/script&amp;gt;&lt;/code&gt; 같은 값이 그대로 페이지에 뿌려지면 공격자가 코드 심을 수 있기 때문에 들어오는 데이터는 전부 &lt;b&gt;검사 + 안전하게 변환&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: &lt;code&gt;&amp;lt;&lt;/code&gt;를 &lt;code&gt;&amp;amp;lt;&lt;/code&gt; 로 바꿔주면 브라우저는 단순 텍스트로만 보여줌.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;sanitize&lt;/b&gt;는 불필요하거나 위험한 태그를 아예 잘라내는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CSP(Content Security Policy) 적용해 스크립트 실행 제어
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저에게 이 사이트에서 어떤 스크립트만 실행해도 되는지를 규칙으로 알려주는 방식으로, 외부에서 몰래 심은 스크립트는 실행되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ CSRF (Cross-Site Request Forgery)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용자가 로그인된 상태를 악용해, 사용자를 속여 원치 않는 요청을 보내는 공격&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 은행에 로그인한 상태에서 공격자가 숨겨둔 폼을 클릭하게 만들면, 사용자의 브라우저는 은행 서버로 요청을 보내면서 &lt;b&gt;&lt;code&gt;피해자의 세션 쿠키가 자동으로 전송&lt;/code&gt;&lt;/b&gt;됩니다. 결과적으로 공격자가 의도한 계좌 이체 요청이 실행될 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;탈취할 수 있는 것&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용자 계정의 권한으로 요청 실행&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예: 비밀번호 변경 요청, 송금 요청, 게시글 작성 요청&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버에 저장된 정보 조작&lt;/b&gt; 가능&lt;/li&gt;
&lt;li&gt;예: &quot;깃짱 계좌에 10만원 송금&quot; 같은 요청 실행&lt;br /&gt;단, &lt;b&gt;쿠키/세션 정보 자체는 탈취 불가&lt;/b&gt;(Same-Origin Policy 때문에 응답을 읽을 수 없음)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방어 전략&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠키에 &lt;b&gt;SameSite 옵션&lt;/b&gt; 설정 (&lt;code&gt;Lax&lt;/code&gt; 또는 &lt;code&gt;Strict&lt;/code&gt;)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SameSite 옵션&lt;/b&gt;: 쿠키를 외부 사이트 요청에 포함할지 말지 정하는 규칙
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Strict&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;철저히 차단&lt;/li&gt;
&lt;li&gt;다른 사이트에서 온 모든 요청에는 쿠키 안 붙음&lt;/li&gt;
&lt;li&gt;단점: 소셜 로그인, 외부 링크 후 로그인 유지 등 불편함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Lax (권장)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적인 안전 보장 + &lt;i&gt;사용성&lt;/i&gt; 확보&lt;/li&gt;
&lt;li&gt;링크 클릭(GET) 요청에는 쿠키 포함 가능&lt;/li&gt;
&lt;li&gt;하지만 폼 &lt;b&gt;&lt;i&gt;전송(POST), 스크립트 요청 등은 차단&lt;/i&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;None&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 사이트 요청에도 &lt;b&gt;항상 쿠키 전송&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;단, &lt;code&gt;Secure&lt;/code&gt; 옵션(&lt;b&gt;HTTPS&lt;/b&gt; 전송만 허용)도 같이 걸어야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;code&gt;SameSite=Lax&lt;/code&gt;를 기본으로 쓰고, 민감한 쿠키는 &lt;code&gt;Strict&lt;/code&gt;를 쓰는 게 좋습니당!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CSRF Token 사용: 폼이나 요청마다 무작위 토큰을 발급하고, 서버에서 검증
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 페이지를 열면, 서버가 &lt;b&gt;랜덤 토큰&lt;/b&gt;을 발급 &amp;rarr; HTML 폼 안에 숨겨서 넣어둠&lt;/li&gt;
&lt;li&gt;사용자가 폼 제출 시, 토큰도 함께 서버로 전송&lt;/li&gt;
&lt;li&gt;서버는 &lt;b&gt;세션에 저장된 토큰&lt;/b&gt;과 일치하는지 검사
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일치 &amp;rarr; 정상 요청으로 판단해 처리함&lt;/li&gt;
&lt;li&gt;불일치 or 없음 &amp;rarr; CSRF 공격으로 간주하고 차단&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;중요한 요청에는 재인증 요구 (비밀번호 재입력, OTP 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ SQL Injection (SQLi)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;입력값을 조작해 서버의 SQL 쿼리를 변형하는 공격&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버(Java 코드)에서 로그인 검증을 다음처럼 작성했다고 합시다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;String sql = &quot;SELECT * FROM users WHERE id = '&quot; + userId + &quot;' AND password = '&quot; + password + &quot;'&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 사용자가 입력한 값(&lt;code&gt;userId&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;)을 그대로 문자열에 끼워 넣는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 문자열로 만들면, 사용자가 입력값에 &lt;code&gt;&quot;' OR '1'='1&quot;&lt;/code&gt; 같은 SQL 구문을 넣어버릴 수 있습니다. 그러면 비밀번호 검증이 무시되고 모든 조건이 참이 되어 누구든 로그인할 수 있게 됩니다. 이것이 SQL Injection의 핵심 문제입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;공격 특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공격 경로: 서버가 사용자 입력을 그대로 SQL 쿼리에 포함시킬 때&lt;/li&gt;
&lt;li&gt;목표: DB 데이터 조회&amp;middot;수정&amp;middot;삭제, 관리자 권한 탈취&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방어 전략&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Prepared Statement만 잘 써도 SQL Injection은 사실상 막을 수 있습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;ORM 사용으로 직접 쿼리 작성 최소화 &lt;i&gt;(보조수단, 절대적인 방어책은 아님)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;입력값 검증 및 화이트리스트 기반 필터링&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Command Injection&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서버에서 실행되는 명령어를 조작해 임의의 커맨드를 실행하게 만드는 공격&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 파일 업로드 기능에서 입력값을 제대로 검증하지 않으면, 공격자가 &lt;code&gt;; rm -rf /&lt;/code&gt; 같은 명령을 주입할 수 있습니다. 이렇게 되면 서버에서 원래 의도와 전혀 다른 명령이 실행될 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;공격 특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공격 경로: 애플리케이션이 사용자 입력을 OS 명령에 직접 전달할 때&lt;/li&gt;
&lt;li&gt;목표: 서버 장악, 시스템 명령 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방어 전략&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 입력을 OS 명령어에 직접 연결하지 않기&lt;/li&gt;
&lt;li&gt;실행 가능한 명령은 화이트리스트 기반으로 제한&lt;/li&gt;
&lt;li&gt;최소 권한 원칙 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Directory Traversal&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상대 경로(&lt;code&gt;../&lt;/code&gt;) 등을 이용해 원래 접근하면 안 되는 디렉토리의 파일에 접근하는 공격&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;GET /download?file=../../etc/passwd&lt;/code&gt; 같은 요청을 통해 민감한 서버 파일을 열람할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;공격 특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공격 경로: 파일 경로 입력값 검증 부족&lt;/li&gt;
&lt;li&gt;목표: 서버 내부 민감 파일 접근&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방어 전략&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력값 정규화(normalization)&lt;/li&gt;
&lt;li&gt;접근 가능한 경로를 화이트리스트로 제한&lt;/li&gt;
&lt;li&gt;파일 다운로드 시 절대경로 매핑 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ DoS / DDoS (Denial of Service)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서버 자원을 고갈시켜 정상 사용자가 서비스를 이용하지 못하게 하는 공격&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 공격자가 과도한 요청을 보내는 경우를 DoS, 다수의 분산된 공격자가 동시에 공격하는 경우를 DDoS라고 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;공격 특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공격 경로: 네트워크 트래픽 과부하, 애플리케이션 자원 고갈&lt;/li&gt;
&lt;li&gt;목표: 서비스 마비, 가용성 저하&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방어 전략&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Rate Limiting 적용 &lt;i&gt;(로그인은 1분에 5번 이상 불가)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;WAF(Web Application Firewall) 사용 &lt;i&gt;(보조 방어책)&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 트래픽을 서버 앞단에서 검사하고 의심스로운 요청을 차단하는 방화벽&lt;/li&gt;
&lt;li&gt;SQLi, XSS, 잘 알려진 취약점 공격 패턴들을 막을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CDN을 통한 트래픽 분산
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청을 전세계 여러 캐시 서버에서 나누어 처리하기 때문에 트래픽을 분산해 서버가 죽는 것을 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비정상 트래픽 필터링
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초당 1000번 로그인 시도 같은 비정상적인 접근을 탐지&lt;/li&gt;
&lt;li&gt;AI 기반 보안 솔루션이나 로그 분석을 활용해 자동으로 차단&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Brute Force / Credential Stuffing&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비밀번호를 무작위로 대입하거나, 유출된 계정 정보를 자동으로 시도하는 공격&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Brute Force는 비밀번호를 무작위 또는 흔한 비밀번호 목록으로 계속 대입하는 것이고, Credential Stuffing은 이미 다른 서비스에서 유출된 계정 또는 비밀번호 조합을 재활용해서 시도합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;공격 특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공격 경로: 로그인 API&lt;/li&gt;
&lt;li&gt;목표: 계정 탈취&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방어 전략&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인 시도 횟수 제한 및 지연 적용&lt;/li&gt;
&lt;li&gt;CAPTCHA 도입 &lt;i&gt;(사람인가요?)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;MFA(이중 인증) 적용&lt;/li&gt;
&lt;li&gt;유출 비밀번호 차단 정책 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchX0BJ%2FbtsALWnWIgA%2FXMkMDnLKRussvcLggSXEq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;367&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/네트워크</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/429</guid>
      <comments>https://engineerinsight.tistory.com/429#entry429comment</comments>
      <pubDate>Fri, 12 Sep 2025 14:00:16 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] 인증을 구현하는 방법들: 클라이언트는 쿠키 이외에 인증 정보를 어디에 저장할 수 있을까?</title>
      <link>https://engineerinsight.tistory.com/428</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;인증을 구현하는 방법들&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 쿠키 기반 인증&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠키 = 서버가 클라이언트를 구분하기 위해 발급하는 작은 데이터 조각&lt;/li&gt;
&lt;li&gt;클라이언트 브라우저에 저장되어 이후 &lt;b&gt;요청마다 자동으로 함께 전송&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;보통 서버에서 &lt;b&gt;Set-Cookie&lt;/b&gt; 응답 헤더로 내려주고, 브라우저는 이후 요청 시 &lt;b&gt;Cookie&lt;/b&gt; 헤더로 전송&lt;/li&gt;
&lt;li&gt;단순히 상태를 기억하는 용도(장바구니에 넣은 물건을 기억한다던가,,)로도 쓰이지만, 보안 토큰이나 세션 ID 전달에도 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키 기반 인증은 초창기 웹에서 가장 많이 쓰였던 방식입니다. 다만 브라우저에 그대로 저장되다 보니 탈취 위험이 있고, 용량이 제한적이며, 보통 도메인 단위로만 사용이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&amp;lsquo;쿠키 기반&amp;rsquo;이라는 것은 그냥 데이터를 서버로 전달하는 매개체가 쿠키라는 뜻이지, 실제 저장하는 로그인 정보가 서버이면 세션 기반, 모든 정보를 클라이언트가 가지고 있게 되면 JWT 기반으로 나뉘게 됩니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;또 실제로는 *&lt;/i&gt;&amp;ldquo;쿠키 기반 인증&amp;rdquo;이라는 표현이 곧 세션 기반 인증을 지칭하는 경우가 많습니다.***&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 세션 기반 인증&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 클라이언트의 로그인 상태(로그인 시각, user id 등등,,)를 서버 메모리나 외부 저장소(Redis, DB 등)에 저장하고, 그 상태의 식별자인 id만 클라이언트에 반환&lt;/li&gt;
&lt;li&gt;클라이언트는 쿠키에 세션 ID만 담아서 전송&lt;/li&gt;
&lt;li&gt;서버는 세션 ID를 키로 삼아 저장된 로그인 상태를 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 기반 인증은 쿠키 단독 방식보다 안전합니다. 클라이언트에는 단순한 세션 키만 남기고, 실제 민감한 정보는 서버 쪽에만 저장되기 때문입니다. 하지만 모든 요청마다 서버가 상태를 조회해야 하므로 &lt;b&gt;Stateless 원칙&lt;/b&gt;과 맞지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 세션 정보를 메모리나 저장소에 들고 있기 때문에, 요청이 항상 같은 서버로 가야 하는 Sticky Session 문제가 생깁니다. 이를 해결하려면 Redis 같은 중앙 저장소를 두거나 JWT처럼 Stateless 인증을 쓰는 게 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ JWT (JSON Web Token)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 클라이언트에게 &lt;b&gt;서명된 토큰&lt;/b&gt;을 발급&lt;/li&gt;
&lt;li&gt;토큰은 세 부분(헤더, 페이로드, 서명)으로 구성&lt;/li&gt;
&lt;li&gt;클라이언트는 이후 요청마다 Authorization 헤더에 토큰을 담아 전송&lt;/li&gt;
&lt;li&gt;서버는 서명 검증을 통해 토큰의 위&amp;middot;변조 여부를 확인하고, 별도의 세션 저장소 없이 클라이언트 인증 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 세션과 달리 서버가 상태를 저장하지 않아도 됩니다. 즉, 완전히 &lt;b&gt;Stateless 인증&lt;/b&gt;을 구현할 수 있다는 장점이 있습니다. 하지만 토큰이 클라이언트에 그대로 노출되기 때문에 탈취되면 큰 위험이 있고, 토큰이 만료되기 전까지는 강제로 무효화하기가 어렵습니다. 그래서 대규모 서비스에서는 보통 블랙리스트 테이블이나 리프레시 토큰 전략을 함께 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰에 들어가는 정보는 사용자 ID, 권한, 만료 시각 등 인증&amp;middot;인가 관련 클레임(claim) 등이고, 비밀번호 같은 민감한 건 안 넣습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ OAuth&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 직접 아이디와 비밀번호를 입력하지 않고, 다른 서비스(구글, 카카오 등)의 인증을 빌려오는 방식&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리소스 소유자(사용자), 클라이언트(앱), 인증 서버, 자원 서버&lt;/b&gt; 네 가지 주체가 존재&lt;/li&gt;
&lt;li&gt;대표 흐름은 &lt;b&gt;Authorization Code Grant&lt;/b&gt; 방식
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 로그인 버튼 클릭 &amp;rarr; 인증 서버로 이동&lt;/li&gt;
&lt;li&gt;인증 서버에서 로그인 후, 클라이언트에 Authorization Code 반환&lt;/li&gt;
&lt;li&gt;클라이언트가 이 코드를 인증 서버에 제출해 Access Token 발급&lt;/li&gt;
&lt;li&gt;Access Token으로 자원 서버(API)에 접근&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth는 흔히 &lt;b&gt;소셜 로그인&lt;/b&gt;에서 볼 수 있습니다. 사용자가 &amp;ldquo;구글로 로그인&amp;rdquo;을 누르면 구글이 대신 인증을 수행하고, 서비스는 구글이 발급한 토큰을 활용해 사용자 정보를 가져옵니다. 이렇게 하면 서비스가 사용자의 비밀번호를 직접 다루지 않아도 되기 때문에 보안이 강화됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  클라이언트는 쿠키 이외에 로그인 정보를 어디에 저장할 수 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 분들이 &quot;쿠키 기반 인증&quot;, &quot;세션 기반 인증&quot;, &quot;JWT 기반 인증&quot;을 헷갈리곤 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 &lt;b&gt;쿠키는 단순히 서버와 클라이언트 간 데이터를 전달하는 매개체일 뿐&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키 이외에도 다양한 곳에 로그인 정보를 저장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세션 기반 인증&lt;/b&gt;에서는 로그인 정보를 서버가 메모리나 DB, Redis 같은 저장소에 보관하고, 클라이언트에는 세션 ID만 발급합니다. 이 세션 ID는 로그인 정보를 포함하고 있지 않기 떄문에 토큰에 비해서 빡세게 관리할 필요는 없습니다. (문제 생기면 그냥 서버가 해당 세션을 무효화하면 되기 땜시) 따라서 세션 아이디는 보통 쿠키에 담겨서 자동으로 서버로 전송됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 JWT 기반 인증에서는 로그인 정보가 토큰 안에 직접 들어 있고, 클라이언트가 토큰을 들고 다니며 서버에 증명하기 때문에 해당 정보를 어디에 보관하냐는 보안상 조금이 아니라 많이 중요해질 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 쿠키에 저장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;쿠키&lt;/b&gt;는 브라우저에서 관리되며, 기본적으로 같은 도메인으로 요청을 보낼 때마다 자동으로 서버로 전송됩니다. 이 특성 덕분에 인증 흐름이 단순해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;&lt;code&gt;CSRF&lt;/code&gt;&lt;/b&gt;(Cross-Site Request Forgery)에 취약합니다. 공격자가 사용자의 브라우저를 이용해 강제로 요청을 보내면, 브라우저는 쿠키를 자동으로 포함하기 때문입니다. 다만 &lt;code&gt;HttpOnly&lt;/code&gt; 옵션을 설정하면 자바스크립트에서 쿠키에 접근할 수 없어 XSS(Cross-Site Scripting) 공격에는 비교적 안전해집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;LocalStorage,&lt;/b&gt; &lt;b&gt;SessionStorage&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LocalStorage&lt;/b&gt;와 &lt;b&gt;SessionStorage&lt;/b&gt;는 브라우저 내 저장소입니다. 둘 다 서버로 자동 전송되지는 않기 때문에 토큰을 여기에 저장하면 매 요청마다 자바스크립트 코드에서 꺼내 &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; 형태로 직접 실어 보내야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 요청에 자동으로 토큰 정보가 포함되지는 않기 때문에 CSRF에는 안전합니다. 하지만 JS로 접근이 가능하기 때문에 &lt;b&gt;&lt;code&gt;XSS 공격&lt;/code&gt;&lt;/b&gt;에 취약합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 여기서 두 가지의 차이점은 &lt;b&gt;&lt;i&gt;LocalStorage는 브라우저를 닫아도 남아 있고, SessionStorage는 탭이나 창을 닫으면 사라진다&lt;/i&gt;&lt;/b&gt;는 것입니다. 또 &lt;b&gt;&lt;i&gt;LocalStorage에 토큰을 두면 같은 브라우저의 모든 탭에서 공유되지만, SessionStorage는 탭마다 분리&lt;/i&gt;&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 메모리(자바스크립트 변수)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리(자바스크립트 변수)에 토큰을 보관할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 페이지 새로고침이나 브라우저를 닫으면 바로 사라지기 때문에 보안상 유출 위험은 가장 적지만, UX 측면에서 사용자가 페이지를 새로고침할 때마다 로그인이 풀리는 불편함이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ 어디에 저장해야 할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 저장 방식을 섞어 사용해 이점만 취해볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;b&gt;Access Token은 LocalStorage나 메모리에 짧게 저장&lt;/b&gt;해 쓰고, &lt;b&gt;Refresh Token은 HttpOnly 쿠키에 보관&lt;/b&gt;하는 식입니다. 이렇게 하면 토큰 탈취 위험을 줄이면서도 자동 갱신을 통해 사용자는 로그인 상태를 유지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access Token은 유효 기간이 짧습니다. 보통 몇 분에서 길어야 한두 시간 정도만 쓸 수 있게 발급합니다. (이 시간은 서버에서 결정합니다) 이 토큰은 클라이언트가 API를 호출할 때 Authorization 헤더에 붙여서 서버에 보냅니다. 이렇게 짧게 설정하는 이유는, 혹시 토큰이 유출되더라도 피해를 최소화하기 위해서입니다. Access Token은 브라우저의 LocalStorage나 자바스크립트 메모리에 저장하는 경우가 많습니다. 자동으로 서버에 전송되지 않기 때문에 CSRF 공격에는 안전하지만, 자바스크립트에서 접근할 수 있기 때문에 XSS 공격에는 취약합니다. 그래서 보통은 유효 기간을 짧게 두는 식으로 리스크를 줄입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Refresh Token은 만료 시간이 길고, 새 Access Token을 계속 발급받을 수 있는 &amp;ldquo;열쇠 같은 권한&amp;rdquo;을 갖고 있습니다. 만약 해커가 이걸 빼돌린다면, Access Token이 만료되어도 다시 Refresh Token으로 새로운 Access Token을 받아낼 수 있습니다. 결국 사용자의 계정이 장기간 완전히 털릴 수 있는 위험이 생기게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Refresh Token은 보통 &lt;b&gt;HttpOnly 쿠키&lt;/b&gt;에 보관합니다. HttpOnly 쿠키는 LocalStorage나 SessionStorage처럼 자바스크립트에서 직접 접근 가능한 공간에 두는 것보다 &lt;b&gt;XSS 공격에 훨씬 안전하기 때문&lt;/b&gt;입니다. HttpOnly 옵션이 걸린 쿠키는 자바스크립트 코드에서 읽을 수 없기 때문에, 설령 브라우저에 악성 스크립트가 심어져도 토큰을 훔쳐갈 수 없습니다. 물론 쿠키 특성상 CSRF 공격 위험이 있으므로, &lt;code&gt;SameSite&lt;/code&gt; 옵션을 걸어두는 것이 일반적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ Refresh Token 좀더 안전하게 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token은 탈취되면 피해가 매우 크기 때문에 단순히 HttpOnly 쿠키에 넣는 것만으로는 완벽하지 않습니다. 따라서 &lt;b&gt;추가적인 검증 장치&lt;/b&gt;가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 환경 검증&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 서버가 Refresh Token을 검증할 때, 토큰 자체뿐 아니라 발급 당시의 &lt;b&gt;IP 주소나 User-Agent 정보와 함께 비교&lt;/b&gt;해 일치하지 않으면 거부하는 방식입니다. 이렇게 하면 토큰이 외부로 유출되어도 다른 환경에서는 쉽게 사용할 수 없게 막을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 토큰 회전 (Token Rotation)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 중요한 방법은 &lt;b&gt;토큰 회전(Token Rotation)&lt;/b&gt; 전략입니다. 클라이언트가 Refresh Token으로 새 Access Token을 발급받을 때마다 서버가 새로운 Refresh Token을 발급하고, 이전 토큰은 즉시 폐기하는 방식입니다. 이렇게 하면 공격자가 Refresh Token을 탈취해도, 사용자가 이미 새 토큰을 발급받았다면 탈취한 토큰은 무효화되어 사용할 수 없게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchX0BJ%2FbtsALWnWIgA%2FXMkMDnLKRussvcLggSXEq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;367&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/네트워크</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/428</guid>
      <comments>https://engineerinsight.tistory.com/428#entry428comment</comments>
      <pubDate>Thu, 11 Sep 2025 10:00:54 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] HTTP Header: Virtual Hosting, 컨텐츠 협상, Accept의 우선순위, Authorization, Cache-Control</title>
      <link>https://engineerinsight.tistory.com/427</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  HTTP Header 주요 필드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Host&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 요청을 보낼 때 &lt;b&gt;도메인 이름&lt;/b&gt;을 명시하는 헤더&lt;/li&gt;
&lt;li&gt;하나의 IP 주소에서 여러 도메인을 운영하는 &lt;b&gt;가상 호스팅(Virtual Hosting)&lt;/b&gt; 환경에서 필수적&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP/1.1부터 Host 헤더가 필수&lt;/b&gt;가 됨 &lt;i&gt;(HTTP/1.0까지는 선택이었음)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Host: www.example.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ Virtual Hosting이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 서버(즉, 하나의 IP 주소)에서 여러 웹사이트를 동시에 운영하는 환경을 가상 호스팅(Virtual Hosting)이라고 하는데, 이때 Host 헤더가 필수적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 같은 서버에서 &lt;code&gt;www.cafe.com&lt;/code&gt;, &lt;code&gt;www.shop.com&lt;/code&gt;, &lt;code&gt;www.blog.com&lt;/code&gt; 세 사이트를 운영한다면, 서버 입장에서는 단순히 IP만 보고는 요청이 어느 사이트를 위한 것인지 알 수 없습니다. 그래서 클라이언트가 요청을 보낼 때 &lt;code&gt;Host: www.shop.com&lt;/code&gt;이라는 헤더를 함께 전달하면, 서버는 이를 보고 &amp;ldquo;아, 이건 쇼핑몰 사이트 요청이구나&amp;rdquo;라고 구분해 올바른 자원으로 라우팅할 수 있습니다. 결국 Host 헤더는 &lt;b&gt;&lt;i&gt;하나의 서버에서 여러 도메인을 동시에 서비스&lt;/i&gt;&lt;/b&gt;할 수 있도록 만들어주는 핵심적인 장치입니당&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Content-Type&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청이나 응답 본문이 어떤 형식(MIME Type)인지 알려주는 헤더
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청에서 쓰이면 클라이언트가 서버로 보내는 데이터의 형식 지정 (주로 POST 메서드 같은 것)&lt;/li&gt;
&lt;li&gt;응답에서 쓰이면 서버가 클라이언트로 돌려줄 데이터 형식 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서버와 클라이언트가 데이터를 올바르게 해석할 수 있도록 도와줌&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자주 쓰이는 값&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;application/json&lt;/code&gt; &amp;rarr; JSON 데이터&lt;/li&gt;
&lt;li&gt;&lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt; &amp;rarr; HTML 폼 데이터 기본 형식&lt;/li&gt;
&lt;li&gt;&lt;code&gt;multipart/form-data&lt;/code&gt; &amp;rarr; 파일 업로드 시 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Content-Type: application/json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Content-Type&lt;/code&gt;은 &lt;b&gt;본문이 있을 때 반드시 필요&lt;/b&gt;한 헤더는 아니지만, 없으면 클라이언트/서버가 데이터를 해석할 수 없습니다. 따라서 POST/PUT/PATCH 요청에서 본문을 보낼 때는 사실상 필수적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Accept&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 서버에게 &amp;ldquo;나는 이런 형식의 응답을 원해요&amp;rdquo;라고 알리는 헤더 &lt;i&gt;(요청 헤더로만 보통 쓰임)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;서버는 이 값을 참고해서 응답 콘텐츠 타입을 결정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Accept: application/json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Accept: text/html&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ 컨텐츠 협상 (Content Negotiation)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹에서 클라이언트와 서버가 데이터를 주고받을 때는 단순히 요청과 응답만 있는 게 아니라, &lt;b&gt;서로 어떤 형식으로 데이터를 주고받을지 협상하는 과정&lt;/b&gt;이 있습니다. = Content Negotiation&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 서버에 요청을 보낼 때 &lt;code&gt;Accept&lt;/code&gt; 헤더를 통해 &amp;ldquo;나는 이런 형식의 데이터를 선호한다&amp;rdquo;라고 알려줍니다. 예를 들어 &lt;code&gt;Accept: application/json&lt;/code&gt;이라고 보내면, 클라이언트는 JSON 데이터를 원한다는 뜻이 됩니다. 만약 브라우저라면 &lt;code&gt;Accept: text/html&lt;/code&gt;을 보내 HTML 페이지를 요청하기도 하고, API 클라이언트라면 JSON을 선호한다고 알려줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 이 정보를 보고, 자신이 제공할 수 있는 응답 형식 중 클라이언트가 원하는 것을 선택해 돌려줍니다. 즉, 같은 엔드포인트를 호출해도 클라이언트가 &lt;code&gt;Accept&lt;/code&gt;를 어떻게 설정했는지에 따라 응답이 달라질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 콘텐츠 협상은 &lt;b&gt;클라이언트와 서버가 서로 원하는 데이터 형식을 맞춰가는 대화 방식&lt;/b&gt;입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ Accept 우선순위 값 (q)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 &lt;code&gt;q&lt;/code&gt;라는 &lt;b&gt;우선순위&lt;/b&gt; 값을 붙여 &amp;ldquo;나는 JSON을 제일 원하지만, HTML도 괜찮음&amp;rdquo;이라고 표현할 수도 있습니다. 예를 들어 &lt;code&gt;Accept: application/json; q=0.9, text/html; q=0.8&lt;/code&gt;이라면 서버는 JSON을 우선적으로 주되, JSON이 안 되면 HTML로 응답해도 괜찮다는 의미입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Authorization&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 서버에 자신을 증명할 때 사용하는 인증 정보 헤더&lt;/li&gt;
&lt;li&gt;&lt;b&gt;형식&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Authorization: &amp;lt;타입&amp;gt; &amp;lt;자격 증명&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이때 타입에 따라 인증 방식이 달라집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Basic&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 단순한 방식. 아이디와 비밀번호를 &lt;code&gt;아이디:비밀번호&lt;/code&gt; 형태로 합쳐 Base64로 인코딩해 전송합니다.&lt;/li&gt;
&lt;li&gt;대놓고 비밀번호가 떠다니기 때문에 보안상 안전하지 않아 반드시 HTTPS와 함께 써야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Bearer&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰 기반 인증 방식. 서버가 발급한 토큰(JWT, OAuth 액세스 토큰 등)을 담아 요청을 보냅니다.&lt;/li&gt;
&lt;li&gt;서버는 이 토큰을 검증해 클라이언트가 유효한 사용자인지 판단합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API Key&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스에서 발급한 키를 Authorization 헤더나 쿼리스트링으로 전달합니다.&lt;/li&gt;
&lt;li&gt;API Key는 Authorization 헤더가 아니라 &lt;code&gt;x-api-key&lt;/code&gt; 같은 커스텀 헤더에 담는 경우도 많습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Cache-Control&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청이나 응답에 대해 &lt;b&gt;캐싱 정책&lt;/b&gt;을 지정하는 헤더&lt;/li&gt;
&lt;li&gt;브라우저, 프록시, CDN이 캐시를 어떻게 처리해야 하는지 알려줌&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 디렉티브&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;no-cache&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐싱 가능하지만 사용 전 반드시 서버 검증 필요&lt;/li&gt;
&lt;li&gt;다시 사용할 때는 서버에 &lt;code&gt;ETag&lt;/code&gt;나 &lt;code&gt;Last-Modified&lt;/code&gt; 등을 확인한 후 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no-store&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아예 &lt;b&gt;캐싱 금지&lt;/b&gt;입니다. 로컬 디스크나 메모리에 저장조차 하면 안 됩니다.&lt;/li&gt;
&lt;li&gt;금융 정보, 개인정보처럼 민감한 데이터를 다룰 때 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max-age=60&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;60초 동안 캐시 유효&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Cache-Control: no-store&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cache-Control: max-age=3600&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchX0BJ%2FbtsALWnWIgA%2FXMkMDnLKRussvcLggSXEq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;367&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/네트워크</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/427</guid>
      <comments>https://engineerinsight.tistory.com/427#entry427comment</comments>
      <pubDate>Wed, 10 Sep 2025 17:00:45 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] Stateless &amp;amp; Connectionless: 서버는 왜 Stateless? HTTP/1.1부터 왜 Persistent Connection?</title>
      <link>https://engineerinsight.tistory.com/425</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Stateless &amp;amp; Connectionless&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Stateless&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 클라이언트의 상태 정보를 &lt;b&gt;유지하지 않는 특성&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;각 요청은 &lt;b&gt;독립적인 트랜잭션&lt;/b&gt;으로 취급, 이전 요청의 맥락을 알지 못함&lt;/li&gt;
&lt;li&gt;클라이언트가 매 요청마다 필요한 모든 정보를 담아서 전달해야 함&lt;/li&gt;
&lt;li&gt;HTTP 기본 동작이 Stateless하기 때문에 세션(Session)이나 JWT를 통해 상태 유지를 별도로 구현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 확장성 향상, 부하 분산 용이&lt;/li&gt;
&lt;li&gt;장애 복구 및 로드 밸런싱에 유리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 클라이언트라 하더라도 요청마다 인증 정보나 세션 토큰 등을 포함해야 함&lt;/li&gt;
&lt;li&gt;상태를 유지하려면 별도의 저장소(Redis, DB 등)에 위임 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q. HTTP는 왜 Stateless라고 하나요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP는 각 요청을 독립적으로 처리하고, 서버가 클라이언트의 이전 상태를 기억하지 않기 때문에 Stateless라고 합니다. 그래서 클라이언트는 매 요청마다 인증 정보나 필요한 데이터를 함께 전달해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q. REST 아키텍처 원칙에서 Stateless가 왜 중요한가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 상태를 기억하지 않으면 어떤 서버가 요청을 받아도 같은 결과를 줄 수 있습니다. 그래서 서버 확장성과 로드 밸런싱에 유리하고, 장애 복구도 간단해집니다. 이게 REST의 핵심 원칙 중 하나입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 서버가 상태를 저장하면 같은 클라이언트 요청이 항상 같은 서버로 가야 해서 Sticky Session 문제가 생깁니다. 확장성이 떨어지고 장애 복구도 어려워집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Connectionless&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트와 서버가 &lt;b&gt;연결 상태를 유지하지 않는 특성&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;요청을 보낸 후 응답을 받으면 연결을 끊음&lt;/li&gt;
&lt;li&gt;요청과 응답이 끝날 때마다 &lt;b&gt;새로운 연결이 생성되고 종료&lt;/b&gt;됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불필요한 연결 유지 비용 절감&lt;/li&gt;
&lt;li&gt;다수의 클라이언트를 동시에 처리하기 유리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연결 재사용 불가, 매번 핸드셰이크 비용 발생&lt;/li&gt;
&lt;li&gt;실시간 통신에는 비효율적&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UDP는 대표적인 Connectionless 프로토콜&lt;/li&gt;
&lt;li&gt;TCP는 Connection-oriented라 대비됨&lt;/li&gt;
&lt;li&gt;HTTP/1.0은 Connectionless 기반이었으나, HTTP/1.1부터는 Keep-Alive로 연결 유지 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q. Connectionless인 HTTP/1.0이 왜 HTTP/1.1부터 Persistent Connection으로 바뀌었나요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.0은 요청마다 TCP 연결을 새로 열고 닫아야 해서 오버헤드가 컸습니다. 그래서 HTTP/1.1부터는 Keep-Alive를 기본으로 해서 연결을 재사용하도록 바꿨고, 덕분에 성능이 크게 개선됐습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TCP Handshake 비용 감소&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매번 3-way handshake를 하지 않아도 되므로 지연 시간 줄어듦&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 자원 절약&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연결 생성/종료에 필요한 CPU, 메모리, 포트 소모 줄어듦&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 향상&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 연결에서 여러 리소스(HTML, CSS, JS, 이미지 등)를 순차적으로 빠르게 가져올 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연결을 오래 유지하면 서버 리소스를 계속 점유 &amp;rarr; 동시 접속자 많을 때 부하 가능&lt;/li&gt;
&lt;li&gt;유휴 연결을 끊지 않으면 리소스 낭비 &amp;rarr; &lt;code&gt;Keep-Alive: timeout=5, max=100&lt;/code&gt; 같은 식으로 제한 설정 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchX0BJ%2FbtsALWnWIgA%2FXMkMDnLKRussvcLggSXEq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;367&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/네트워크</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/425</guid>
      <comments>https://engineerinsight.tistory.com/425#entry425comment</comments>
      <pubDate>Wed, 10 Sep 2025 14:00:41 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] HTTP Method: 멱등성과 안전성, 메서드와 캐싱의 관계</title>
      <link>https://engineerinsight.tistory.com/424</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&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;787&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5OxDQ/btsQqZPV2Do/Uklsk7A3fiwFkwimk8E5j0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5OxDQ/btsQqZPV2Do/Uklsk7A3fiwFkwimk8E5j0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5OxDQ/btsQqZPV2Do/Uklsk7A3fiwFkwimk8E5j0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5OxDQ%2FbtsQqZPV2Do%2FUklsk7A3fiwFkwimk8E5j0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;589&quot; height=&quot;362&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;787&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  HTTP Method&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ GET&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에서 데이터를 조회(Read)할 때 사용하는 메서드&lt;/li&gt;
&lt;li&gt;요청 데이터는 주로 쿼리스트링(Query String)으로 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;URL에 노출됨 &amp;rarr; 북마크 가능, 캐싱 가능&lt;/li&gt;
&lt;li&gt;보통 서버 상태를 바꾸지 않음 (멱등성 O)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;GET /users?id=1&lt;/code&gt; &amp;rarr; id=1인 사용자 정보 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ POST&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에 새로운 리소스를 생성(Create)할 때 사용&lt;/li&gt;
&lt;li&gt;요청 데이터는 &lt;b&gt;Request Body&lt;/b&gt;에 담겨 전송&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐싱 불가, URL에 보이지 않음&lt;/li&gt;
&lt;li&gt;멱등성 X (POST 요청을 여러 번 하면 리소스가 계속 생김)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;POST /users&lt;/code&gt; &amp;rarr; 새로운 사용자 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ PUT&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버의 리소스를 전체 교체(Update)할 때 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 시 해당 리소스 전체를 덮어씀&lt;/li&gt;
&lt;li&gt;멱등성 O (같은 PUT 요청을 여러 번 보내도 결과는 동일)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;PUT/users/1&lt;/code&gt; &amp;rarr; id=1인 사용자의 전체 정보를 새로운 데이터로 교체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ PATCH&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 리소스의 일부만 수정(Update)할 때 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부분 변경에 최적화&lt;/li&gt;
&lt;li&gt;멱등성은 보장하지 않음 (패치 내용에 따라 다름)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;PATCH /users/1&lt;/code&gt; &amp;rarr; id=1인 사용자 이메일만 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PUT vs PATCH&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PUT은 리소스를 &lt;b&gt;전체 교체,&lt;/b&gt; PATCH는 &lt;b&gt;부분 수정&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;REST API 설계 시, 필드 몇 개만 바꾸려는데 PUT을 쓰면 불필요한 데이터까지 전송해야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ DELETE&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버의 리소스를 삭제(Delete)할 때 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멱등성 O (여러 번 삭제 요청해도 결과는 동일)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;DELETE /users/1&lt;/code&gt; &amp;rarr; id=1인 사용자 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ OPTIONS&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 지원하는 &lt;b&gt;메서드와 CORS 정책&lt;/b&gt;을 확인할 때 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 리소스를 가져오지 않고 &amp;ldquo;&lt;b&gt;&lt;i&gt;사전 요청&lt;/i&gt;&lt;/b&gt;(Preflight Request)&amp;rdquo;으로 사용&lt;/li&gt;
&lt;li&gt;특히 브라우저에서 CORS 처리 시 자주 등장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;OPTIONS /users&lt;/code&gt; &amp;rarr; 이 엔드포인트가 GET, POST, DELETE 중 어떤 걸 지원하는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기부터는 자주 쓰이지는 않는 Method들입니당&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ HEAD&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET과 거의 동일하지만 &lt;b&gt;Response Body는 제외&lt;/b&gt;하고 &lt;b&gt;헤더만 응답&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안전(Safe)하고 멱등(Idempotent)합니다.&lt;/li&gt;
&lt;li&gt;트래픽 절약 효과가 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;활용 예시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스가 존재하는지 빠르게 확인할 때&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Content-Length&lt;/code&gt; 헤더를 보고 파일 용량 확인 후 다운로드 여부 결정&lt;/li&gt;
&lt;li&gt;캐싱 유효성 확인 (&lt;code&gt;Last-Modified&lt;/code&gt;, &lt;code&gt;ETag&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ TRACE&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 보낸 요청을 서버가 그대로 반사(Echo)해서 돌려주는 메서드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;본래 &lt;b&gt;디버깅용&lt;/b&gt;이라 일반 서비스 API에서는 거의 쓰이지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안상 위험&lt;/b&gt;(XSS, 정보 노출 등) 때문에 대부분 서버에서 차단되어 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;네트워크 경로를 따라가며 프록시, 게이트웨이, 방화벽이 요청을 어떻게 변형했는지 디버깅할 때 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  멱등성(Idempotency) &amp;amp; 안전성(Safety)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;안전성(Safe)&lt;/b&gt;: 서버의 상태를 변경하지 않음 (예: GET, HEAD, OPTIONS)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;멱등성(Idempotent)&lt;/b&gt;: 같은 요청을 여러 번 보내도 결과가 변하지 않음 (예: GET, PUT, DELETE)&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST가 멱등하지 않은 이유: POST는 호출할 때마다 &lt;b&gt;새로운 리소스를 생성&lt;/b&gt;하기 때문에 같은 POST 요청을 여러 번 보내면 데이터가 계속 추가되므로 결과가 달라짐&lt;/li&gt;
&lt;li&gt;DELETE는 멱등한데, 안전하지는 않은 이유: DELETE는 같은 요청을 여러 번 보내도 결과는 동일(리소스가 삭제됨)하므로 멱등성은 있지만 서버의 상태를 바꾸는 동작(리소스 삭제)이므로 &lt;b&gt;안전하지는 않음&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  메서드와 캐싱의 관계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDN(Content Delivery Network)이나 Reverse Proxy 서버(Nginx, Varnish 등)는 클라이언트와 서버 사이에서 응답을 캐싱해주지만, 모든 HTTP Method의 응답을 똑같이 캐싱하지는 않고 &lt;b&gt;메서드별로 다르게 취급&lt;/b&gt;합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET은 &lt;b&gt;기본적으로 캐싱 가능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지, CSS, JS 같은 정적 파일은 CDN이 GET 응답을 캐싱해서 다음 요청 때 빠르게 제공합니다.&lt;/li&gt;
&lt;li&gt;브라우저나 프록시는 &lt;code&gt;Cache-Control&lt;/code&gt;, &lt;code&gt;Expires&lt;/code&gt;, &lt;code&gt;ETag&lt;/code&gt; 같은 헤더를 기준으로 캐싱 여부를 결정합니다.&lt;/li&gt;
&lt;li&gt;cf. HEAD도 GET과 동일하게 캐싱하지만, Response Body는 없으니 헤더 정보만 캐싱&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;POST, PUT, DELETE, PATCH는 원칙적으로 캐싱 안함
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 상태를 바꾸는 성격이 있어 캐싱이 위험하기 때문 (주문이 반복 처리된다던가,,)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchX0BJ%2FbtsALWnWIgA%2FXMkMDnLKRussvcLggSXEq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;367&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/네트워크</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/424</guid>
      <comments>https://engineerinsight.tistory.com/424#entry424comment</comments>
      <pubDate>Wed, 10 Sep 2025 10:00:18 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] HTTP/1.1 &amp;rarr; HTTP/2 &amp;rarr; HTTP/3</title>
      <link>https://engineerinsight.tistory.com/423</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtUCL2/btsQqrGb7xM/yNxKcHuIhkmBVka5aAgvGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtUCL2/btsQqrGb7xM/yNxKcHuIhkmBVka5aAgvGk/img.png&quot; data-alt=&quot;이미지 출처:&amp;amp;nbsp; https://dev.to/saikumar2121/http-11-vs-2-vs-3-370f&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtUCL2/btsQqrGb7xM/yNxKcHuIhkmBVka5aAgvGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtUCL2%2FbtsQqrGb7xM%2FyNxKcHuIhkmBVka5aAgvGk%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;875&quot; height=&quot;438&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 출처:&amp;nbsp; https://dev.to/saikumar2121/http-11-vs-2-vs-3-370f&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;  HTTP/1.1 &amp;rarr; HTTP/2 &amp;rarr; HTTP/3&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ HTTP/1.0&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 웹이 퍼질 때 만들어진 규칙으로 가장 간단함&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 &lt;b&gt;한 연결당 한 요청-응답만 처리 후 연결 종료 (Non-persistent)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청마다 TCP 연결을 새로 맺어야 했기 때문에 오버헤드가 매우 큼&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ HTTP/1.1&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Persistent Connection&lt;/b&gt;(Keep-Alive)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 Persistent Connection(Keep-Alive)이 &lt;b&gt;기본 동작&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 한 연결을 여러 요청/응답에 재사용할 수 있음&lt;/li&gt;
&lt;li&gt;여전히 &lt;b&gt;한 연결에서 요청은 순차적으로 처리&lt;/b&gt;되기 때문에, 앞 요청이 느리면 뒤 요청도 막히는 &lt;b&gt;HOL Blocking&lt;/b&gt; 문제가 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;cf. 병렬 처리를 위해 파이프라이닝(Pipelining)을 지원하긴 했지만, &lt;b&gt;응답 순서 문제(HOL Blocking)&lt;/b&gt; 때문에 거의 쓰이지 않았고, 브라우저는 동시에 여러 리소스를 가져오려고 보통 &lt;b&gt;6~8개의 병렬 TCP 연결&lt;/b&gt;을 열어 사용함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;HOL Blocking이란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에 있는 데이터가 막히면, 뒤에 오는 데이터도 같이 기다려야 하는 현상&lt;br /&gt;= 네트워크 전송에서 &lt;b&gt;앞부분 패킷의 지연/손실 때문에 뒤 데이터까지 지연되는 현상&lt;br /&gt;= TCP 기반 프로토콜(순서를 보장하는 프로토콜)의 구조적 한계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 &lt;b&gt;데이터를 순서대로&lt;/b&gt; 보내고, 순서대로 받아야 합니다. 만약 &lt;code&gt;패킷 #2&lt;/code&gt;가 네트워크에서 유실되면, 수신 측은 &lt;code&gt;#3, #4, #5&lt;/code&gt;가 이미 도착했어도 &lt;b&gt;&lt;code&gt;#2&lt;/code&gt;가 올 때까지 버퍼에 묵혀둡니다.&lt;/b&gt; 결과적으로 하나의 패킷 손실이 전체 데이터 흐름을 막아버리게 됩니다. TCP는 &lt;b&gt;신뢰성과 순서를 위해 성능을 희생&lt;/b&gt;하는 구조입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ HTTP/2 (Multiplexing)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서비스가 커지고, 성능이 중요해지면서 한계를 해결하려고 나온 게 HTTP/2&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;멀티플렉싱&lt;/code&gt; (Multiplexing)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 TCP 연결 위에서 여러 요청과 응답을 &lt;b&gt;동시에 병렬 처리&lt;/b&gt;할 수 있도록 스트림(Stream) 단위로 데이터를 분할 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;rarr; 한 연결에서 여러 요청을 &lt;b&gt;&lt;i&gt;동시에&lt;/i&gt;&lt;/b&gt; 보낼 수 있게 만든 것.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;헤더 압축&lt;/code&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP/1.1에서는 요청마다 쿠키, User-Agent 등 동일한 헤더 정보를 반복적으로 전송했습니다.&lt;/li&gt;
&lt;li&gt;HTTP/2는 중복되는 헤더를 테이블에 저장하고, 이후에는 짧은 인덱스만 참조하도록 해 네트워크 효율을 극대화했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;서버 푸시&lt;/code&gt;&lt;/b&gt; (Server Push)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 HTML을 요청하면, 서버가 의존 리소스(CSS, JS 등)를 클라이언트 요청 전에 미리 전송할 수 있게 되어, 초기 로딩 속도를 줄이고 사용자 경험을 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한계&lt;/b&gt;: 여전히 &lt;b&gt;&lt;i&gt;TCP 기반&lt;/i&gt;&lt;/b&gt;이라서, &lt;b&gt;패킷 하나가 유실되면 그 뒤에 오는 것도 같이 막히는 HOL Blocking&lt;/b&gt; 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ HTTP/3 (UDP + QUIC)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TCP를 버리고 &lt;code&gt;UDP 기반 QUIC&lt;/code&gt;&lt;/b&gt;으로 다시 만든 게 HTTP/3&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;HOL Blocking 문제 해결&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UDP의 특징인 &amp;ldquo;빠르지만 신뢰성이 없음&amp;rdquo; + QUIC이라는 프로토콜
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;QUIC 프로토콜 = 구글이 TCP가 하던 &lt;b&gt;재전송, 순서 보장, 암호화&lt;/b&gt; 같은 기능을 UDP 위에서 직접 구현함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;패킷 하나 잃어버려도 나머지는 그냥 흘러가기 때문에 그래서 끊김이 훨씬 줄어듦.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;0-RTT 연결 수립&lt;/b&gt;으로 지연 시간 단축&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Connection ID 기반 연결 관리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IP가 바뀌어도 세션이 끊기지 않음 &amp;rarr; 연결이 자주 끊기는 모바일 환경 최적화&lt;/li&gt;
&lt;li&gt;유튜브, 넷플릭스 같은 스트리밍 서비스에서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;QUIC 프로토콜이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글이 개발한 &lt;b&gt;UDP 기반 전송 계층 프로토콜&lt;/b&gt;로, TCP가 가진 한계인 HOL Blocking 문제를 해결하기 위해 개발&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchX0BJ%2FbtsALWnWIgA%2FXMkMDnLKRussvcLggSXEq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;367&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/네트워크</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/423</guid>
      <comments>https://engineerinsight.tistory.com/423#entry423comment</comments>
      <pubDate>Tue, 9 Sep 2025 17:00:14 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] TCP vs UDP: 특징과 차이</title>
      <link>https://engineerinsight.tistory.com/422</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  TCP vs UDP&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AW9UR/btsQqInlTic/K7qVfiKWxrCTYkOQ7lJlTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AW9UR/btsQqInlTic/K7qVfiKWxrCTYkOQ7lJlTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AW9UR/btsQqInlTic/K7qVfiKWxrCTYkOQ7lJlTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAW9UR%2FbtsQqInlTic%2FK7qVfiKWxrCTYkOQ7lJlTK%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;646&quot; height=&quot;224&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;224&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ TCP (Transmission Control Protocol)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;TCP는 연결형 프로토콜로 3-way handshake를 통해 신뢰성을 보장하며, 데이터 순서와 재전송을 관리합니다. 대신 속도가 느리고 오버헤드가 큰 편입니다.&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;연결형&lt;/code&gt; (Connection-oriented) &amp;rarr; &lt;b&gt;3-way handshake 필요&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;신뢰성 보장&lt;/b&gt;: 데이터 손실 시 &lt;b&gt;재전송&lt;/b&gt;, &lt;b&gt;순서 보장&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;흐름 제어(Flow control), 혼잡 제어(Congestion control) 지원&lt;/li&gt;
&lt;li&gt;속도는 비교적 느림&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단위&lt;/b&gt;: 세그먼트 (Segment)&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보내는 쪽, 받는 쪽에 '커넥션'을 유지함&lt;/li&gt;
&lt;li&gt;에러 체킹 등 이 프로토콜 쓰면 데이터가 믿을 만 함&amp;nbsp;&lt;/li&gt;
&lt;li&gt;데이터를 특정 순서로 보낼 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UDP에 비해서 느림 (동시에 처리할 수 있는 처리량이 적음)&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특히 File Transfer 초기에 엄청나게 느림&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일부 데이터가 없으면 로딩을 안함, 예를 들어서 웹 페이지에 모든 데이터가 도착하기 전에는 페이지 자체 로딩을 안함&amp;nbsp;&lt;/li&gt;
&lt;li&gt;네트워크 혼잡하면 더더더더 악화되어 더더 느려짐&lt;/li&gt;
&lt;li&gt;멀티캐스팅이나 broadcasting은 못함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어디에 사용하나?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신뢰가 중요하고, 일관성이 중요한 서비스, 예를 들어-&lt;/li&gt;
&lt;li&gt;웹 브라우징 (&lt;b&gt;&lt;code&gt;HTTP&lt;/code&gt;&lt;/b&gt;/HTTPS)&lt;/li&gt;
&lt;li&gt;이메일 (SMTP, IMAP, POP3)&lt;/li&gt;
&lt;li&gt;파일 전송 (FTP, SFTP)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ UDP (User Datagram Protocol)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;UDP는 비연결형으로 빠르고 가볍지만 신뢰성이 없어서, 스트리밍&amp;middot;게임&amp;middot;DNS 같은 실시간성이 중요한 서비스에 주로 사용됩니다.&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;비연결형&lt;/code&gt; (Connectionless) &amp;rarr; handshake 없음&lt;/li&gt;
&lt;li&gt;신뢰성 없음: 순서&amp;middot;재전송 보장 안 함&lt;/li&gt;
&lt;li&gt;오버헤드 적음 &amp;rarr; 빠른 전송 가능&lt;/li&gt;
&lt;li&gt;단순 구조, 실시간성에 유리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단위&lt;/b&gt;: 데이터그램 (Datagram)&lt;/li&gt;
&lt;li&gt;장점&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커넥션 없어서 빠름&amp;nbsp;&lt;/li&gt;
&lt;li&gt;broadcast, multicast 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제대로 전달되었는지 알 수가 없음&lt;/li&gt;
&lt;li&gt;에러 검출은 되지만, 복구는 안함 (그냥 버려버림)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;어디에 사용하나?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고속성, 실시간성이 중요한 서비스, 예를 들어-&lt;/li&gt;
&lt;li&gt;스트리밍 (영상/음성)&lt;/li&gt;
&lt;li&gt;온라인 게임&lt;/li&gt;
&lt;li&gt;DNS, DHCP&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ TCP vs UDP&lt;/h3&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;TCP&lt;/th&gt;
&lt;th&gt;UDP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;연결 방식&lt;/td&gt;
&lt;td&gt;연결형 (3-way handshake)&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;/tr&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;큼 (헤더 20바이트 이상)&lt;/td&gt;
&lt;td&gt;작음 (헤더 8바이트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용 예시&lt;/td&gt;
&lt;td&gt;웹, 메일, 파일 전송&lt;/td&gt;
&lt;td&gt;스트리밍, 게임, DNS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  TCP 과정을 간단히 보자면??&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 1. 연결 (Connection)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clSdaZ/btsQpCnDiDY/gfcPujLifc30fWRgOu1pkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clSdaZ/btsQpCnDiDY/gfcPujLifc30fWRgOu1pkk/img.png&quot; data-alt=&quot;이미지 출처: https://afteracademy.com/blog/what-is-a-tcp-3-way-handshake-process/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clSdaZ/btsQpCnDiDY/gfcPujLifc30fWRgOu1pkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclSdaZ%2FbtsQpCnDiDY%2FgfcPujLifc30fWRgOu1pkk%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;638&quot; height=&quot;290&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 출처: https://afteracademy.com/blog/what-is-a-tcp-3-way-handshake-process/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TCP&lt;/b&gt;: 3-way handshake 과정을 거쳐 연결을 확정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 &amp;rarr; 서버: &lt;b&gt;SYN&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;서버 &amp;rarr; 클라이언트: &lt;b&gt;SYN + ACK&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;클라이언트 &amp;rarr; 서버: &lt;b&gt;ACK&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&amp;rarr; 이 과정을 통해 양쪽이 데이터 송수신 준비가 되었음을 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UDP&lt;/b&gt;: 이런 과정 없음. &lt;b&gt;그냥 데이터그램을 던짐.&lt;/b&gt; (상대가 받을지 말지는 모름)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 2. 순서 보장&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TCP&lt;/b&gt;: 각 세그먼트에 &lt;code&gt;시퀀스 번호&lt;/code&gt;를 붙여서 수신자가 순서대로 조립
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중간에 하나라도 빠지면 재전송 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UDP&lt;/b&gt;: 순서 개념 없음. 받은 순서대로 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 3. 신뢰성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TCP&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;송신자는 데이터를 보내고 ACK(응답)를 기다림.&lt;/li&gt;
&lt;li&gt;일정 시간 내 ACK가 안 오면 &lt;b&gt;재전송&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;혼잡 시 전송 속도를 줄이는 &lt;b&gt;혼잡 제어 알고리즘&lt;/b&gt;(CUBIC, Reno 등)까지 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UDP&lt;/b&gt;: 재전송, 응답 확인 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 4. 흐름 제어&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TCP&lt;/b&gt;: 수신자가 처리 가능한 속도를 &lt;code&gt;윈도우 크기&lt;/code&gt;로 알려주고, 송신자가 그에 맞춰 전송 속도를 조절.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UDP&lt;/b&gt;: 없음. 보내는 쪽이 무제한으로 보낼 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 5. 헤더 크기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xzKco/btsQnPIomg7/t9VIi6gakZ0NwwWvQK52v1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xzKco/btsQnPIomg7/t9VIi6gakZ0NwwWvQK52v1/img.jpg&quot; data-alt=&quot;이미지 출처: https://afteracademy.com/blog/what-is-a-tcp-3-way-handshake-process/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xzKco/btsQnPIomg7/t9VIi6gakZ0NwwWvQK52v1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxzKco%2FbtsQnPIomg7%2Ft9VIi6gakZ0NwwWvQK52v1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;611&quot; height=&quot;458&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 출처: https://afteracademy.com/blog/what-is-a-tcp-3-way-handshake-process/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TCP&lt;/b&gt;: 헤더가 20바이트 이상. (시퀀스 번호, ACK 번호, 윈도우 크기, 플래그 등 많은 필드 포함) &amp;rArr; 오버헤드 &lt;i&gt;BIG BIG BIG&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UDP&lt;/b&gt;: 헤더가 8바이트. (소스 포트, 목적지 포트, 길이, 체크섬)만 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.avast.com/c-tcp-vs-udp-difference&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.avast.com/c-tcp-vs-udp-difference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchX0BJ%2FbtsALWnWIgA%2FXMkMDnLKRussvcLggSXEq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;367&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/네트워크</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/422</guid>
      <comments>https://engineerinsight.tistory.com/422#entry422comment</comments>
      <pubDate>Tue, 9 Sep 2025 14:00:32 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] 코테 기본기(4): 소수점 처리 완벽 정리</title>
      <link>https://engineerinsight.tistory.com/420</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 1. 반올림 (round)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소수 첫째 자리에서 반올림 &amp;rarr; 가장 가까운 정수&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Math.round(x)&lt;/code&gt; &amp;rarr; &lt;code&gt;long&lt;/code&gt; 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;System.out.println(Math.round(3.14)); // 3
System.out.println(Math.round(3.5));  // 4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소수 n자리 반올림&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;double x = 3.14159;
double r2 = Math.round(x * 100) / 100.0; // 소수 둘째 자리까지
System.out.println(r2); // 3.14&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 2. 올림 (ceil)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;항상 위쪽 정수&lt;/b&gt;로 올림&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Math.ceil(x)&lt;/code&gt; &amp;rarr; &lt;code&gt;double&lt;/code&gt; 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;System.out.println(Math.ceil(3.14)); // 4.0
System.out.println(Math.ceil(-3.14)); // -3.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소수 n자리 올림&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;double x = 3.14159;
double c2 = Math.ceil(x * 100) / 100.0; // 소수 둘째 자리 올림
System.out.println(c2); // 3.15&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 3. 내림 (floor)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;항상 아래쪽 정수&lt;/b&gt;로 내림&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Math.floor(x)&lt;/code&gt; &amp;rarr; &lt;code&gt;double&lt;/code&gt; 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;System.out.println(Math.floor(3.99));  // 3.0
System.out.println(Math.floor(-3.14)); // -4.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소수 n자리 내림&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;double x = 3.14159;
double f2 = Math.floor(x * 100) / 100.0; // 소수 둘째 자리 내림
System.out.println(f2); // 3.14&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 4. 출력 포맷팅과 함께 쓰기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;%.nf&lt;/code&gt;는 &lt;b&gt;반올림&lt;/b&gt;이 자동 적용됨&lt;/li&gt;
&lt;li&gt;올림/내림이 필요할 때는 &lt;code&gt;ceil&lt;/code&gt;, &lt;code&gt;floor&lt;/code&gt;로 값을 처리한 뒤 &lt;code&gt;printf&lt;/code&gt;로 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;double x = 3.14159;

System.out.printf(&quot;%.2f\n&quot;, x); // 3.14 (반올림)
System.out.printf(&quot;%.2f\n&quot;, Math.floor(x * 100) / 100.0); // 3.14 (내림)
System.out.printf(&quot;%.2f\n&quot;, Math.ceil(x * 100) / 100.0);  // 3.15 (올림)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchX0BJ%2FbtsALWnWIgA%2FXMkMDnLKRussvcLggSXEq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;367&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>언어+프레임워크/JAVA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/420</guid>
      <comments>https://engineerinsight.tistory.com/420#entry420comment</comments>
      <pubDate>Tue, 9 Sep 2025 10:00:40 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] OSI 7계층: 각 계층 별 특징, 장비, TCP/IP와 차이점?</title>
      <link>https://engineerinsight.tistory.com/421</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  OSI 7계층 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 큰 그림&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;목적&lt;/b&gt;: 네트워크 통신을 추상화하여 단계별로 나눈 표준 모델&lt;/li&gt;
&lt;li&gt;실제 네트워크 체계의 구조는 OSI 7계층으로 이루어져 있지는 않아서 참조모델임&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구조&lt;/b&gt;: 7계층 (상위 &amp;rarr; 하위)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Application (응용)&lt;/li&gt;
&lt;li&gt;Presentation (표현)&lt;/li&gt;
&lt;li&gt;Session (세션)&lt;/li&gt;
&lt;li&gt;Transport (전송)&lt;/li&gt;
&lt;li&gt;Network (네트워크)&lt;/li&gt;
&lt;li&gt;Data Link (데이터링크)&lt;/li&gt;
&lt;li&gt;Physical (물리)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vkNAS/btsQnJ2A8A4/squhkrUrBw31b4MGLSoZn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vkNAS/btsQnJ2A8A4/squhkrUrBw31b4MGLSoZn1/img.png&quot; data-alt=&quot;이미지 출처: https://www.cloudflare.com/ko-kr/learning/ddos/glossary/open-systems-interconnection-model-osi/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vkNAS/btsQnJ2A8A4/squhkrUrBw31b4MGLSoZn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvkNAS%2FbtsQnJ2A8A4%2FsquhkrUrBw31b4MGLSoZn1%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;2048&quot; height=&quot;1024&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 출처: https://www.cloudflare.com/ko-kr/learning/ddos/glossary/open-systems-interconnection-model-osi/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 각 계층별 특징&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1계층 Physical Layer (물리 계층)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 0/1 비트 신호를 실제 전송 매체(전선, 광섬유, 무선파)로 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단위&lt;/b&gt;: 비트(Bit)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장비&lt;/b&gt;: 허브, 리피터, 케이블&lt;/li&gt;
&lt;li&gt;신호 세기 약하면 통신 오류 발생, 케이블 불량 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2계층 Data Link Layer (데이터 링크 계층)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 물리적 전송에서 오류 제어, 프레임 단위 전송&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단위&lt;/b&gt;: 프레임(Frame)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주소체계&lt;/b&gt;: &lt;code&gt;MAC 주소&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로토콜&lt;/b&gt;: Ethernet, PPP, ARP(같은 네트워크 내에서 Broadcast)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장비&lt;/b&gt;: L2 스위치 (단순 유선 LAN 내부 간 통신에 쓰이는 정도), 브리지&lt;/li&gt;
&lt;li&gt;동일 LAN 내부 통신, MAC 충돌 문제, VLAN 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3계층 Network Layer (네트워크 계층)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 목적지까지 경로 설정 (라우팅)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단위&lt;/b&gt;: 패킷(Packet)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주소체계&lt;/b&gt;: &lt;code&gt;IP 주소&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로토콜&lt;/b&gt;: IP, ICMP, RIP, OSPF, BGP&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장비&lt;/b&gt;: 라우터, L3 스위치&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ping&lt;/code&gt;은 ICMP 활용함
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ICMP: TCP/IP 구현 시에 컴퓨터 사이 통신 상태를 확인하기 위한 프로토콜인데, IP 위에서 동작하지만 암튼 3계층으로 분류됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;라우팅 테이블 문제 시 경로 잘못 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4계층 Transport Layer (전송 계층)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 종단 간 통신 보장 (에러 복구, 흐름 제어)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단위&lt;/b&gt;: 세그먼트(TCP) / 데이터그램(UDP) &amp;rarr; &lt;i&gt;둘이 차이 중요!&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로토콜&lt;/b&gt;: TCP, UDP&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;: &lt;code&gt;포트 번호&lt;/code&gt; 사용 (예: 80 HTTP, 443 HTTPS)&lt;/li&gt;
&lt;li&gt;공부할 키워드: TCP 3-way handshake, UDP 기반 DNS/스트리밍 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5계층 Session Layer (세션 계층)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 세션 생성, 유지, 종료 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로토콜&lt;/b&gt;: NetBIOS, RPC, PPTP&lt;/li&gt;
&lt;li&gt;로그인 세션 관리, 영상 통화 연결/종료 시 세션 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6계층 Presentation Layer (표현 계층)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 데이터 형식/인코딩/암호화 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로토콜&lt;/b&gt;: &lt;code&gt;SSL/TLS&lt;/code&gt;, JPEG, MPEG&lt;/li&gt;
&lt;li&gt;HTTPS에서 TLS로 암호화, JSON &amp;harr; XML 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7계층 Application Layer (응용 계층)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 최종 사용자에게 서비스 제공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로토콜&lt;/b&gt;: HTTP, FTP, SMTP, DNS, SSH&lt;/li&gt;
&lt;li&gt;웹 브라우저의 HTTP 요청/응답, 이메일 송수신&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  OSI vs TCP/IP 모델&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ TCP/IP 4계층&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Application (HTTP, FTP, SMTP 등)&lt;/li&gt;
&lt;li&gt;Transport (TCP, UDP)&lt;/li&gt;
&lt;li&gt;Internet (IP, ICMP)&lt;/li&gt;
&lt;li&gt;Network Access (Ethernet, Wi-Fi)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;OSI 7계층&lt;/b&gt;을 4단계로 &lt;code&gt;단순화&lt;/code&gt;한 모델 (실제로는 주로 TCP/IP 사용)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[질문] TCP/IP 4계층과 OSI 7계층 차이는?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSI 7계층은 통신 과정을 세부적으로 7단계로 나눠 이론적 모델을 제공하고,&lt;br /&gt;TCP/IP는 실제 인터넷 프로토콜 스택으로 4계층으로 단순화되어 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rArr; OSI는 학습&amp;middot;개념용, TCP/IP는 실무 구현용 모델입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchX0BJ%2FbtsALWnWIgA%2FXMkMDnLKRussvcLggSXEq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;367&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터과학/네트워크</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/421</guid>
      <comments>https://engineerinsight.tistory.com/421#entry421comment</comments>
      <pubDate>Tue, 9 Sep 2025 10:00:15 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] 코테 기본기(3): 빠른 입출력 템플릿 (Scanner + System.out.println 쓰면 망해요!)</title>
      <link>https://engineerinsight.tistory.com/419</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩테스트에서는 알고리즘을 잘 짜는 것도 중요하지만, &lt;b&gt;입출력 속도&lt;/b&gt; 때문에 정답을 맞추고도 시간 초과(TLE)가 나는 경우가 자주 있습니다. 특히 자바는 &lt;code&gt;Scanner&lt;/code&gt;와 &lt;code&gt;System.out.println&lt;/code&gt;을 그대로 쓰면 위험합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이 포스팅은 &amp;lsquo;프로그래머스&amp;rsquo; 플랫폼을 통해서 시험을 본다면 전혀 볼 필요가 없습니다&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 왜 Scanner + println이 망할까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Scanner&lt;/code&gt;는 내부에서 정규식을 써서 입력을 파싱하기 때문에 &lt;b&gt;매우 느립니다&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;System.out.println&lt;/code&gt;은 출력할 때마다 버퍼를 비우기 때문에 &lt;b&gt;출력이 많으면 병목&lt;/b&gt;이 생깁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 입력이 수천~수만 줄 이상 들어오는 문제에서는 그대로 쓰면 시간 초과가 날 확률이 높습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로도 입출력만 바꿔도 통과하는 경우가 생길 정도입니다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 빠른 입력 (BufferedReader + StringTokenizer)&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        // 입력
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        int a = Integer.parseInt(st.nextToken());
        int b = Integer.parseInt(st.nextToken());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;BufferedReader&lt;/code&gt; &amp;rarr; 입력 전체를 버퍼에 모아두고 빠르게 읽음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StringTokenizer&lt;/code&gt; &amp;rarr; 공백 단위로 쪼개기 좋음 (split 대체)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 빠른 출력 (StringBuilder)&lt;/h3&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;StringBuilder sb = new StringBuilder();
for (int i = 1; i &amp;lt;= 5; i++) {
    sb.append(i).append(&quot;\n&quot;);
}
System.out.print(sb);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;println&lt;/code&gt;을 매번 쓰지 말고,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StringBuilder&lt;/code&gt;에 모아서 한 번에 출력하면 속도가 크게 개선됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchX0BJ%2FbtsALWnWIgA%2FXMkMDnLKRussvcLggSXEq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;367&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>언어+프레임워크/JAVA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/419</guid>
      <comments>https://engineerinsight.tistory.com/419#entry419comment</comments>
      <pubDate>Mon, 8 Sep 2025 10:00:50 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] 코테 기본기(2): 배열 to 리스트 변환 완벽 정리</title>
      <link>https://engineerinsight.tistory.com/418</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6529969659931599&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 코딩테스트에서 &lt;b&gt;배열을 리스트로 변환&lt;/b&gt;하는 경우가 종종 있습니다. 자바에서는 배열 타입(&lt;code&gt;int[]&lt;/code&gt;, &lt;code&gt;String[]&lt;/code&gt;, &lt;code&gt;Integer[]&lt;/code&gt; 등)에 따라 방법이 달라집니다. 이번 편은 중요도는 아주 높지는 않지만 모르면 당황스럽기 때문에 정리해보려고 합니다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  배열 &amp;rarr; 리스트 변환&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 1. 객체 배열 (Integer[], String[] 등)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 배열을 쓸 때는 &lt;code&gt;String&lt;/code&gt; 제외하고는 객체(boxed) 형태로 쓰지는 않기 때문에 &lt;code&gt;String[]&lt;/code&gt;의 경우에 이렇게 사용한다 정도로 생각하면 좋을 것 같습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;String[] arr = {&quot;apple&quot;, &quot;banana&quot;, &quot;cherry&quot;};

// 1) Arrays.asList()
List&amp;lt;String&amp;gt; list1 = Arrays.asList(arr);
// 고정 크기 리스트 (추가/삭제 불가)

// 2) ArrayList 생성자로 감싸기
List&amp;lt;String&amp;gt; list2 = new ArrayList&amp;lt;&amp;gt;(Arrays.asList(arr));
// 수정 가능 (추가/삭제 가능)

// 3) Collections.addAll()
List&amp;lt;String&amp;gt; list3 = new ArrayList&amp;lt;&amp;gt;();
Collections.addAll(list3, arr);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 2. 기본형 배열 (int[], double[] 등)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Arrays.asList()&lt;/code&gt;는 기본형 배열을 직접 처리하지 못합니다. 가장 골치아프고 당황스러울 수 있는 케이스입니다. 따라서 스트림(Stream)이나 반복문을 사용해야 합니다. 스트림이 생각난다면 사용하고, 그냥 모르겠으면 반복문으로 추가하는 것이 안전합니다!&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;int[] arr = {1, 2, 3, 4, 5};

// 1) 반복문 사용
List&amp;lt;Integer&amp;gt; list1 = new ArrayList&amp;lt;&amp;gt;();
for (int x : arr) list1.add(x);

// 2) Stream 사용 (자바 8 이상)
List&amp;lt;Integer&amp;gt; list2 = Arrays.stream(arr) // IntStream
                            .boxed()     // int &amp;rarr; Integer
                            .toList();   // List&amp;lt;Integer&amp;gt; (Java 16 이상)

// 3) Collectors 사용 (Java 8~15)
List&amp;lt;Integer&amp;gt; list3 = Arrays.stream(arr)
                            .boxed()
                            .collect(Collectors.toList());&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 3. 2차원 배열 (int[][] &amp;rarr; List&amp;lt;int[]&amp;gt;)&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;int[][] arr2d = {{1, 2}, {3, 4}, {5, 6}};

// 배열 그대로 List로 (int[] 단위)
List&amp;lt;int[]&amp;gt; list = Arrays.asList(arr2d);

// 각 원소를 List&amp;lt;Integer&amp;gt;로 변환
List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; nestedList = Arrays.stream(arr2d)
                                       .map(a -&amp;gt; Arrays.stream(a).boxed().toList())
                                       .toList();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;사실 이렇게까지 사용할 경우는 많지는 않은 것 같습니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chX0BJ/btsALWnWIgA/XMkMDnLKRussvcLggSXEq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchX0BJ%2FbtsALWnWIgA%2FXMkMDnLKRussvcLggSXEq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;367&quot; data-filename=&quot;3D636ADC-F0C1-4AEF-8CAF-8197BC829E02_1_201_a.jpeg&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;도움이 되었다면, 공감/댓글을 달아주면&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;깃짱&lt;/b&gt;에게 큰 힘이 됩니다! &lt;br /&gt;&lt;i&gt;&lt;b&gt;비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>언어+프레임워크/JAVA</category>
      <author>깃짱</author>
      <guid isPermaLink="true">https://engineerinsight.tistory.com/418</guid>
      <comments>https://engineerinsight.tistory.com/418#entry418comment</comments>
      <pubDate>Fri, 5 Sep 2025 08:00:44 +0900</pubDate>
    </item>
  </channel>
</rss>