본문 바로가기
MSA (Micro Service Architecture)/Legacy to Domain-Driven Platform

[MSA 말고 Modular Monolith] 6편 — FastAPI 프로젝트 구조 : Vertical Slice + DDD 적용

by kellis 2026. 7. 29.
반응형

 


이 시리즈는 글쓴이 본인이 수행한 레거시 서비스를 리뉴얼 전환한 과정에서 수행한 의사 결정 사항들을 정리한 글입니다. 
실제 운영 중인 PHP 레거시 에듀테크 플랫폼을 Python + React + K3S 환경으로 전환하는 과정에서 
아키텍처 설계와 인프라 구성에 대한 내용이 포함되어 있습니다. 

 

목차

 

 

 


 

4편과 5편을 통해 인프라 구조를 세웠다. k3s가 돌고 있고, namespace가 나뉘어져 있으며, Treafik이 이 트래픽을 받을 준비가 되었다. 이제 그 위에 올릴 첫번째 실 서비스를 만들 차례다.

그렇다면 코드를 치기 전에 해야할 질문이 있다. "프로젝트 구조를 어떻게 잡을 것인가?"

 

우리는 2편과 3편에서 공들여 8개의 도메인 경계를 그었다. 이 경계가 폴더 구조로, 코드로 내려와야한다. 구조를 잘못 잡으면 애써 그은 경계가 코드에서 뭉개진다. 레거시에서 겪은 "모든 것이 모든것을 참조하는" 레거시 구조가 언어만 Python으로 바뀐 채 재현된다. 

 

이번 글은 그 구조를 잡는 이야기이다. Vertical Slice 방식으로 어떻게 폴더 구조를 잡으면 되는지, 뼈대를 만드는 내용을 담아본다.


 

 

1. 두 가지 구조: Layered vs Vertical Slice

이 글에서 다루는 백엔드 프로젝트 구조는 두 가지다.

 

Layered — 기술 계층으로 자르기

전통적인 방식이다. 같은 기술 역할끼리 묶는다.

app/
├── routers/          # 모든 라우터
│   ├── student.py
│   ├── exam.py
│   ├── payment.py
│   └── ...
├── services/         # 모든 서비스
│   ├── student.py
│   ├── exam.py
│   └── ...
├── models/           # 모든 모델
│   └── ...
└── repositories/     # 모든 저장소
    └── ...

 

Java, Spring Framework 구조로 개발해본 개발자들이라면 

Controller / Service / Repository 패키지 구조를 떠올리면 정확하다. 익숙하고, 작은 프로젝트에서는 문제가 되지 않는다. 

이러한 구조에서의 단점은 코드가 여러 폴더에 흩어져 분산되어 있다는 것이다. 

예를 들어, 

Assessment 도메인의 코드가 4개 폴더로 흩어진다. 시험지 기능을 수정하려면 routers, services, models, repositories 를 오가야 한다. 도메인 경계는 당연히 폴더로는 구현되어 있지 않다. 

 

 

Vertical Slice — 도메인으로 자르기 

반대로 자른다. 같은 도메인끼리 폴더로 묶는다.

app/
└── modules/
    ├── assessment/       # Assessment 도메인의 모든 것
    │   ├── router.py
    │   ├── service.py
    │   ├── models.py
    │   └── repository.py
    ├── learning/         # Learning 도메인의 모든 것
    │   └── ...
    └── ...

 

시험지 기능을 수정하려면 modules/assessment/ 폴더 하나만 보면 된다. 케이크를 위에서 아래로 수직으로 자르듯, 기능 하나가 라우터부터 저장소까지 통째로 한조각(slice)이 된다. 그래서 Vertical Slice이다.

 

 

왜 Vertical Slice인가

이 선택은 단순히 글쓴이의 취향으로 고른 것이 아니다. 

도메인 경계를 코드에서 직접적으로 보이게 만드는 유일한 방법이기 때문이다.

 

폴더 구조가 곧 도메인 지도가 된다. 새로 인력이 충원되더라도 개발자가 modules/ 아래 폴더 목록만 봐도 이 시스템에 어떤 도메인이 있는지 알 수 있다. Assessment 를 고치는 PR이 learning/폴더를 건드리면 그 자체로 경계 위반이 눈에 보인다. 

 

구조가 규율을 강제한다.

 

Modular Monolith 에서 "Modular"의 실체가 바로 이것이다. 배포는 하나지만(Monolith), 코드는 도메인 단위로 격리되어 있다(Modular). 격리가 폴더 수준에서 물리적으로 보여야 그 격리가 유지된다.

 

 

 


 

 

2. 전체 프로젝트 구조

확정된 구조는 이러하다.

platform-api/
├── app/
│   ├── __init__.py
│   ├── main.py                  # FastAPI 진입점 (조립만 담당)
│   ├── shared/                  # 공통 인프라
│   │   ├── __init__.py
│   │   ├── config.py            # 설정 (환경변수)
│   │   ├── database.py          # DB 연결 (이후 편에서)
│   │   └── events.py            # 이벤트 버스 (7편에서)
│   └── modules/                 # ← 8개 Bounded Context
│       ├── __init__.py
│       ├── identity/            # 인증·계정
│       ├── academy/             # 학원·교사·학생·교실
│       ├── content/             # 교재·단원·문항
│       ├── assessment/          # 출제·시험 (Core)
│       ├── learning/            # 학습·채점 (Core)
│       ├── billing/             # 결제·청구·환불
│       ├── notification/        # 알림
│       └── support/             # 공지·FAQ·문의
├── requirements.txt
└── .venv/                       # (git 제외)

 

몇 가지 배치 논리를 짚는다.

 

  • modules/ 가 시스템의 본체다.
    • 8개 도메인이 폴더 8개 그대로 내려왔고, 이름 역시 Bounded Context 그대로이다.
  • shared/는 얇게 유지한다.
    • 공통코드는 편리하나 위험하다. 아무거나 넣기 시작하면 모든 모듈이 shared를 통해 결합된다. 이곳에는 순수하게 기술적인 것(DB연결, 설정, 이벤트 버스)만 둔다. 도메인 로직은 절대 shared에 넣지 않는다.
  • main.py는 조립만 한다. 
    • 비즈니스 로직없이, 각 모듈의 라우터를 모아 등록하는 역할만 한다.

 

각 모듈의 내부 구조

 

모듈 하나를 열어보면 이러하다. 

modules/assessment/
├── __init__.py
├── router.py        # API 엔드포인트 정의
├── schemas.py       # 요청/응답 형태 (Pydantic)
├── service.py       # 유스케이스 로직
├── models.py        # 도메인 모델 (3편의 Aggregate가 여기 구현됨)
└── repository.py    # DB 접근

 

Java, Spring 에 대응하자면 아래와 같다.

router.py        @RestController        엔드포인트
schemas.py       DTO                    요청/응답 객체
service.py       @Service               비즈니스 로직
models.py        @Entity + 도메인 로직    Aggregate, Entity, VO
repository.py    @Repository            데이터 접근

 

즉, 모듈 하나가 작은 애플리케이션 하나의 완전한 구조를 갖는다.

Layered에서 프로젝트 전체에 한 벌 있던 계층이, 여기서는 모듈마다 한벌씩 있다. 이게 Vertical Slice의 "수직으로 자른다" 의 실체이다. 

요청이 흐르는 경로는 모듈안에서 이렇게 움직인다. 

HTTP 요청
  → router.py     (엔드포인트 매칭, schemas로 입력 검증)
  → service.py    (유스케이스 실행)
  → models.py     (도메인 규칙 — Aggregate 불변식)
  → repository.py (저장/조회)
  → 응답 (schemas로 출력 형태 결정)

 

도메인 규칙이 라우터나 서비스에 흩어지지 않고 모델에 모이는 것 — 이게 DDD 전술적 설계가 코드에 안착하는 지점이다. 각 도메인의 실제 구현은 해당 기능을 만드는 편에서 다룬다.

 

 

 


 

3. Python 환경 구성

구조를 만들기 앞서, 개발 환경부터 세팅한다. 

 

venv — 프로젝트 전용 패키지 공간

python -m venv .venv
.venv\Scripts\activate

 

venv는 프로젝트 전용의 격리된 패키지 공간이다. 이를 사용하는 실질적 이유는 requirements.txt의 정확성 때문이다.

전역 Python에 패키지를 설치하면, 나중에 pip freeze로 의존성을 뽑을 때 이 프로젝트와 무관한 패키지(언젠가 설치했던 도구들)까지 다 딸려 나온다. venv 안에서 작업하면 이 프로젝트가 쓰는 것만 정확히 기록된다. 그리고 이러한 기록이 곧 Docker 이미지 빌드에 쓰여진다. 

더보기
Windows PowerShell에서 activate가 실행 정책 오류로 막히면:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

한 번만 설정하면 된다. 로컬 스크립트는 허용하되 인터넷에서 받은 서명 없는 스크립트는 막는 수준이라 안전하다.

 

FastAPI + uvicorn 설치

pip install fastapi "uvicorn[standard]"
pip freeze > requirements.txt

 

각 명령어를 설명하자면 ,


uvicorn - FastAPI는 Spring Framework처럼 요청을 어떻게 처리할지 정의하는 프레임워크일 뿐이다. 실제로 포트를 열고 HTTP를 받아주는 서버가 따로 필요한데 이것이 uvicorn이다. Spring의 embeded tomcat 과 대응된다고 생각하면 된다.
[standard] - uvicorn의 성능 옵션 묶음이다. 이벤트 루프의 C 구현(uvloop), 빠른 HTTP 파서(httptools), 웹소켓 등이 포함된다. 실무 기본 옵션들이라고 보면 된다.

Spring Boot                                             FastAPI
내장 Tomcat (서블릿 컨테이너)   =    uvicorn (ASGI 서버)
Spring MVC (프레임워크)          =    FastAPI (프레임워크)
서블릿 스펙 (표준 인터페이스)    =    ASGI (표준 인터페이스)

 

pip freeze > requirements.txt - 설치된 패키지를 버전까지 파일로 박제하는 명령이다. Spring의 build.gradle dependencies 와 대응되며, 이후 Docker 빌드에서 pip install -r requirements.txt 로 동일 환경을 재현한다. 패키지를 새로 설치할 때마다 freeze를 다시 떠서 갱신해야 한다. 이를 수행하지 않으면 "내 로컬 피씨에서는 동작하는데 컨테이너에서는 안 된다"는 문제가 발생한다.

 

 


 

4. 뼈대 만들기

 

폴더 구조 생성

 

8개 모듈 폴더와 내부 파일들을 만든다.

mkdir app, app\shared, app\modules
New-Item app\__init__.py, app\main.py, app\shared\__init__.py, app\shared\config.py

$domains = "identity","academy","content","assessment","learning","billing","notification","support"
foreach ($d in $domains) {
  mkdir app\modules\$d
  New-Item app\modules\$d\__init__.py, app\modules\$d\router.py, app\modules\$d\service.py, app\modules\$d\models.py, app\modules\$d\repository.py, app\modules\$d\schemas.py
}
New-Item app\modules\__init__.py

 

 

 

모듈 라우터 — APIRouter

 

각 모듈은 자기 엔드포인트를 자기 폴더 안에서 정의한다. FastAPI의 APIRouter가 그 도구이다. Identity 도메인 모듈을 예로 들면,

# app/modules/identity/router.py
from fastapi import APIRouter

router = APIRouter(prefix="/identity", tags=["Identity"])


@router.get("/ping")
def ping():
    return {"module": "identity", "status": "ok"}

 

APIRouter는 모듈 단위의 미니 앱이다. Spring으로 대응하자면 

@RequestMapping("/identity")가 붙은 컨트롤러 클래스다.

 

prefix 덕에 이 모듈의 모든 경로는 /identity/... 아래에 묶이고, tags 덕에 API 문서에서 도메인별로 그룹핑된다. 

현재는 확인용 /ping 메서드만 만들어둔 상태이다. 

 

 

main.py — 조립만 한다

# app/main.py
from fastapi import FastAPI

from app.modules.identity.router import router as identity_router
from app.modules.academy.router import router as academy_router
from app.modules.content.router import router as content_router
from app.modules.assessment.router import router as assessment_router
from app.modules.learning.router import router as learning_router
from app.modules.billing.router import router as billing_router
from app.modules.notification.router import router as notification_router
from app.modules.support.router import router as support_router

app = FastAPI(
    title="Platform API",
    description="에듀테크 플랫폼 통합 API — DDD Modular Monolith",
    version="0.1.0",
)

# 도메인 모듈 라우터 등록 (8개 Bounded Context)
app.include_router(identity_router)
app.include_router(academy_router)
app.include_router(content_router)
app.include_router(assessment_router)
app.include_router(learning_router)
app.include_router(billing_router)
app.include_router(notification_router)
app.include_router(support_router)


@app.get("/health")
def health_check():
    return {"status": "healthy"}

 

 

main.py에는 비즈니스 로직이 한 줄도 없다. 모듈들을 가져와 등록하는 조립 코드 뿐이다. 이 파일만 봐도 시스템에 어떤 도메인이 있는지 한 눈에 볼 수 있다.

 

/health 는 나중에 k3s가 이 Pod가 살아있는지 체크하고자 할때 (liveness probe) 사용할 헬스체크 엔드포인트이다. 

 

 


 

 

5. 실행 — 8개 도메인이 뜨는 순간

 

이제 서버를 띄워보자.

uvicorn app.main:app --reload

 

app.main:app은 "app/main.py의 app 변수(FastAPI 인스턴스)를 실행하라"는 뜻이고, --reload는 코드 수정 시 자동 재시작하는 개발용 옵션이다.

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Application startup complete.

 

브라우저에서도 동일하게 확인할 수 있다.

http://127.0.0.1:8000/identity/ping → {"module":"identity","status":"ok"}
http://127.0.0.1:8000/health → {"status":"healthy"}

 

참고로 FastAPI는 API 문서를 자동으로 생성해준다.

http://127.0.0.1:8000/docs

 

Swagger UI문서가 기본 내장되어 있기 때문에 도메인 태그별로 분류된 문서가 자동으로 생성된다. 

 

 

 

 

 


 

마치며

 

이번 글에서 한 일들은 이러하다.

 

✓  Layered vs Vertical Slice 비교 — 도메인 경계가 보이는 구조 선택

✓  전체 폴더 구조 확정 (modules/ 8개 + shared/ 최소화)

✓  모듈 내부 구조 정의 (router/schemas/service/models/repository)

✓  Python 환경 구성 (venv, FastAPI + uvicorn, requirements.txt)

✓  8개 도메인 모듈 뼈대 생성, APIRouter로 main.py에 조립

  서버 실행, /docs에서 8개 Bounded Context 확인  

 

중요 한 것은 

폴더 구조가 곧 도메인 지도다.
modules/assessment가 곧 Assessment Bounded Context다.

main.py는 조립만 한다. 도메인 로직은 각 모듈 안에, shared는 기술 인프라만.

 

 

 

다음 글에서는 이 뼈대에 실제 기능을 올려볼 것이다. 모든 기능을 다루지는 않을 것이고, Notification 도메인의 실시간 알림 기능을 예시로 살펴보겠다. FastAPI의 SSE(Server-Sent Events) 와 Redis Streams로, 구현한다. 이벤트 흐름이 실제로 흐르는 것을 볼 것 이다. 

 

 

 

 

**참고 자료

 

FastAPI 공식 문서: https://fastapi.tiangolo.com/

 

FastAPI - FastAPI

FastAPI FastAPI framework, high performance, easy to learn, fast to code, ready for production Documentation: https://fastapi.tiangolo.com Source Code: https://github.com/fastapi/fastapi FastAPI is a modern, fast (high-performance), web framework for build

fastapi.tiangolo.com

 

FastAPI Bigger Applications (APIRouter): https://fastapi.tiangolo.com/tutorial/bigger-applications/

 

Bigger Applications - Multiple Files - FastAPI

FastAPI Learn Tutorial - User Guide Bigger Applications - Multiple Files If you are building an application or a web API, it's rarely the case that you can put everything in a single file. FastAPI provides a convenience tool to structure your application w

fastapi.tiangolo.com

 

Vertical Slice Architecture (Jimmy Bogard): https://www.jimmybogard.com/vertical-slice-architecture/

 

Vertical Slice Architecture

Many years back, we started on a new, long term project, and to start off with, we built the architecture around an onion architecture. Within a couple of months, the cracks started to show around this style and we moved away from that architecture and tow

www.jimmybogard.com

 

 

 

 

 

 

 

반응형

댓글