본문으로 건너뛰기
설명서와 실제 제품 — 아키텍처가 코드로 내려앉는 자리

생성 수정

IBM * 레드햇 AX 과정 · 11강

설명서와 실제 제품 — 아키텍처가 코드로 내려앉는 자리

2026년 6월 1일(월) ~ 6월 5일(금)

이 강의 문제풀이 — 배운 내용을 주관식 문답으로 확인해보세요.풀어보기 →

이 장을 다 읽으면

  1. 추상(Abstraction)과 구상(Concretion)을 "설명서와 실제 제품"으로 정의하고, 왜 추상은 메모리에 올라갈 수 없는지 설명할 수 있다.
  2. 헥사고날 아키텍처의 일곱 칸(Front end / inbound / input / use case / output / outbound / database)을 표준 명칭·역할·실제 구현 컴포넌트로 각각 말할 수 있다.
  3. 이 구조가 주는 세 가지 이점(테스트 용이성·기술 교체 유연성·유지보수성)을 각각 "무엇이 없기 때문에 가능한가"로 설명할 수 있다.
  4. gateway가 왜 Facade 패턴인지, Route(inbound 어댑터)와 무엇이 다른지 구별할 수 있다.
  5. 책임 연쇄 패턴(Chain of Responsibility)을 아키텍처의 세 지점에 어떻게 붙이는지 말하고, 그 대가로 무엇을 잃는지 안다.
  6. Router → (Schema) → Interactor → (DTO) → Repository → (ORM Entity) → DB 흐름에서 경계마다 데이터의 형태가 바뀌는 이유를 설명할 수 있다.
  7. 제어의 흐름과 의존성의 방향이 서로 반대일 수 있다는 것을 그림으로 그릴 수 있다.
  8. 파이썬에서 상속받을 수 있는 클래스의 종류를 분류하고, bool을 상속할 수 없는 이유를 안다.
  9. Pydantic 2.0의 네 가지 모델과 1.x에서 바뀐 표기법을 구별할 수 있다.
  10. 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.pyjames_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 endClient / UI사용자의 요청이 시작되는 외부 화면 또는 클라이언트 시스템웹 브라우저, 모바일 앱, REST 클라이언트
inboundInbound Adapter (Driving·구동하는 쪽)외부 요청을 받아 시스템이 이해할 수 있는 형태로 변환한 뒤 내부 유스케이스를 호출Controller, FastAPI Router, GraphQL Resolver, CLI
input (점선)Inbound Port (문)외부 어댑터가 내부 비즈니스 로직으로 진입할 수 있도록 인터페이스 규격을 제공CreateOrderUseCase, WalterRoasterUseCase (Interface)
use caseDomain / Service시스템의 핵심 비즈니스 규칙과 흐름이 살아 숨 쉬는 영역. 외부 의존성이 없다OrderService, WalterRoasterInteractor (Implementation)
output (점선)Outbound Port (문)비즈니스 로직이 외부 시스템(DB 등)을 필요로 할 때 호출하는 추상화된 인터페이스OrderRepository, UserRegistry (Interface)
out boundOutbound Adapter (Driven·구동당하는 쪽)아웃바운드 포트 인터페이스를 실제로 구현해 외부 기술과 통신OrderJpaRepository, JamesDirectorPgRepository, UserMongoAdapter
data baseExternal 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 PortOutbound 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

얻는 것

  1. 결합도 하락 — 프론트는 백엔드 내부가 몇 개의 모듈로 쪼개져 있는지 몰라도 됩니다.
  2. 횡단 관심사의 자리 — 인증(JWT), 로깅, 속도 제한 같은 모든 요청에 공통인 일을 한 곳에서 처리할 수 있습니다.
  3. 내부 재편의 자유 — 라우터를 쪼개거나 합쳐도 바깥 주소가 안 바뀝니다.

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)
의존성 제거: 기술 변경 시 코어 로직 보호디버깅 복잡성: 호출 흐름이 체인을 타고 이동하므로 추적이 어려움
높은 확장성: 새로운 기능/필터를 체인에 쉽게 플러그인추적성 보완: 각 체인 통과 시 명확한 컨텍스트 로깅 시스템 필수
단일 책임 원칙: 각 클래스가 단 하나의 역할만 수행미처리 예외: 체인 끝까지 도달 시 처리할 디폴트 핸들러 설계 필요

세 가지 단점은 각각 대응책이 정해져 있습니다.

  1. 디버깅이 어렵다 → 체인의 각 단계에서 "내가 처리했다 / 다음으로 넘긴다"를 반드시 로그로 남깁니다. if-else 는 코드를 읽으면 흐름이 보이지만, 체인은 런타임에 조립되므로 로그 없이는 안 보입니다.
  2. 추적이 안 된다 → 요청마다 추적 ID(trace id) 를 붙여 체인 전체에 들고 다닙니다.
  3. 아무도 처리 못 하면? → 체인 맨 끝에 디폴트 핸들러를 둡니다. "여기까지 왔다는 것은 처리 불가"라고 명시적으로 실패시킵니다. 디폴트 핸들러가 없으면 요청이 조용히 사라집니다. 가장 찾기 어려운 버그입니다.

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 객체를 생성하거나 수정하여, 실제 데이터베이스에 쿼리나 영속화 명령을 수행합니다.

세 그릇을 한 표로 비교하면 이렇습니다.

SchemaDTOORM Entity
어느 경계Router ↔ InteractorInteractor ↔ RepositoryRepository ↔ DB
답하는 질문"형식이 맞는가""무엇을 전달하는가""어느 테이블의 어느 칸인가"
누구의 언어바깥(HTTP·JSON) 의 언어내부 약속 의 언어DB(테이블·컬럼) 의 언어
도구Pydantic BaseModel, Zod@dataclassSQLAlchemy 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

정리하면 이렇습니다.

  1. DB ➔ ORM Entity 형태로 Repository가 조회
  2. Repository는 이를 순수한 내장 객체나 Output DTO 로 변환하여 Interactor에게 반환
  3. Interactor는 필요한 비즈니스 가공 후 Response Schema 에 담아
  4. 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) 시키는 구조를 지향합니다.

클린 아키텍처 다이어그램의 「추상 공간」

이 수업에서 그린 클린 아키텍처 계층도는 다섯 층이었습니다.

번호계층 이름상단 라벨하단 라벨
1Inbound Adapter Layerrouterinbound
2Application / Domain Core Layeruse_case / Interactor Implementationinput / use_case
3Persistence Interface Layerrepositoryoutput
4Outbound Adapter Layerpg_repositoryoutbound
5Data LayerDBDB

계층 사이의 흐름 라벨은 이렇습니다.

  • router → use_case : schema
  • interactor → repository : dto
  • repository → pg_repository : dto
  • pg_repository → DB : ORM

그리고 그림 위에는 빨간 타원 두 개가 그려져 있었습니다. 하나는 use_case / input 을 감싸고, 다른 하나는 repository / output 을 감싸며, 두 타원이 하나의 라벨로 이어집니다.

⭐ 그 라벨이 「추상 공간」 입니다. 2번 계층의 input 과 3번 계층의 output — 이 둘만이 추상이고, 나머지는 전부 구상입니다. 그리고 모든 의존성 화살표는 이 추상 공간을 향합니다. 시스템에서 절대 바뀌지 않아야 하는 것은 이 두 칸뿐이고, 나머지는 전부 교체 가능합니다.

⚠️ 흔한 오해: 화살표를 데이터 흐름으로 읽는 것. 클린 아키텍처 그림의 화살표는 코드 관계(누가 누구를 알고 있는가) 이지 데이터가 흐르는 방향이 아닙니다. 데이터는 안팎으로 자유롭게 흐릅니다. 아는 관계만 한 방향입니다.

5.6 헥사고날 표기와 레이어드 표기 대조표

같은 것을 두 가지 이름으로 부르기 때문에 계속 헷갈립니다. 한 번에 정리합니다.

헥사고날 용어레이어드 용어클린 계층 이름이 수업의 폴더추상/구상
Front endClient—(프론트엔드 프로젝트)구상
gateway—(Facade)gateways/ middlewares/구상
Inbound AdapterRouter / ControllerInbound Adapter Layeradapter/inbound/api/v1/구상
Inbound Port (input)—Application Core (input)app/ports/input/추상
Use CaseInteractor / ServiceApplication Core (use_case)app/use_cases/구상
Outbound Port (output)Repository (인터페이스)Persistence Interface Layerapp/ports/output/추상
Outbound AdapterRepository 구현체Outbound Adapter Layeradapter/outbound/pg/구상
Data BaseDBData 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 가 정석입니다.

세 가지 이유

  1. 클린코드의 "자료구조"에 정확히 해당합니다. 클린코드는 객체(기능을 가짐) 와 자료구조(데이터만 가짐) 를 엄격히 나눕니다. DTO는 후자입니다. 3주차의 뺄셈식으로 쓰면 DTO = 객체 − 기능 = 개체(Entity) 입니다.
  2. 보일러플레이트가 사라집니다. __init__, __repr__, __eq__ 를 손으로 쓰지 않아도 됩니다. 데코레이터 한 줄이 만들어 줍니다.
  3. 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 가 기본 조합입니다.

@dataclassPydantic BaseModel
타입 검사❌ 힌트만✅ 런타임 검증
속도빠름 (순수 파이썬 객체)검증 비용이 있음
주 용도DTO (내부 전달)Schema (경계 검증)
불변frozen=Truemodel_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 + @abstractmethod1.3, 2.2
DTO@dataclass(frozen=True) (상속 아님, 데코레이터)5.7
SchemaPydantic BaseModel7.2
설정 객체BaseSettings (pydantic-settings)7.2
ORM EntitySQLAlchemy DeclarativeBase10.6
도메인 예외Exception 또는 그 하위6.3
공통 필드·기능 얹기믹스인6.3