
생성 수정
IBM * 레드햇 AX 과정 · 11강
설명서와 실제 제품 — 아키텍처가 코드로 내려앉는 자리
2026년 6월 1일(월) ~ 6월 5일(금)
이 장을 다 읽으면
- 추상(Abstraction)과 구상(Concretion)을 "설명서와 실제 제품"으로 정의하고, 왜 추상은 메모리에 올라갈 수 없는지 설명할 수 있다.
- 헥사고날 아키텍처의 일곱 칸(Front end / inbound / input / use case / output / outbound / database)을 표준 명칭·역할·실제 구현 컴포넌트로 각각 말할 수 있다.
- 이 구조가 주는 세 가지 이점(테스트 용이성·기술 교체 유연성·유지보수성)을 각각 "무엇이 없기 때문에 가능한가"로 설명할 수 있다.
- gateway가 왜 Facade 패턴인지, Route(inbound 어댑터)와 무엇이 다른지 구별할 수 있다.
- 책임 연쇄 패턴(Chain of Responsibility)을 아키텍처의 세 지점에 어떻게 붙이는지 말하고, 그 대가로 무엇을 잃는지 안다.
Router → (Schema) → Interactor → (DTO) → Repository → (ORM Entity) → DB흐름에서 경계마다 데이터의 형태가 바뀌는 이유를 설명할 수 있다.- 제어의 흐름과 의존성의 방향이 서로 반대일 수 있다는 것을 그림으로 그릴 수 있다.
- 파이썬에서 상속받을 수 있는 클래스의 종류를 분류하고,
bool을 상속할 수 없는 이유를 안다. - Pydantic 2.0의 네 가지 모델과 1.x에서 바뀐 표기법을 구별할 수 있다.
walter: WalterRoasterUseCase = WalterRoasterInteractor()한 줄이 왜 DIP와 OCP를 동시에 위반하는지, 어떻게 고치는지 말할 수 있다.
0. 이 장의 판별 축
앞 장들과 같습니다. 새 용어를 만나면 비유를 찾기 전에 아래 축 중 어디에 꽂히는지부터 정하세요.
| # | 축 | 왼쪽 (0) | 오른쪽 (1) | 이 축으로 갈리는 것 |
|---|---|---|---|---|
| 1 | 추상인가 구상인가 | 추상 (Port·설명서) | 구상 (Adapter·실제 제품) | 인터페이스 ↔ 구현체 / UseCase ↔ Interactor / Repository ↔ PgRepository |
| 2 | 들어오는가 나가는가 | 진입 (Inbound·Driving) | 진출 (Outbound·Driven) | Router ↔ Repository / input 포트 ↔ output 포트 |
| 3 | 흐름인가 방향인가 | 제어의 흐름 (실행 순서) | 의존성의 방향 (누가 누구를 아는가) | 안쪽으로 흐르는 요청 ↔ 안쪽을 향하는 화살표 |
| 4 | 누가 만드는가 | 직접 생성 (= Interactor()) | 주입받기 (Depends(...)) | 강한 결합 ↔ 느슨한 결합 / OCP 위반 ↔ 준수 |
| 5 | 한 곳인가 여럿인가 | 한 곳에 몰기 (Facade) | 체인으로 넘기기 (CoR) | gateway ↔ Handler 체인 |
축 1이 이 장의 중심입니다. 나머지 네 축은 전부 축 1에서 파생됩니다. 추상과 구상을 나눴기 때문에 진입과 진출을 따로 이름 붙일 수 있고(축 2), 추상을 사이에 끼웠기 때문에 흐름과 방향이 반대가 될 수 있고(축 3), 추상만 알기 때문에 밖에서 주입받을 수 있고(축 4), 추상이 있기 때문에 창구를 하나로 모으거나 여럿으로 늘려도 안쪽이 안 바뀝니다(축 5).
앞 장에서 이어집니다 5주차에서 SOLID 다섯 원칙, 포트 & 어댑터, 퍼사드, 그리고 "DIP는 Dependency Injection이 아니라 Dependency Inversion(의존 역전)" 이라는 정정을 배웠습니다. 3주차에서는 전략 패턴·DIP·IoC·DI·포트&어댑터가 같은 하나의 다섯 가지 이름이라는 것을 배웠습니다. 이 장은 그 원칙들이 실제 파이썬 파일과 폴더로 내려앉는 자리입니다. 이 장에서 원칙은 더 이상 원칙이 아니라 파일명이 됩니다.
1. 추상과 구상 — 설명서와 실제 제품
1.1 정의
이 장 전체가 이 한 쌍 위에 서 있습니다. 먼저 유(類)와 종차(種差)로 정의합니다.
- 추상 (Abstraction) — [코드로 쓰인 약속]인데, ["무엇을 할 것인가"만 정하고 "어떻게"는 비워둔다]는 점에서 구상과 구별된다.
- 구상 (Concretion) — [코드로 쓰인 약속]인데, ["어떻게 할 것인가"를 실제로 채워 넣어 실행 가능하다]는 점에서 추상과 구별된다.
기억 보조용 비유는 그다음입니다.
추상은 설명서, 구상은 실제 제품입니다.
| 추상 (Abstraction) | 구상 (Concretion) | |
|---|---|---|
| 답하는 질문 | "무엇을 할 것인가" (What) | "어떻게 할 것인가" (How) |
| 정체 | 설명서 · 설계도 | 실제 제품 · 완성품 |
| 코드 형태 | 인터페이스, 추상 클래스(ABC) | 일반 구현 클래스 |
| 인스턴스 생성 | 불가능 | 가능 |
| 메모리에 올라가는가 | ❌ 실체가 없어 올릴 수 없음 | ✅ 객체로 살아 움직임 |
| 헥사고날 이름 | 포트 (Port) | 어댑터 (Adapter) |
| 이 수업의 파일명 예 | james_use_case.py, james_repository.py | james_command.py, james_director_pg_repository.py |
1.2 왜 추상은 메모리에 올라갈 수 없는가
3주차의 디스크와 메모리 축이 여기서 다시 쓰입니다.
- 설명서에는 "커피를 볶는다"라고만 적혀 있습니다. 실제로 볶을 손이 없습니다.
- 그래서 추상 클래스는 디스크에
.py파일로는 존재하지만, 메모리에 인스턴스로 세울 수는 없습니다. - 파이썬에서 추상 클래스를 억지로
()로 호출하면TypeError: Can't instantiate abstract class ...가 납니다.
from abc import ABC, abstractmethod
class WalterRoasterUseCase(ABC): # 설명서
@abstractmethod
def introduce_myself(self, schema): ...
WalterRoasterUseCase() # ❌ TypeError — 설명서는 커피를 볶지 못한다
설명서를 아무리 오래 들여다봐도 커피는 나오지 않습니다. 누군가 그 설명서대로 만든 실제 기계가 있어야 합니다. 그 기계가 구상이고, 이 수업에서는
Interactor(유스케이스 구현체) 또는PgRepository(저장소 구현체)라는 이름을 갖습니다.
1.3 파이썬에서 추상을 만드는 법 — ABC
파이썬에는 자바의 interface 키워드가 없습니다. 대신 표준 라이브러리 abc(Abstract Base Class) 모듈을 씁니다.
from abc import ABC, abstractmethod
class JamesDirectorUseCase(ABC): # ← 입력 포트 (Inbound Port)
@abstractmethod
def register(self, command) -> None:
"""탑승자 정보를 등록한다. 어떻게 할지는 여기서 정하지 않는다."""
class JamesDirectorRepository(ABC): # ← 출력 포트 (Outbound Port)
@abstractmethod
def save(self, command) -> None:
"""어딘가에 저장한다. 어디에 저장할지는 여기서 정하지 않는다."""
ABC 는 Abstract Base Class 의 약자입니다. 3주차의 "Base = 기초가 아니라 거점·저장소" 규칙이 여기에도 적용됩니다.
ABC 는 "기초 추상 클래스"가 아니라 "추상의 거점이 되는 클래스" 라고 읽어야 정확합니다. 다른 클래스들이 여기서 출발합니다.
1.4 추상 클래스의 메서드에도 self 를 붙인다
한동안 "추상 메서드는 몸통이 없으니 self 를 생략해도 되지 않느냐"는 주장이 돌았습니다. 이 수업에서는 붙이는 쪽으로 통일합니다.
이유는 하나, 상속 때문입니다.
- 추상 메서드는 결국 자식 클래스가 오버라이딩(재정의) 합니다.
- 자식의 구현 메서드는 인스턴스 메서드이므로 반드시
self를 받습니다. - 부모(추상)와 자식(구상)의 시그니처(매개변수 모양)가 어긋나면 도구·타입 체커가 경고를 냅니다.
- 설명서와 제품의 규격이 어긋나면 안 됩니다. 그래서 설명서 쪽에도 똑같이 씁니다.
class WalterRoasterUseCase(ABC):
@abstractmethod
def introduce_myself(self, schema): ... # ← self 를 붙인다
class WalterRoasterInteractor(WalterRoasterUseCase):
def introduce_myself(self, schema): # ← 시그니처가 정확히 일치
print(f"[Interactor] {schema}")
1.5 인스턴스명 : 추상타입 = 구상생성자() 라는 표기
이 수업에서 추상 클래스로 만든 객체를 선언할 때 쓰기로 한 형식입니다.
walter : WalterRoasterUseCase = WalterRoasterInteractor()
# └── 왼쪽: 추상 타입 ──┘ └── 오른쪽: 구상 생성자 ──┘
이 표기의 의도는 좋습니다.
- 왼쪽(타입 힌트) 은 "이 변수는 설명서 규격으로만 다룬다"는 선언입니다.
- 그래서 아래 코드에서는
WalterRoasterUseCase에 정의된 메서드만 부를 수 있습니다. - 구현체가 자기 마음대로 추가한 메서드를 실수로 부르는 일이 원천 차단됩니다.
⚠️ 하지만 이 한 줄은 이 장의 마지막에서 "잘못된 코드"의 대표 사례로 다시 등장합니다. 왼쪽이 아니라 오른쪽이 문제입니다. 왜 문제인지는 9장에서 해부합니다.
앞으로 다시 만납니다 9장 「실전 — DIP·OCP 위반 한 줄 해부」에서 이 표기가 어떻게 바뀌어야 하는지 봅니다. 결론만 미리 말하면, 왼쪽은 그대로 두고 오른쪽을 지웁니다. 오른쪽은 밖에서 넣어줍니다.
1.6 추상 / 구상 판별 훈련
이름만 보고 판별하는 연습입니다. 이 수업의 네이밍 규칙이 그대로 판별 기준이 됩니다.
| 이름 | 추상? 구상? | 근거 |
|---|---|---|
WalterRoasterUseCase | 추상 | UseCase 접미사 = 입력 포트 = 설명서 |
WalterRoasterInteractor | 구상 | Interactor 접미사 = 유스케이스 구현체 |
JamesDirectorRepository | 추상 | Repository 접미사 = 출력 포트 |
JamesDirectorPgRepository | 구상 | Pg(PostgreSQL) 라는 기술 이름이 붙는 순간 구상 |
JamesDirectorSchema | 어느 쪽도 아님 | 데이터 그릇(자료구조). 5장 참조 |
JamesDirectorDto | 어느 쪽도 아님 | 데이터 그릇(자료구조). 5장 참조 |
💡 판별 요령: 이름에 특정 기술의 상표(
Pg,Mongo,Redis,Jpa,Http)가 들어 있으면 거의 언제나 구상입니다. 추상은 기술을 몰라야 하므로 기술 이름을 가질 수 없습니다.
2. 헥사고날 아키텍처의 완성 — 일곱 칸 전체 매핑
5주차에서 포트 & 어댑터의 원리를 배웠습니다. 이 장에서는 그림의 모든 칸에 정확한 이름을 붙입니다.
2.1 한 줄 흐름도
[ Front-end ] ─▶ [ Inbound Adapter ] ─▶ ( Inbound Port ) ─▶ [ Use Case ] ─▶ ( Outbound Port ) ─▶ [ Outbound Adapter ] ─▶ [ Data Base ]
외부 진입 어댑터 진입 포트 핵심 로직 진출 포트 진출 어댑터 외부
(구상·남의 것) (구상·내 것) (추상) (구상·핵심) (추상) (구상·내 것) (구상·남의 것)
대괄호 [ ] 는 구상, 소괄호 ( ) 는 추상입니다. 이 괄호 모양만 봐도 축 1이 읽힙니다.
추상 두 개가 핵심 로직을 양쪽에서 감싸고 있는 것이 이 아키텍처의 전부입니다.
2.2 전체 매핑표
손으로 그린 스케치의 표기를 아키텍처 표준 명칭과 실제 구현 컴포넌트로 이어 붙인 표입니다. 이 표 하나가 이 장의 뼈대입니다.
| 다이어그램 표기 | 아키텍처 표준 명칭 | 역할 및 설명 | 실제 구현 컴포넌트 예시 |
|---|---|---|---|
| Front end | Client / UI | 사용자의 요청이 시작되는 외부 화면 또는 클라이언트 시스템 | 웹 브라우저, 모바일 앱, REST 클라이언트 |
| inbound | Inbound Adapter (Driving·구동하는 쪽) | 외부 요청을 받아 시스템이 이해할 수 있는 형태로 변환한 뒤 내부 유스케이스를 호출 | Controller, FastAPI Router, GraphQL Resolver, CLI |
| input (점선) | Inbound Port (문) | 외부 어댑터가 내부 비즈니스 로직으로 진입할 수 있도록 인터페이스 규격을 제공 | CreateOrderUseCase, WalterRoasterUseCase (Interface) |
| use case | Domain / Service | 시스템의 핵심 비즈니스 규칙과 흐름이 살아 숨 쉬는 영역. 외부 의존성이 없다 | OrderService, WalterRoasterInteractor (Implementation) |
| output (점선) | Outbound Port (문) | 비즈니스 로직이 외부 시스템(DB 등)을 필요로 할 때 호출하는 추상화된 인터페이스 | OrderRepository, UserRegistry (Interface) |
| out bound | Outbound Adapter (Driven·구동당하는 쪽) | 아웃바운드 포트 인터페이스를 실제로 구현해 외부 기술과 통신 | OrderJpaRepository, JamesDirectorPgRepository, UserMongoAdapter |
| data base | External System / Infra | 영속성 데이터 저장소나 외부 서드파티 API 등 인프라스트럭처 영역 | MySQL, PostgreSQL, Redis, 외부 API |
점선으로 그려진 두 칸(input·output)이 포트입니다. 점선인 이유가 있습니다. 실체가 없기 때문입니다. 문틀은 있는데 문짝이 없는 상태 — 그게 추상입니다.
2.3 하단 표기 두 줄의 해석
스케치 하단에 짧게 적힌 두 줄이 있습니다. 이게 이 그림에서 가장 압축적인 부분입니다.
① 좌측 하단 — usecase(구현) ──▶ ABC
- 읽는 법:
inbound영역의 컨트롤러가input포트(인터페이스)를 호출하면, 실제로 실행되는 주체는 핵심 비즈니스 로직이 담긴 유스케이스 구현체(Application Service) 다. - 매핑: 따라서
ABC가 가리키는 실제 컴포넌트는Use Case Implementation(서비스 구현 클래스) 입니다. - 한 문장으로: 라우터는
UseCase(ABC, 설명서)를 부르지만, 실제로 일하는 것은Interactor(구현, 제품)다.
# 라우터가 보는 것 : 설명서
walter : WalterRoasterUseCase # ← ABC
# 실제로 일하는 것 : 제품
# WalterRoasterInteractor # ← usecase(구현)
② 우측 하단 — output ──▶ repository
- 읽는 법: 유스케이스가 데이터를 조회하거나 저장하려 할 때 데이터베이스에 직접 접근하지 않고,
output점선으로 표현된 인터페이스를 호출한다. - 매핑: 이
output포트 역할을 하는 인터페이스를 도메인 패턴에서는Repository(인터페이스) 라고 부릅니다. 이를 실질적으로 구현하는out bound블록이Repository Implementation(인프라 저장소 구현체) 입니다. - 한 문장으로:
output 포트의 정식 이름이Repository다. 두 단어는 같은 칸을 부르는 다른 이름이다.
⭐ 여기서 이름이 두 번 겹칩니다. 반드시 구별하세요.
Repository(추상) = output 포트 = 설명서 = 도메인 계층에 있음PgRepository(구상) = outbound 어댑터 = 실제 제품 = 인프라 계층에 있음3주차 「어댑터 패턴 — DB를 연결하는 4단계」의 1단계(Port 정의) / 2단계(Adapter 구현) 가 정확히 이 두 줄입니다.
2.4 왜 육각형인가 — 좌우 대칭이 핵심이다
육각형이라는 모양 자체에는 의미가 없습니다. "여섯 개의 변"이 중요한 게 아니라 "여러 면에 문을 낼 수 있다" 는 뜻입니다.
| 왼쪽 (Driving·구동하는 쪽) | 오른쪽 (Driven·구동당하는 쪽) |
|---|---|
| 시스템에게 일을 시키는 쪽 | 시스템이 부려 쓰는 쪽 |
| 사람, 브라우저, 스케줄러, 테스트 코드 | DB, 파일 시스템, 외부 API, 메시지 큐 |
| Inbound Adapter → Inbound Port | Outbound Port → Outbound Adapter |
| 어댑터가 포트를 부른다 | 포트를 어댑터가 구현한다 |
⭐ 좌우가 대칭처럼 보이지만 방향이 다릅니다. 이게 초보자가 가장 많이 틀리는 곳입니다.
- 왼쪽: 어댑터(라우터)가 포트를 호출합니다. 화살표가 어댑터 → 포트.
- 오른쪽: 어댑터(PgRepository)가 포트를 구현합니다. 화살표가 어댑터 → 포트 (상속 화살표).
두 화살표 모두 바깥에서 안쪽을 향합니다. 이게 의존성 역전(DIP)의 그림입니다. 요청은 왼쪽에서 오른쪽으로 흐르지만, 모든 화살표는 가운데를 향합니다.
2.5 이 아키텍처가 주는 세 가지 이점
각 이점을 "무엇이 없기 때문에 가능한가" 로 읽으면 외우지 않아도 됩니다.
① 테스트 용이성 (Testability)
Use Case 영역에 외부 DB나 프레임워크 의존성이 없으므로, 모킹(Mocking)을 통해 순수한 비즈니스 로직만 독립적으로 유닛 테스트(Unit Test)하기 매우 쉽습니다.
- DB를 켜지 않고도 테스트가 돕니다.
- 출력 포트(
Repository)를 가짜 구현체(FakeRepository)로 바꿔 끼우면 됩니다. - 테스트가 밀리초 단위로 끝납니다. 네트워크 왕복이 없기 때문입니다.
② 기술 교체 유연성 (Flexibility)
오른쪽에 있는 Database 를 MySQL에서 MongoDB나 오라클로 변경하더라도, 중앙의 Use Case 코드는 단 한 줄도 수정할 필요가 없습니다. out bound 어댑터만 새로 구현하면 됩니다.
- 이게 3주차에서 배운 전략 패턴 = DIP = IoC = DI = 포트&어댑터 다섯 이름의 실물입니다.
- 골프백에서 클럽을 바꿔 끼우는 그림 그대로입니다.
③ 유지보수성 (Maintainability) 업무 로직과 기술 인프라가 명확히 분리되어 있어 변경 영향도가 최소화되며, 코드가 깔끔하게 정돈됩니다.
- "이 파일은 왜 바뀌는가"라는 질문에 한 가지 답만 나오게 됩니다 (SRP).
- AI에게 코드를 맡길 때도 결정적입니다. 결합도가 낮으면 AI가 엉뚱한 파일을 건드리지 않습니다.
이점 ①②③은 사실 하나입니다. 핵심에 외부 의존성이 없다 — 이 한 문장에서 셋이 전부 나옵니다. 그래서 이 아키텍처를 지켰는지 확인하는 질문도 하나뿐입니다: "use case 폴더 안의 파일이
import psycopg2나import fastapi를 하고 있는가?" 하고 있다면 이미 무너진 것입니다.
2.6 gateway를 포함한 확장 흐름도
실제 스케치에는 위 일곱 칸 앞에 한 칸이 더 붙어 있었습니다. gateway 입니다.
┌───────────────────────────────────────────────────────────────────────────────┐
│ [ BACKEND CORE & BOUNDARIES ] │
│ │
│ [Frontend] ──▶ [gateway] ──▶ [Route] ──▶ [Input] ──▶ [Use Case] ──▶ [Output] │
│ (Facade) (Inbound) (Inbound Port) (Core) (Outbound │
│ Port) │
│ │ │
│ ▼ │
│ [PostgreSQL] ◀──────────────────────────────────────────── [PG_Repository] │
│ (External DB) (Outbound │
│ Adapter) │
└───────────────────────────────────────────────────────────────────────────────┘
계층별 핵심 역할을 정리하면 이렇습니다.
1. 진입 영역 (Inbound / Frontend)
- Frontend (외부 영역): 사용자와 상호작용하는 UI 레이어
- gateway (Facade 패턴): 복잡한 백엔드 내부 진입 경로를 단순화하여 단 하나의 창구로 결합도를 낮춤
- Route (Inbound 어댑터): 외부 HTTP 요청을 수신하여 백엔드 코어가 이해할 수 있는 프로토콜로 변환
- Input (use cases / Inbound 포트): 코어 비즈니스 로직으로 진입하기 위한 진입점 인터페이스
2. 핵심 영역 (Core / Backend)
- Use Case (비즈니스 로직): 외부 프레임워크나 라이브러리, DB 기술의 변경에 전혀 영향을 받지 않는 시스템의 순수한 핵심 엔진
3. 출구 영역 (Outbound / DB)
- Output (Repository / 추상 메서드 패턴): 데이터 영속성 계층을 추상화한 인터페이스(Outbound 포트). 의존성 역전(DIP)의 핵심 축
- PG_Repository (Outbound 어댑터):
Output인터페이스를 구현하여 실제 PostgreSQL DB에 접근하는 구체 클래스
gateway 는 다음 장의 주제입니다.
3. gateway = Facade 패턴 — 창구를 하나로
3.1 정의
⭐ 정의: 퍼사드(Facade)는 [구조 패턴]인데, [복잡한 서브시스템 여러 개 앞에 단순한 창구 하나를 세운다]는 점에서 다른 구조 패턴과 구별된다.
Facade 는 불어로 '정면', 건물의 앞면을 뜻합니다.
백화점 정면 출입구를 생각하면 됩니다. 안에는 수백 개의 매장·창고·엘리베이터가 있지만, 손님이 아는 것은 정문 하나입니다.
3.2 왜 백엔드 앞에 gateway를 두는가
gateway가 없으면 프론트엔드는 백엔드의 모든 라우터 주소를 각각 알아야 합니다.
❌ gateway 없음 — 프론트가 알아야 할 주소가 계속 늘어난다
프론트 ──▶ /titanic/james/upload
프론트 ──▶ /titanic/rose/list
프론트 ──▶ /titanic/walter/myself
프론트 ──▶ /auth/login
...
✅ gateway 있음 — 프론트가 아는 것은 하나
프론트 ──▶ [gateway] ──┬─▶ james_router
├─▶ rose_router
└─▶ walter_router
얻는 것
- 결합도 하락 — 프론트는 백엔드 내부가 몇 개의 모듈로 쪼개져 있는지 몰라도 됩니다.
- 횡단 관심사의 자리 — 인증(JWT), 로깅, 속도 제한 같은 모든 요청에 공통인 일을 한 곳에서 처리할 수 있습니다.
- 내부 재편의 자유 — 라우터를 쪼개거나 합쳐도 바깥 주소가 안 바뀝니다.
5주차에서 정리한 네이밍 규칙이 여기에 그대로 붙습니다. 최상단에서 JWT 처리와 요청 분배를 맡는 계층은
gateways,middlewares,auth중에서 고릅니다. 반면 내부에서 여러 서비스를 조율하는 계층은usecases(헥사고날·클린 표준) /application_services(DDD) /orchestrators중에서 고릅니다. 두 계층은 이름이 다릅니다. 섞어 쓰면 폴더 구조가 무너집니다.
3.3 gateway와 Route는 다르다
가장 헷갈리는 지점입니다. 둘 다 "요청을 받는 곳"처럼 보이기 때문입니다.
| gateway (Facade) | Route (Inbound Adapter) | |
|---|---|---|
| 목적 | 여러 입구를 하나로 합침 | 하나의 HTTP 요청을 내부 형식으로 변환 |
| 아는 것 | 어느 라우터로 보낼지 | 어떤 유스케이스를 부를지 |
| 관심사 | 인증·로깅·라우팅 (공통) | URL 파라미터 매핑, 상태 코드 (개별) |
| 패턴 | 퍼사드 (Facade) | 어댑터 (Adapter) |
| 개수 | 시스템에 하나 | 기능마다 여러 개 |
| FastAPI에서 | APIRouter() 로 묶는 상위 라우터 / 미들웨어 | 각 @router.get(...) 함수 |
3.4 FastAPI에서 gateway를 만드는 실제 코드
이 수업에서 실제로 한 작업이 바로 이것입니다. __init__.py 에서 상위 라우터 하나를 만들고, 개별 라우터들을 include 로 흡수시킵니다.
# adapter/inbound/api/v1/__init__.py
from fastapi import APIRouter
from .james_router import james_router
from .rose_router import rose_router
from .walter_router import walter_router
titanic_router = APIRouter() # ← 창구 하나 (Facade)
titanic_router.include_router(james_router)
titanic_router.include_router(rose_router)
titanic_router.include_router(walter_router)
이제 main.py 는 라우터를 하나만 알면 됩니다.
# main.py
from fastapi import FastAPI
from adapter.inbound.api.v1 import titanic_router
app = FastAPI()
app.include_router(titanic_router, prefix="/titanic")
⭐ 여기서 __init__.py 의 역할이 3주차와 정확히 이어집니다.
3주차에서 __init__.py 를 "폴더를 부품으로 바꾸는 신분증이자 초기화 코드" 라고 배웠습니다.
지금 이 파일은 신분증 역할만 하는 게 아니라 퍼사드 그 자체가 되었습니다.
api/v1 이라는 폴더가 통째로 하나의 부품이 된 것입니다.
3.5 Facade · Adapter · Proxy — 구조 패턴 삼형제 구별
셋 다 "무언가 앞에 서는 것"이라 계속 헷갈립니다. 무엇을 바꾸는가로 갈립니다.
| 패턴 | 무엇을 하는가 | 인터페이스가 바뀌는가 | 대상이 몇 개인가 |
|---|---|---|---|
| 퍼사드 (Facade) | 복잡한 것을 단순하게 보여줌 | 바뀜 (더 쉬워짐) | 여러 개 |
| 어댑터 (Adapter) | 안 맞는 것을 맞게 바꿈 | 바뀜 (호환됨) | 하나 |
| 프록시 (Proxy) | 접근을 제어함 | 안 바뀜 (똑같은 얼굴) | 하나 |
- 퍼사드: 백화점 정문 — 안에 매장이 200개
- 어댑터: 110V ↔ 220V 변환 플러그 — 대상은 콘센트 하나
- 프록시: 비서 — 사장님과 똑같은 창구지만 아무나 못 들어감
4. 책임 연쇄 패턴 — 한 사람이 다 하지 않는다
4.1 정의
⭐ 정의: 책임 연쇄 패턴(Chain of Responsibility, CoR)은 [행위 패턴]인데, [요청을 처리할 수 있는 객체들을 사슬로 엮어, 앞에서 처리하지 못하면 다음으로 넘긴다]는 점에서 다른 행위 패턴과 구별된다.
3주차 GoF 23패턴 표의 행위(Behavioral) 칸 맨 앞에 있던 그 패턴입니다.
비유는 회사의 결재 라인입니다. 대리가 처리할 수 있으면 거기서 끝나고, 못 하면 과장으로, 과장이 못 하면 부장으로 올라갑니다. 요청을 보낸 사람은 누가 처리했는지 모릅니다. 그게 핵심입니다.
4.2 이 패턴이 없으면 무슨 일이 생기는가
없으면 하나의 함수 안에 if-else 가 끝없이 쌓입니다.
# ❌ 체인이 없을 때 — 정책이 늘어날 때마다 이 함수를 연다
def roast(order):
if not is_authenticated(order): raise ...
if not has_role(order): raise ...
if not is_valid(order): raise ...
if violates_law(order): raise ...
if over_capacity(order): raise ...
if inefficient(order): raise ...
... # 정책이 하나 늘 때마다 여기가 한 줄씩 길어진다
이 코드의 문제는 "길다"가 아닙니다. 정책 하나를 추가할 때마다 기존 코드를 연다는 것입니다. 5주차에서 배운 OCP(개방-폐쇄 원칙) 위반입니다. 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 합니다.
4.3 세 곳에 붙일 수 있다
책임 연쇄는 아키텍처의 세 지점에 붙습니다. 세 곳 모두 "여러 개의 검사·시도가 순서대로 이어지는 자리"라는 공통점이 있습니다.
💡 포인트 A — 인바운드 진입로 프리필터 (Request Pre-processing Chain)
요청이 Route 를 거쳐 Use Case 에 도달하기 전, 공통적인 전처리 단계를 독립된 객체의 사슬로 연결합니다.
요청 ─▶ [인증(Auth) 핸들러] ─▶ [인가(Role) 핸들러] ─▶ [데이터 검증(Validation) 핸들러] ─▶ [로깅(Logging) 핸들러] ─▶ Use Case
- 효과: 유스케이스 코드가 보안이나 검증 로직으로 오염되는 것을 완벽히 방지합니다.
- 유스케이스에 도착한 요청은 이미 "인증됐고, 권한 있고, 형식이 맞고, 기록된" 요청입니다.
- 유스케이스는 비즈니스만 생각하면 됩니다.
💡 포인트 B — 유스케이스 내부 정책 검사 (Core Business Rules Chain)
비즈니스 로직 내 복잡한 조건문(if-else)의 나열을 제거하고 정책을 유연하게 관리합니다.
주문 ─▶ [법적 규제 검사] ─▶ [설비 용량 검사] ─▶ [운영 효율성 검사] ─▶ 승인
- 효과: 정책의 순서를 바꾸거나, 새로운 검증 정책 조건을 추가·삭제할 때 기존 코드를 수정하지 않고 체인 부품만 교체할 수 있습니다. (개방-폐쇄 원칙 만족)
- "법이 바뀌었다" → 법적 규제 핸들러 하나만 교체.
- "용량 검사를 먼저 해야 빠르다" → 체인의 순서만 바꿈. 각 핸들러 코드는 그대로.
💡 포인트 C — 아웃바운드 다중 데이터 전략 (Fallback / Cache Chain)
Repository 가 데이터를 조회하거나 저장할 때 발생하는 문제를 유연하게 처리합니다.
조회 ─▶ [로컬 캐시(Redis) 핸들러] ─▶ [메인 DB(PostgreSQL) 핸들러] ─▶ [백업 백플레인 핸들러] ─▶ 결과
- 효과: 코어(Use Case)는 단순히 데이터를 달라고 요청할 뿐이며, 내부 체인이 가용한 자원을 판단하여 최적의 데이터를 반환합니다.
- 캐시에 있으면 캐시에서, 없으면 메인 DB에서, 메인 DB가 죽었으면 백업에서.
- 유스케이스는 이 셋 중 어디서 왔는지 알 필요도 없고 알 수도 없습니다.
이 포인트 C가 3주차의 폴리글랏 퍼시스턴스(서비스 특성에 따라 다른 DB를 쓰는 전략)와 만나는 자리입니다. 여러 저장소를 쓰기로 했으면, 그 여러 저장소를 어떤 순서로 시도할지를 정해야 합니다. 그 순서가 체인입니다.
4.4 트레이드오프 — 얻는 것과 잃는 것
⚠️ 책임 연쇄는 공짜가 아닙니다. 3주차에서 배운 대로, 아키텍트의 일은 트레이드오프 결정입니다.
| 장점 (Pros) | 단점 및 보완책 (Cons & Mitigations) |
|---|---|
| 의존성 제거: 기술 변경 시 코어 로직 보호 | 디버깅 복잡성: 호출 흐름이 체인을 타고 이동하므로 추적이 어려움 |
| 높은 확장성: 새로운 기능/필터를 체인에 쉽게 플러그인 | 추적성 보완: 각 체인 통과 시 명확한 컨텍스트 로깅 시스템 필수 |
| 단일 책임 원칙: 각 클래스가 단 하나의 역할만 수행 | 미처리 예외: 체인 끝까지 도달 시 처리할 디폴트 핸들러 설계 필요 |
세 가지 단점은 각각 대응책이 정해져 있습니다.
- 디버깅이 어렵다 → 체인의 각 단계에서 "내가 처리했다 / 다음으로 넘긴다"를 반드시 로그로 남깁니다.
if-else는 코드를 읽으면 흐름이 보이지만, 체인은 런타임에 조립되므로 로그 없이는 안 보입니다. - 추적이 안 된다 → 요청마다 추적 ID(trace id) 를 붙여 체인 전체에 들고 다닙니다.
- 아무도 처리 못 하면? → 체인 맨 끝에 디폴트 핸들러를 둡니다. "여기까지 왔다는 것은 처리 불가"라고 명시적으로 실패시킵니다. 디폴트 핸들러가 없으면 요청이 조용히 사라집니다. 가장 찾기 어려운 버그입니다.
4.5 축 5 다시 보기 — Facade와 CoR은 정반대 해법이다
이 장의 판별 축 5번이 여기서 완성됩니다.
| Facade (한 곳에 몰기) | CoR (체인으로 넘기기) | |
|---|---|---|
| 해결하는 문제 | 입구가 너무 많다 | 한 곳이 일을 너무 많이 한다 |
| 방향 | 여럿 → 하나 | 하나 → 여럿 |
| 쓰는 자리 | 시스템의 경계 (gateway) | 처리 파이프라인 내부 |
| 얻는 것 | 단순한 진입점 | 유연한 확장 |
| 잃는 것 | 게이트웨이가 뚱뚱해질 위험 | 추적의 어려움 |
둘은 모순되지 않습니다. 같은 시스템에 동시에 씁니다. 바깥에서는 창구를 하나로 모으고(Facade), 그 창구 안쪽에서는 일을 여러 손으로 나눕니다(CoR). 백화점 정문은 하나지만, 들어온 손님은 안내 → 보안 → 층별 안내로 넘겨집니다.
5. 레이어드 아키텍처와 데이터 전달 객체
헥사고날이 "안과 밖" 의 그림이라면, 레이어드는 "위와 아래" 의 그림입니다. 같은 시스템을 다른 각도에서 본 것이므로, 두 그림은 한 칸씩 대응됩니다. (5.6에서 대조표를 봅니다.)
5.1 네 개의 층과 세 개의 그릇
[ Client ]
│
▼
┌──────────────┐
│ Router │ ◀── HTTP 요청 진입점 & 1차 검증
└──────────────┘
│
│ ■ Schema (Validation)
▼
┌──────────────┐
│ Interactor │ ◀── 핵심 비즈니스 로직 (Use Case)
└──────────────┘
│
│ ■ DTO (Data Transfer Object)
▼
┌──────────────┐
│ Repository │ ◀── 데이터 액세스 추상화 (인프라 격리)
└──────────────┘
│
│ ■ ORM Entity
▼
┌──────────────┐
│ Database │ ◀── 최종 데이터 영구 저장
└──────────────┘
한 줄로 쓰면 이렇습니다.
Router → (Schema) → Interactor → (DTO) → Repository → (ORM Entity) → DB
네 개의 상자 사이에 세 개의 화살표가 있고, 화살표마다 데이터의 형태가 다릅니다. 3주차 「데이터가 흘러가는 전체 길 — 화살표마다 형태가 바뀐다」에서 예고한 그림이 여기서 완성됩니다.
5.2 층별 역할
📱 Router (Controller Layer)
- 주요 역할: 애플리케이션의 최외곽 진입점으로, 클라이언트의 HTTP 요청(GET, POST, PUT, DELETE 등)을 수신합니다.
- 세부 기능
- URL 파라미터 및 쿼리 스트링 매핑
- HTTP 상태 코드 반환 및 에러 핸들링
- 클라이언트가 전달한 거친 데이터(Raw Data)를 1차적으로 검증(Validation) 하여 올바른 형식인지 확인
⚙️ Interactor (Service / Use Case Layer)
- 주요 역할: 애플리케이션의 핵심 비즈니스 규칙(Business Logic) 을 수행합니다.
- 세부 기능
- "회원가입 처리", "주문 및 결제", "아이템 시뮬레이션" 등 실제 서비스가 돌아가는 규칙을 정의
- 외부 인프라(특정 데이터베이스, 프레임워크 등)의 지식에서 철저히 독립되어 있어야 하며, 오직 순수한 비즈니스 로직에만 집중
🗄️ Repository (Data Access Layer)
- 주요 역할: 데이터베이스 영역을 캡슐화하여 비즈니스 레이어가 데이터 저장소의 세부 구현을 알지 못하도록 차단합니다.
- 세부 기능
- Interactor로부터 데이터 영속화 요청을 받아 저장·수정·조회·삭제를 실행
- 실제 데이터베이스가 MySQL, PostgreSQL에서 MongoDB 등으로 변경되더라도 Repository 내부 구현만 바뀔 뿐, Interactor 코드는 영향을 받지 않도록 완충지대 역할
💾 DB (Database Layer)
- 주요 역할: 시스템의 데이터가 영구적으로 보존되는 영속성 저장소입니다.
5.3 세 개의 그릇 — 경계마다 왜 형태가 바뀌는가
각 레이어 경계를 넘어갈 때 데이터의 형태가 변환되면서 결합도를 낮추고 안정성을 확보합니다.
① Schema (Router ➔ Interactor)
- 목적: 입력 데이터의 무결성 검증과 타입 안정성 보장
- 설명: 클라이언트가 보낸 JSON 등의 페이로드를 검증하기 위한 객체입니다. (예: FastAPI의
Pydantic Schema, Node.js의Zod/Joi) - 흐름: Router는 수신된 요청을 Schema와 대조하여 필수 필드 유무, 데이터 타입 등을 검증한 뒤, 안전함이 증명된 데이터만 내부 레이어(Interactor)로 진입시킵니다.
Schema는 문지기입니다. 스케치에도
schema 사용 (요청 검증 / 문지기)라고 적혀 있습니다. 문지기가 없으면 바깥의 지저분한 데이터가 그대로 핵심까지 들어옵니다.
② DTO — Data Transfer Object (Interactor ➔ Repository)
- 목적: 비즈니스 로직과 데이터베이스 인프라 레이어 간의 완전한 디커플링(Decoupling)
- 설명: 레이어 간 데이터 전송만을 위해 설계된 순수한 데이터 컨테이너입니다. 어떠한 비즈니스 로직이나 DB 종속적인 코드도 포함하지 않습니다.
- 흐름: Interactor는 데이터베이스 구조를 알 필요가 없으므로, 시스템 내부적으로 약속된 형태의
DTO에 조회 조건이나 저장할 데이터를 담아 Repository에 던져줍니다.
③ ORM Entity / Model (Repository ➔ DB)
- 목적: 프로그래밍 언어의 객체(Object) 와 관계형 데이터베이스의 테이블(Table) 매핑
- 설명: 데이터베이스 테이블 구조와 1:1로 매핑되는 객체입니다. (예:
SQLAlchemy,Prisma,TypeORM모델) - 흐름: Repository는 Interactor로부터 전달받은
DTO의 데이터를 기반으로ORM Entity객체를 생성하거나 수정하여, 실제 데이터베이스에 쿼리나 영속화 명령을 수행합니다.
세 그릇을 한 표로 비교하면 이렇습니다.
| Schema | DTO | ORM Entity | |
|---|---|---|---|
| 어느 경계 | Router ↔ Interactor | Interactor ↔ Repository | Repository ↔ DB |
| 답하는 질문 | "형식이 맞는가" | "무엇을 전달하는가" | "어느 테이블의 어느 칸인가" |
| 누구의 언어 | 바깥(HTTP·JSON) 의 언어 | 내부 약속 의 언어 | DB(테이블·컬럼) 의 언어 |
| 도구 | Pydantic BaseModel, Zod | @dataclass | SQLAlchemy Mapped |
| 로직을 가지는가 | 검증 규칙만 | ❌ 없음 | 매핑 정보만 |
| 3주차 분류로는 | 개체(Entity, 속성만) | 개체 | 개체 |
⭐ 왜 굳이 세 번이나 바꾸는가 — 한 문장으로
한 그릇을 끝까지 들고 가면, 어느 한쪽이 바뀔 때 전부가 바뀌기 때문입니다.
- DB 컬럼명
sex를gender로 바꿨다고 프론트엔드의 JSON 키가 바뀌면 안 됩니다. - 반대로 프론트가 필드 하나를 더 보내기 시작했다고 DB 테이블이 바뀌면 안 됩니다.
- 세 그릇 사이의 변환 코드가 그 충격을 흡수합니다.
⚠️ 3주차의 경고가 여기에 그대로 적용됩니다. "영속성 전이"를 막아야 합니다. DB 엔티티(ORM Entity)를 도메인 계층까지 끌고 들어오면 이 구조를 쓰는 의미가 사라집니다. 번거롭더라도 도메인 모델과 DB 모델을 별도로 관리하세요.
5.4 역방향(Response) 흐름 — 내려간 것은 올라와야 한다
교재나 강의에서 자주 빠지는 부분입니다. 요청만 그리고 응답은 안 그립니다. 하지만 응답도 똑같이 세 번 변환됩니다.
DB
│ ① Repository 가 ORM Entity 로 조회
▼
[ORM Entity]
│ ② Repository 가 순수 객체 또는 Output DTO 로 변환
▼
[Output DTO]
│ ③ Interactor 가 필요한 비즈니스 가공 수행
▼
[Response Schema]
│ ④ Router 가 JSON 으로 직렬화해 클라이언트에 전달
▼
Client
정리하면 이렇습니다.
DB➔ORM Entity형태로 Repository가 조회- Repository는 이를 순수한 내장 객체나
Output DTO로 변환하여 Interactor에게 반환 - Interactor는 필요한 비즈니스 가공 후
Response Schema에 담아 - Router를 통해 클라이언트에게 최종 전달
💡 Request Schema와 Response Schema는 다른 클래스입니다. 받을 때 필요한 필드와 돌려줄 때 필요한 필드가 다르기 때문입니다. 예: 회원가입 요청에는
password가 필요하지만, 응답에는 절대 들어가면 안 됩니다. 한 클래스를 양쪽에 쓰면 이런 사고가 납니다.
5.5 축 3 — 제어의 흐름과 의존성의 방향은 반대다
⭐ 이 장에서 가장 중요한 문장입니다.
제어의 흐름은
Router➔Interactor➔Repository➔DB로 이동하지만, 코드의 의존성은 언제나 안쪽(Interactor)을 향해야 합니다.
제어의 흐름 (실행 순서) 의존성의 방향 (누가 누구를 아는가)
Router Router
│ │
▼ ▼
Interactor [ 추상 공간 ] ◀── Interactor 는 여기만 안다
│ ▲
▼ │
Repository Repository (구현체가 추상을 향해 화살표를 쏜다)
│
▼
DB
상위 개념(Interactor)이 하위 구현체(Repository 세부 코드)를 의존하지 않도록, 인터페이스(Interface / 추상 클래스)를 활용해 의존성을 역전(Dependency Inversion) 시키는 구조를 지향합니다.
클린 아키텍처 다이어그램의 「추상 공간」
이 수업에서 그린 클린 아키텍처 계층도는 다섯 층이었습니다.
| 번호 | 계층 이름 | 상단 라벨 | 하단 라벨 |
|---|---|---|---|
| 1 | Inbound Adapter Layer | router | inbound |
| 2 | Application / Domain Core Layer | use_case / Interactor Implementation | input / use_case |
| 3 | Persistence Interface Layer | repository | output |
| 4 | Outbound Adapter Layer | pg_repository | outbound |
| 5 | Data Layer | DB | DB |
계층 사이의 흐름 라벨은 이렇습니다.
router → use_case: schemainteractor → repository: dtorepository → pg_repository: dtopg_repository → DB: ORM
그리고 그림 위에는 빨간 타원 두 개가 그려져 있었습니다.
하나는 use_case / input 을 감싸고, 다른 하나는 repository / output 을 감싸며, 두 타원이 하나의 라벨로 이어집니다.
⭐ 그 라벨이 「추상 공간」 입니다. 2번 계층의
input과 3번 계층의output— 이 둘만이 추상이고, 나머지는 전부 구상입니다. 그리고 모든 의존성 화살표는 이 추상 공간을 향합니다. 시스템에서 절대 바뀌지 않아야 하는 것은 이 두 칸뿐이고, 나머지는 전부 교체 가능합니다.
⚠️ 흔한 오해: 화살표를 데이터 흐름으로 읽는 것. 클린 아키텍처 그림의 화살표는 코드 관계(누가 누구를 알고 있는가) 이지 데이터가 흐르는 방향이 아닙니다. 데이터는 안팎으로 자유롭게 흐릅니다. 아는 관계만 한 방향입니다.
5.6 헥사고날 표기와 레이어드 표기 대조표
같은 것을 두 가지 이름으로 부르기 때문에 계속 헷갈립니다. 한 번에 정리합니다.
| 헥사고날 용어 | 레이어드 용어 | 클린 계층 이름 | 이 수업의 폴더 | 추상/구상 |
|---|---|---|---|---|
| Front end | Client | — | (프론트엔드 프로젝트) | 구상 |
| gateway | — | (Facade) | gateways/ middlewares/ | 구상 |
| Inbound Adapter | Router / Controller | Inbound Adapter Layer | adapter/inbound/api/v1/ | 구상 |
| Inbound Port (input) | — | Application Core (input) | app/ports/input/ | 추상 |
| Use Case | Interactor / Service | Application Core (use_case) | app/use_cases/ | 구상 |
| Outbound Port (output) | Repository (인터페이스) | Persistence Interface Layer | app/ports/output/ | 추상 |
| Outbound Adapter | Repository 구현체 | Outbound Adapter Layer | adapter/outbound/pg/ | 구상 |
| Data Base | DB | Data Layer | (Neon PostgreSQL) | 구상 |
| — | Schema | (경계 라벨) | api/schemas/ | 자료구조 |
| — | DTO | (경계 라벨) | app/dtos/ | 자료구조 |
| — | ORM Entity | (경계 라벨) | adapter/outbound/orm/ | 자료구조 |
💡 폴더 구조가 곧 아키텍처 그림입니다. 폴더 이름만 보고 "이건 추상이구나 / 구상이구나"를 판별할 수 있다면 구조를 이해한 것입니다. 반대로
app/use_cases/안에import sqlalchemy가 있다면 폴더는 나눴지만 아키텍처는 없는 상태입니다.
⚠️ 폴더를 네 개로 나눈다고 클린 아키텍처가 되는 게 아닙니다. 폴더 이름보다 코드의 역할과 의존 관계가 중요합니다.
5.7 DTO는 @dataclass 가 정석이다
DTO를 만들 때 무엇을 쓸 것인가 — 이 수업의 결론은 명확합니다.
DTO는
@dataclass가 정석입니다.
세 가지 이유
- 클린코드의 "자료구조"에 정확히 해당합니다. 클린코드는 객체(기능을 가짐) 와 자료구조(데이터만 가짐) 를 엄격히 나눕니다. DTO는 후자입니다. 3주차의 뺄셈식으로 쓰면 DTO = 객체 − 기능 = 개체(Entity) 입니다.
- 보일러플레이트가 사라집니다.
__init__,__repr__,__eq__를 손으로 쓰지 않아도 됩니다. 데코레이터 한 줄이 만들어 줍니다. frozen=True로 불변을 강제할 수 있습니다. 전달 도중에 값이 바뀌면 추적이 불가능해집니다. DTO는 한 번 담으면 안 바뀌는 상자여야 합니다.
from dataclasses import dataclass
@dataclass(frozen=True)
class PersonCommand:
passenger_id: str
name: str
gender: str
age: str
@dataclass(frozen=True)
class BookingCommand:
passenger_id: str
ticket: str
fare: str
cabin: str
embarked: str
⚠️ 비즈니스 로직은 절대 넣지 마세요. DTO에 메서드를 하나 넣는 순간 그것은 더 이상 자료구조가 아니라 도메인 모델입니다. 로직이 필요하다면 도메인 모델로 분리하세요.
💡 강한 검증이 필요하면 Pydantic BaseModel 을 씁니다.
@dataclass 는 타입 힌트를 적어둘 뿐 실제로 검사하지 않습니다. age: str 에 정수를 넣어도 조용히 들어갑니다.
Pydantic은 실제로 검사하고 거부합니다. 그래서 바깥에서 오는 데이터(Schema)는 Pydantic, 내부 전달(DTO)은 @dataclass 가 기본 조합입니다.
@dataclass | Pydantic BaseModel | |
|---|---|---|
| 타입 검사 | ❌ 힌트만 | ✅ 런타임 검증 |
| 속도 | 빠름 (순수 파이썬 객체) | 검증 비용이 있음 |
| 주 용도 | DTO (내부 전달) | Schema (경계 검증) |
| 불변 | frozen=True | model_config = ConfigDict(frozen=True) |
5.8 스키마 → DTO 변환은 단순 복사가 아니다
이 수업에서 실제로 한 변환 작업의 규칙입니다. 경계를 넘을 때 무엇이 바뀌는지를 보여주는 좋은 사례입니다.
api/schemas/james_director_schema.py (인바운드) → app/dtos/james_director_dto.py (유스케이스)
| 변환 규칙 | 왜 그렇게 하는가 |
|---|---|
| Port·Booking은 역정규화 | 유스케이스는 조인을 몰라야 합니다. DB의 정규화 사정은 안쪽 관심사가 아닙니다. |
country 컬럼 삭제 | 유스케이스가 쓰지 않는 필드는 넘기지 않습니다. 안 쓰는 필드는 결합만 늘립니다. |
Person → PersonCommand | 이름을 Command 로 바꿉니다. "이건 명령이다"라는 의도를 이름에 담습니다. |
Booking + Port → BookingCommand | 두 스키마를 하나의 DTO로 합칩니다. 스키마 개수와 DTO 개수는 같을 필요가 없습니다. |
컬럼 타입을 전부 str 로 통일 | 이 단계에서는 형 변환보다 흐름 확인이 먼저입니다. 파이프가 뚫렸는지부터 봅니다. |
⭐ 마지막 규칙이 중요합니다. "전부 str 로 통일"은 최종 설계가 아니라 작업 순서입니다.
먼저 데이터가 끝까지 흘러가는 것을 확인하고, 그다음에 타입을 조입니다.
파이프에 물이 흐르는지부터 보고, 그다음에 수압을 맞춥니다.
성공 판정 기준: CSV → 라우터 → 유스케이스 → 레포지토리까지 데이터가 이동하며 각 단계에서 로그가 찍히면 성공입니다. 화면에 예쁘게 나오는 것은 그다음 문제입니다.
6. 파이썬에서 상속받을 수 있는 클래스의 종류
추상 클래스를 만들려면 ABC 를 상속해야 합니다. 그러면 파이썬에서 상속할 수 있는 것에는 무엇이 있는지 전체 지도를 한 번 봐야 합니다.
6.1 전제 — 모든 클래스는 object 의 자식이다
파이썬은 다중 상속을 기본적으로 지원하며, 모든 클래스는 최상위 클래스인 object 를 암시적으로 상속받습니다.
class Foo: ...
print(Foo.__mro__) # (<class 'Foo'>, <class 'object'>)
아무것도 안 적어도 이미 object 의 자식입니다. 그래서 __repr__, __eq__ 같은 것이 처음부터 있습니다.
6.2 정의 방식에 따른 분류
① 사용자 정의 클래스 (User-defined Classes)
개발자가 class 키워드를 사용해 직접 정의한 일반적인 클래스입니다.
메서드와 속성을 그대로 물려받거나 오버라이딩하여 재정의할 수 있습니다.
② 내장 클래스 (Built-in Classes)
list, dict, str, int, set 등 파이썬에 기본적으로 내장된 자료형 클래스입니다.
이를 상속받아 기존 자료형의 기능을 확장한 커스텀 자료형을 만들 수 있습니다.
class Fare(float): # 내장 클래스 상속
def to_krw(self, rate: float) -> float:
return float(self) * rate
③ 표준 라이브러리 및 서드파티 클래스
파이썬 표준 라이브러리(threading.Thread, collections.UserDict)나 외부 패키지(Django의 models.Model, SQLAlchemy의 DeclarativeBase 등)에서 제공하는 클래스입니다.
6.3 목적 및 형태에 따른 분류
① 추상 클래스 (Abstract Base Classes, ABC)
abc 모듈의 ABC 클래스를 상속받아 만듭니다.
직접 인스턴스를 생성할 수 없으며, 하위 클래스에서 반드시 구현해야 할 인터페이스(추상 메서드)를 규정합니다.
→ 이 장의 포트(Port) 가 전부 여기에 해당합니다.
② 데이터 클래스 (Data Classes)
dataclasses 모듈을 사용한 클래스로, 주로 데이터를 저장하는 구조체 용도로 사용되며 상속을 통해 필드를 확장할 수 있습니다.
→ 이 장의 DTO 가 여기에 해당합니다.
③ 예외 클래스 (Exception Classes)
커스텀 에러(예외 처리)를 만들 때 파이썬의 기본 예외 클래스인 Exception 또는 그 하위 클래스들(ValueError, TypeError 등)을 상속받습니다.
class RoastingCapacityExceeded(ValueError):
"""설비 용량 초과 — 책임 연쇄의 '설비 용량 검사' 핸들러가 던진다"""
④ 믹스인 클래스 (Mixin Classes) 독자적인 인스턴스를 생성하기보다는, 다중 상속을 활용해 다른 클래스에 특정 기능(메서드)만 주입하기 위해 설계된 클래스입니다.
class TimestampMixin: # 혼자서는 아무 의미가 없다
created_at: str
updated_at: str
class PassengerOrm(Base, TimestampMixin): ... # 기능만 얹는다
6.4 ⚠️ 상속 불가 예외 및 주의사항
bool 클래스는 상속할 수 없습니다.
class MyBool(bool): ...
# TypeError: type 'bool' is not an acceptable base type
파이썬 내부의 최적화 및 데이터 무결성을 위해 상속이 엄격히 금지되어 있습니다.
- 파이썬에서
True와False는 프로그램 전체에 딱 하나씩만 존재하는 싱글톤 객체입니다. - 상속을 허용하면 "참인데 True가 아닌 것"이 생겨
is True같은 비교가 전부 깨집니다. - 3주차에서 배운 싱글톤 패턴이 언어 차원에서 강제된 사례입니다.
슬롯(__slots__) 제약
부모와 자식 클래스 간에 __slots__ 배치가 충돌할 경우 다중 상속 시 제약이 발생할 수 있습니다.
__slots__ 는 인스턴스가 가질 수 있는 속성을 미리 못 박아 메모리를 아끼는 장치인데, 서로 다른 __slots__ 를 가진 두 부모를 동시에 상속하면 레이아웃 충돌로 TypeError 가 납니다.
6.5 어느 것을 언제 쓰는가
| 만들려는 것 | 상속할 것 | 이 장의 어디 |
|---|---|---|
| 포트(인터페이스) | ABC + @abstractmethod | 1.3, 2.2 |
| DTO | @dataclass(frozen=True) (상속 아님, 데코레이터) | 5.7 |
| Schema | Pydantic BaseModel | 7.2 |
| 설정 객체 | BaseSettings (pydantic-settings) | 7.2 |
| ORM Entity | SQLAlchemy DeclarativeBase | 10.6 |
| 도메인 예외 | Exception 또는 그 하위 | 6.3 |
| 공통 필드·기능 얹기 | 믹스인 | 6.3 |
LangTailor