본문으로 건너뛰기
추상과 구상 — 아키텍처의 심장부

생성 수정

IBM * 레드햇 AX 과정 · 9강

추상과 구상 — 아키텍처의 심장부

2026년 5월 25일(월) ~ 5월 29일(금)

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

3주차에서 우리는 "객체는 무엇으로 이루어져 있는가"를 배웠습니다. 이번 주는 그 객체들을 어떤 자리에 놓을 것인가를 배웁니다. 이 부트캠프 전체에서 가장 밀도가 높은 한 주이고, 모든 내용이 결국 한 문장으로 수렴합니다.

"변하는 것과 변하지 않는 것을 갈라놓고, 변하지 않는 것 쪽에 내 코드를 세운다."

CQRS도, SQL 명령어 분류도, SOLID도, 클린 아키텍처도, 헥사고날도, 심지어 함수의 매개변수까지도 전부 이 한 문장의 서로 다른 크기입니다.

이 장을 다 읽으면

  1. 웹 3계층(프론트·백엔드·DB)과 GoF 3분류(행위·생성·구조)가 왜 짝을 이루는지 설명할 수 있다.
  2. CQRS를 "질의는 상태를 바꾸지 않고, 명령은 결과를 반환하지 않는다" 한 문장으로 정의하고, 1단계·2단계 분리를 구별할 수 있다.
  3. SQL 명령어를 목적별(DQL·DML)과 대상별(DDL·DCL·TCL)의 두 축으로 나눠 설명할 수 있다.
  4. 프로젝트 폴더 이름만 보고 그 폴더가 DDL인지 DML인지 인프라인지 판별할 수 있다.
  5. SOLID 5원칙을 각각 한 문장으로 말하고, 각 원칙이 클린 아키텍처의 어느 규칙으로 이어지는지 짚을 수 있다.
  6. 클린 아키텍처의 의존성 규칙과 4계층을 그림 없이 그릴 수 있다.
  7. 헥사고날의 Port(추상)와 Adapter(구상)를 구별하고, 제어의 흐름과 의존성의 방향이 왜 다른지 설명할 수 있다.
  8. "추상화하는 근본 이유"를 방파제 비유가 아니라 정의로 말할 수 있고, 추상화가 나타나는 5곳을 열거할 수 있다.
  9. 스코프 계층(로컬 → 클래스 → 컨텍스트·전역·환경)을 Next.js와 FastAPI에 각각 매핑할 수 있다.
  10. 매개변수가 왜 "관문(Gate)"이고, 그것이 Port와 같은 원리인지 말할 수 있다.

0. 이 장의 판별 축

3주차에서 그랬듯이, 새 용어를 만나면 비유를 찾기 전에 아래 축 중 어디에 꽂히는지부터 정하세요. 이번 주는 축이 특히 중요합니다. 이 장의 개념 대부분이 같은 축을 서로 다른 배율로 반복하기 때문입니다.

#축왼쪽 (0)오른쪽 (1)이 축으로 갈리는 것
1추상 ↔ 구상추상 (What·계약·인터페이스)구상 (How·구현·기술)Port ↔ Adapter / 추상클래스 ↔ 구현클래스
2안쪽 ↔ 바깥쪽안쪽 (도메인·비즈니스)바깥쪽 (인프라·프레임워크)Entities·UseCase ↔ FastAPI·PostgreSQL
3읽기 ↔ 쓰기질의 (Query·상태 안 바꿈)명령 (Command·상태 바꿈)SELECT ↔ INSERT·UPDATE·DELETE / Read DB ↔ Write DB
4구조 정의 ↔ 데이터 조작구조 (틀을 만든다)데이터 (틀 안을 채운다)DDL ↔ DML·DQL / alembic/ ↔ apps/
5좁은 스코프 ↔ 넓은 스코프좁음 (로컬 변수·메서드)넓음 (전역·환경 변수)로컬변수 ↔ 필드 ↔ 컨텍스트 ↔ .env

축 1이 이 장의 중심입니다. 축 2는 축 1을 시스템 크기로 확대한 것이고, 축 5는 함수 크기로 축소한 것입니다. 같은 칼로 세 번 자른다고 생각하면 됩니다.

⭐ 3주차와 이어지는 축 3주차 축 5("하나로 뭉침 ↔ 여럿으로 쪼갬")가 이번 주 축 3(읽기/쓰기 분리)으로 이어집니다. 3주차 5.4절의 전략 패턴 = DIP = IoC = DI = 포트&어댑터는 이번 주 7.7절에서 완전히 회수됩니다.


1. 웹 3계층과 GoF 3분류 — 왜 짝이 맞는가

1.1 전체 지도

3주차에서 GoF 23패턴이 생성(Creational)·구조(Structural)·행위(Behavioral) 세 갈래로 나뉜다는 것을 배웠습니다. 이번 주에는 그 세 갈래가 우리가 만드는 시스템의 세 계층과 그대로 겹친다는 것을 봅니다.

[ 프론트엔드 (Frontend) ] ─── API (GET/POST) ───► [ 백엔드 (Backend) ] ─── Connect ───► [ 데이터베이스 (DB) ]
    └─ 행위(Behavioral) 패턴              └─ 생성(Creational) 패턴          └─ 구조(Structural) 패턴
계층그 계층에서 주로 일어나는 일대응 GoF 분류대표 패턴
프론트엔드사용자의 클릭·입력·이동 = 행동과 이벤트행위 (Behavioral)옵저버, 전략
백엔드요청마다 필요한 객체를 만들고 수명을 관리생성 (Creational)싱글톤, 팩토리 메서드
데이터베이스외부 저장소와 안전하게 연결되는 구조를 형성구조 (Structural)프록시, 어댑터, 퍼사드

이 매핑은 억지로 맞춘 것이 아닙니다. 각 계층이 감당하는 변화의 종류가 다르기 때문에 자연히 그렇게 됩니다. 프론트에서 변하는 것은 사용자가 무슨 짓을 하는가(→ 행동을 갈아끼우는 패턴), 백엔드에서 변하는 것은 요청량과 객체의 개수(→ 생성을 통제하는 패턴), DB 쪽에서 변하는 것은 어떤 기술로 붙느냐(→ 연결 구조를 정돈하는 패턴)입니다.

1.2 프론트엔드 계층 — 행위(Behavioral) 패턴

프론트엔드는 사용자의 클릭·입력·페이지 이동 등 수많은 이벤트(요청)와 클라이언트의 행동이 발생하는 영역입니다. 상태 변화에 따라 화면을 동적으로 제어하고 백엔드로 API 요청을 보냅니다.

옵저버 패턴 (Observer Pattern)

  • 설명 — 한 객체의 상태 변화를 관찰하는 다른 객체들에게 자동으로 통지하는 구조입니다.
  • 웹에서의 적용 — React, Vue 등 현대 프론트엔드 프레임워크에서 데이터(State)가 변경되면 이를 감지해 화면(DOM)을 자동 업데이트하는 핵심 원리 그 자체입니다.
  • useState로 값을 바꿨을 뿐인데 화면이 다시 그려지는 이유가 이것입니다. 상태가 통지했기 때문입니다.

전략 패턴 (Strategy Pattern)

  • 설명 — 알고리즘 제품군을 정의하고 각각을 캡슐화하여 상호 교환이 가능하게 만듭니다.
  • 웹에서의 적용 — 로그인 방식(구글 로그인, 카카오 로그인, 자체 로그인)이나 결제 수단(신용카드, 네이버페이, 카카오페이)에 따라 인증·결제 알고리즘을 동적으로 스위칭할 때 씁니다.

1.3 백엔드 계층 — 생성(Creational) 패턴

백엔드는 프론트엔드로부터 API 요청(GET, POST 등)을 받아 CRUD 비즈니스 로직을 처리하는 핵심 영역입니다. 시스템 운영에 필요한 객체의 생성 과정과 수명 주기를 효율적으로 관리합니다.

싱글톤 패턴 (Singleton Pattern)

  • 설명 — 클래스의 인스턴스가 오직 하나만 생성되도록 보장합니다.
  • 웹에서의 적용 — 백엔드에서 가장 필수적인 패턴입니다. 요청마다 컨트롤러·서비스 객체를 새로 생성하면 서버 메모리가 고갈되므로, 단 하나의 인스턴스만 상주시켜 공유 처리합니다.

팩토리 메서드 패턴 (Factory Method Pattern)

  • 설명 — 객체 생성 처리를 서브 클래스로 분리하여 대행하게 합니다.
  • 웹에서의 적용 — 사용자의 권한 등급(일반 회원, 관리자, 파트너)이나 요청 파라미터에 따라 서로 다른 비즈니스 로직 서비스 객체를 동적으로 생성해 반환해야 할 때 씁니다.

1.4 데이터베이스 계층 — 구조(Structural) 패턴

데이터베이스 계층 및 데이터 접근 영역은, 백엔드가 외부 저장소(DB)와 안전하고 효율적으로 연결(Connect)되도록 구조를 형성하는 영역입니다. 복잡하게 얽힌 클래스와 외부 라이브러리를 정돈합니다.

프록시 패턴 (Proxy Pattern)

  • 설명 — 실제 객체에 대한 대리자나 자리채움자(Placeholder)를 제공하여 접근을 제어하거나 부가 기능을 수행합니다.
  • 웹에서의 적용 — DB에서 데이터를 매번 새로 조회하는 대신, 중간에 가짜(Proxy) 객체를 두어 캐싱(Caching) 처리를 하거나, 실제 필요한 시점까지 DB 조회를 미루는 지연 로딩(Lazy Loading) 을 구현할 때 씁니다.

어댑터 패턴 (Adapter Pattern)

  • 설명 — 호환성이 없는 인터페이스 때문에 함께 쓸 수 없는 클래스들을 연결해 줍니다.
  • 웹에서의 적용 — 백엔드 도메인 로직 코드와, 서로 다른 데이터베이스 드라이버 라이브러리(MySQL, Oracle, MongoDB 등) 사이에서 인터페이스 표준을 맞춰 통신하게 하는 중간 가교입니다.

퍼사드 패턴 (Facade Pattern)

  • 설명 — 서브시스템에 있는 복잡한 인터페이스 집합에 대해 하나의 단순화된 고수준 인터페이스를 제공합니다.
  • 어원 — 불어 façade, 건물의 '정면' 입니다. 건물 안이 아무리 복잡해도 밖에서 보이는 건 정면 하나뿐입니다. 그 정면이 곧 출입구입니다.
  • 웹에서의 적용 — DB 내의 복잡한 스키마 관계, 여러 테이블의 조인, 여러 리포지토리 호출 과정을, 비즈니스 서비스 계층이 단순한 인터페이스 하나로 접근하게 묶어 가독성을 높입니다.

💡 퍼사드와 어댑터의 차이 둘 다 "사이에 끼는 것"이라 헷갈립니다. 축은 이렇게 갈립니다.

  • 어댑터 — 인터페이스가 안 맞아서 낀다. 목적은 호환.
  • 퍼사드 — 인터페이스가 너무 많아서 낀다. 목적은 단순화.

1.5 이 매핑의 결론

사용자의 행위(Behavioral) 가 프론트엔드에서 시작되어 API를 타고 넘어오면, 백엔드가 효율적으로 객체를 생성(Creational) 하여 비즈니스 로직을 처리하고, 안정적이고 유연한 구조(Structural) 를 통해 데이터베이스에 연결되어 데이터를 CRUD 합니다.

1.6 흔한 오해 — "그 계층엔 그 패턴만 쓴다"

⚠️ 아닙니다. 프론트에도 팩토리를 쓰고, DB 계층에도 전략 패턴을 씁니다. 이 매핑은 금지 규칙이 아니라 무게중심입니다. 진짜 쓸모는 패턴 이름 암기가 아니라 새 코드를 어디에 둘지 정할 때 있습니다. "이건 행동을 갈아끼우는 일인가, 객체를 만드는 일인가, 연결을 정돈하는 일인가"를 물으면 그 코드가 살 자리가 정해집니다.


2. CQRS — 읽기와 쓰기를 갈라놓기

2.1 정의부터

CQRS(Command Query Responsibility Segregation, 명령 조회 책임 분리) — [아키텍처 패턴]인데, [시스템의 데이터 모델을 상태를 변경하는 모델(Command)과 상태를 조회하는 모델(Query)로 명확히 분리한다]는 점에서 단일 모델 CRUD 아키텍처와 구별된다.

                       ┌───► [ Command Path ] ───► 상태 변경 (Write DB)
[ Client / Frontend ] ─┤
                       └───► [ Query Path ] ───► 상태 조회 (Read DB)

⭐ 핵심 한 줄

이 절에서 딱 한 줄만 가져간다면 이것입니다.

"질의(Query)는 상태를 바꾸지 않고, 명령(Command)은 결과를 반환하지 않는다."

이 문장은 정의이자 판별 기준이자 코드 리뷰 체크리스트입니다. 어떤 함수가 값을 바꾸면서 동시에 데이터를 리턴한다면, 그 함수는 CQRS를 어기고 있는 것입니다.

2.2 ❌ 기존 CRUD 아키텍처의 한계

왜 굳이 나누느냐. 안 나눴을 때 무엇이 무너지는지부터 봐야 합니다.

① 모델의 복잡성 증가 동일한 도메인 모델이나 DB 테이블을 조회(R)와 변경(C·U·D)에 모두 사용하면, 비즈니스 로직과 화면 UI 요구사항이 한데 섞여 코드가 비대해집니다. "저장할 때 필요한 검증 규칙"과 "화면에 보여줄 때 필요한 가공 규칙"은 성격이 완전히 다른데 한 클래스에 같이 들어앉게 됩니다.

② 성능 최적화의 한계 대다수의 웹 서비스는 변경보다 조회 트래픽이 압도적으로 높습니다. 그런데 쓰기용 모델은 무결성을 위해 정규화되어 있고, 정규화된 테이블은 조회할 때 조인(Join) 이 많습니다. 단일 DB 구조에서는 이 조인 쿼리와 인덱스 경합 때문에 전체 시스템 성능이 저하됩니다.

⭐ 여기가 CQRS의 출발점입니다. "읽기 빈도가 쓰기보다 훨씬 높은데, 쓰기용으로 최적화된 모델을 읽기에 그대로 쓰고 있다"는 불균형이 문제입니다. 하나의 모델로 두 개의 상반된 요구를 동시에 만족시킬 수 없다는 것 — 3주차에서 배운 트레이드오프입니다.

2.3 CQRS의 두 측면

✍️ 명령 (Command) 측면

항목내용
역할시스템의 상태를 생성·수정·삭제 (Create, Update, Delete)
SQLINSERT, UPDATE, DELETE
특징데이터 무결성과 비즈니스 검증(Validation) 규칙을 철저히 준수
반환 원칙수행 후 비즈니스 데이터 자체를 반환하지 않음. 성공/실패 여부 또는 식별자(ID)만 반환
웹 계층 매칭백엔드의 생성(Creational) 계층이 중심. DB의 정규화된 상태를 안전하게 관리

🔍 조회 (Query) 측면

항목내용
역할화면에 데이터를 표시하기 위해 읽는 작업 (Read)
SQLSELECT
특징비즈니스 로직을 거의 포함하지 않음. 오직 화면에 최적화된 빠른 응답 속도만이 목적
반환 원칙데이터만 반환하고 시스템에 부작용(Side-effect)을 남기지 않음
웹 계층 매칭프론트엔드의 다양한 데이터 표현 요구에 맞춰, 미리 가공·정렬된 구조(Structural) 계층의 데이터를 다이렉트로 전달

⚠️ "명령이 결과를 반환하지 않는다"가 이해가 안 될 때 — 은행 창구를 떠올리세요. 입금 전표를 냈을 때 돌려받는 건 "처리됐습니다"라는 확인증(그리고 거래번호) 이지 은행의 전체 잔고 장부가 아닙니다. 잔고를 보려면 따로 조회 요청을 해야 하고, 그게 Query입니다.

2.4 🔹 1단계: 논리적 분리 (코드 레벨 분리)

CQRS는 한 번에 다 도입하는 게 아닙니다. 시스템의 규모와 트래픽에 따라 점진적으로 갑니다.

항목내용
구조동일한 데이터베이스를 공유하지만, 백엔드 애플리케이션 내부에서 클래스를 분리
예시비즈니스 로직을 처리하는 ProductService(명령)와, 화면 조회를 전담하는 ProductQueryService(조회)를 분리
장점복잡한 비즈니스 규칙과 단순 조회 코드가 뒤섞이지 않아 가독성·유지보수성이 대폭 향상
비용거의 없음. 인프라를 하나도 바꾸지 않음

우리 프로젝트에서 ..._command.py와 ..._query.py처럼 파일 이름을 나누는 것이 정확히 이 1단계입니다. DB는 하나인데 코드만 두 갈래로 갈라 놓은 상태입니다.

2.5 🔹 2단계: 물리적 분리 (DB 레벨 분리)

항목내용
구조데이터 저장소 자체를 명령용 DB(Write DB) 와 조회용 DB(Read DB) 로 물리적으로 나눔
Write DB관계형 데이터베이스(RDB) — 데이터를 정규화하고 트랜잭션의 안전성(ACID)을 보장
Read DB조회·검색 속도가 뛰어난 NoSQL(MongoDB, Redis)이나 검색엔진(Elasticsearch) — 화면 맞춤형 데이터를 역정규화 상태로 유지
Write DBRead DB
대표 기술PostgreSQL, MySQLMongoDB, Redis, Elasticsearch
데이터 형태정규화 (중복 제거)역정규화 (미리 합쳐 둠)
최적화 목표무결성·트랜잭션조회 속도
조인많음없거나 거의 없음
3주차 연결RDBMS / ACIDNoSQL / 역색인

💡 3주차의 역색인(Inverted Index) 이 여기서 다시 등장합니다. Elasticsearch를 Read DB로 쓴다는 것은 조회 전용 저장소를 아예 검색에 최적화된 자료구조로 갈아 끼운다는 뜻입니다.

2.6 데이터 동기화와 최종 일관성

Write DB와 Read DB를 물리적으로 나누면 곧바로 질문이 하나 생깁니다. "그럼 두 DB의 내용은 언제 맞춰지는가?"

  • 명령 계층에서 데이터가 변경되면 이벤트(Event) 나 비동기 메시징 시스템을 통해 조회 계층의 DB로 전송하여 동기화합니다.
  • 이 동기화는 즉시가 아닙니다. 짧은 시간(보통 밀리초~수초) 동안 두 DB의 내용이 다를 수 있고, 이 상태를 인정하고 설계하는 것을 최종 일관성(Eventual Consistency) — "지금은 다를 수 있지만 결국 같아진다" — 이라고 합니다.
[Command] ──► Write DB (RDB)
                 │
                 ├── 이벤트 발행 (비동기)
                 ▼
              Read DB (NoSQL / 검색엔진) ◄── [Query]
강한 일관성 (Strong Consistency)최종 일관성 (Eventual Consistency)
쓴 직후 읽으면반드시 최신값잠깐 예전 값일 수 있음
성능낮음 (기다려야 함)높음 (안 기다림)
어울리는 곳계좌 잔액, 재고 차감게시글 조회수, 상품 목록, 검색 결과

⚠️ 최종 일관성을 아무데나 쓰면 안 됩니다. "결제했는데 잔액이 그대로"는 사고이고, "좋아요 눌렀는데 숫자가 1초 늦게 오른다"는 사고가 아닙니다. 틀려도 되는 시간이 있느냐를 기준으로 판단합니다.

2.7 1장의 벤다이어그램과 이어붙이기

1장에서 그린 웹 3계층 ↔ GoF 3분류 매핑에 CQRS를 얹으면 방향이 두 개로 갈라집니다.

① 명령 경로 — F(행위) ── API(Command) ──► B(생성) ──► Write DB 사용자의 입력 행위가 API를 타고 들어오면, 백엔드가 규칙에 맞게 객체를 생성하고 비즈니스 유효성을 체크한 뒤 Write DB에 안전하게 기록합니다.

② 조회 경로 — F(행위) ◄── API(Query) ◄── B(조회) ◄── Read DB 사용자가 화면을 요청하면, 백엔드는 별도의 복잡한 가공 과정 없이 화면 맞춤형 구조로 저장된 Read DB에서 데이터를 긁어와 프론트에 즉시 반환합니다.

여기서 중요한 관찰이 하나 있습니다. 조회 경로에는 "생성"이 없습니다. 객체를 새로 만들 필요가 없으니 생성 패턴도 필요 없고, 그래서 조회는 구조적으로 가벼울 수 있습니다. CQRS는 이 "가벼울 수 있음"을 최대한 끌어내는 설계입니다.

2.8 언제 CQRS를 쓰지 말아야 하는가

⚠️ CQRS는 공짜가 아닙니다. 2단계 물리적 분리는 특히 비쌉니다.

상황권장
읽기·쓰기 비율이 비슷하고 트래픽도 적음도입 안 함. 평범한 CRUD가 정답
조회 로직과 비즈니스 로직이 뒤엉킴1단계(논리적 분리)만 도입
조회 트래픽이 쓰기의 수십~수백 배, 조인이 무거움2단계(물리적 분리) 검토
실시간 정확성이 절대적인 도메인최종 일관성이 허용되는 부분만 부분 도입

1단계는 거의 언제나 이득입니다(인프라 비용 없이 코드만 정리). 2단계는 문제가 실제로 생긴 뒤에 갑니다.

2.9 곁가지 — 백엔드에서 CSV 파일을 제거한 이유 = 무상태(Stateless)

CQRS와 같은 뿌리에서 나오는 실무 판단이 하나 있습니다. 백엔드 서버 안에 CSV 파일 같은 상태를 두지 않는다는 원칙입니다.

왜 제거하는가

  1. 수평 확장이 쉬움 — 서버를 여러 대로 늘려도 각 서버가 아무 상태도 안 갖고 있으므로, 특정 사용자를 특정 서버에 묶어두는 Sticky Session이 필요 없습니다.
  2. 장애 극복(Failover) — 서버가 죽었다 살아나도 잃을 상태가 없으므로 무영향입니다.

FastAPI의 Depends가 여기 딱 맞는 이유 Depends는 요청마다 DB 세션·인증 객체를 만들고, 요청이 끝나면 정리합니다. 요청 사이에 아무것도 남기지 않으므로 무상태에 최적입니다.

그럼 상태는 어디에 두는가 — 전부 위임합니다.

상태의 종류위임처
인증 정보JWT (토큰 자체에 담아 클라이언트가 들고 다님)
단기 세션·장바구니Redis
영속 데이터RDBMS / NoSQL

⭐ 연결점: "서버는 아무것도 기억하지 않는다"와 "질의는 상태를 바꾸지 않는다"는 같은 사고방식입니다. 상태를 다루는 자리를 최대한 좁게, 명시적으로 몰아넣는다 — 이번 주 내내 반복되는 태도입니다.


3. SQL 명령어 체계 — 두 개의 분류축

3.1 왜 SQL 분류가 헷갈리는가

DDL, DML, DQL, DCL, TCL. 다섯 개의 약어를 한 줄에 나열해놓고 외우려 하면 반드시 실패합니다. 이 다섯은 같은 층위가 아니기 때문입니다. 서로 다른 두 개의 질문에서 나온 답들입니다.

축질문관점여기 속하는 것
목적별"데이터를 어떻게 다루는가?"애플리케이션 개발자 관점DQL, DML
대상별"데이터베이스 시스템의 무엇을 제어하는가?"데이터베이스 관리자(DBA) 관점DDL, DCL, TCL

이 두 축을 먼저 갈라놓고 시작하면 다섯 개가 한 번에 정리됩니다.

3.2 목적에 따른 분류 (CRUD 기반 — 애플리케이션 관점)

데이터의 '상태 변화' 를 중심으로 조작·조회하는 명령어입니다.

구분명령어역할CRUD 매핑
DQLSELECT데이터를 조회하여 현재 상태를 확인합니다.Read
DMLINSERT새로운 데이터를 생성(추가)하여 상태를 변화시킵니다.Create
DMLUPDATE기존 데이터를 수정하여 상태를 변화시킵니다.Update
DMLDELETE기존 데이터를 삭제하여 상태를 변화시킵니다.Delete
  • DQL — Data Query Language, 데이터 질의어
  • DML — Data Manipulation Language, 데이터 조작어

⭐ DQL과 DML이 갈리는 지점 = CQRS와 정확히 같은 선 SELECT 하나만 DQL이고 나머지 셋이 DML인 이유는 상태를 바꾸느냐 안 바꾸느냐입니다. 즉 DQL = Query, DML = Command. 2장의 CQRS가 SQL 문법 층위에 이미 새겨져 있었던 것입니다.

3.3 대상에 따른 분류 (데이터베이스 시스템 기반 — 관리자 관점)

데이터베이스의 환경, 구조, 보안을 제어하는 명령어입니다.

구분명령어역할
DDLCREATE, ALTER, DROP, TRUNCATE데이터베이스 객체(테이블 등)의 구조를 생성·변경·삭제합니다.
DCLGRANT, REVOKE데이터베이스에 접근하거나 작업을 수행할 권한을 관리합니다.
TCLCOMMIT, ROLLBACK여러 개의 DML 작업을 하나의 단위로 묶어 처리하거나 취소합니다.
  • DDL — Data Definition Language, 데이터 정의어 → 틀을 만든다
  • DCL — Data Control Language, 데이터 제어어 → 누가 만질 수 있는지 정한다
  • TCL — Transaction Control Language, 트랜잭션 제어어 → 묶어서 확정하거나 되돌린다

3.4 ⚠️ 헷갈리기 딱 좋은 자리 — DELETE vs TRUNCATE vs DROP

셋 다 "지운다"인데 소속이 다릅니다. 이 표 하나로 정리됩니다.

DELETETRUNCATEDROP
분류DMLDDLDDL
지우는 것행(row)행 전체테이블 자체
테이블 구조남음남음사라짐
WHERE 조건가능불가해당 없음
롤백(ROLLBACK)가능대체로 불가대체로 불가
속도느림빠름빠름

⚠️ 가장 흔한 사고: "전부 지우려고 TRUNCATE 썼는데 ROLLBACK이 안 돼요." TRUNCATE는 DDL이라 데이터를 하나씩 지우는 게 아니라 테이블을 통째로 비워 새로 만드는 동작에 가깝습니다. 되돌릴 수 있는 삭제가 필요하면 DELETE(DML)를 쓰세요.

3.5 요약 — 상태변화가 중심

  • 상태의 변화(CRUD): DQL과 DML을 통해 데이터의 내용을 읽거나 변경합니다.
  • 시스템의 관리: DDL, DCL, TCL을 통해 데이터베이스의 틀을 잡고 안전하게 관리합니다.

⭐ 상태변화가 중심 명령어를 알파벳순으로 외우면 절대 안 붙습니다. "이 명령이 무엇의 상태를 바꾸는가" 를 물으면 전부 제자리를 찾습니다. 데이터의 상태를 바꾼다 → DML / 안 바꾸고 보기만 한다 → DQL / 구조의 상태를 바꾼다 → DDL / 권한의 상태를 바꾼다 → DCL / 상태 변화의 확정 여부를 바꾼다 → TCL

3.6 곁가지 — 정규화 2NF와 3NF

2장에서 "Write DB는 정규화되어 있다"고 했습니다. 그 정규화의 기본 두 단계는 반드시 구별해야 합니다.

단계제거하는 것한 줄 설명
2NF (제2정규형)부분적 종속복합 키의 일부에만 딸린 컬럼을 떼어낸다
3NF (제3정규형)이행적 종속키가 아닌 컬럼에 딸린 컬럼을 떼어낸다 (A→B→C의 C)
  • 부분적 종속 — 기본키가 (주문번호, 상품번호)인데 상품명이 상품번호에만 딸려 있는 경우. 키의 일부에만 종속된 것을 떼어내는 게 2NF입니다.
  • 이행적 종속 — 사원번호 → 부서번호 → 부서명. 부서명이 키에 직접이 아니라 부서번호를 거쳐 딸린 경우. 이걸 떼어내는 게 3NF입니다.

정규화를 하면 중복이 줄고 무결성이 올라가지만 조인이 늘어납니다. 그래서 Read DB는 이걸 일부러 되돌립니다(역정규화). 정규화와 역정규화는 우열이 아니라 목적이 다른 방향입니다.


4. 프로젝트 디렉터리 ↔ 개념 매핑

4.1 폴더 이름이 곧 개념이다

SQL 분류를 배웠으니 백엔드 폴더를 다시 봅시다. 폴더 이름과 SQL 분류가 일대일로 맞아떨어집니다.

backend/
├── alembic/          → DDL
├── apps/             → DML, DQL
├── data/
├── gateway/
├── services/
└── core/             → Docker
    ├── __init__.py
    ├── .env
    ├── .gitignore
    ├── alembic.ini
    ├── database.py
    ├── main.py
    ├── README.md
    └── requirements.txt
docs/
frontend/
.cursorrules
.gitmodules

4.2 세 폴더의 정체

① alembic/ ➔ 💾 DDL (Data Definition Language)

  • 개념 — 데이터 정의어
  • 역할 — 데이터베이스 마이그레이션 관리 (SQLAlchemy 연동)
  • 상세
    • CREATE, ALTER, DROP 등 데이터베이스 객체 구조(스키마)를 생성·변경하는 역할을 담당합니다.
    • alembic 내의 버전별 마이그레이션 스크립트들이 테이블 구조의 변천사를 정의하므로 DDL과 정확히 매치됩니다.
  • 영문 정리 — MANAGE DB SCHEMA / CREATE, ALTER, DROP TABLES / VERSIONED MIGRATIONS

💡 왜 마이그레이션이 DDL인가 — 마이그레이션 파일 하나하나가 "이 시점에 테이블 구조를 이렇게 바꿨다"는 DDL 실행 기록입니다. Git이 소스코드의 변천사를 관리하듯, alembic은 DB 구조의 변천사를 관리합니다.

② apps/ ➔ 🔄 DML & 🔍 DQL

비즈니스 로직과 API 엔드포인트가 모여 있는 핵심 애플리케이션 레이어입니다. 내부 처리 데이터의 성격에 따라 DML과 DQL로 기능이 갈립니다.

  • DML (데이터 조작어)
    • INSERT, UPDATE, DELETE를 통해 데이터의 상태를 변형합니다.
    • 주로 서비스 레이어나 어댑터에서 새로운 데이터를 생성·수정·삭제할 때 발생합니다.
  • DQL (데이터 질의어)
    • SELECT를 통해 저장된 데이터를 조회합니다.
    • @app.get("/food-stats/yearly") 같은 조회용 GET API 엔드포인트들이 대표적인 DQL의 실행 통로가 됩니다.

⭐ 여기서 CQRS가 폴더 구조로 내려옵니다. apps/ 안에서 DML 파일과 DQL 파일을 나누는 것이 2.4절의 CQRS 1단계(논리적 분리) 이고, ..._command.py / ..._query.py 관행이 여기서 나옵니다.

③ core/ ➔ 🐳 Docker

  • 개념 — 컨테이너라이제이션 및 인프라 환경 구축
  • 역할 — 애플리케이션 공통 설정, 보안, 데이터베이스 세션 관리 (database.py)
  • 상세
    • 시스템 전반에서 참조하는 환경 변수(.env)와 전역 설정 정보가 집약되어 있습니다.
    • 이 정보들은 Docker 컨테이너 환경과 밀접하게 연동되어 구동 환경(Development, Production)을 격리하고 인프라를 표준화하는 기반이 됩니다.

4.3 매핑 요약표

디렉터리 / 파일대응 개념주요 기능 및 역할
alembic/DDL테이블 생성 및 변경 등 DB 스크립트 버전 관리
apps/ (CUD 로직)DML데이터 삽입·수정·삭제 (비즈니스 로직 반영)
apps/ (R 로직 / GET API)DQL대시보드 통계 및 조회 API
core/ & 루트 설정Docker Env컨테이너 환경에서 참조할 환경 변수 및 핵심 인프라 설정

하단 산출물은 두 갈래로 나옵니다.

  • API ENDPOINTS ← apps/ 에서
  • SYSTEM CONFIG ← core/ 의 .env, database.py 에서

⭐ 판별 축 4 적용 — alembic/은 구조를 정의하고 apps/는 데이터를 조작합니다. 둘을 폴더로 갈라놓는 이유는 변경 주기가 다르기 때문입니다(구조는 가끔, 데이터는 매 요청마다). 변경 주기가 다른 것은 같은 자리에 두지 않는다 — 5장에서 배울 SRP입니다.

4.4 서비스 이름 짓기 — gateway와 그 아래

폴더 이름은 개념을 담아야 하지만, 팀이 알아볼 수 있어야 합니다. 이 프로젝트는 서비스별 코드네임을 이렇게 씁니다.

코드네임담당 도메인
friday13th보안
imitation_game튜링머신 — 딥러닝
inception비전 처리
matrixAPI 키메이커
social_network채팅 관리
titanic머신러닝

각 서비스는 gateway 아래에 붙습니다. gateway가 곧 퍼사드(Facade) 입니다. 밖에서 보면 문이 하나인데, 안에서는 여러 서비스로 갈라집니다.

4.5 ⚠️ 이름 충돌 정리 — 이 계층을 뭐라고 부를 것인가

헥사고날·클린·DDD를 섞어 쓰면 같은 것을 서로 다른 이름으로 부르는 문제가 반드시 생깁니다. 팀에서 미리 하나를 골라 못 박아야 합니다.

이 계층은후보 이름권장
내부에서 여러 서비스를 조율하는 계층usecases / application_services / orchestrators⭐ usecases (헥사고날·클린 표준)
최상단에서 JWT 처리·요청 분배를 하는 계층gateways / middlewares / auth프로젝트 성격에 따라 택일
  • application_services는 DDD의 어휘이고, orchestrators는 여러 서비스를 지휘한다는 뉘앙스를 강조하는 이름입니다. 우리는 헥사고날·클린을 주축으로 쓰므로 usecases 로 통일합니다.

⚠️ 셋을 섞어 쓰면 "이 로직은 서비스인가 유스케이스인가"라는 답 없는 논쟁이 매주 반복됩니다. 이름 통일은 취향 문제가 아니라 유지보수 비용 문제입니다. (DDD는 이걸 보편적 언어(Ubiquitous Language) 라고 부릅니다.)


5. ⭐ SOLID — 컴포넌트 설계의 5대 규칙

5.1 SOLID란 무엇인가

SOLID — [객체지향 설계 원칙의 모음]인데, [코드의 변경 유연성·확장성·재사용성을 높이는 다섯 가지 법칙의 머리글자]라는 점에서 특정 언어의 문법이나 패턴 이름과 구별된다.

패턴(GoF)이 "이런 문제엔 이런 해법을 써라"라는 레시피라면, SOLID는 "코드를 이렇게 배치해라"라는 원칙입니다. 레시피는 상황마다 고르지만 원칙은 항상 적용됩니다.

5.2 다섯 원칙 한눈에

원칙명칭핵심 개념클린 아키텍처와의 연계성
SRP단일 책임 원칙<br>(Single Responsibility Principle)하나의 모듈은 오직 하나의 변경 이유(하나의 액터) 를 가져야 한다.비즈니스 로직(Use Case)과 UI 처리, DB 접근 코드를 완전히 격리하고 독립적인 컴포넌트로 분리하는 근거가 됨.
OCP개방-폐쇄 원칙<br>(Open-Closed Principle)확장에는 열려 있고, 수정에는 닫혀 있어야 한다.새로운 기능이나 프레임워크를 추가할 때 기존 핵심 비즈니스 로직의 코드를 단 한 줄도 수정하지 않도록 인터페이스 경계를 설정함.
LSP리스코프 치환 원칙<br>(Liskov Substitution Principle)하위 타입은 언제나 상위 타입으로 교체되어도 구조적 결함 없이 대체 가능해야 한다.아키텍처 내부에서 인터페이스 기반의 다형성을 안전하게 활용할 수 있도록 보장하는 규칙.
ISP인터페이스 분리 원칙<br>(Interface Segregation Principle)사용하지 않는 메서드에 의존하지 않도록 인터페이스를 작고 구체적으로 분리해야 한다.각 레이어와 어댑터가 자신에게 꼭 필요한 최소한의 데이터 진입점·메서드에만 결합하도록 제한하여 불필요한 재컴파일과 결합도를 방지함.
DIP의존성 역전 원칙<br>(Dependency Inversion Principle)고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화(인터페이스)에 의존해야 한다.안쪽 원(핵심 도메인 로직)이 바깥쪽 원(프레임워크·DB 등 세부 구현)을 절대 알지 못하게 통제하는 '의존성 규칙' 의 기술적 기반.

5.3 SRP — 단일 책임 원칙

"하나의 모듈은 오직 하나의 변경 이유를 가져야 한다."

"하나의 클래스는 하나의 일만"으로 외우면 실전에서 판단이 안 섭니다. 정확한 기준은 "이 코드를 고쳐 달라고 요청할 사람이 몇 명인가" 이고, 그 사람을 액터(Actor) 라고 부릅니다.

상황액터판단
급여 계산 로직회계팀액터 1
근무시간 집계 로직인사팀액터 2
위 둘이 같은 클래스에 있음액터 2명❌ SRP 위반

왜 문제인가 — 회계팀 요청으로 코드를 고쳤는데 인사팀 기능이 깨집니다. 우리 구조에서 — apps/(DML·DQL)와 alembic/(DDL)을 나눈 것, 라우터·유스케이스·리포지토리를 나눈 것이 전부 SRP입니다.

5.4 OCP — 개방-폐쇄 원칙

"확장에는 열려 있고, 수정에는 닫혀 있어야 한다." — 새 기능을 추가하는 것은 자유롭되(Open), 기존 코드를 수정하지는 않는다(Closed).

추상화(인터페이스) 활용 → 새로운 기능은 코드 추가로 확장 (Open) → 기존 코드는 수정 없이 유지 (Closed)

중앙에 Core Component(핵심 로직) 와 Common Interface(공통 인터페이스) 가 있고 그 바깥에 Extensions(확장 기능) 이 붙습니다. Router / Use Case / Interactor / Repository → pg_repository 로 이어지는 우리 구조가 그대로 이 형태입니다.

판별법 — 새 결제수단을 추가할 때 기존 코드에 if 카카오페이: 를 한 줄 더 넣어야 하면 위반. 새 클래스 파일 하나만 추가하면 끝나면 준수.

5.5 LSP — 리스코프 치환 원칙

"하위 타입은 언제나 상위 타입으로 교체되어도 구조적 결함 없이 대체 가능해야 한다."

Repository 인터페이스의 save() 를 PostgresRepository가 구현했다면, 유스케이스는 그것이 Postgres인지 MongoDB인지 알 필요도 없고 동작이 달라져서도 안 됩니다. 어떤 구현체만 예외를 던지거나 반환값의 의미가 다르면 위반이고, 그 순간 호출하는 쪽이 if isinstance(...) 를 쓰기 시작해 OCP도 함께 무너집니다.

⚠️ LSP가 깨지면 다형성이 거짓말이 됩니다. 3주차의 다형성은 결과의 계약이 같을 때만 안전합니다. "다르게 동작"은 어떻게(How) 가 다른 것이지, 무엇(What) 이 달라지는 게 아닙니다.

5.6 ISP — 인터페이스 분리 원칙

"사용하지 않는 메서드에 의존하지 않도록 인터페이스를 작고 구체적으로 분리해야 한다."

거대한 인터페이스 하나를 만들면, 그중 두 개만 쓰는 구현체도 나머지 전부를 억지로 구현해야 하고, 안 쓰는 메서드가 바뀔 때마다 상관없는 클래스가 재빌드·재배포 됩니다.

❌ 나쁜 예⭕ 좋은 예
UserRepository 하나에 save, find, sendEmail, exportCsv, generatePdfUserWriter(save) / UserReader(find) / Notifier(sendEmail) 로 분리

⭐ 8장에서 볼 ports/input/ 과 ports/output/ 을 나누는 것 자체가 ISP입니다.

5.7 DIP — 의존성 역전 원칙

"고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화(인터페이스)에 의존해야 한다."

  • 고수준 모듈(High-level) — 비즈니스 로직. 무엇을 할 것인가를 아는 쪽.
  • 저수준 모듈(Low-level) — DB, 파일, 외부 API. 어떻게 할 것인가를 아는 쪽.
  • 목적 — 하위 기술(저수준)의 변경이 상위 비즈니스(고수준)를 흔들지 못하게 소스코드의 의존 방향을 역전시킵니다.

⚠️⚠️ 정정 — DIP는 Dependency Injection이 아닙니다

이건 아주 많이 나오는 오해라서 따로 박아둡니다.

약어풀네임무엇인가
DIPDependency Inversion Principle원칙 — 의존 역전. "둘 다 추상에 의존해라"
DIDependency Injection기법 — 의존성 주입. "필요한 객체를 밖에서 넣어줘라"
  • DIP는 설계 원칙이고, DI는 그 원칙을 실현하는 구현 기법입니다.
  • DI를 쓴다고 DIP가 저절로 지켜지는 것도 아닙니다. 구상 클래스를 주입하면 DI는 맞지만 DIP는 위반입니다.
  • 반대로 DI 컨테이너 없이 생성자에 손으로 넣어줘도 DIP는 지킬 수 있습니다.

⭐ 3주차에서 "DIP·IoC·DI·포트&어댑터는 전부 같은 것의 다른 이름"이라고 배웠습니다. 정확히는 "같은 목표를 향한 서로 다른 층위" 입니다. 7.7절에서 층위별로 완전히 정리합니다.


6. ⭐ 클린 아키텍처 (Clean Architecture)

6.1 정의

클린 아키텍처(Clean Architecture) — [소프트웨어 아키텍처]인데, ["관심사의 분리(Separation of Concerns)"를 원형 계층 구조로 시각화하고, 모든 의존성이 안쪽을 향하도록 강제한다]는 점에서 다른 계층형 아키텍처와 구별된다.

로버트 C. 마틴(Robert C. Martin)이 제안했습니다. 시스템의 본질적인 비즈니스 가치와 기술적인 세부사항을 엄격히 분리하는 데 주안점을 둡니다.

6.2 의존성 규칙 (The Dependency Rule)

이 아키텍처의 규칙은 딱 하나입니다.

⭐ 모든 의존성은 안쪽 원을 향해서만 수렴해야 합니다.

  • 안쪽 원의 코드는 바깥쪽 원의 그 어떤 세부 구현체(DB 이름, 웹 프레임워크 라이브러리, UI 컴포넌트)도 참조하거나 알 수 없습니다. 바깥쪽은 언제든 교체 가능한 변두리 세부사항에 불과합니다.

판별법이 놀랄 만큼 간단합니다. 안쪽 파일의 import 문에 sqlalchemy, fastapi, psycopg2 같은 단어가 하나라도 있으면 그 순간 규칙 위반입니다.

6.3 4계층

[ Frameworks & Drivers (외곽 계층: Next.js, FastAPI, PostgreSQL) ]
       ↓
   [ Interface Adapters (인터페이스 어댑터: Controller, Presenter, Gateway) ]
          ↓
      [ Use Cases (애플리케이션 특화 비즈니스 로직) ]
             ↓
         [ Entities (핵심 도메인 규칙 및 데이터) ]

① Entities — 핵심 업무 규칙

  • 시스템 전체에서 가장 고수준이며 순수한 비즈니스 개념입니다.
  • 프레임워크나 UI, 특정 서비스 흐름에 종속되지 않는 독립적인 비즈니스 객체와 범용적인 핵심 규칙을 포함합니다.
  • 운영체제나 데이터베이스 기술이 바뀌어도 이 영역은 절대 영향을 받지 않습니다.

② Use Cases — 애플리케이션 특화 업무 규칙

  • 엔티티를 제어하여 시스템의 실제 사용 시나리오와 비즈니스 동작 흐름을 조율하는 영역입니다.
  • 사용자가 입력한 데이터가 어떻게 가공되고 어떤 절차를 거쳐 출력되는지 전체 워크플로우를 서술합니다.
  • 화면 UI나 DB 테이블 구조의 변화에는 무관하지만, 비즈니스 요구사항(예: 주문 취소 절차 변경)이 바뀌면 영향을 받습니다.

③ Interface Adapters — 인터페이스 어댑터

  • 가장 바깥쪽의 프레임워크 데이터 구조와, 가장 안쪽의 비즈니스 로직이 이해할 수 있는 데이터 포맷 사이에서 '통역' 을 담당하는 계층입니다.
  • 여기 배치되는 것들:
    • Controller — 웹 요청을 처리
    • Presenter — 비즈니스 실행 결과를 화면 데이터로 가공
    • Repository Implementation / Gateway — 데이터베이스 접근을 추상화

④ Frameworks & Drivers — 외곽 구현 계층

  • 웹 프레임워크(Next.js, FastAPI), DBMS(PostgreSQL), 외부 연동 API 라이브러리 등 플랫폼 고유의 세부 기술이 위치합니다.
  • 이 영역의 코드는 언제든 다른 프레임워크나 툴로 완전히 대체될 수 있도록 핵심 설계 밖에서 플러그인 형태로 동작해야 합니다.
계층변경 이유변경 빈도3주차 어휘로는
Entities비즈니스의 본질이 바뀔 때거의 없음개체(Entity)
Use Cases업무 절차가 바뀔 때가끔객체(Object)의 협력
Interface Adapters입출력 형식이 바뀔 때자주DTO·매퍼
Frameworks & Drivers기술 트렌드가 바뀔 때자주인프라

⭐ 동심원의 진짜 의미 — 안쪽일수록 변하지 않는 것, 바깥쪽일수록 자주 변하는 것입니다. 클린 아키텍처는 "잘 안 변하는 것 위에 자주 변하는 것을 얹어라"는 배치 규칙입니다. 반대로 프레임워크 위에 비즈니스 로직을 얹으면, 프레임워크 버전이 오를 때마다 비즈니스가 흔들립니다.

6.4 SOLID와 클린 아키텍처의 결합 메커니즘 — DIP의 실전 활용

여기서 가장 중요한 질문이 나옵니다.

"안쪽 계층(Use Case)이 바깥쪽 계층(PostgreSQL DB)의 데이터를 가져와야 할 때, 의존성 규칙(안쪽은 바깥쪽을 모른다)을 어떻게 안 어기지?"

답이 DIP(의존성 역전 원칙) 입니다.

[ Use Case 계층 (내부 원) ]                        [ Frameworks & DB 계층 (외부 원) ]
┌─────────────────────────────┐                  ┌──────────────────────────────┐
│  Use Case Logic             │                  │  PostgreSQLDatabase          │
│       ↓ (직접 호출)          │                  │  (실제 SQL 쿼리 실행)         │
│  UserRepository (인터페이스) │ ←─ (상속/구현) ───│  PostgreSQLUserRepository    │
└─────────────────────────────┘                  └──────────────────────────────┘
  1. 내부 원에서의 추상화 정의 — Use Case 계층 내부에 데이터를 저장·조회할 때 필요한 명세서인 UserRepository 인터페이스를 정의합니다.
    • 핵심은 이 인터페이스가 바깥이 아니라 안쪽에 산다는 것입니다.
  2. 외부 원에서의 구체화 구현 — 가장 바깥 계층인 Framework/DB 영역에서 이 인터페이스를 상속받아 실제로 DB에 쿼리를 날리는 PostgreSQLUserRepository 구현체 클래스를 작성합니다.
  3. 런타임 의존성 주입 (DI) — 애플리케이션 실행 시점에 제어권을 가진 외부 컨테이너가 구현체 객체를 Use Case에 주입해 줍니다.
  4. 결과 — 코드 작성(컴파일) 시점의 의존성은 외곽에서 중심(인터페이스)으로 향하므로 아키텍처 규칙이 완벽히 유지됩니다. 향후 DB를 MySQL이나 MongoDB로 바꾸더라도 내부 Use Case 코드는 수정할 필요가 없습니다. 이것이 OCP의 극대화입니다.

⭐ 이 네 단계가 이번 주 전체의 엔진입니다. 인터페이스를 누가 소유하느냐가 전부입니다. 소유자가 안쪽이면 의존성이 뒤집힙니다.

6.5 요약 결론

  • SOLID 가 모듈과 클래스를 안전하게 빌드하기 위한 코드 레벨의 지침이라면, 클린 아키텍처 는 시스템 전체의 계층 경계를 허물어지지 않게 통제하는 거시적 지도입니다.
  • 기술 트렌드 변화(프레임워크 교체, 인프라 이전)에 종속되지 않고 비즈니스의 고유 가치와 도메인 로직을 끝까지 보호하는 것이 두 개념의 궁극적 지향점입니다.

7. ⭐ 헥사고날 아키텍처 (Hexagonal Architecture)

7.1 정의와 이름

헥사고날 아키텍처(Hexagonal Architecture) — [아키텍처 패턴]인데, [핵심 비즈니스 로직(Core)을 DB·웹 프레임워크·외부 API 같은 구체적 기술 환경(Infrastructure)으로부터 Port와 Adapter로 철저히 분리한다]는 점에서 전통적인 계층형(Layered) 아키텍처와 구별된다.

  • 다른 이름: 포트와 어댑터 아키텍처 (Ports and Adapters Architecture). 이 이름이 내용을 더 정확히 말해줍니다.
  • 왜 육각형인가? — 육각형 자체에 의미가 있는 게 아니라, "외부와 맞닿는 면이 여럿이고 각 면마다 어댑터를 꽂을 수 있다"는 걸 시각화하려고 다각형을 쓴 것뿐입니다. 오각형이든 팔각형이든 상관없습니다.

이 구조의 핵심은 이것입니다.

"추상(Interface)"이라는 벽을 세워, 외부의 "기술적 구상"과 내부의 "비즈니스적 구상"이 서로 직접 의존하지 않고 오직 계약(Contract)을 통해서만 소통하게 만든다.

7.2 핵심 경계 — 추상(Abstraction)과 구상(Concretization)

헥사고날을 올바르게 구현하려면, 어떤 코드가 '추상'이고 어떤 코드가 '구상'인지 명확히 인지해야 합니다. 이것이 이 장의 판별 축 1입니다.

🧱 추상(Abstraction) 영역: Ports

추상 영역은 "우리 시스템이 무엇을 할 것인가(What)" 에 대한 명세서(계약) 입니다. 실제 동작 방식이나 프레임워크 문법은 전혀 포함되지 않으며, 오직 메서드 이름·입력 파라미터·반환 타입(시그니처)만 정의합니다.

Input Port (인바운드 포트)

  • 역할 — 외부 시스템(API 라우터, CLI, 웹소켓 등)이 우리 애플리케이션 코어를 호출할 때 거쳐야 하는 진입로입니다.
  • 형태 — 파라미터와 반환 타입이 정의된 인터페이스나 추상 클래스 (Python의 ABC 또는 Protocol).
  • 예시 — TitanicQueryPort, OrderUseCasePort

Output Port (아웃바운드 포트)

  • 역할 — 비즈니스 로직 수행 도중 영속성 저장소(DB) 접근, 외부 머신러닝 모델 로드, 알림 전송 등이 필요할 때 내부 코어가 외부 세계에 요청하는 통로입니다.
  • 형태 — 코어가 선언한 인터페이스/추상 클래스.
  • 예시 — TitanicRepository, NotificationPort

⚠️ Output Port에서 가장 중요한 단어는 "코어가 선언한" 입니다. DB 쪽이 만들어 코어에 주는 게 아니라, 코어가 먼저 "나는 이런 게 필요하다"고 선언하고 DB 쪽이 맞춥니다. 이 방향이 곧 6.4절의 DIP입니다.

🛠️ 구상(Concretization) 영역

구상 영역은 "어떻게 실제로 작동하게 할 것인가(How)" 를 기술합니다. 그런데 구상이 두 종류라는 것이 이 절의 핵심입니다.

① 기술적 구상: Adapters (외부 인프라 영역)

외부 환경 및 하위 기술 프레임워크와 직접 맞닿는, 언제든 바뀔 수 있는 가공 기술의 집합체입니다.

  • Inbound Adapter (Driving Adapter, 주도하는 어댑터) — 외부 요청을 받아 물리적 프로토콜(HTTP JSON, GraphQL, WebSocket)을 해석하고 데이터 형식을 코어에 맞게 변환해 Input Port를 호출하는 구현체. 예시 — FastAPI 라우터(titanic_query_router.py), CLI 스크립트.
  • Outbound Adapter (Driven Adapter, 주도당하는 어댑터) — 코어의 Output Port 인터페이스를 상속받아 실제로 구현하는 영역. DB에 쿼리를 날리거나 .joblib 같은 모델 파일을 읽는 코드가 여기 위치합니다. 예시 — SQLAlchemy 기반 리포지토리 구현체, 모델 로더.

💡 Driving / Driven 이라는 이름의 뜻 — Driving은 시스템을 움직이게 하는 쪽(사람·브라우저·스케줄러, 밖에서 우리를 부름), Driven은 시스템에 의해 움직여지는 쪽(DB·메일 서버·파일 시스템, 우리가 밖을 부름)입니다. 자동차의 운전자(driving)와 바퀴(driven)를 떠올리면 됩니다.

② 비즈니스적 구상: Use Cases / Services (애플리케이션 코어 영역)

⭐ 여기가 가장 자주 오해받는 자리입니다.

기술 영역(DB, 웹)에는 전혀 의존하지 않지만, 비즈니스 규칙의 관점에서는 실제 실행 가능한 흐름과 논리를 구체적으로 기술하므로 구상에 해당합니다.

  • 역할 — Input Port를 구현(Implements) 하며, 비즈니스 시나리오의 순서와 규칙을 통제합니다. 데이터를 가공하고 유효성을 검증하며, 필요 시 Output Port를 호출합니다.
  • 특징 — DB가 MySQL에서 MongoDB로 바뀌거나 프레임워크가 FastAPI에서 Django로 바뀌어도 단 한 줄도 수정되어서는 안 됩니다. 예시 — TitanicQueryImpl, TrainUseCase

⭐ "Use Case는 코어 안에 있는데 왜 구상인가?" 추상/구상의 축(축 1)과 안쪽/바깥쪽의 축(축 2)은 서로 다른 축이기 때문입니다.

추상구상
안쪽 (코어)Input Port, Output Port, Domain ModelUse Case ← 여기
바깥쪽 (인프라)(없음)Inbound Adapter, Outbound Adapter

즉 Use Case는 안쪽의 구상입니다. "무엇을 할지"는 Port가 정하고, "비즈니스적으로 어떤 순서로 할지"는 Use Case가 정합니다. 기술적으로 어떻게 할지만 Adapter가 정합니다. 구상이 두 종류라는 것을 놓치면 이 아키텍처는 절대 이해되지 않습니다.

7.3 전체 흐름도 — 5단계

[ 외부 세계 / 인프라 레이어 ]
       │
       ▼ (1) 요청 진입
┌────────────────────────────────────────────────────────┐
│ inbound (router) [구상]                                │ ◀── Driving Adapter
└────────────────────────────────────────────────────────┘
       │
       ▼ (2) 인터페이스 호출
    ┌────────────────────────────────────────────────┐
    │ input (use_case) [추상]                        │ ◀── Inbound Port
    └────────────────────────────────────────────────┘
           ▲
           │ (3) 인터페이스 구현 (비즈니스 로직 실행)
┌────────────────────────────────────────────────────────┐
│ [ 코어 / 도메인 레이어 ]                               │
│ use_case                                               │ ◀── Interactor / Service
└────────────────────────────────────────────────────────┘
           │
           ▼ (4) 데이터 영속화 요청 (추상 메서드 호출)
    ┌────────────────────────────────────────────────┐
    │ output (repository) [추상]                     │ ◀── Outbound Port
    └────────────────────────────────────────────────┘
           ▲
           │ (5) 인터페이스 구현 (실제 인프라 바인딩)
┌────────────────────────────────────────────────────────┐
│ outbound (pg_repository) [구상]                        │ ◀── Driven Adapter
└────────────────────────────────────────────────────────┘
       │
       ▼ (6) DB 쿼리 실행
[ PostgreSQL / 데이터베이스 ]

표로 다시 보면 이렇습니다.

단계이름태그아키텍처 역할설명
1INBOUND (ROUTER)구상Driving AdapterHTTP·CLI 등 외부 입력을 최초로 받는 엔드포인트. 외부 프레임워크(FastAPI 등)에 의존하며, 요청을 정제해 내부 포트로 전달
2INPUT (USE_CASE)추상Inbound Port외부가 코어를 쓰기 위해 진입해야 하는 통로를 정의한 인터페이스. 라우터는 이것만 보고 호출
3USE_CASE—Interactor / Serviceinput 인터페이스를 상속받아 구현. 프레임워크·DB 기술에 오염되지 않고 순수한 비즈니스 언어로만 작성
↓DIP (의존성 역전 원칙) 적용
4OUTPUT (REPOSITORY)추상Outbound Port저장·조회에 필요한 기능만 정의한 인터페이스. 코어는 save()·find_by_id() 같은 규칙에만 의존. ⭐ DIP의 핵심 구간
5OUTBOUND (PG_REPOSITORY)구상Driven Adapter위 인터페이스의 실제 구현체. ORM(SQLAlchemy 등)이나 생 쿼리로 DB와 통신. DB를 바꿔도 이 칸만 새로 만들면 됨

범례 — 구상(Concrete Implementation)은 실제 구현, 추상(Abstract Interface)은 인터페이스 정의입니다.

영문 개념판으로 다시 한 번

같은 구조를 영어 문서에서 만나면 이렇게 쓰여 있습니다.

  • PORTS = INTERFACES (ABSTRACT CONTRACTS)
  • ADAPTERS = TECHNOLOGY IMPLEMENTATIONS (CONCRETE TECHNOLOGY)
  • CORE = LOGIC (ABSTRACT DOMAIN / CONCRETE FLOW)

구역 구분 — INBOUND ADAPTERS (DRIVING) | CONCRETE CORE (IMPLEMENTATION) | ABSTRACT CORE (DOMAIN/LOGIC) | APPLICATION LOGIC (USE CASES)

번호 순 흐름

  1. EXTERNAL REQUEST 🌐 → INBOUND ADAPTERS (HTTP/REST API, GQL, CLI)
  2. → INPUT PORT INTERFACE (ABSTRACT INTERFACES)
  3. → USE CASE IMPLEMENTATION
  4. → OUTPUT PORT INTERFACE
  5. → DOMAIN MODELS (ABSTRACT DEFINITION)
  6. → OUTBOUND ADAPTERS (DRIVEN) — POSTGRES DB / REDIS CACHE / FILE SYSTEM / EMAIL SERVICE → DB: EXTERNAL ACTION

중앙 육각형이 APPLICATION CORE, 좌측이 USE CASE INTERFACES (ABSTRACT DEFINITION) 입니다.

이 그림에는 부제가 붙어 있습니다 — "INDEPENDENT OF SPECIFIC CODE FILENAMES" (구체적인 파일 이름과 무관하다). 파일 이름이 james_router.py든 titanic_router.py든 자리와 역할이 같으면 같은 것이라는 뜻입니다.

7.4 ⭐⭐ 제어의 흐름 ≠ 의존성의 방향

이번 주에서 딱 하나만 가져가야 한다면 이 절입니다.

헥사고날 아키텍처가 강력한 유연성을 갖는 비결은, 제어의 흐름(Flow of Control) 과 의존성의 방향(Dependency Direction) 이 일치하지 않고 역전되기 때문입니다.

[제어의 흐름 (데이터가 흘러가는 방향)]
Inbound Adapter (FastAPI)  ──>  Input Port  ──>  Use Case (비즈니스 로직)  ──>  Output Port  ──>  Outbound Adapter (DB/파일)

[의존성의 방향 (코드가 import 하는 방향)]
Inbound Adapter (FastAPI)  ──>  Input Port  <──  Use Case (비즈니스 로직)  ──>  Output Port  <──  Outbound Adapter (DB/파일)

두 줄을 겹쳐 보세요. 화살표 방향이 다른 곳이 두 군데 있습니다.

① Inbound 흐름 외부 요청이 들어오면 Inbound Adapter가 Input Port를 통해 Use Case를 호출합니다. 의존성은 외부에서 내부 Core(app)로 향합니다. 여기까지는 흐름과 의존성이 같은 방향입니다.

② Outbound 흐름 — ⭐ DIP의 핵심 Use Case가 동작하면서 DB 조회가 필요할 때, 직접적인 DB 구현체를 import 하지 않고 내부 영역에 있는 Output Port 인터페이스를 호출합니다. 실제 DB 기술을 가진 Outbound Adapter는 오히려 Output Port 인터페이스를 상속받아 구현하기 때문에,

의존성의 화살표가 외부(인프라)에서 내부 Core(app)로 꺾여 들어옵니다.

제어의 흐름의존성의 방향
다른 이름Runtime FlowCompile-time Dependency
언제 정해지는가프로그램이 실행될 때코드를 작성할 때
무엇을 뜻하는가누가 누구를 호출하는가누가 누구를 import 하는가
Use Case → DB⭕ 흐른다❌ import 하지 않는다
DB → Use Case❌ 흐르지 않는다⭕ Port를 import 한다

이것이 왜 "역전(Inversion)"인가 — 평범하게 짜면 "쓰는 쪽이 쓰이는 쪽을 import" 합니다. 그런데 그러면 DB가 바뀔 때 유스케이스가 깨집니다. 그래서 방향을 일부러 뒤집어, 쓰이는 쪽(DB)이 쓰는 쪽(코어)의 규격을 import 하게 만듭니다. 이게 의존성 역전입니다.

결과

이 경계를 철저히 지키면 시스템 전체가 플러그인(Plugin) 구조처럼 결합도가 낮아져, 테스트가 매우 쉬워지고 유지보수 비용이 극적으로 낮아집니다.

테스트가 쉬워지는 이유가 특히 중요합니다. Use Case를 테스트할 때 진짜 DB가 필요 없습니다. Output Port를 구현한 가짜(Fake/Mock) 어댑터를 넣으면 되고, 5.5절의 LSP가 이걸 보장합니다.

7.5 ⭐ 3주차 회수 — 전략 패턴 = DIP = IoC = DI = 포트&어댑터

3주차 5.4절에서 이렇게 말했습니다.

"전략 패턴 · DIP · IoC · DI · 포트&어댑터는 전부 같은 것의 다른 이름이다."

그때는 결론만 던졌습니다. 이제 왜 그런지, 그리고 어떻게 다른지 정확히 회수합니다. 다섯은 같은 목표를 향하지만 층위가 다릅니다.

이름층위무엇인가이 장에서 배운 자리
전략 패턴 (Strategy)패턴 (미시)알고리즘을 인터페이스로 묶어 갈아끼운다1.2 프론트 행위 패턴
DIP원칙고수준·저수준 둘 다 추상에 의존한다5.7
IoC (제어의 역전)원리제어권이 내 코드에서 프레임워크로 넘어간다7.4 (흐름 ≠ 의존성)
DI (의존성 주입)기법필요한 객체를 밖에서 넣어준다6.4 3단계
포트 & 어댑터아키텍처 (거시)시스템 경계 전체를 인터페이스로 가른다7장 전체

공통 구조는 완전히 같습니다.

     [ 쓰는 쪽 ]  ──의존──►  [ 추상 (인터페이스) ]  ◄──구현──  [ 쓰이는 쪽 ]
  • 전략 패턴에서 쓰는 쪽 = Context, 추상 = Strategy 인터페이스, 쓰이는 쪽 = 구체 전략들
  • DIP에서 쓰는 쪽 = 고수준 모듈, 추상 = 인터페이스, 쓰이는 쪽 = 저수준 모듈
  • 헥사고날에서 쓰는 쪽 = Use Case, 추상 = Output Port, 쓰이는 쪽 = Outbound Adapter

세 개가 다른 크기의 같은 그림입니다. 3주차의 프랙탈(Fractal) — 같은 구조가 크기를 바꿔 반복되는 것 — 이 그대로 나타납니다.

크기예
함수 하나가장 작음매개변수 (11장)
클래스 하나작음전략 패턴
모듈 사이중간DIP / DI
시스템 경계큼포트 & 어댑터
서비스 사이가장 큼MSA의 API 계약

⭐ 이번 주 내용을 "새로 배우는 다섯 가지"로 외우면 절대 안 붙습니다. 하나를 다섯 번 다른 배율로 보는 것입니다.