infra
Platform

모듈 맵

[Database] 3-Tier, MSA, CQRS 아키텍처에서의 최적의 DB 역할 분담

0 / 37 완료

펼치기
0 / 37 완료0%

Database · 10 / 37

[Database] 3-Tier, MSA, CQRS 아키텍처에서의 최적의 DB 역할 분담

소프트웨어 아키텍처에서 데이터베이스가 어떤 위치와 역할을 담당하는지 실무 관점에서 이해합니다

🚨INCIDENT ALERT
HIGH

백엔드 장애를 보면 코드보다 DB 경계가 흐릿해서 생기는 문제가 많습니다. 어떤 데이터는 트랜잭션이 필요하고, 어떤 데이터는 캐시나 검색엔진으로 분리해야 합니다. 아키텍처 안에서 DB의 역할을 이해해야 무리한 설계를 피할 수 있습니다.

이번 챕터에서 배울 것

아키텍처 패턴마다 DB를 다루는 방식이 다릅니다. 전통적인 3-Tier부터 현대적인 MSA와 CQRS까지, 각 패턴에서 DB의 역할을 이해하면 더 나은 설계 결정을 내릴 수 있습니다.

  • 13-Tier 아키텍처에서 DB 계층의 역할과 위치
  • 2MSA의 DB-per-Service 패턴과 공유 DB의 문제점
  • 3CQRS 패턴으로 읽기와 쓰기 분리하기
  • 4채용공고에서 요구하는 DB 관련 아키텍처 역량

아키텍처에서의 DB 역할 — 3-Tier, MSA, CQRS 패턴

소프트웨어를 구축할 때 데이터베이스는 단순히 데이터를 저장하는 창고가 아닙니다. 어떤 아키텍처 패턴을 선택하느냐에 따라 DB가 놓이는 위치, 접근 방식, 그리고 서비스 간 관계가 근본적으로 달라집니다. 이 모듈에서는 3-Tier, MSA, CQRS라는 세 가지 핵심 아키텍처 패턴에서 DB가 어떤 역할을 하는지, 특히 DB 역할에 초점을 맞춰 살펴봅니다.


💡개념

3-Tier와 MSA에서의 DB 위치 — 어디에 어떻게 붙는가

프론트엔드에서 직접 DB에 쿼리를 날리는 코드를 발견했습니다. 또 다른 팀의 마이크로서비스가 우리 팀의 DB 테이블을 직접 읽고 있습니다. 아무도 스키마를 함부로 바꿀 수 없는 상황이 됩니다. DB가 아키텍처의 어디에 위치해야 하는지, 어느 계층만 접근해야 하는지 이해하지 못하면 이런 결합 문제가 시스템 전체로 퍼집니다.

3-Tier와 MSA에서의 DB 위치 — 어디에 어떻게 붙는가

3-Tier 아키텍처: DB는 비즈니스 계층만 접근한다

3-Tier 아키텍처는 소프트웨어를 세 개의 독립적인 계층으로 나누는 가장 보편적인 패턴입니다. 프론트엔드 코드에서 SELECT * FROM users WHERE id = ?를 직접 날리거나, 반대로 DB가 UI 로직을 포함하면 보안 구멍이 생기고 코드 변경이 전체 스택에 파급됩니다. 3-Tier는 각 계층이 자신의 역할만 담당하도록 강제해 이 문제를 근본적으로 차단합니다.

┌─────────────────────────────────────┐
│  Presentation Tier (프레젠테이션)    │  ← 브라우저, 모바일 앱
│  React, Vue, Angular                 │
└──────────────┬──────────────────────┘
               │ HTTP/HTTPS
┌──────────────▼──────────────────────┐
│  Business Logic Tier (비즈니스)      │  ← API 서버
│  Node.js, Spring Boot, Django        │
└──────────────┬──────────────────────┘
               │ SQL / ORM
┌──────────────▼──────────────────────┐
│  Data Tier (데이터)                  │  ← DB 서버
│  PostgreSQL, MySQL, Oracle           │
└─────────────────────────────────────┘

각 계층의 역할에서 핵심은 DB 접근 경로입니다. 프레젠테이션 계층은 DB와 직접 통신하지 않고, 비즈니스 로직 계층만이 SQL을 실행하고 결과를 가공합니다. 데이터 계층은 오직 비즈니스 계층만 접근합니다. 프론트엔드가 DB에 직접 쿼리를 날리는 구조는 보안과 유지보수 관점에서 심각한 문제를 만듭니다.

계층DB 역할DB와의 관계
프레젠테이션데이터 표시직접 접근 금지
비즈니스 로직쿼리 실행, 결과 가공유일한 접근 주체
데이터영구 저장소수동적 응답자

MSA에서의 DB-per-Service 패턴

마이크로서비스 아키텍처(MSA)에서는 각 서비스가 자신만의 데이터베이스를 소유합니다. DB가 서비스 내부에 캡슐화되는 것이 핵심입니다. User Service와 Order Service가 같은 PostgreSQL을 공유하면, users 테이블에 컬럼을 추가할 때 Order Service 코드도 함께 수정·배포해야 합니다. MSA가 약속하는 독립 배포가 불가능해지는 것입니다. DB-per-Service는 이 결합을 근본적으로 끊습니다.

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│ User Service │    │Order Service│    │Product Svc  │
│             │    │             │    │             │
│  ┌────────┐ │    │  ┌────────┐ │    │  ┌────────┐ │
│  │ Users  │ │    │  │ Orders │ │    │  │Products│ │
│  │   DB   │ │    │  │   DB   │ │    │  │   DB   │ │
│  └────────┘ │    │  └────────┘ │    │  └────────┘ │
└─────────────┘    └─────────────┘    └─────────────┘
     PostgreSQL          MySQL             MongoDB

DB-per-Service 패턴의 핵심 장점은 세 가지입니다. 첫째, 독립적 배포—User Service의 스키마를 변경해도 Order Service에 영향이 없습니다. 둘째, 기술 선택 자유—제품 카탈로그는 MongoDB, 주문은 PostgreSQL, 세션은 Redis를 선택할 수 있습니다. 셋째, 독립적 스케일링—주문 서비스의 DB만 따로 스케일 아웃할 수 있습니다.

공유 DB 패턴의 문제점

MSA에서 여러 서비스가 하나의 DB를 공유하면 DB가 서비스 간 강한 결합의 원인이 됩니다.

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│ User Service │    │Order Service│    │Product Svc  │
└──────┬──────┘    └──────┬──────┘    └──────┬──────┘
       │                  │                  │
       └──────────────────┼──────────────────┘
                          │
                  ┌───────▼───────┐
                  │   공유 DB      │  ← 이게 문제!
                  │  PostgreSQL   │
                  └───────────────┘

Order Service가 users 테이블을 직접 JOIN하면, users 테이블의 컬럼 이름을 바꿀 때 Order Service도 함께 수정해야 합니다. 스키마 변경 시 모든 관련 서비스를 동시에 배포해야 하고, 공유 DB가 다운되면 모든 서비스가 동시에 장애를 겪습니다.

실무 팁: MSA 초기에는 공유 DB로 시작해서 서비스가 안정되면 분리하는 "Strangler Fig" 전략을 쓰는 경우도 많습니다. 처음부터 완벽한 분리를 강요하면 오히려 개발 속도가 느려질 수 있습니다.

API Gateway 패턴과 DB

MSA에서 클라이언트는 각 서비스에 직접 접근하지 않고 API Gateway를 거칩니다.

Client → API Gateway → User Service    → Users DB
                    → Order Service   → Orders DB
                    → Product Service → Products DB

API Gateway는 라우팅, 인증, 속도 제한을 담당하며 DB에는 직접 접근하지 않습니다. 여러 서비스의 데이터를 조합해야 할 때는 API Gateway가 각 서비스를 호출해 결과를 합칩니다(Backend for Frontend 패턴).

User Service와 Order Service가 같은 PostgreSQL 인스턴스의 테이블에 직접 SQL로 접근하면, 두 서비스는 스키마 수준에서 강하게 결합됩니다. users 테이블에 컬럼을 추가하거나 이름을 바꾸면 Order Service의 쿼리도 깨집니다. 독립 배포라는 MSA의 핵심 장점이 사라집니다.

해결책은 각 서비스가 자신의 DB만 소유하고, 다른 서비스의 데이터가 필요하면 반드시 해당 서비스의 API를 통해 요청하는 것입니다. 데이터 접근이 아닌 서비스 계약(API)을 통해 통신해야 MSA의 독립성이 유지됩니다.

💡개념

CQRS 패턴 — 읽기와 쓰기 DB를 분리하는 이유

대시보드용 집계 쿼리가 느려서 인덱스를 추가했는데, 오히려 주문 생성 API의 INSERT가 느려졌습니다. 읽기와 쓰기가 같은 DB에 있어서 최적화 방향이 서로 충돌합니다. CQRS는 이 문제를 쓰기 모델과 읽기 모델을 분리해서 각각 독립적으로 최적화하는 패턴으로 해결합니다.

CQRS 패턴 — 읽기와 쓰기 DB를 분리하는 이유

CQRS란 무엇인가

CQRS(Command Query Responsibility Segregation)는 데이터를 **변경하는 Command(명령)**와 데이터를 **조회하는 Query(질의)**의 처리 경로를 완전히 분리하는 패턴입니다. 2010년 Greg Young이 체계화했으며, 현재는 대형 서비스에서 보편적으로 사용됩니다. 주문 목록 API가 매번 users, orders, order_items, products를 JOIN하는 무거운 쿼리를 실행하는데 쓰기는 드물고 읽기가 폭발적으로 많은 상황을 생각해보세요. 정규화된 단일 DB 하나로는 한계가 옵니다. CQRS는 읽기와 쓰기 경로를 아예 다른 DB로 분리해 이 문제를 해결합니다.

         ┌─────────────────────────────────────┐
         │           애플리케이션                │
         └────────────┬──────────┬─────────────┘
                      │          │
              ┌───────▼───┐  ┌───▼────────┐
              │ Command    │  │  Query     │
              │ Handler    │  │  Handler   │
              └───────┬───┘  └───┬────────┘
                      │          │
              ┌───────▼───┐  ┌───▼────────┐
              │ Write DB  │  │  Read DB   │
              │(정규화된) │  │(비정규화)  │
              │PostgreSQL │  │PostgreSQL  │
              └───────────┘  └────────────┘
                      │  동기/비동기 복제
                      └──────────────────►

Command 모델: 쓰기에 최적화

Write DB는 데이터 무결성에 집중합니다. 정규화된 구조로 외래키와 제약조건을 엄격하게 적용합니다. Command는 CreateOrder, UpdateOrderStatus, CancelOrder처럼 의도를 명확히 표현하는 이름을 사용합니다.

SQL
CREATE TABLE orders (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id     UUID NOT NULL REFERENCES users(id),
    status      VARCHAR(20) NOT NULL DEFAULT 'pending',
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE order_items (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_id    UUID NOT NULL REFERENCES orders(id),
    product_id  UUID NOT NULL REFERENCES products(id),
    quantity    INT NOT NULL CHECK (quantity > 0),
    unit_price  NUMERIC(10, 2) NOT NULL
);
OUTPUT
실행 완료 또는 조회 결과가 표시됩니다.
🔍실행 후 확인할 것
  • 책임 분리DB, 캐시, 검색엔진이 각각 맡는 데이터가 명확한지 확인합니다.
  • 정합성 요구트랜잭션이 필요한 경로와 eventual consistency가 가능한 경로를 구분합니다.
  • 장애 영향저장소 하나가 느려졌을 때 API 전체가 막히는지 봅니다.

Query 모델: 읽기에 최적화

Read DB는 조회 성능에 집중합니다. 정규화를 포기하고 조회에 필요한 데이터를 미리 합쳐 놓습니다. 이 구조에서는 복잡한 JOIN 없이 단순 SELECT 한 번으로 주문 목록 화면을 구성할 수 있습니다.

SQL
CREATE TABLE order_summary_view (
    order_id        UUID PRIMARY KEY,
    user_name       VARCHAR(100),
    user_email      VARCHAR(255),
    status          VARCHAR(20),
    total_amount    NUMERIC(10, 2),
    item_count      INT,
    product_names   TEXT[],
    created_at      TIMESTAMPTZ
);

Write DB에서 Read DB로 데이터 동기화

두 DB를 동기화하는 방식은 크게 세 가지입니다.

이벤트 기반 동기화는 Write DB에서 변경이 발생하면 이벤트를 발행하고 Event Bus를 거쳐 Read DB를 업데이트합니다. 가장 유연하고 권장되는 방식입니다.

PostgreSQL Logical Replication은 DB 수준에서 변경 내용을 스트리밍합니다. Publisher(Write DB)와 Subscriber(Read DB)를 아래와 같이 설정합니다.

SQL
CREATE PUBLICATION orders_pub FOR TABLE orders, order_items;

CREATE SUBSCRIPTION orders_sub
    CONNECTION 'host=write-db ...'
    PUBLICATION orders_pub;

Materialized View는 단일 DB 내에서 조회용 스냅샷을 주기적으로 갱신합니다. 별도 인프라 없이 시작하기 좋은 방식입니다.

SQL
CREATE MATERIALIZED VIEW order_summary AS
SELECT
    o.id AS order_id,
    u.name AS user_name,
    u.email AS user_email,
    o.status,
    SUM(oi.quantity * oi.unit_price) AS total_amount,
    COUNT(oi.id) AS item_count,
    o.created_at
FROM orders o
JOIN users u ON o.user_id = u.id
LEFT JOIN order_items oi ON o.id = oi.order_id
GROUP BY o.id, u.name, u.email, o.status, o.created_at;

REFRESH MATERIALIZED VIEW CONCURRENTLY order_summary;

CQRS를 언제 적용해야 하는가

CQRS는 강력하지만 복잡성을 크게 높입니다. 다음 기준으로 적용 여부를 판단하세요.

조건CQRS 도입 고려
읽기/쓰기 비율이 100:1 이상O
쓰기는 적지만 조회 쿼리가 매우 복잡O
읽기와 쓰기의 스케일링 요구가 다름O
팀 규모가 작고 트래픽이 낮음X
데이터 일관성이 최우선X (Eventual Consistency 수용해야 함)

채용공고에서 "CQRS/Event Sourcing 경험 우대", "Read Replica 운영 경험", "MSA 환경에서 데이터 정합성 관리 경험" 같은 표현이 보이면 이 패턴들을 이해하고 있음을 보여줄 수 있어야 합니다.

💼
실무 맥락커머스 플랫폼에서 주문 목록 API가 느려서 CQRS 패턴으로 읽기 성능을 개선한 사례
현업 패턴

주문 목록 조회 API에서 orders, users, order_items, products 테이블을 매번 JOIN하는 쿼리가 100ms 이상 걸렸습니다. 주문 생성(쓰기)은 하루 수천 건이지만 주문 조회(읽기)는 초당 수백 건으로 비율 차이가 컸습니다.

CQRS를 적용해 Write DB는 정규화 구조를 유지하고, 주문 생성/변경 이벤트가 발생할 때마다 Read DB의 order_summary_view 테이블을 비동기로 업데이트했습니다. 조회 쿼리는 단순 SELECT * WHERE user_id = $1로 단순화됐고 응답 시간이 100ms에서 5ms로 줄었습니다. 작은 팀에서 전체 CQRS 인프라를 구축하기 어렵다면, PostgreSQL의 Materialized View와 REFRESH CONCURRENTLY로 시작하는 것도 좋은 선택입니다.

다음 모듈에서는 DBeaver, TablePlus, psql 등 실무에서 자주 쓰는 DB 클라이언트 도구 활용법을 다룹니다.

지식 확인

퀴즈 — 5문제

Q1

API 서버 100대가 각각 DB에 직접 연결하면 발생하는 가장 심각한 문제는?

Q2

결제 서비스가 사용자 서비스의 users 테이블을 직접 JOIN해 사용하다가, 사용자 서비스팀이 컬럼 이름을 바꾸면서 결제 서비스가 갑자기 오류났다. 이 문제의 근본 원인과 MSA 해결책은?

Q3

소셜 피드 서비스의 '게시글 작성'은 초당 100건이지만 '피드 조회'는 초당 10,000건이다. 단일 DB로는 조회 부하를 감당하기 어려워서 읽기를 분리하려 한다. 어떤 패턴이 적합한가?

Q4

MSA에서 공유 DB 패턴의 가장 큰 문제점은 무엇입니까?

Q5

CQRS에서 Read DB를 별도로 두는 주된 이유는?

0 / 5 답변

🧪 실습으로 확인하기

PostgreSQL 설치 및 기본 설정

초급

Ubuntu 서버에 PostgreSQL을 설치하고, 데이터베이스와 사용자를 생성한 뒤 외부 접속이 가능하도록 설정한다.

40📋 5단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

database중급 · 60
[Database] INNER, LEFT, RIGHT, FULL JOIN의 최적화 실행 조건
Database 트랙 계속
linux입문 · 30
[Linux] 개발자가 왜 리눅스 서버와 커맨드라인을 반드시 배워야 하는가
Linux 트랙 시작점