<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Architecture · 인재덕</title>
    <link>https://jared.lynskey.co.nz/ko/tags/architecture/</link>
    <description>소프트웨어 엔지니어링과 개발 조직 리더십에 대한 이야기. 성과 리뷰, 채용과 팀 확장, CI/CD, 데브옵스 모니터링, 에이전틱 AI. 서울에서 씁니다.</description>
    <generator>Hugo</generator>
    <language>ko</language>
    <copyright>© 2026 인재덕</copyright>
    <lastBuildDate>Wed, 28 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ko/tags/architecture/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>의사결정 문서란? 템플릿과 함께 보는 실전 가이드</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2024/2024-12-03-decision-docs/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2024/2024-12-03-decision-docs/</guid>
      <pubDate>Tue, 03 Dec 2024 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>decision-making</category>
      <category>documentation</category>
      <category>ADR</category>
      <category>architecture</category>
      <category>team alignment</category>
      <description>의사결정 문서란 결정을 내리는 그 순간의 배경, 검토한 선택지, 그중 하나를 고른 이유를 기록한 문서입니다. 6개월 뒤 같은 논쟁을 반복하지 않게 해 주죠. 실제로 쓰이는 ADR과 RFC 작성법을, ADR 예시와 가져다 쓸 수 있는 템플릿과 함께 정리했습니다.</description>
      <content:encoded><![CDATA[<p>모든 팀은 결정을 내립니다. 그리고 대부분의 팀은 왜 그렇게 결정했는지를 잊어버립니다. 6개월 뒤 누군가 &ldquo;우리 왜 MongoDB 말고 PostgreSQL을 골랐죠?&ldquo;라고 묻고, 당시의 제약을 기억하는 사람은 아무도 없고, 같은 논쟁이 처음부터 다시 벌어집니다. 보통은 지난번보다 적은 인원으로요.</p>
<p>의사결정 문서가 이걸 해결합니다. ADR이든 RFC든 결정 로그든 이름은 중요하지 않습니다. 중요한 건 선택하는 그 순간에 배경과 선택지와 근거를 적어두는 일입니다. 올해 개발팀을 맡게 되면서 그 가치를 어느 때보다 실감하고 있습니다. 조직의 기억이 되느냐, 조직의 구전 설화가 되느냐의 갈림길이거든요.</p>
<hr>

<h3 class="relative group">공들일 가치가 있는 이유
    <div id="공들일-가치가-있는-이유" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ea%b3%b5%eb%93%a4%ec%9d%bc-%ea%b0%80%ec%b9%98%ea%b0%80-%ec%9e%88%eb%8a%94-%ec%9d%b4%ec%9c%a0" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>가장 뻔한 보상은 같은 논쟁을 두 번 하지 않아도 된다는 겁니다. &ldquo;API 버저닝 전략, 이미 정하지 않았나요?&rdquo; 무엇을 왜 결정했는지 기록이 명확히 남아 있으면 결론 난 문제는 결론 난 채로 있고, 팀은 왔던 길을 되짚는 대신 실행에 에너지를 씁니다.</p>
<p>더 조용한 보상은 시간이 지나면서 드러납니다. 프로젝트는 몇 달, 몇 년씩 이어지고 사람은 들어오고 나가는데, 끝까지 남는 건 코드입니다. 문제는 코드가 &ldquo;무엇을&rdquo; 결정했는지는 알려줘도 &ldquo;왜&quot;는 절대 말해주지 않는다는 점이죠. 의사결정 문서는 그 추론을 보존합니다. 당시 존재했던 제약, 기각된 대안, 최종 판단을 내린 사람까지요. 새 팀원은 구전 설화 대신 맥락을 얻습니다. 팀을 넘나드는 결정은 결론만이 아니라 저울에 올랐던 요소까지 모두에게 보이니 진짜 동의를 얻기 쉬워집니다. 그리고 의사결정자와 기여자의 이름이 박히면, 후속 실행에도 확실한 주인이 생깁니다.</p>
<hr>

<h3 class="relative group">언제 쓰는가
    <div id="언제-쓰는가" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%96%b8%ec%a0%9c-%ec%93%b0%eb%8a%94%ea%b0%80" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>모든 결정에 문서가 필요하진 않습니다. 저는 결정이 다음에 해당할 때 문서를 씁니다.</p>
<ul>
<li><strong>되돌리기 비싼 결정</strong> — 기술 스택 선택(프레임워크, 데이터베이스, 클라우드 제공업체), 아키텍처 패턴(마이크로서비스 vs 모놀리스, 이벤트 기반 vs 요청-응답), 서드파티 벤더 선정, 보안·규정 준수 접근 방식</li>
<li><strong>오래 남는 결정</strong> — 외부 시스템이 의존하는 API 계약, 여러 서비스에 걸치는 데이터 모델, 확립된 패턴에 대한 파괴적 변경</li>
<li><strong>팀을 가로지르는 결정</strong> — 여러 팀이나 비기술 이해관계자의 동의가 필요한 것, 리소스 배분과 우선순위 판단</li>
<li><strong><a href="https://jared.lynskey.co.nz/ko/posts/2023/2023-10-18-spike/" >스파이크</a>의 결론</strong> — 타임박스 조사가 끝났다면, 발견과 결정이 증발하기 전에 문서로 남기기</li>
</ul>
<p>반대로 쉽게 되돌릴 수 있는 결정, 팀 내부 구현 세부사항, 내 범위 밖에는 거의 영향이 없는 선택이라면 형식을 갖춘 문서는 건너뛰세요. 아무도 필요로 하지 않는 문서는 그냥 숙제일 뿐입니다.</p>
<hr>

<h3 class="relative group">라이프사이클
    <div id="라이프사이클" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%9d%bc%ec%9d%b4%ed%94%84%ec%82%ac%ec%9d%b4%ed%81%b4" aria-label="앵커">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
</span></span><span class="line"><span class="cl">│   초안      │ ──► │   리뷰      │ ──► │   승인      │ ──► │   구현      │
</span></span><span class="line"><span class="cl">│   제안      │     │  피드백     │     │   수락      │     │   완료      │
</span></span><span class="line"><span class="cl">└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
</span></span><span class="line"><span class="cl">                           │
</span></span><span class="line"><span class="cl">                           ▼
</span></span><span class="line"><span class="cl">                    ┌─────────────┐
</span></span><span class="line"><span class="cl">                    │   거부/     │
</span></span><span class="line"><span class="cl">                    │   대체      │
</span></span><span class="line"><span class="cl">                    └─────────────┘</span></span></code></pre></div></div>
<p>작성자가 문제와 후보 솔루션을 초안으로 정리하고, 이해관계자들이 리뷰합니다. 이 과정의 본전을 뽑는 곳이 리뷰 단계입니다. 가정을 따져 묻고 맹점이 드러나는 지점이니까요. 그다음 의사결정자(보통 기술 리드, 아키텍트, 프로덕트 오너)가 최종 판단을 내리고, 팀이 구현하며, 구현 중 현실 때문에 바뀐 부분이 있으면 문서도 갱신합니다. 리뷰에서 죽는 문서도 있고, 상황이 변해 나중에 대체되는 문서도 있습니다. 둘 다 분명하게 표시하세요. &ldquo;승인됨&rdquo; 상태로 낡아버린 문서는 다음에 그걸 발견할 사람에게 함정이 됩니다.</p>
<hr>

<h3 class="relative group">읽을 만한 문서를 만드는 것들
    <div id="읽을-만한-문서를-만드는-것들" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9d%bd%ec%9d%84-%eb%a7%8c%ed%95%9c-%eb%ac%b8%ec%84%9c%eb%a5%bc-%eb%a7%8c%eb%93%9c%eb%8a%94-%ea%b2%83%eb%93%a4" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>애착이 가는 솔루션이 아니라 문제에서 시작하세요. 무엇을 해결하려는지, 왜 지금 중요한지, 아무것도 안 하면 어떻게 되는지. 문제에 대한 합의가 없는 상태에서 솔루션을 논쟁하는 건 연극일 뿐입니다.</p>
<p>그다음 진짜 선택지를 제시합니다. 강한 선호가 있어도 최소 2~3개의 대안을요. 각 옵션에는 간단한 설명, 장단점, 대략적인 공수 추정(티셔츠 사이즈면 충분합니다: S/M/L/XL), 그리고 리스크와 완화 방안을 답니다. 대안을 기록해 두는 것이야말로 미래의 독자가 &ldquo;왜 승자가 이겼는지&rdquo; 이해하는 방법입니다.</p>
<p>트레이드오프는 소리 내어 말하세요. &ldquo;아키텍처의 순수함보다 시장 출시 속도를 택합니다.&rdquo; &ldquo;더 나은 확장성을 위해 운영 복잡도 증가를 감수합니다.&rdquo; &ldquo;순수 성능보다 개발자 경험을 우선합니다.&rdquo; 이런 문장 하나가 분석 몇 페이지보다 미래의 독자에게 더 도움이 됩니다.</p>
<p>논의는 구체적인 것에 붙들어 매세요. 솔루션이 어떻게 구현될지 보여주는 코드 스니펫, 시스템 상호작용 다이어그램, <a href="https://jared.lynskey.co.nz/ko/posts/2023/2023-10-18-spike/" >스파이크</a>에서 만든 개념 증명 링크. 추상적인 논의에서는 추상적인 결정밖에 안 나옵니다.</p>
<p>마지막으로 기한입니다. 언제까지 결정해야 하는지, 누가 언제까지 의견을 내야 하는지, 결정이 미뤄지면 무엇이 막히는지. 기한 없는 결정은 영원히 떠다닙니다.</p>
<hr>

<h3 class="relative group">예시: 아키텍처 결정 기록(ADR)
    <div id="예시-아키텍처-결정-기록adr" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%98%88%ec%8b%9c-%ec%95%84%ed%82%a4%ed%85%8d%ec%b2%98-%ea%b2%b0%ec%a0%95-%ea%b8%b0%eb%a1%9dadr" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>의사결정 문서의 실전 예시입니다:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gh"># ADR-001: API Authentication Strategy
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Status:**</span> Accepted
</span></span><span class="line"><span class="cl"><span class="gs">**Date:**</span> 2024-11-15
</span></span><span class="line"><span class="cl"><span class="gs">**Decision Maker:**</span> Sarah Chen (Platform Architect)
</span></span><span class="line"><span class="cl"><span class="gs">**Contributors:**</span> Backend Team, Security Team, Mobile Team
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Context
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Our public API currently uses API keys for authentication. As we expand to
</span></span><span class="line"><span class="cl">support third-party integrations and mobile apps, we need a more robust
</span></span><span class="line"><span class="cl">authentication mechanism that supports:
</span></span><span class="line"><span class="cl"><span class="k">-</span> Token expiration and refresh
</span></span><span class="line"><span class="cl"><span class="k">-</span> Scoped permissions
</span></span><span class="line"><span class="cl"><span class="k">-</span> User-level authentication (not just service-level)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Decision
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">We will implement OAuth 2.0 with JWT tokens for API authentication.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Options Considered
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Option 1: OAuth 2.0 with JWT (Recommended)
</span></span></span><span class="line"><span class="cl">**Description:** Industry-standard protocol with self-contained tokens
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">| Pros | Cons |
</span></span><span class="line"><span class="cl">|------|------|
</span></span><span class="line"><span class="cl">| Industry standard, well-documented | More complex initial implementation |
</span></span><span class="line"><span class="cl">| Self-contained tokens reduce database lookups | Tokens cannot be revoked instantly |
</span></span><span class="line"><span class="cl">| Broad library support | Requires refresh token management |
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Effort:**</span> Medium (2-3 sprints)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Option 2: Session-based Authentication
</span></span></span><span class="line"><span class="cl">**Description:** Traditional server-side sessions with cookies
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">| Pros | Cons |
</span></span><span class="line"><span class="cl">|------|------|
</span></span><span class="line"><span class="cl">| Simple to implement | Not suitable for mobile apps |
</span></span><span class="line"><span class="cl">| Easy to revoke sessions | Requires sticky sessions or shared session store |
</span></span><span class="line"><span class="cl">| Familiar to most developers | Doesn&#39;t scale as well |
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Effort:**</span> Small (1 sprint)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Option 3: Custom Token System
</span></span></span><span class="line"><span class="cl">**Description:** Build our own token-based authentication
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">| Pros | Cons |
</span></span><span class="line"><span class="cl">|------|------|
</span></span><span class="line"><span class="cl">| Fully customizable | Reinventing the wheel |
</span></span><span class="line"><span class="cl">| No external dependencies | Security risks from custom implementation |
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Effort:**</span> Large (4+ sprints)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Consequences
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Mobile team can implement standard OAuth flows
</span></span><span class="line"><span class="cl"><span class="k">-</span> We&#39;ll need to set up a token refresh mechanism
</span></span><span class="line"><span class="cl"><span class="k">-</span> API documentation will need updates for OAuth flows
</span></span><span class="line"><span class="cl"><span class="k">-</span> Existing API key users will need a migration path (6-month deprecation)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## References
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> [<span class="nt">OAuth 2.0 RFC 6749</span>](<span class="na">https://tools.ietf.org/html/rfc6749</span>)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Internal spike document: [<span class="nt">Authentication Options Spike</span>](<span class="na">/spikes/auth-spike-2024</span>)
</span></span><span class="line"><span class="cl">- Security team review: SEC-2024-042</span></span></code></pre></div></div>
<hr>

<h3 class="relative group">훔쳐 써도 되는 템플릿
    <div id="훔쳐-써도-되는-템플릿" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ed%9b%94%ec%b3%90-%ec%8d%a8%eb%8f%84-%eb%90%98%eb%8a%94-%ed%85%9c%ed%94%8c%eb%a6%bf" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>이걸 출발점으로 삼고 팀에 맞게 덜어내세요.</p>
<table>
	<thead>
			<tr>
					<th><strong>속성</strong></th>
					<th><strong>세부사항</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>결정/이슈 이름</strong></td>
					<td>[명확하고 설명적인 제목]</td>
			</tr>
			<tr>
					<td><strong>상태</strong></td>
					<td>[초안 / 검토 중 / 승인됨 / 거부됨 / 대체됨]</td>
			</tr>
			<tr>
					<td><strong>영향도</strong></td>
					<td>[높음 / 중간 / 낮음]</td>
			</tr>
			<tr>
					<td><strong>오너</strong></td>
					<td>[결정을 추진하는 사람 이름]</td>
			</tr>
			<tr>
					<td><strong>의사결정자</strong></td>
					<td>[최종 권한을 가진 사람]</td>
			</tr>
			<tr>
					<td><strong>기한</strong></td>
					<td>[결정을 내려야 하는 기한]</td>
			</tr>
	</tbody>
</table>

<h4 class="relative group">문제 설명
    <div id="문제-설명" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%ac%b8%ec%a0%9c-%ec%84%a4%eb%aa%85" aria-label="앵커">#</a>
    </span>
    
</h4>
<p><em>어떤 문제를 해결하려고 하는가? 왜 중요한가? 아무것도 하지 않을 때의 비용은?</em></p>

<h4 class="relative group">배경
    <div id="배경" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%b0%b0%ea%b2%bd" aria-label="앵커">#</a>
    </span>
    
</h4>
<p><em>맥락, 이력, 관련 세부사항을 제공합니다. 관련 문서, 이전 결정, 스파이크 결과에 대한 링크를 포함합니다.</em></p>

<h4 class="relative group">제약사항
    <div id="제약사항" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%a0%9c%ec%95%bd%ec%82%ac%ed%95%ad" aria-label="앵커">#</a>
    </span>
    
</h4>
<p><em>모든 솔루션이 준수해야 하는 제한이나 요구사항은 무엇인가?</em></p>
<ul>
<li>예산 제약</li>
<li>타임라인 요구사항</li>
<li>기술적 제약 (기존 시스템, 스킬셋)</li>
<li>규정 준수 또는 보안 요구사항</li>
</ul>

<h4 class="relative group">검토한 솔루션
    <div id="검토한-솔루션" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ea%b2%80%ed%86%a0%ed%95%9c-%ec%86%94%eb%a3%a8%ec%85%98" aria-label="앵커">#</a>
    </span>
    
</h4>
<table>
	<thead>
			<tr>
					<th><strong>옵션</strong></th>
					<th><strong>설명</strong></th>
					<th><strong>장점</strong></th>
					<th><strong>단점</strong></th>
					<th><strong>공수</strong></th>
					<th><strong>리스크</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>옵션 1</strong></td>
					<td><em>설명</em></td>
					<td><em>장점A, 장점B</em></td>
					<td><em>단점A, 단점B</em></td>
					<td><em>S/M/L/XL</em></td>
					<td><em>낮음/중간/높음</em></td>
			</tr>
			<tr>
					<td><strong>옵션 2</strong></td>
					<td><em>설명</em></td>
					<td><em>장점A, 장점B</em></td>
					<td><em>단점A, 단점B</em></td>
					<td><em>S/M/L/XL</em></td>
					<td><em>낮음/중간/높음</em></td>
			</tr>
			<tr>
					<td><strong>옵션 3</strong></td>
					<td><em>설명</em></td>
					<td><em>장점A, 장점B</em></td>
					<td><em>단점A, 단점B</em></td>
					<td><em>S/M/L/XL</em></td>
					<td><em>낮음/중간/높음</em></td>
			</tr>
	</tbody>
</table>

<h4 class="relative group">권장사항
    <div id="권장사항" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ea%b6%8c%ec%9e%a5%ec%82%ac%ed%95%ad" aria-label="앵커">#</a>
    </span>
    
</h4>
<p><em>권장하는 옵션을 명시하고 제약과 트레이드오프를 고려할 때 왜 최선의 선택인지 요약합니다.</em></p>

<h4 class="relative group">결정
    <div id="결정" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ea%b2%b0%ec%a0%95" aria-label="앵커">#</a>
    </span>
    
</h4>
<p><em>결정이 내려지면 최종 결정을 문서화합니다. 권장사항과 다르면 이유를 설명합니다.</em></p>

<h4 class="relative group">결과
    <div id="결과" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ea%b2%b0%ea%b3%bc" aria-label="앵커">#</a>
    </span>
    
</h4>
<p><em>이 결정의 영향은 무엇인가? 필요한 후속 작업은 무엇인가?</em></p>

<h4 class="relative group">액션 아이템
    <div id="액션-아이템" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%95%a1%ec%85%98-%ec%95%84%ec%9d%b4%ed%85%9c" aria-label="앵커">#</a>
    </span>
    
</h4>
<table>
	<thead>
			<tr>
					<th><strong>액션</strong></th>
					<th><strong>담당자</strong></th>
					<th><strong>기한</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><em>액션 1</em></td>
					<td><em>이름</em></td>
					<td><em>날짜</em></td>
			</tr>
			<tr>
					<td><em>액션 2</em></td>
					<td><em>이름</em></td>
					<td><em>날짜</em></td>
			</tr>
	</tbody>
</table>

<h4 class="relative group">참조
    <div id="참조" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%b0%b8%ec%a1%b0" aria-label="앵커">#</a>
    </span>
    
</h4>
<p><em>관련 문서, 외부 리소스, 스파이크 결과, 이전 결정에 대한 링크.</em></p>
<hr>

<h3 class="relative group">습관으로 정착시키기
    <div id="습관으로-정착시키기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8a%b5%ea%b4%80%ec%9c%bc%eb%a1%9c-%ec%a0%95%ec%b0%a9%ec%8b%9c%ed%82%a4%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>문서는 일관된 곳에 두세요. 저장소의 전용 폴더든 위키 공간이든 상관없지만, 찾을 수 없는 결정은 존재하지 않는 것과 같습니다. 그리고 가볍게 유지하세요. 쓰는 데 며칠 걸리는 문서는 애초에 쓰이지 않습니다. 코드 주석, 커밋 메시지, 풀 리퀘스트에서 문서를 참조해서 &ldquo;왜&quot;가 &ldquo;무엇&rdquo; 곁에 붙어 있게 하세요. 결정이 대체되면 옛 문서를 지우지 말고 대체됨으로 표시한 뒤 새 결정으로 링크를 겁니다. 역사가 곧 핵심이니까요. 그리고 <a href="https://jared.lynskey.co.nz/ko/posts/2023/2023-09-01-retrospecs/" >회고</a>에서 가끔 과거 결정을 돌아보세요. 어떤 결정이 버텨냈고, 어떤 건 지금이라면 다르게 할지.</p>
<p>실패 패턴도 그만큼 예측 가능합니다. 분석 마비에는 기한을 정하고 가진 정보로 결정하는 것이 답입니다. 대부분의 결정은 나중에 다시 볼 수 있으니까요. 의미 있는 비평이 한 번도 안 나오는 도장 찍기 리뷰는 가치 없이 절차만 더합니다. 쓰였는데 실행되지 않는 고아 문서는 문서가 없는 것보다 나쁩니다. 과한 디테일도 조심하세요. 문서가 다룰 것은 &ldquo;왜&quot;와 &ldquo;무엇&quot;이지 &ldquo;어떻게&quot;가 아닙니다. 구현 세부사항은 기술 명세서의 몫입니다. 그리고 이해관계자가 반대하면 그 반대도 기록하세요. 결정은 그대로 갈 수 있지만, 그 반론이야말로 1년 뒤 누군가에게 꼭 필요한 맥락입니다.</p>
<p>다음번 중요한 결정부터 시작해 보세요. 거칠어도 일단 쓰고, 프로세스는 굴리면서 다듬으면 됩니다. 누군가 ADR을 읽는 것만으로 &ldquo;우리 왜 X를 골랐죠?&rdquo; 회의가 사라질 때마다, 그만큼의 시간이 팀에게 돌아옵니다.</p>
<hr>

<h3 class="relative group">참고 자료
    <div id="참고-자료" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%b0%b8%ea%b3%a0-%ec%9e%90%eb%a3%8c" aria-label="앵커">#</a>
    </span>
    
</h3>
<p><strong>이 사이트의 관련 글:</strong></p>
<ul>
<li><a href="https://jared.lynskey.co.nz/ko/posts/2023/2023-10-18-spike/" >애자일 소프트웨어 개발에서 스파이크 이해하기</a> - 큰 결정 전에 스파이크로 정보 모으기</li>
<li><a href="https://jared.lynskey.co.nz/ko/posts/2023/2023-09-01-retrospecs/" >회고: 피드백 루프의 힘</a> - 지속적 개선의 일부로 과거 결정 돌아보기</li>
<li><a href="https://jared.lynskey.co.nz/ko/posts/2023/2023-10-08-planning-an-api/" >API 계획</a> - 의사결정 문서의 사고방식을 API 설계에 적용하기</li>
<li><a href="https://jared.lynskey.co.nz/ko/posts/2024/2024-01-04-githubforprojectmanagement/" >프로젝트 관리를 위한 GitHub</a> - 의사결정 문서를 코드 옆에 두고 관리하기</li>
</ul>
<p><strong>외부 리소스:</strong></p>
<ul>
<li><a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions"  target="_blank" rel="noreferrer">Documenting Architecture Decisions - Michael Nygard</a> - ADR을 대중화한 원본 글</li>
<li><a href="https://adr.github.io/"  target="_blank" rel="noreferrer">ADR GitHub Organization</a> - ADR을 위한 도구, 템플릿, 예시</li>
<li><a href="https://www.industrialempathy.com/posts/design-docs-at-google/"  target="_blank" rel="noreferrer">Design Docs at Google</a> - Google의 설계 문서 접근법</li>
<li><a href="https://rust-lang.github.io/rfcs/"  target="_blank" rel="noreferrer">RFC Process at Rust</a> - Rust 언어의 잘 정비된 RFC 프로세스</li>
<li><a href="https://engineering.atspotify.com/2020/04/when-should-i-write-an-architecture-decision-record/"  target="_blank" rel="noreferrer">Spotify&rsquo;s Decision Record Template</a> - Spotify가 ADR을 쓰는 시점에 대한 글</li>
</ul>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2024/2024-12-03-decision-docs/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>싱글 테넌트 vs 멀티 테넌트 SaaS: 언제 무엇을 쓸까</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2024/2024-04-07-singletenantsingleinstance/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2024/2024-04-07-singletenantsingleinstance/</guid>
      <pubDate>Sat, 06 Apr 2024 00:30:03 +0000</pubDate>
      <dc:creator>Me</dc:creator>
      <category>SaaS</category>
      <category>architecture</category>
      <category>single-tenant</category>
      <category>multi-tenant</category>
      <description>싱글 테넌트(멀티 인스턴스)와 멀티 테넌트 SaaS는 무엇이 다를까요. 격리, 비용, 운영 비교, 그 사이의 선택지, 언제 무엇을 쓸지, 공유 데이터베이스에서 테넌트를 분리하는 방법, 그리고 고객별 인스턴스 운영을 버틸 만하게 만드는 도구들을 정리했습니다.</description>
      <content:encoded><![CDATA[<p>싱글 테넌트 SaaS는 고객마다 애플리케이션과 데이터베이스를 한 벌씩 따로 줍니다. 멀티 테넌트 SaaS는 모든 고객을 하나의 공유 애플리케이션과 데이터베이스에 올리고 테넌트 ID로 구분합니다. 이 확인은 모든 쿼리에서 해야 하죠. 싱글 테넌트는 격리, 고객별 제어, 쉬운 컴플라이언스 대응을 얻는 대신 수많은 복사본을 운영하는 비용을 치릅니다. 멀티 테넌트는 저렴하고 운영도 훨씬 단순하지만, 고객끼리 섞이지 않게 하는 일이 코드의 몫이 됩니다. 대부분의 제품은 멀티 테넌트로 시작해서, 꼭 필요한 일부 고객에게만 전용 인스턴스를 제공합니다.</p>
<p>B2B SaaS를 하다 보면 언젠가는 반드시 테넌시 문제에 부딪힙니다. 보통은 큰 잠재 고객이 &ldquo;우리 데이터를 다른 회사와 분리해서 둘 수 있나요?&ldquo;라고 묻는 날이죠. 저도 이 논의를 여러 번 가까이서 지켜봤습니다. 답은 기본적으로 둘 중 하나입니다. 고객마다 전용 인스턴스를 주거나(싱글 테넌트, 멀티 인스턴스), 모두를 하나의 공유 인스턴스에서 운영하거나(멀티 테넌트, 싱글 인스턴스). 어느 쪽이 정답인 건 아닙니다. 그저 망가지는 방식이 다를 뿐입니다.</p>

<h2 class="relative group">인프라 설계
    <div id="인프라-설계" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9d%b8%ed%94%84%eb%9d%bc-%ec%84%a4%ea%b3%84" aria-label="앵커">#</a>
    </span>
    
</h2>
<figure class="diagram">
  <div role="img" aria-label="싱글 테넌트와 멀티 테넌트의 차이"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 790" aria-hidden="true" focusable="false">
<defs><marker id="tn-ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="d-head" d="M0,0 L10,5 L0,10 z"/></marker></defs>
<text class="d-title" x="320" y="164" text-anchor="middle" font-size="42">멀티 인스턴스</text>
<text class="d-sub" x="320" y="204" text-anchor="middle" font-size="24">싱글 테넌트: 고객마다 한 벌씩</text>
<rect class="d-panel" x="12" y="234" width="616" height="304" rx="36"/>
<rect class="d-group" x="25" y="250" width="186" height="272" rx="22"/>
<rect class="d-box" x="33" y="280" width="170" height="86" rx="14"/>
<text class="d-label" x="118.0" y="337.64" text-anchor="middle" font-size="24">데이터베이스</text>
<polygon class="d-badge" points="118.0,256.0 143.7,274.7 133.9,304.8 102.1,304.8 92.3,274.7"/>
<g class="d-icon">
<ellipse cx="118" cy="273" rx="10" ry="4"/>
<path d="M108,273 v15 a10,4 0 0 0 20,0 v-15 M108,280 a10,4 0 0 0 20,0"/>
</g>
<rect class="d-box" x="33" y="418" width="170" height="86" rx="14"/>
<text class="d-label" x="118.0" y="475.64" text-anchor="middle" font-size="24">애플리케이션</text>
<polygon class="d-badge" points="118.0,394.0 143.7,412.7 133.9,442.8 102.1,442.8 92.3,412.7"/>
<g class="d-icon">
<rect x="110" y="411" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="114.5" y="415.5" width="7" height="7"/>
<path stroke-width="1.4" d="M114,411 v-4 M114,427 v4 M110,415 h-4 M126,415 h4 M118,411 v-4 M118,427 v4 M110,419 h-4 M126,419 h4 M122,411 v-4 M122,427 v4 M110,423 h-4 M126,423 h4"/>
</g>
<line class="d-line" x1="118" y1="508" x2="118" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="84" cy="710.88" r="14.44"/><path d="M58.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="152" cy="710.88" r="14.44"/><path d="M126.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="118" cy="704.0" r="19.0"/><path d="M84.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<rect class="d-group" x="227" y="250" width="186" height="272" rx="22"/>
<rect class="d-box" x="235" y="280" width="170" height="86" rx="14"/>
<text class="d-label" x="320.0" y="337.64" text-anchor="middle" font-size="24">데이터베이스</text>
<polygon class="d-badge" points="320.0,256.0 345.7,274.7 335.9,304.8 304.1,304.8 294.3,274.7"/>
<g class="d-icon">
<ellipse cx="320" cy="273" rx="10" ry="4"/>
<path d="M310,273 v15 a10,4 0 0 0 20,0 v-15 M310,280 a10,4 0 0 0 20,0"/>
</g>
<rect class="d-box" x="235" y="418" width="170" height="86" rx="14"/>
<text class="d-label" x="320.0" y="475.64" text-anchor="middle" font-size="24">애플리케이션</text>
<polygon class="d-badge" points="320.0,394.0 345.7,412.7 335.9,442.8 304.1,442.8 294.3,412.7"/>
<g class="d-icon">
<rect x="312" y="411" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="316.5" y="415.5" width="7" height="7"/>
<path stroke-width="1.4" d="M316,411 v-4 M316,427 v4 M312,415 h-4 M328,415 h4 M320,411 v-4 M320,427 v4 M312,419 h-4 M328,419 h4 M324,411 v-4 M324,427 v4 M312,423 h-4 M328,423 h4"/>
</g>
<line class="d-line" x1="320" y1="508" x2="320" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="286" cy="710.88" r="14.44"/><path d="M260.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="354" cy="710.88" r="14.44"/><path d="M328.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="320" cy="704.0" r="19.0"/><path d="M286.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<rect class="d-group" x="429" y="250" width="186" height="272" rx="22"/>
<rect class="d-box" x="437" y="280" width="170" height="86" rx="14"/>
<text class="d-label" x="522.0" y="337.64" text-anchor="middle" font-size="24">데이터베이스</text>
<polygon class="d-badge" points="522.0,256.0 547.7,274.7 537.9,304.8 506.1,304.8 496.3,274.7"/>
<g class="d-icon">
<ellipse cx="522" cy="273" rx="10" ry="4"/>
<path d="M512,273 v15 a10,4 0 0 0 20,0 v-15 M512,280 a10,4 0 0 0 20,0"/>
</g>
<rect class="d-box" x="437" y="418" width="170" height="86" rx="14"/>
<text class="d-label" x="522.0" y="475.64" text-anchor="middle" font-size="24">애플리케이션</text>
<polygon class="d-badge" points="522.0,394.0 547.7,412.7 537.9,442.8 506.1,442.8 496.3,412.7"/>
<g class="d-icon">
<rect x="514" y="411" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="518.5" y="415.5" width="7" height="7"/>
<path stroke-width="1.4" d="M518,411 v-4 M518,427 v4 M514,415 h-4 M530,415 h4 M522,411 v-4 M522,427 v4 M514,419 h-4 M530,419 h4 M526,411 v-4 M526,427 v4 M514,423 h-4 M530,423 h4"/>
</g>
<line class="d-line" x1="522" y1="508" x2="522" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="488" cy="710.88" r="14.44"/><path d="M462.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="556" cy="710.88" r="14.44"/><path d="M530.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="522" cy="704.0" r="19.0"/><path d="M488.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<text class="d-title" x="982" y="50" text-anchor="middle" font-size="42">멀티 테넌트</text>
<text class="d-sub" x="982" y="88" text-anchor="middle" font-size="24">모든 고객이 인스턴스 하나를 공유</text>
<rect class="d-panel" x="698" y="104" width="568" height="434" rx="44"/>
<rect class="d-box" x="887" y="126" width="190" height="86" rx="14"/>
<text class="d-label" x="982.0" y="183.64" text-anchor="middle" font-size="24">데이터베이스</text>
<polygon class="d-badge" points="982.0,102.0 1007.7,120.7 997.9,150.8 966.1,150.8 956.3,120.7"/>
<g class="d-icon">
<ellipse cx="982" cy="119" rx="10" ry="4"/>
<path d="M972,119 v15 a10,4 0 0 0 20,0 v-15 M972,126 a10,4 0 0 0 20,0"/>
</g>
<line class="d-line" x1="982" y1="216" x2="982" y2="246" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<rect class="d-group" x="704" y="240" width="556" height="156" rx="30"/>
<rect class="d-box" x="714" y="282" width="536" height="90" rx="16"/>
<polygon class="d-badge" points="982.0,248.0 1007.7,266.7 997.9,296.8 966.1,296.8 956.3,266.7"/>
<g class="d-icon">
<rect x="974" y="265" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="978.5" y="269.5" width="7" height="7"/>
<path stroke-width="1.4" d="M978,265 v-4 M978,281 v4 M974,269 h-4 M990,269 h4 M982,265 v-4 M982,281 v4 M974,273 h-4 M990,273 h4 M986,265 v-4 M986,281 v4 M974,277 h-4 M990,277 h4"/>
</g>
<rect class="d-box" x="722" y="306" width="168" height="56" rx="12"/>
<text class="d-label" x="806.0" y="347.92" text-anchor="middle" font-size="22">애플리케이션</text>
<rect class="d-box" x="898" y="306" width="168" height="56" rx="12"/>
<text class="d-label" x="982.0" y="347.92" text-anchor="middle" font-size="22">애플리케이션</text>
<rect class="d-box" x="1074" y="306" width="168" height="56" rx="12"/>
<text class="d-label" x="1158.0" y="347.92" text-anchor="middle" font-size="22">애플리케이션</text>
<rect class="d-box" x="887" y="440" width="190" height="86" rx="14"/>
<text class="d-label" x="982.0" y="497.64" text-anchor="middle" font-size="24">로드 밸런서</text>
<polygon class="d-badge" points="982.0,404.0 1007.7,422.7 997.9,452.8 966.1,452.8 956.3,422.7"/>
<g class="d-icon">
<path d="M982,416 l6,9 h-12 z M982,425 v5 M972,433 h20 M972,433 v3 M982,433 v3 M992,433 v3"/>
<rect class="d-icon-fill" x="969" y="436" width="6" height="5"/>
<rect class="d-icon-fill" x="979" y="436" width="6" height="5"/>
<rect class="d-icon-fill" x="989" y="436" width="6" height="5"/>
</g>
<line class="d-line" x1="806" y1="364" x2="922" y2="438" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<line class="d-line" x1="982" y1="364" x2="982" y2="398" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<line class="d-line" x1="1158" y1="364" x2="1042" y2="438" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<line class="d-line" x1="932" y1="528" x2="780" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="716" cy="710.88" r="14.44"/><path d="M690.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="784" cy="710.88" r="14.44"/><path d="M758.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="750" cy="704.0" r="19.0"/><path d="M716.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<line class="d-line" x1="982" y1="528" x2="982" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="948" cy="710.88" r="14.44"/><path d="M922.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="1016" cy="710.88" r="14.44"/><path d="M990.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="982" cy="704.0" r="19.0"/><path d="M948.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<line class="d-line" x1="1032" y1="528" x2="1184" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="1180" cy="710.88" r="14.44"/><path d="M1154.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="1248" cy="710.88" r="14.44"/><path d="M1222.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="1214" cy="704.0" r="19.0"/><path d="M1180.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
</svg>
</div>
</figure>


<h3 class="relative group">싱글 테넌트, 멀티 인스턴스
    <div id="싱글-테넌트-멀티-인스턴스" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8b%b1%ea%b8%80-%ed%85%8c%eb%84%8c%ed%8a%b8-%eb%a9%80%ed%8b%b0-%ec%9d%b8%ec%8a%a4%ed%84%b4%ec%8a%a4" aria-label="앵커">#</a>
    </span>
    
</h3>

  



<div
  
    class="flex px-4 py-3 rounded-md shadow bg-primary-100 dark:bg-primary-900"
  
  >
  <span
    
      class="text-primary-400 pe-3 flex items-center"
    
    >
    <span class="relative block icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M112.1 454.3c0 6.297 1.816 12.44 5.284 17.69l17.14 25.69c5.25 7.875 17.17 14.28 26.64 14.28h61.67c9.438 0 21.36-6.401 26.61-14.28l17.08-25.68c2.938-4.438 5.348-12.37 5.348-17.7L272 415.1h-160L112.1 454.3zM191.4 .0132C89.44 .3257 16 82.97 16 175.1c0 44.38 16.44 84.84 43.56 115.8c16.53 18.84 42.34 58.23 52.22 91.45c.0313 .25 .0938 .5166 .125 .7823h160.2c.0313-.2656 .0938-.5166 .125-.7823c9.875-33.22 35.69-72.61 52.22-91.45C351.6 260.8 368 220.4 368 175.1C368 78.61 288.9-.2837 191.4 .0132zM192 96.01c-44.13 0-80 35.89-80 79.1C112 184.8 104.8 192 96 192S80 184.8 80 176c0-61.76 50.25-111.1 112-111.1c8.844 0 16 7.159 16 16S200.8 96.01 192 96.01z"/></svg>
</span>
  </span>

  <span
    
      class="dark:text-neutral-300"
    
    ><strong>싱글 테넌트, 멀티 인스턴스</strong>: 이 설정에서는 여러 인프라 인스턴스가 여러 테넌트에 서비스를 제공합니다.</span>
</div>

<p>이 방식의 매력은 결국 격리입니다. 장점 대부분이 여기서 나옵니다. 고객 데이터가 각자 독립된 환경에 있으니 보안 리뷰와 컴플라이언스 이야기가 훨씬 수월해집니다. 배포 사고나 무거운 워크로드가 터져도 전체가 아니라 고객 한 곳만 영향을 받습니다. 인스턴스별로 독립적으로 확장하고 튜닝하고 커스터마이징할 수 있고, 리소스가 전용이라 옆 테넌트가 뭘 하든 내 성능이 흔들리지 않습니다. 유지보수도 고객 단위로 잡을 수 있어서 다운타임이 작고 국소적입니다. 가격 설계만 잘하면 인프라 비용을 테넌트별 실제 사용량에 맞출 수도 있습니다.</p>
<p>대가는 모든 걸 N벌씩 운영하게 된다는 점입니다. 리소스 오버헤드가 쌓이고, 인스턴스 간 설정이 조금씩 어긋나고, 전체 플릿의 버전과 데이터를 맞추는 일 자체가 하나의 프로젝트가 됩니다. 확장, 격리, 비용 관리 문제는 사라지는 게 아니라 늘어납니다.</p>

<h3 class="relative group">멀티 테넌트, 싱글 인스턴스
    <div id="멀티-테넌트-싱글-인스턴스" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%a9%80%ed%8b%b0-%ed%85%8c%eb%84%8c%ed%8a%b8-%ec%8b%b1%ea%b8%80-%ec%9d%b8%ec%8a%a4%ed%84%b4%ec%8a%a4" aria-label="앵커">#</a>
    </span>
    
</h3>

  



<div
  
    class="flex px-4 py-3 rounded-md shadow bg-primary-100 dark:bg-primary-900"
  
  >
  <span
    
      class="text-primary-400 pe-3 flex items-center"
    
    >
    <span class="relative block icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M112.1 454.3c0 6.297 1.816 12.44 5.284 17.69l17.14 25.69c5.25 7.875 17.17 14.28 26.64 14.28h61.67c9.438 0 21.36-6.401 26.61-14.28l17.08-25.68c2.938-4.438 5.348-12.37 5.348-17.7L272 415.1h-160L112.1 454.3zM191.4 .0132C89.44 .3257 16 82.97 16 175.1c0 44.38 16.44 84.84 43.56 115.8c16.53 18.84 42.34 58.23 52.22 91.45c.0313 .25 .0938 .5166 .125 .7823h160.2c.0313-.2656 .0938-.5166 .125-.7823c9.875-33.22 35.69-72.61 52.22-91.45C351.6 260.8 368 220.4 368 175.1C368 78.61 288.9-.2837 191.4 .0132zM192 96.01c-44.13 0-80 35.89-80 79.1C112 184.8 104.8 192 96 192S80 184.8 80 176c0-61.76 50.25-111.1 112-111.1c8.844 0 16 7.159 16 16S200.8 96.01 192 96.01z"/></svg>
</span>
  </span>

  <span
    
      class="dark:text-neutral-300"
    
    ><strong>멀티 테넌트, 싱글 인스턴스</strong>: 단일 소프트웨어 애플리케이션이 여러 테넌트에 서비스를 제공합니다.</span>
</div>

<p>이게 기본값인 데는 이유가 있습니다. 배포도 하나, 백업할 데이터베이스도 하나, 프로덕션 버전도 하나. 리소스 효율이 좋고 운영이 단순하며, 업데이트와 보안 패치가 모든 고객에게 동시에 적용됩니다.</p>
<p>어려운 부분은 공유의 이면입니다. 한 테넌트가 무겁게 쓰면 모두의 성능이 끌려 내려가고, 고객별 커스터마이징에는 한계가 있습니다. 그리고 데이터 격리가 인프라가 아니라 애플리케이션 코드의 책임이 되기 때문에, 버그 하나가 테넌트 간 데이터 유출로 이어질 수 있습니다. 확장성 병목은 모든 고객에게 동시에 오고, &ldquo;이 고객만&quot;이라는 영향 범위가 없어지는 만큼 유지보수에도 더 신경을 써야 합니다.</p>
<p><a href="https://www.scaleway.com/en/blog/saas-multi-tenant-vs-multi-instance-architectures/"  target="_blank" rel="noreferrer">SaaS 솔루션에 대한 자세한 정보 - 멀티 인스턴스 vs. 멀티 테넌트 아키텍처</a></p>

<h2 class="relative group">멀티 테넌트 vs 멀티 인스턴스
    <div id="멀티-테넌트-vs-멀티-인스턴스" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%a9%80%ed%8b%b0-%ed%85%8c%eb%84%8c%ed%8a%b8-vs-%eb%a9%80%ed%8b%b0-%ec%9d%b8%ec%8a%a4%ed%84%b4%ec%8a%a4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>같은 선택을 &lsquo;멀티 테넌트 vs 멀티 인스턴스&rsquo;라고 부르는 경우도 많은데, 같은 것을 반대편에서 본 것뿐입니다. &lsquo;테넌트&rsquo;는 고객 이야기로, 소프트웨어 한 벌을 몇 고객이 함께 쓰느냐입니다. &lsquo;인스턴스&rsquo;는 배포 이야기로, 소프트웨어를 몇 벌 돌리느냐입니다. 싱글 테넌트 제품은 고객마다 한 벌씩 돌리는 멀티 인스턴스 배포이고, 멀티 테넌트 제품은 보통 모두가 함께 쓰는 싱글 인스턴스입니다. 그러니 누군가 멀티 인스턴스를 원한다고 하면, 싱글 테넌트를 원한다는 뜻입니다.</p>
<p>실제로는 두 칸짜리 선택이라기보다 스펙트럼에 가깝고, 진짜 결정은 대부분 데이터베이스에서 내려집니다. 가장 많이 공유하는 쪽은 모든 고객의 행을 같은 테이블에 넣고 <code>tenant_id</code> 컬럼을 둔 다음, 모든 쿼리에서 그걸로 거르는 방식입니다. 운영 비용은 가장 싸고, 실수하기도 가장 쉽습니다. 한 단계 나아가면 공유 데이터베이스 안에 고객마다 스키마를 따로 두는 방식입니다. 분리가 깔끔해지고 고객 단위 백업도 가능하지만, 마이그레이션을 스키마 수만큼 돌려야 합니다. 더 나아가면 애플리케이션은 공유하고 데이터베이스만 고객마다 따로 두는 방식이 있습니다. 고객마다 스택 전체를 돌리지 않고도 데이터를 확실히 분리할 수 있어서 많이 쓰이는 절충안이죠. 반대쪽 끝이 완전한 싱글 테넌트로, 애플리케이션과 데이터베이스 모두 전용입니다. AWS의 SaaS 가이드에서는 양쪽 끝을 사일로(silo)와 풀(pool), 둘을 섞은 것을 브리지(bridge)라고 부르는데, 고객사 보안팀에 구성을 설명할 때 쓰기 좋은 말입니다.</p>

<h2 class="relative group">싱글 테넌트 vs 멀티 테넌트 한눈에 비교
    <div id="싱글-테넌트-vs-멀티-테넌트-한눈에-비교" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8b%b1%ea%b8%80-%ed%85%8c%eb%84%8c%ed%8a%b8-vs-%eb%a9%80%ed%8b%b0-%ed%85%8c%eb%84%8c%ed%8a%b8-%ed%95%9c%eb%88%88%ec%97%90-%eb%b9%84%ea%b5%90" aria-label="앵커">#</a>
    </span>
    
</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>싱글 테넌트(멀티 인스턴스)</th>
					<th>멀티 테넌트(싱글 인스턴스)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>데이터 격리</td>
					<td>고객마다 별도 인프라</td>
					<td>코드나 데이터베이스 정책으로 보장</td>
			</tr>
			<tr>
					<td>고객당 비용</td>
					<td>높음: 고객마다 스택 한 벌</td>
					<td>낮음: 모두가 하나를 공유</td>
			</tr>
			<tr>
					<td>운영</td>
					<td>배포, 업그레이드, 대시보드가 고객 수만큼</td>
					<td>전부 하나씩</td>
			</tr>
			<tr>
					<td>시끄러운 이웃</td>
					<td>없음</td>
					<td>무거운 테넌트 하나가 모두를 느리게 할 수 있음</td>
			</tr>
			<tr>
					<td>커스터마이징</td>
					<td>고객별로 가능, 버전을 달리할 수도 있음</td>
					<td>대부분 설정과 피처 플래그 수준</td>
			</tr>
			<tr>
					<td>릴리스</td>
					<td>고객별로 차례차례</td>
					<td>모두에게 한 번에</td>
			</tr>
			<tr>
					<td>배포 사고의 영향</td>
					<td>고객 하나</td>
					<td>모든 고객</td>
			</tr>
			<tr>
					<td>보안 리뷰와 데이터 위치</td>
					<td>답하기 쉬움</td>
					<td>설명이 더 필요함</td>
			</tr>
	</tbody>
</table>

<h2 class="relative group">언제 무엇을 쓸까
    <div id="언제-무엇을-쓸까" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%96%b8%ec%a0%9c-%eb%ac%b4%ec%97%87%ec%9d%84-%ec%93%b8%ea%b9%8c" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>특별한 이유가 없다면 멀티 테넌트로 가세요. 적은 금액을 내는 고객이 많을 때, 사용자가 직접 가입할 때, 그리고 환경 서른 개를 떠안으면 팀이 버티지 못할 만큼 작을 때 멀티 테넌트가 맞는 기본값입니다.</p>
<p>싱글 테넌트가 그 수고만큼의 값을 하는 건 몇 가지 경우입니다. 의료, 금융, 공공 분야 고객은 데이터를 물리적으로 분리하거나 특정 국가에 두라고 요구하는 경우가 많은데, 보안 리뷰에서 거기에 정직하게 답하는 가장 간단한 방법이 전용 인스턴스입니다. 엔터프라이즈 계약을 맺은 대형 고객 몇 곳이라면 전용 스택 비용을 감당할 수 있고, 애초에 그걸 기대하는 경우도 많습니다. 그리고 고객마다 릴리스 일정을 따로 가져가거나, 버전을 고정하거나, 깊게 커스터마이징해야 한다면, 인스턴스를 나누는 건 오버헤드가 아니라 파는 제품의 일부가 됩니다.</p>
<p>결국 대부분의 팀은 둘 다 하게 됩니다. 거의 모든 고객에게는 멀티 테넌트 제품을, 필요하고 비용도 낼 일부 고객에게는 엔터프라이즈 요금제로 전용 인스턴스를요. 멀티 테넌트 버전에서 처음부터 모든 곳에 테넌트 ID를 달아 두면, 나중에 고객 하나를 전용 데이터베이스로 옮기는 일은 재작성이 아니라 마이그레이션으로 끝납니다.</p>

<h2 class="relative group">멀티 테넌트 격리를 쉽게 깨지지 않게 하기
    <div id="멀티-테넌트-격리를-쉽게-깨지지-않게-하기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%a9%80%ed%8b%b0-%ed%85%8c%eb%84%8c%ed%8a%b8-%ea%b2%a9%eb%a6%ac%eb%a5%bc-%ec%89%bd%ea%b2%8c-%ea%b9%a8%ec%a7%80%ec%a7%80-%ec%95%8a%ea%b2%8c-%ed%95%98%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>멀티 테넌트에서 가장 무서운 버그는 테넌트 필터를 빠뜨린 쿼리입니다. 실패하지도 않습니다. 다른 고객의 데이터를 조용히 돌려줄 뿐이고, 코드 리뷰가 이걸 전부 잡아내지도 못합니다. Postgres를 쓴다면 행 수준 보안(RLS)으로 이 확인을 데이터베이스로 옮길 수 있습니다. 필터를 빠뜨린 쿼리라도 현재 고객의 행만 보이게 되죠.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">invoices</span><span class="w"> </span><span class="n">ENABLE</span><span class="w"> </span><span class="k">ROW</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">SECURITY</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">POLICY</span><span class="w"> </span><span class="n">tenant_isolation</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">invoices</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">USING</span><span class="w"> </span><span class="p">(</span><span class="n">tenant_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">current_setting</span><span class="p">(</span><span class="s1">&#39;app.tenant_id&#39;</span><span class="p">)::</span><span class="n">uuid</span><span class="p">);</span></span></span></code></pre></div></div>
<p>그다음 각 요청이 트랜잭션 안에서 <code>SET LOCAL app.tenant_id = '…'</code>로 테넌트를 지정합니다. 이렇게 하면 커넥션 풀의 연결이 앞 고객의 ID를 다음 요청으로 넘기지 않고, 지정을 깜빡한 요청은 행 대신 오류를 받습니다. 주의할 점은 두 가지입니다. 앱은 테이블 소유자가 아닌 역할로 접속해야 합니다. 소유자는 <code>FORCE ROW LEVEL SECURITY</code>를 추가하지 않는 한 이 정책을 건너뛰거든요. 그리고 슈퍼유저로는 절대 접속하지 마세요. 슈퍼유저는 무조건 건너뜁니다.</p>

<h2 class="relative group">싱글 테넌트, 어디서부터 시작할까
    <div id="싱글-테넌트-어디서부터-시작할까" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8b%b1%ea%b8%80-%ed%85%8c%eb%84%8c%ed%8a%b8-%ec%96%b4%eb%94%94%ec%84%9c%eb%b6%80%ed%84%b0-%ec%8b%9c%ec%9e%91%ed%95%a0%ea%b9%8c" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>싱글 테넌트로 간다면, 이게 &ldquo;관리 가능한 일&quot;이 될지 &ldquo;슬로모션 재난&quot;이 될지는 도구가 결정합니다. 서른 개의 환경을 손으로 돌볼 수는 없으니까요.</p>
<p>핵심은 컨테이너와 Infrastructure as Code입니다. Docker로 모든 테넌트에 동일한 형태로 앱을 패키징하고, Kubernetes로 그 많은 인스턴스의 배포와 확장을 처리합니다. Terraform, Pulumi, AWS CloudFormation으로 테넌트 스택 전체를 코드로 정의해 두면, 31번째 고객 셋업이 주말을 갈아 넣는 일이 아니라 풀 리퀘스트 하나가 됩니다. VM 세계에 더 가깝다면 Ansible, Chef, Puppet 같은 구성 관리 도구가 같은 역할을 해줍니다. CI/CD 파이프라인(Jenkins, GitLab CI/CD)은 평소보다 훨씬 중요해집니다. 모든 릴리스를 일일이 붙어 있지 않고도 전체 인스턴스에 배포해야 하거든요.</p>
<p>그다음부터는 제품의 형태에 달렸습니다. 일부 워크로드는 서버리스(AWS Lambda, Azure Functions)로 서버 계층 자체를 없앨 수 있습니다. Amazon RDS나 Azure SQL Database 같은 관리형 데이터베이스는 테넌트마다 DB를 손으로 관리하는 고행에서 구해줍니다. 모니터링(Prometheus, Grafana, New Relic)은 플릿이 생기는 순간부터 선택이 아닙니다. 어느 인스턴스가 아픈지는 고객이 말해주기 전에 알아야 하니까요. 인증은 Auth0나 Okta 같은 IAM 서비스에 맡기면 테넌트마다 새로 만들 필요가 없습니다. 어떤 언어를 쓰든 이런 배관 작업을 덜어주는 SaaS 지향 프레임워크나 라이브러리가 있을 것이고, 호스팅은 AWS, Azure, Google Cloud Platform 같은 대형 클라우드 어디든 무리 없이 받아줍니다.</p>
<p>첫날부터 이 목록을 전부 살 필요는 없습니다. 컨테이너와 IaC로 시작해서, 테넌트가 쌓이는 만큼 모니터링과 IAM을 더하고, 나머지는 실제로 아픈 순서대로 고르면 됩니다.</p>

<h2 class="relative group">짧게 정리하면
    <div id="짧게-정리하면" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%a7%a7%ea%b2%8c-%ec%a0%95%eb%a6%ac%ed%95%98%eb%a9%b4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>싱글 테넌트, 즉 멀티 인스턴스는 고객마다 애플리케이션과 데이터베이스를 따로 두는 방식입니다. 강한 격리와 고객별 제어를 얻는 대신 운영할 플릿이 생깁니다. 멀티 테넌트는 하나를 모두가 함께 쓰는 방식으로, 저렴하고 단순하지만 격리는 코드로 보장합니다. 대부분의 제품은 멀티 테넌트로 시작해서, 모든 곳에 테넌트 ID를 두고 데이터베이스가 그걸 강제하게 한 뒤, 필요하고 비용도 낼 일부 고객에게만 전용 인스턴스를 주는 게 좋습니다. 싱글 테넌트로 운영한다면 관리 가능한 상태를 지켜주는 건 컨테이너와 Infrastructure as Code입니다.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2024/2024-04-07-singletenantsingleinstance/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
