본문으로 건너뛰기
기억하지 않는 것과 여러 번 해도 같은 것

생성 수정

IBM * 레드햇 AX 과정 · 15강

기억하지 않는 것과 여러 번 해도 같은 것

2026년 6월 15일(월) ~ 6월 19일(금)

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

이 장을 다 읽으면

  1. 무상태(Stateless)와 멱등성(Idempotence)이 같은 축의 양끝이 아니라 서로 다른 두 질문임을 설명할 수 있다.
  2. DELETE가 두 번째 호출에서 404를 돌려주는데도 왜 여전히 멱등한지 말할 수 있다.
  3. FastAPI Depends 체인이 안쪽부터 역방향으로 풀린다는 것을 순서대로 그릴 수 있다.
  4. 내가 쓴 인터랙터(Interactor)가 클린 아키텍처를 지키고 있는지를 "DB 세션의 존재를 아는가" 한 가지 질문으로 판정할 수 있다.
  5. 어떤 함수에 async를 붙이고 어떤 함수에 붙이면 안 되는지를 CPU-bound / I/O-bound 기준으로 판별할 수 있다.
  6. Docker의 Image·Container·Volume을 클래스·인스턴스·디스크에 각각 대응시키고, 그 대응이 3주차의 어떤 축의 반복인지 말할 수 있다.
  7. 모델과 알고리즘을 구별하고, 같은 "모델"이라는 말이 EEVE와 SVM에서 왜 다른 상태를 가리키는지 설명할 수 있다.
  8. 파라미터·모델 파라미터·하이퍼파라미터의 포함 관계를 그리고, 각각을 누가 언제 정하는지 말할 수 있다.
  9. 수(數)·체(體)·DB 3층 지도로 자료형부터 데이터센터까지를 한 장에 배치할 수 있다.
  10. 아토믹 디자인의 TEMPLATES가 왜 0이 아니라 null인지, 그것이 왜 추상 클래스와 같은 자리인지 설명할 수 있다.
  11. 데이터 파이프라인 설계 3원칙(불변성·타입 변환·프레임워크 분리)을 코드로 구현할 수 있다.

0. 이 장의 판별 축

새 용어를 만나면 비유를 찾기 전에 아래 축 중 어디에 꽂히는지부터 정하세요.

#축왼쪽 (0)오른쪽 (1)이 축으로 갈리는 것
1무엇을 묻는가기억하는가 (상태)여러 번 해도 같은가 (멱등성)무상태 ↔ 멱등성
2정적인가 동적인가정적 템플릿 (Read-Only)동적 실체 (실행 중)Image ↔ Container / Class ↔ Instance
3누가 값을 정하는가기계가 학습으로 결정사람이 학습 전에 설정모델 파라미터 ↔ 하이퍼파라미터
4비어 있음인가 부재인가0 (값이 있음)null (없음)ORGANISMS ↔ TEMPLATES
5어느 층인가수(최소 단위)DB(저장소)자료형 ↔ 자료구조 ↔ 데이터베이스

⚠️ 축 1은 다른 축들과 성격이 다릅니다. 축 2~5는 왼쪽과 오른쪽이 서로 반대편입니다. 하지만 축 1의 왼쪽과 오른쪽은 반대편이 아니라 애초에 서로 다른 질문입니다. 이 두 질문을 하나의 축으로 착각하는 것이 이 장 최대의 함정입니다. 1장 전체가 이 함정을 해체하는 데 쓰입니다.

앞 장에서 이어지는 축

  • 축 2는 3주차의 축 1(디스크 ↔ 메모리) 이 도커라는 새 옷을 입고 그대로 반복된 것입니다.
  • 축 4는 6주차의 추상 ↔ 구상 축이 "그럼 추상은 값이 무엇인가"라는 질문으로 한 걸음 더 들어간 것입니다.
  • 축 5는 3주차 끝에서 본 스칼라 → 벡터 → 매트릭스 → 텐서 확장을, 데이터가 실제로 사는 장소까지 늘려 그린 지도입니다.

1. 무상태와 멱등성 — 완전히 다른 두 개념

1.1 왜 이 둘이 붙어 다니는가

두 단어는 항상 같은 문단에 등장합니다. 그래서 대부분의 학습자가 "무상태 = 멱등" 이라고 외우고 넘어갑니다. 이건 틀렸습니다. 둘은 다른 대상에게 던지는 다른 질문입니다.

무상태 (Stateless)멱등성 (Idempotence)
질문의 대상함수·서버가 내부에 무엇을 들고 있는가그 연산을 여러 번 하면 어떻게 되는가
한 문장 질문"너 이전 일을 기억하니?""한 번 하나 열 번 하나 결과가 같니?"
판정 시점함수 하나만 뜯어봐도 알 수 있음반복 실행해봐야 알 수 있음
관심사내부 상태 보관 여부최종 자원 상태(Resource State)

1.2 정의 — 유(類)와 종차(種差)로

  • 무상태(Stateless) — [시스템·함수의 성질]인데, [이전 실행의 상태나 데이터를 내부에 저장하거나 기억하지 않는다]는 점에서 상태 유지(Stateful)와 구별된다.
  • 멱등성(Idempotence) — [연산의 성질]인데, [같은 연산을 몇 번 수행하든 시스템의 최종 자원 상태가 한 번 수행했을 때와 같다]는 점에서 비멱등 연산과 구별된다.

무상태의 특징: 오직 현재 입력으로 들어온 매개변수(Arguments)만을 기반으로 동작합니다. 외부 상태에 의존하지도, 외부 상태를 변경하지도 않습니다. 즉 순수 함수(Pure Function) 의 형태를 가집니다.

멱등성의 수학적 표현은 f(f(x)) = f(x) 입니다. f를 한 번 적용한 것과 두 번, 세 번, 백 번 적용한 것이 같습니다. 가장 익숙한 예는 절댓값입니다. abs(abs(-5)) = abs(-5) = 5. 엘리베이터 버튼도 그렇습니다. 이미 눌린 버튼을 열 번 더 눌러도 엘리베이터는 한 번만 옵니다.

1.3 ⭐ 가장 흔한 오해 — "응답이 같아야 멱등이다"

이 오해 하나가 멱등성 이해를 통째로 망칩니다.

멱등성은 응답 데이터(Response Message)가 매번 완전히 같아야 함을 의미하지 않습니다. 서버나 시스템의 최종 자원 상태(Resource State)가 변하지 않음을 의미합니다.

증거는 DELETE에 있습니다.

DELETE /articles/42   (1번째)  →  200 OK       ... 42번 글이 삭제됨
DELETE /articles/42   (2번째)  →  404 Not Found ... 지울 게 없음
DELETE /articles/42   (3번째)  →  404 Not Found

응답 메시지는 달라졌습니다. 200에서 404로 바뀌었습니다. 그런데도 DELETE는 멱등합니다. 왜냐하면 세 번을 다 실행한 뒤의 서버 상태와 한 번만 실행한 뒤의 서버 상태가 똑같이 "42번 글이 없음" 이기 때문입니다.

멱등성이 보는 곳은 응답 봉투가 아니라 서버 안의 자원입니다.

1.4 HTTP 메서드별 판정표

개발 실무에서 멱등성은 주로 HTTP 메서드 설계 규칙을 통해 가장 명확히 드러납니다.

HTTP Method기능무상태성멱등성설명 및 상태 변화 예시
GET조회OO데이터를 여러 번 조회해도 서버의 데이터는 변하지 않고 그대로 유지됩니다.
PUT전체 수정 / 덮어쓰기OO유저_이름 = "홍길동" 요청을 1번 보내든 100번 보내든, 최종 서버의 유저 이름은 "홍길동"으로 동일합니다.
DELETE삭제OO첫 요청에 자원이 삭제됩니다. 이후 요청은 '이미 없음(404)'이 반환될 수 있으나, 자원이 삭제된 최종 상태는 동일하므로 멱등합니다.
POST생성 / 추가OX게시글 작성 요청을 3번 연속 보내면 서버에 동일한 게시글이 3개 생성됩니다. 실행 횟수에 따라 상태가 계속 변하므로 멱등하지 않습니다.

이 표에서 눈여겨볼 곳은 무상태성 열이 전부 O라는 점입니다. 네 메서드 모두 무상태입니다. HTTP 프로토콜 자체가 무상태로 설계됐기 때문에, 어떤 메서드를 쓰든 서버는 "이 요청자가 아까 뭘 했는지"를 기억하지 않습니다. 그런데 멱등성 열만 POST에서 X로 갈립니다.

무상태성은 프로토콜 전체의 성질이고, 멱등성은 각 메서드의 성질입니다. 층위가 다릅니다. 그래서 같은 축에 놓을 수 없습니다.

1.5 GET에만 붙는 또 하나의 이름 — 안전(Safe)

GET에는 멱등성 위에 성질이 하나 더 붙습니다. 안전(Safe) — 자원의 상태를 아예 바꾸지 않는다는 성질입니다.

안전(Safe)  ⊂  멱등(Idempotent)
   GET           GET · PUT · DELETE

안전한 것은 반드시 멱등입니다(아무것도 안 바꾸니 몇 번을 해도 같습니다). 하지만 멱등하다고 안전한 것은 아닙니다. PUT과 DELETE는 자원을 바꿉니다. 다만 몇 번을 바꿔도 도착지가 같을 뿐입니다.

1.6 두 개념의 연관성 — 왜 붙어 다니는가

이제 왜 두 단어가 항상 같이 나오는지 설명할 수 있습니다.

무상태 함수는 과거의 상태에 영향을 받지 않고 외부 상태를 직접 누적하여 바꾸지 않기 때문에, 설계 구조상 멱등성을 보장하거나 구현하기가 매우 유리합니다.

"유리하다"이지 "같다"가 아닙니다. 무상태는 멱등 구현을 쉽게 해줄 뿐 자동으로 보장하지 않습니다.

무상태인데 멱등하지 않은 예가 실제로 존재합니다. POST가 그렇습니다. POST 요청은 무상태입니다(서버는 이전 요청을 기억하지 않습니다). 그런데 세 번 보내면 글이 세 개 생깁니다. 기억하지 않는 것과 반복해도 같은 것은 다른 문제라는 가장 깨끗한 증거입니다.

1.7 ⭐ 왜 중요한가 — 분산 환경의 재전송(Retry)

이 두 개념이 학문적 취미가 아니라 실무 안정성의 핵심인 이유는 딱 하나, 네트워크는 반드시 실패한다는 것입니다. 그리고 그 실패는 이렇게 생겼습니다.

[클라이언트] ── 요청 전송 ──▶ [서버] ─ 요청 정상 처리됨 (자원 생성 완료)
[클라이언트] ◀── 응답 전송 ── ✕ 여기서 끊김
[클라이언트] "응답이 안 왔네? 실패했나 보다." ── 같은 요청 재전송 ──▶ [서버]

클라이언트는 서버가 처리에 실패한 것인지, 처리는 성공했는데 응답만 유실된 것인지 구별할 수 없습니다. 그래서 재전송(Retry)합니다. 이때 그 연산이 멱등하지 않다면 어떻게 될까요.

  • 결제 API가 비멱등이면 → 같은 금액이 두 번 빠져나갑니다.
  • 주문 생성 API가 비멱등이면 → 같은 물건이 두 번 배송됩니다.

클라우드 네이티브 시스템, MSA, 서버리스처럼 호출이 네트워크를 건너다니는 구조에서는 재전송이 예외가 아니라 일상입니다. 그래서 무상태성과 멱등성을 고려한 설계가 시스템 안정성의 핵심이 됩니다.

3주차에서 배운 MSA를 떠올려 보세요. 기능을 여러 서버로 쪼갠다는 것은 함수 호출을 네트워크 호출로 바꾼다는 뜻입니다. 함수 호출은 실패하지 않지만 네트워크 호출은 실패합니다. 멱등성은 그 대가로 치러야 하는 설계 비용입니다.

1.8 POST를 멱등하게 만드는 법 — 멱등성 키

POST가 본질적으로 비멱등이라면, 결제 같은 위험한 작업은 어떻게 할까요. 요청 자체에 신분증(Idempotency-Key 헤더)을 붙입니다. 서버는 이 키를 저장해 두고, 처음 보는 키면 실제로 결제를 실행한 뒤 결과를 저장하고, 이미 있는 키면 결제를 실행하지 않고 저장해둔 결과를 그대로 돌려줍니다.

여기서 반전이 있습니다. 멱등성 키를 쓰려면 서버가 키를 기억해야 합니다. 즉 무상태를 일부 포기해서 멱등성을 사는 것입니다.

⭐ 이 한 장면이 축 1의 왼쪽과 오른쪽이 서로 다른 질문이라는 최종 증거입니다. 같은 축의 양끝이었다면 하나를 포기해서 다른 하나를 얻는 일이 성립할 수 없습니다.

1.9 판별 훈련

각 항목이 무상태인지, 멱등인지 따로 판정해 보세요.

상황무상태?멱등?이유
abs(x) 함수OO아무것도 기억 안 함 / abs(abs(x)) = abs(x)
게시글 조회 GET /posts/1OO서버가 기억 안 함 / 읽기만 함
프로필 이름을 "홍길동"으로 덮어쓰기OO몇 번을 해도 이름은 "홍길동"
게시글 작성 POST /postsOX누를 때마다 글이 하나씩 늘어남
조회수 +1 증가OX호출할 때마다 숫자가 커짐
장바구니에 담긴 수량을 3으로 설정OO최종 수량은 항상 3
장바구니 수량을 1 증가OX부를 때마다 늘어남
로그인 세션을 서버 메모리에 보관X—서버가 사용자를 기억함 → Stateful
JWT 토큰으로 매 요청 인증O—서버가 기억하지 않고 매번 신분증을 확인

마지막 두 줄이 중요합니다. "설정(set)"은 멱등하고 "증가(increment)"는 비멱등입니다. API를 설계할 때 이 차이를 알고 동사를 고르면, 재전송 사고의 절반이 사라집니다.

1.10 두 문장으로 정리

무상태(Stateless): "나는 과거를 기억하지 않고, 오직 지금 입력된 값으로만 일한다." 멱등성(Idempotence): "동일한 요청을 실수로 여러 번 반복해도 시스템은 안전하게 한 번만 수행된 상태를 유지한다."


2. FastAPI 의존성 주입 — Depends는 안쪽부터 풀린다

2.1 이 절이 답하는 질문

6주차에서 이런 코드가 DIP와 OCP를 동시에 위반한다는 것을 배웠습니다.

walter: WalterRoasterUseCase = WalterRoasterInteractor()   # ❌ 라우터가 구상을 직접 만듦

해법은 의존성 주입(DI) 이라고 했고, 조립을 담당하는 별도 파일(의존성 조립소)을 만든다고 했습니다. 이번 주에는 그 조립소가 DB 세션까지 포함해 3단으로 깊어집니다. 그리고 그 3단이 어떤 순서로 풀리는지를 정확히 봅니다.

2.2 실제 코드

# adapter/inbound/api/dependencies/andrews_architect_dependency.py

from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession

from core.database import get_db
from domain.repositories.andrews_architect_repository import AndrewsArchitectRepository
from domain.usecases.andrews_architect_use_case import AndrewsArchitectUseCase
from adapter.outbound.persistence.andrews_architect_pg_repository import (
    AndrewsArchitectPgRepository,
)
from app.interactors.andrews_architect_interactor import AndrewsArchitectInteractor


def get_andrews_architect_repository(
    db: AsyncSession = Depends(get_db),
) -> AndrewsArchitectRepository:
    #        ↑ 반환 타입은 추상 인터페이스              ↓ 실제로 만드는 것은 구현체
    return AndrewsArchitectPgRepository(session=db)


def get_andrews_architect_use_case(
    repository: AndrewsArchitectRepository = Depends(get_andrews_architect_repository),
) -> AndrewsArchitectUseCase:
    return AndrewsArchitectInteractor(repository=repository)

세션을 만드는 쪽은 이렇게 생겼습니다.

# core/database.py

from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

AsyncSessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False)


async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with AsyncSessionLocal() as session:
        yield session      # ← 요청이 끝나면 여기로 돌아와 세션을 닫는다

그리고 라우터는 조립된 결과물만 받습니다.

# adapter/inbound/api/v1/andrews_architect_router.py

@andrews_architect_router.post("/analyze")
async def analyze(
    schema: AndrewsArchitectSchema,
    use_case: AndrewsArchitectUseCase = Depends(get_andrews_architect_use_case),
):
    return await use_case.execute(schema.to_dto())

라우터 안에 AndrewsArchitectInteractor도, AndrewsArchitectPgRepository도, AsyncSession도 한 글자도 등장하지 않습니다. 라우터가 아는 것은 추상 타입 AndrewsArchitectUseCase 하나뿐입니다.

2.3 레이어 매칭표

아키텍처 레이어코드 내 변수명 / 함수명실제 생성되는 클래스반환 타입(추상)
인프라 (Infrastructure)db / get_dbAsyncSession (SQLAlchemy 비동기 세션)—
데이터 액세스 (Repository)repository / get_andrews_architect_repositoryAndrewsArchitectPgRepositoryAndrewsArchitectRepository
비즈니스 로직 (Use Case)get_andrews_architect_use_caseAndrewsArchitectInteractorAndrewsArchitectUseCase

6주차의 헥사고날 7칸으로 옮기면 라우터는 inbound 어댑터, AndrewsArchitectUseCase는 input 포트(추상), AndrewsArchitectInteractor는 use case, AndrewsArchitectRepository는 output 포트(추상), AndrewsArchitectPgRepository는 outbound 어댑터, AsyncSession은 database 칸에 각각 해당합니다.

2.4 ⭐ 해석 순서 — 안쪽부터 역으로 올라간다

FastAPI의 Depends 시스템은 하위(안쪽) 의존성부터 역으로 해석하며 올라갑니다.

코드를 읽는 순서와 실행되는 순서가 반대입니다. 이것이 처음 배울 때 가장 헷갈리는 지점입니다.

[ 1. Database Session ] (AsyncSession)
           │
           ▼  db 객체 주입 (Depends(get_db))
[ 2. Repository Layer ] (AndrewsArchitectPgRepository)
           │
           ▼  repository 객체 주입 (Depends(get_andrews_architect_repository))
[ 3. Use Case / Interactor ] (AndrewsArchitectInteractor)
           │
           ▼  use_case 객체 주입 (Depends(get_andrews_architect_use_case))
[ 4. Router ] ← 클라이언트 요청이 도달한 지점

클라이언트 요청이 엔드포인트에 도달했을 때 실제로 벌어지는 일을 순서대로 쓰면 이렇습니다.

  1. DB 세션 생성 (get_db) 데이터베이스와의 비동기 연결을 관리하는 AsyncSession 인스턴스(db)가 생성됩니다.
  2. 레포지토리 주입 (get_andrews_architect_repository) 생성된 db 세션이 AndrewsArchitectPgRepository 인스턴스의 생성자로 주입됩니다. 외부 프레임워크(SQLAlchemy)와 직접 소통하는 구현체이지만, 반환할 때는 추상 인터페이스인 AndrewsArchitectRepository 타입으로 반환되어 상위 레이어와의 결합도를 낮춥니다.
  3. 유스케이스/인터랙터 주입 (get_andrews_architect_use_case) 주입이 완료된 repository 객체가 비즈니스 로직의 핵심인 AndrewsArchitectInteractor로 주입됩니다.
  4. 라우터 실행 조립이 다 끝난 유스케이스 하나가 라우터 함수의 인자로 들어옵니다.

FastAPI가 이 순서를 어떻게 아는가: 함수 시그니처의 기본값에 붙은 Depends(...)를 따라가며 트리를 만들고 잎(leaf)부터 계산합니다. 이건 3주차에서 배운 IoC(제어의 역전) 그 자체입니다. 내가 순서를 짜는 게 아니라 프레임워크가 순서를 계산합니다.

이 구조를 프랙탈이라고 부르는 이유가 있습니다. get_X_use_case → get_X_repository → get_db 라는 똑같은 3단 모양이, 앤드루스든 잭이든 칼이든 이름만 바꿔 그대로 복제되기 때문입니다. 3주차에서 배운 "같은 구조가 크기만 바꿔 반복된다"가 파일 단위로 나타난 것입니다.

2.5 ⭐ 판정 기준 — 인터랙터는 DB 세션의 존재를 몰라야 한다

여기가 이 절의 핵심이자, 내 코드가 클린 아키텍처를 지키는지 판정하는 단 하나의 질문입니다.

칠판에 처음 그려진 흐름도에는 인터랙터 자리에도 session이 적혀 있었습니다. 실제 코드는 그렇지 않습니다.

세션(db)은 레포지토리 레이어 내부로만 주입됩니다. 클린 아키텍처 원칙에 따라 비즈니스 로직을 담당하는 인터랙터(Interactor)는 DB 세션의 존재를 직접 알지 못하며, 오직 추상화된 레포지토리 인터페이스(AndrewsArchitectRepository)를 통해서만 데이터에 접근합니다.

코드로 확인하면 이렇습니다.

# ✅ 올바른 인터랙터 — AsyncSession 이라는 단어가 없다
class AndrewsArchitectInteractor(AndrewsArchitectUseCase):
    def __init__(self, repository: AndrewsArchitectRepository):
        self.repository = repository        # 추상만 안다

    async def execute(self, command):
        return await self.repository.save(command)
# ❌ 잘못된 인터랙터 — 세션이 비즈니스 로직까지 올라와 버렸다
class AndrewsArchitectInteractor(AndrewsArchitectUseCase):
    def __init__(self, session: AsyncSession, repository):
        self.session = session              # SQLAlchemy 를 안다 = 기술에 오염됨
        self.repository = repository

    async def execute(self, command):
        result = await self.repository.save(command)
        await self.session.commit()         # 비즈니스 로직이 DB 트랜잭션을 직접 조작
        return result

왜 아래쪽이 문제인가: 인터랙터는 "무엇을 할 것인가(업무 규칙)"를 담는 자리입니다. 여기에 AsyncSession이 들어오는 순간, 이 업무 규칙은 SQLAlchemy 없이는 테스트조차 할 수 없는 코드가 됩니다. DB를 MongoDB로 바꾸면 업무 규칙 파일을 열어 고쳐야 합니다. 이것이 6주차에서 본 DIP 위반의 다른 얼굴입니다.

판정 질문 한 줄 내 인터랙터 파일 안에서 AsyncSession, session, commit, select( 를 검색해 보세요. 하나라도 잡히면 클린 아키텍처가 깨진 것입니다.

2.6 구현체를 추상 타입으로 반환한다

def get_andrews_architect_repository(...) -> AndrewsArchitectRepository:
    return AndrewsArchitectPgRepository(session=db)

이 두 줄에는 6주차에서 본 표기법이 함수 형태로 다시 나타나 있습니다.

walter : WalterRoasterUseCase = WalterRoasterInteractor()
         └── 왼쪽: 추상 타입 ──┘  └── 오른쪽: 구상 생성자 ──┘

def get_...(...) -> AndrewsArchitectRepository:      ← 왼쪽에 해당 (추상)
    return AndrewsArchitectPgRepository(session=db)  ← 오른쪽에 해당 (구상)

같은 원리이고 위치만 다릅니다. 6주차에서는 이 한 줄이 라우터 안에 있어서 문제였습니다. 이번에는 조립소 파일 안으로 옮겨졌기 때문에 문제가 되지 않습니다.

구상은 사라지지 않습니다. 어딘가에서는 반드시 new 를 해야 합니다. 클린 아키텍처가 요구하는 것은 "구상을 없애라"가 아니라 "구상이 태어나는 자리를 한 군데로 몰아라" 입니다. 그 자리가 의존성 조립소입니다.

2.7 이 구조가 실제로 사주는 것

유연한 DB 전환 향후 PostgreSQL에서 다른 데이터베이스(MongoDB, MySQL 등)로 변경하거나, 단위 테스트를 위해 메모리용 가짜(Mock) 레포지토리를 주입해야 할 때 유스케이스 코드는 단 한 줄도 수정할 필요가 없습니다. 오직 get_andrews_architect_repository 함수의 반환 객체만 교체하면 됩니다.

# 운영
def get_andrews_architect_repository(db=Depends(get_db)) -> AndrewsArchitectRepository:
    return AndrewsArchitectPgRepository(session=db)

# 테스트 — 이 한 줄만 바꾼다
def get_andrews_architect_repository() -> AndrewsArchitectRepository:
    return AndrewsArchitectMemoryRepository()

테스트 용이성 의존성이 상단에서 주입되므로 각 레이어를 독립적으로 분리하여 유닛 테스트(Unit Test)를 수행하기에 매우 용이한 구조입니다. FastAPI는 여기에 app.dependency_overrides[...] 라는 교체용 정식 통로까지 제공합니다.

2.8 ⭐ AI가 만든 코드는 DIP를 절반만 적용한다

이번 주 실습에서 가장 값비싼 교훈입니다.

AI에게 "클린 아키텍처로 만들어줘"라고 요청해서 나온 코드는 DIP를 50%만 적용한 상태였습니다. 폴더는 네 개로 잘 나뉘어 있고, 이름도 repository, use_case로 그럴듯했습니다. 그런데 실제로는 이런 식이었습니다.

AI가 흔히 만드는 절반짜리 코드왜 절반인가
레포지토리 인터페이스를 만들었는데 반환 타입은 구현체이름만 추상, 타입은 구상 → 결합 그대로
인터랙터가 레포지토리를 직접 import 해서 생성주입이 아니라 생성. DIP 위반
세션을 인터랙터 생성자까지 끌고 올라감2.5절의 판정 질문에 걸림
Depends는 썼는데 라우터 안에 조립 코드가 있음조립소가 없어 구상이 여기저기 흩어짐

그래서 수동 재편집이 필요했습니다.

⭐ 생성 코드를 클린 코드 관점에서 검수하는 것이 이 주의 진짜 실습입니다. 코드를 만드는 일은 이제 몇 초면 끝납니다. 남는 일은 만들어진 코드가 규칙을 지키는지 판정하는 일입니다. 그 판정 능력이 없으면, AI는 "동작은 하지만 6개월 뒤에 손댈 수 없는 코드"를 아주 빠르게 대량 생산합니다.

검수 체크리스트 5문항

#질문통과 기준
1라우터 파일에 Interactor·PgRepository 라는 단어가 있는가?없어야 통과
2인터랙터 파일에 AsyncSession·commit 이 있는가?없어야 통과
3의존성 함수의 반환 타입 힌트가 추상인가?추상이어야 통과
4구상 클래스가 new 되는 자리가 조립소 한 곳뿐인가?한 곳이어야 통과
5레포지토리를 갈아끼울 때 인터랙터를 열어야 하는가?안 열어도 되면 통과

앞으로 다시 만납니다 — 이 조립소 패턴은 이후 인증(OAuth·JWT) 어댑터를 붙일 때 그대로 한 층 더 쌓입니다. get_current_user 가 get_db 옆에 나란히 서는 모양이 됩니다.


3. async를 붙일 것인가 — CPU-bound와 I/O-bound

3주차에서 동기와 비동기의 차이는 속도가 아니라 기다림이라고 배웠고, "비동기는 CPU 계산에는 효과가 없다"는 주의사항도 봤습니다. 이번 주에 그 주의사항이 실제 코드에서 문제가 됐습니다.

3.1 ⭐ 판단 기준

CPU-bound는 def, I/O-bound는 async def

구분무엇을 하는가무엇을 기다리는가붙일 키워드
I/O-bound네트워크 호출, DB 조회, 파일 읽기/쓰기바깥세상의 응답async def
CPU-bound형태소 분석, 행렬 연산, 정렬, 암호화아무것도 안 기다림. 자기가 계산 중def

3.2 사례 — 형태소 분석은 def다

앤드루스의 analyze_intent는 Kiwi 형태소 분석기로 한국어 문장을 쪼개는 함수입니다.

# ✅ 올바름 — Kiwi 분석은 순수 CPU 작업
def analyze_intent(self, question: str) -> list[str]:
    tokens = self.kiwi.tokenize(question)
    return [t.form for t in tokens if t.tag.startswith(("NN", "VV", "SN"))]

네트워크도, DB도, 파일 I/O도 없습니다. CPU가 계속 돌고 있을 뿐입니다.

# ❌ 잘못됨 — 겉모습만 비동기
async def analyze_intent(self, question: str) -> list[str]:
    tokens = self.kiwi.tokenize(question)   # 여기서 이벤트 루프가 그대로 막힌다
    ...

왜 더 나쁜가: async def로 바꿔도 코루틴(coroutine) 객체가 하나 더 만들어질 뿐, 이벤트 루프는 여전히 이 계산이 끝날 때까지 통째로 멈춥니다. 그런데 코드만 보면 "비동기니까 다른 요청도 처리되겠지"라고 착각하게 됩니다.

⚠️ 비블로킹처럼 보이는데 실제로는 블로킹인 코드가 가장 나쁩니다. 처음부터 def로 두면 FastAPI가 이 함수를 스레드풀로 보내서 알아서 처리해 줍니다. async def로 잘못 선언하면 FastAPI는 "이건 안 막히는 함수구나" 하고 믿어버리고 이벤트 루프에서 직접 실행합니다.

3.3 라우터에도 같은 기준이 적용된다

월터의 개발·검증 보고서에서 나온 결론입니다.

Pandas는 동기 CPU/IO 작업이므로 라우터를 일반 def로 선언해 FastAPI 스레드풀이 처리하게 한다.

# ✅ Pandas 를 쓰는 라우터는 일반 def
@walter_router.get("/features")
def get_features(use_case: WalterUseCase = Depends(get_walter_use_case)):
    return use_case.feature_engineering()

같은 보고서에서 나온 두 번째 결론은 포트 인터페이스에 관한 것입니다.

DTO 규격 대신 pd.DataFrame을 직접 반환하도록 포트 인터페이스를 통일한다.

6주차에서 계층 경계마다 그릇이 바뀐다(Schema → DTO → ORM Entity)고 배웠습니다. 그런데 데이터 분석 파이프라인에서는 그 원칙을 의도적으로 완화합니다. 이유는 두 가지입니다.

  1. pd.DataFrame은 이미 그 자체로 규격화된 타입입니다. 컬럼과 dtype이 계약 역할을 합니다.
  2. 수십만 행짜리 표를 DTO 객체 리스트로 변환하면 변환 비용이 계산 비용을 넘어섭니다.

⚠️ 원칙을 아는 것과 원칙을 언제 굽힐지 아는 것은 다릅니다. "DTO를 쓰지 않았다"가 아니라 "DataFrame을 DTO로 삼기로 팀이 합의했다" 가 맞는 표현입니다. 합의 없이 굽히면 그냥 위반입니다.

3.4 ⭐ 실전 함정 — from module import engine 의 조기 캡처

이번 주 디버깅에서 가장 오래 걸린 버그입니다. 파이썬을 쓰는 한 언젠가 반드시 만납니다.

증상: init_engine()을 분명히 호출했는데도 engine이 None이라며 터집니다.

# core/database.py
engine = None                    # 모듈 로드 시점의 초깃값

def init_engine(url: str):
    global engine
    engine = create_async_engine(url)
# ❌ 문제의 코드
from core.database import engine       # ← 이 순간의 값(None)을 그대로 복사해 온다

async def fetch():
    async with engine.connect() as conn:      # AttributeError: 'NoneType' object ...
        ...

원인: from X import y는 y가 가리키던 값을 지금 이 자리로 복사합니다. 나중에 X.y가 다른 객체를 가리키게 바뀌어도, 이미 복사해 온 이름은 옛날 값(None)을 계속 붙들고 있습니다. 이것을 조기 캡처(early capture) 라고 부릅니다.

# ✅ 해결 — 모듈을 통째로 가져와 런타임에 속성으로 접근
import core.database as _m

async def fetch():
    async with _m.engine.connect() as conn:   # 호출되는 순간의 최신 값을 읽는다
        ...
표기값을 읽는 시점나중에 재할당되면
from m import ximport 시점 (한 번)못 따라감
import m as _m → _m.x접근 시점 (매번)따라감

💡 일반 규칙: 모듈 안에서 나중에 재할당되는 전역 객체(엔진, 커넥션 풀, 설정, 클라이언트)는 절대 from ... import ... 로 가져오지 마세요. 모듈 자체를 import 하고 매번 속성으로 접근하세요.


4. Docker와 OOP — 축 2가 인프라에서 반복된다

4.1 왜 도커를 객체지향으로 배우는가

도커를 처음 배우면 명령어 목록부터 외우게 됩니다. build, run, exec, ps, rm, volume... 그런데 이 도구는 이미 알고 있는 개념의 다른 이름표일 뿐입니다.

3주차 축 1(디스크 ↔ 메모리) 과 클래스 ↔ 인스턴스를 그대로 가져오면 도커의 뼈대가 한 번에 서 있습니다.

4.2 Image → 클래스 (Class)

  • 정의: 애플리케이션을 실행하는 데 필요한 모든 코드, 런타임, 라이브러리, 환경 변수, 설정 파일을 하나로 묶어놓은 불변(Read-Only)의 템플릿입니다.
  • 대응: 클래스가 객체를 생성하기 위한 '설계도'인 것처럼, 이미지는 컨테이너를 생성하기 위한 '설계도' 역할을 합니다.
  • 위치: 이미지 자체는 디스크에 저장된 정적 파일이며, 그 자체로 실행되어 메모리를 차지하지 않습니다.

4.3 Container → 인스턴스 (Instance)

  • 정의: 이미지를 바탕으로 생성되어 실제 메모리 위에서 격리되어 살아 움직이는 프로세스입니다.
  • 대응: 하나의 클래스(설계도)로부터 서로 영향을 주지 않는 독립된 여러 개의 객체를 동적으로 생성할 수 있듯이, 하나의 Docker 이미지로 여러 개의 독립된 컨테이너를 동시에 실행할 수 있습니다.
  • 핵심: 컨테이너 내부에서 파일 시스템을 수정하거나 앱을 가동해도 원본 이미지(클래스)는 전혀 변하지 않습니다. 3주차에서 인스턴스는 "실행이 끝나면 사라진다"고 배웠습니다. 컨테이너도 똑같습니다. docker rm 하면 그 안에서 만든 파일은 전부 사라집니다. 그래서 세 번째 개념이 필요합니다.

4.4 Volume → 디스크 (외부 저장소)

  • 정의: 컨테이너는 라이프사이클이 종료(삭제)되면 내부에서 변경된 데이터도 함께 사라지는 휘발성을 가집니다. 이를 극복하고 데이터를 안전하게 보존하기 위해 호스트 PC의 특정 디렉터리를 컨테이너와 연결하는 공간이 볼륨입니다.
  • 대응: 프로그램 인스턴스가 종료되면 메모리 상의 데이터가 날아가므로, 중요한 데이터는 외부 파일 시스템(디스크)이나 데이터베이스에 따로 저장해야 하는 메커니즘과 동일합니다. 3주차의 영속성(Persistence) 이 그대로 인프라 레벨로 올라온 것입니다.

4.5 한눈에 보는 요약 비교표

Docker 개념OOP 개념인프라 상태주요 특징 및 역할
ImageClassStatic (정적 / Read-Only)컨테이너 생성을 위한 불변의 설계도 · 실행 환경의 모든 의존성을 포함
ContainerInstanceDynamic (동적 / Read-Write)이미지에 정의된 대로 격리되어 실행되는 프로세스 · 독립적인 메모리 및 CPU 자원 점유
VolumeDisk / DBPersistent (영속성 / Storage)컨테이너 삭제와 무관하게 데이터 보존 · 호스트와 컨테이너 간의 데이터 공유 채널

4.6 구조적 이해를 위한 흐름

[ Image (Class / 설계도) ]  -- ( docker run ) -->  [ Container (Instance / 실체) ]
       ▲                                                   │
       │ (데이터 영속화 보장)                                  ▼
       └─────────────────────────────────────────── [ Volume (Disk / 외부 저장소) ]

4.7 명령어를 개념으로 되읽기

명령어OOP 로 번역하면3주차 용어로
docker build소스로부터 클래스를 정의함디스크에 설계도 작성
docker run인스턴스화(Instantiation)디스크 → 메모리 (LOAD)
docker ps살아 있는 인스턴스 목록실행 중인 프로세스 목록
docker exec인스턴스의 메서드 호출실행 중인 프로세스에 명령
docker rm인스턴스 소멸메모리 해제
docker rmi클래스 정의 삭제디스크에서 설계도 삭제
-v host:container인스턴스를 외부 저장소에 연결영속성 확보

⚠️ 비유가 깨지는 지점도 알아 두세요. 이미지는 레이어(layer)의 겹으로 되어 있어 클래스에는 없는 "쌓임"이 있고, FROM 은 하나만 쓸 수 있어 다중 상속이 없으며, 컨테이너는 격리(isolation) 가 본질이라 일반 인스턴스처럼 메모리를 공유하지 않습니다.


5. 모델과 알고리즘 — 그리고 두 종류의 '모델'

5.1 한 문장 정의

용어쉬운 정의역할비유
모델 (Model)입력을 넣으면 출력을 뱉는 예측 기계데이터 분석의 최종 결과물y = ax + b 같은 수학 공식
알고리즘 (Algorithm)모델을 만들기 위한 논리적 틀데이터를 어떻게 학습할지 결정리액트(React) 라이브러리 자체
키위 (Kiwi)한국어 문장 절단기컴퓨터가 이해하게 단어 쪼개기요리 전 재료를 다듬는 칼

유(類)와 종차(種差)로 다시 쓰면, 알고리즘은 [문제 해결 절차]인데 [아직 데이터를 만나지 않은 빈 틀]이라는 점에서 모델과 구별되고, 모델은 [예측을 수행하는 실체]인데 [알고리즘이 데이터를 학습해 값이 채워진 상태]라는 점에서 알고리즘과 구별됩니다.

리액트 비유가 정확한 이유: 리액트 라이브러리 자체는 아무 화면도 보여주지 못합니다. 틀일 뿐입니다. 거기에 내 컴포넌트와 데이터를 넣어야 비로소 서비스가 됩니다. 알고리즘과 모델의 관계가 정확히 이것입니다.

5.2 ⭐ 같은 '모델'인데 상태가 다르다

이번 프로젝트에서는 '모델'이라는 단어가 완전히 다른 두 대상에 붙습니다. 여기서 대부분 혼란이 생깁니다.

EEVE (한국어 LLM) → 기성품 뇌 대기업이 이미 수천억 개의 문장으로 학습을 끝내놓은 완성형 모델입니다. 사용자가 입력한 자연어 채팅의 '의도' 를 파악하며, 내가 하는 일은 학습이 아니라 다운로드와 호출뿐입니다.

SVM (서포트 벡터 머신) → 조립식 기계 처음에는 그냥 공식(알고리즘)이지만, 타이타닉 데이터를 주입하면 예측 모델로 변합니다. 승객 데이터를 바탕으로 '생존/사망 여부' 를 최종 분류하며, 데이터를 정제하고 학습(fit) 시켜야 비로소 모델이 됩니다.

EEVESVM
받았을 때 상태이미 모델아직 알고리즘
내가 학습시키는가XO
무엇을 다루는가자연어(문장)정형 데이터(표)
축 3으로 보면남이 학습시킨 파라미터를 씀내 데이터로 파라미터를 만듦
비유사 온 완제품 뇌조립 키트

⚠️ "SVM 모델을 다운로드했다" 는 말은 성립하지 않습니다. 다운로드한 것은 scikit-learn 이라는 라이브러리(알고리즘 구현) 입니다. 반대로 "EEVE를 학습시켰다" 도 우리 프로젝트에서는 성립하지 않습니다. 우리는 학습된 것을 가져다 쓸 뿐입니다.

5.3 형태소 분석기 Kiwi — 한국어 문장 절단기

영어는 띄어쓰기만으로도 단어가 잘립니다. "the young man" → the / young / man. 한국어는 안 됩니다. "3등실에 탄 20대 남자는 살았어?" 를 띄어쓰기로 자르면 3등실에, 남자는 같은 조사가 붙은 덩어리가 나옵니다.

형태소 분석기(Morphological Analyzer) 는 이 덩어리에서 의미를 가진 최소 단위를 잘라내는 도구입니다.

"3등실에 탄 20대 남자는 살았어?"
        ↓ Kiwi
3등실 / 에 / 타 / ㄴ / 20 / 대 / 남자 / 는 / 살 / 았 / 어 / ?
        ↓ 명사·수사·동사만 추림
['3등실', '20대', '남자', '살']
  • Kiwi — [한국어 형태소 분석기]인데, [C++로 구현되어 순수 CPU 연산으로 매우 빠르게 동작한다]는 점에서 다른 분석기와 구별된다.
  • 3.2절에서 본 대로, 순수 CPU 작업이므로 async def를 붙이면 안 됩니다.

5.4 ⭐ 4단계 파이프라인 — 세 도구가 맞물리는 순서

사용자가 채팅을 입력했을 때, 다운로드한 도구들은 다음 순서로 맞물려 돌아갑니다.

[1단계: 사용자 입력] ➔ "3등실에 탄 20대 남자는 살았어?"
        ↓
[2단계: 키위 (Kiwi)] ➔ 문장을 키워드 단위로 쪼갬 ('3등실', '20대', '남자')
        ↓
[3단계: EEVE (LLM)]  ➔ 쪼개진 단어를 보고 "타이타닉 생존 예측을 원하는구나!" 하고 의도 파악
        ↓
[4단계: SVM (모델)]  ➔ [3등실, 20대, 남성] 데이터를 입력받아 "사망 확률 높음" 최종 예측

각 단계를 이 교재의 용어로 다시 붙이면 이렇습니다.

단계도구하는 일성격헥사고날 위치
1사용자자연어 질문입력inbound
2Kiwi형태소 분리CPU-bound, 무상태도메인 서비스
3EEVE의도 분류학습 완료된 모델outbound 어댑터(모델 호출)
4SVM생존 예측내가 학습시킨 모델도메인

한 줄 요약: 리액트(알고리즘) 라는 틀로 웹사이트를 완성하면 서비스(모델) 가 되듯이, SVM(알고리즘) 에 타이타닉 데이터를 학습시켜 예측 기계(SVM 모델) 를 만든 뒤, 이미 완성된 말귀 알아듣는 기계(EEVE 모델) 와 연결하는 과정입니다.


6. 파라미터 3종 — 누가 정하는가로 갈린다

6.1 포함 관계부터

'모델 파라미터'와 '하이퍼파라미터'는 '파라미터'라는 큰 범주 안에 포함되는 하위 개념입니다. 이것부터 그려놓지 않으면 세 단어가 계속 평등하게 나열돼 보입니다.

                    파라미터 (Parameter)
                    ─── 모든 변수의 통칭 ───
                    ┌──────────┴──────────┐
        모델 파라미터                   하이퍼파라미터
    (학습으로 자동 결정)              (사람이 학습 전 설정)

6.2 파라미터 (Parameter / 매개변수)

  • 시스템, 함수, 알고리즘, 혹은 모델이 작동하기 위해 외부로부터 입력받거나 내부에서 사용하는 모든 형태의 '변수'를 통칭하는 가장 넓은 개념입니다.
  • 프로그래밍에서는 함수 정의 시 전달받는 인자를 의미하기도 하며, 통계학에서는 모집단의 특성을 나타내는 정량적 수치(모평균, 모분산)를 뜻합니다.

6.3 모델 파라미터 (Model Parameter)

  • 파라미터 중 '모델 내부'에 존재하며, 데이터 학습(Training)을 통해 자동으로 결정되는 변수입니다.
  • 데이터의 특징(Pattern)을 학습하여 최적의 예측을 하기 위해 모델 스스로가 계속 업데이트하는 값입니다.

결정 주체는 데이터와 학습 알고리즘(자동), 결정 시점은 학습이 진행되는 동안 실시간입니다. 예시: 선형 회귀의 가중치(w) 와 편향(b), 인공신경망의 가중치(Weights).

6.4 하이퍼파라미터 (Hyperparameter)

  • 파라미터 중 '모델 외부'에 존재하며, 사용자가 학습 시작 전에 직접 설정하는 변수입니다.
  • 모델 파라미터가 최적의 값을 찾을 수 있도록 학습 환경과 모델의 구조를 통제하는 역할을 합니다.

결정 주체는 사용자(엔지니어/분석가), 결정 시점은 학습을 시작하기 전(미리 지정)입니다. 예시: 학습률(Learning Rate), 에포크(Epoch) 수, 배치 크기(Batch Size), 은닉층의 개수.

6.5 세 개념 한눈에 보는 비교

개념파라미터 (Parameter)모델 파라미터 (Model Parameter)하이퍼파라미터 (Hyperparameter)
범주최상위 개념 (모든 변수의 통칭)파라미터의 하위 범주파라미터의 하위 범주
조정 주체—모델 (데이터 학습을 통해 자동 조정)사용자 (수동 또는 최적화 알고리즘 활용)
목적시스템 구동을 위한 변수 입력데이터 패턴 표현 및 실제 예측 수행학습 과정 제어 및 모델 구조 정의
비유'시험'과 관련된 모든 요소학생이 공부하며 터득하는 '지식과 요령'시험 준비를 위한 '공부 시간, 교재, 강의'

이 비유가 좋은 이유는 경계가 자동으로 잡히기 때문입니다. 공부 시간과 교재는 학생이 시험 전에 정하는 것이고, 지식과 요령은 공부하다 보면 생기는 것입니다. 어느 쪽인지 모르겠으면 "이건 내가 미리 정하는 건가, 하다 보면 생기는 건가" 를 물으면 됩니다. 그게 축 3입니다.

6.6 파라미터 vs 아규먼트 — 프로그래밍 쪽 구별

같은 '파라미터'라는 단어가 파이썬 코드에서는 또 다른 짝을 가집니다. 이건 머신러닝과 무관한 별개의 구별입니다.

def say_hello(name):        # ← name 은 파라미터 (매개변수)
    print(f"안녕, {name}")

say_hello("철수")            # ← "철수" 는 아규먼트 (인자)
용어언제의 것인가무엇인가
파라미터 (Parameter / 매개변수)함수를 정의할 때값을 받을 이름표
아규먼트 (Argument / 인자)함수를 호출할 때실제로 들어가는 값

3주차의 변수와 값 관계와 똑같습니다. 변수가 상자, 값이 알맹이이듯 파라미터가 상자, 아규먼트가 알맹이입니다. 그리고 1.2절에서 무상태 함수가 "오직 현재 입력으로 들어온 매개변수만을 기반으로 동작한다"고 했던 그 매개변수가 바로 이것입니다.


7. 수(數) · 체(體) · DB — 데이터 생태계 3층 지도

7.1 왜 3층으로 보는가

지금까지 배운 데이터 관련 용어를 나열해 보면 이렇습니다. int, float, 변수, 상수, Enum, 리스트, 튜플, 딕셔너리, DataFrame, Dataset, 데이터베이스, 데이터 웨어하우스, 데이터 마트, 데이터센터.

이걸 평면에 나열하면 외울 수 없습니다. 크기 순서로 층을 쌓으면 한 장에 들어갑니다.

층이름다루는 단위사는 곳
4층수(數)값 하나메모리(레지스터·스택)
3층체(體)값들의 묶음메모리(힙)
2층DB묶음들의 영구 보관디스크·스토리지
1층물리 인프라그 모든 것이 도는 건물현실 세계

7.2 수(數) — 변수·상수·자료형 레이어

컴퓨터 메모리에 데이터가 존재하고 연산되는 가장 최소 단위의 규칙입니다.

가치 사슬 3단계 — Data(자료) 는 현실에서 수집한 가공되지 않은 순수한 사실이나 값, Information(정보) 는 데이터를 목적에 맞게 처리·정제해 맥락(Context)을 부여한 결과물, Value(값/가치) 는 정보로 인사이트를 도출해 의사결정에 활용하여 얻는 유용성입니다.

Data (자료)  ──가공·정제·연산──▶  Information (정보)  ──의사결정·활용──▶  Value (가치)
   "38"                          "이 승객의 나이는 38세"           "38세 1등실 여성은 생존 확률 높음"

자료형(Data Type) Value(값)만 가지는 형태로, 메모리에 저장될 형식을 정의하는 규칙입니다.

분류자료형예
숫자형int (정수형)3, -7, 1912
숫자형float (실수형)7.25, 71.2833
문자형문자 / 문자열(string)"S", "Braund, Mr. Owen Harris"

파이썬의 특징 — 동적 타이핑(Dynamic Typing) int나 float 같은 자료형이 명확히 존재하지만, a = 3처럼 개발자가 코드를 쓸 때 직접 지정하지 않아도 실행 시점에 자동으로 판단하는 방식을 씁니다. 편하지만 대가가 있습니다. 실행해 보기 전까지는 타입 실수를 모릅니다. 6주차의 Pydantic과 SQLAlchemy 2.0이 타입 힌트를 규칙으로 승격시킨 이유가 여기 있습니다.

변수(Variable)와 값(Value) 값을 담는 상자(Variable)와 그 안에 들어가는 알맹이(Value)의 관계입니다. a = 3에서 a는 변수, 3은 값입니다.

상수(Constant)와 열거형(Enumeration) 프로그램 실행 동안 절대 변하지 않는 고정 값(Constant)과, 이 상수들을 의미 있는 그룹으로 묶어둔 집합(Enum)입니다.

from enum import Enum

class PClassType(int, Enum):    # 상수 3개를 하나의 의미 그룹으로 묶음
    FIRST = 1
    SECOND = 2
    THIRD = 3

왜 1, 2, 3 그냥 쓰면 안 되는가: 코드 어딘가에 pclass == 1 이라고 쓰여 있으면 6개월 뒤의 나는 그 1이 1등실인지, 남자인지, 참(True)인지 알 수 없습니다. Enum은 숫자에 이름을 돌려주는 장치입니다. 10장에서 실제로 씁니다.

Attribute(정적 속성)와 Property(동적 속성) 객체의 성질을 나타냅니다. 컴파일(선언) 시점에 결정되는 선언적 특징을 Attribute, 런타임에 접근 제어나 계산을 통해 다루는 성질을 Property 로 구분합니다.

@dataclass(frozen=True)
class PClass:
    value: PClassType                  # Attribute — 선언되어 그냥 있는 값

    @property
    def is_first_class(self) -> bool:  # Property — 물어볼 때마다 계산되는 값
        return self.value is PClassType.FIRST
Attribute (정적 속성)Property (동적 속성)
언제 정해지는가선언 시점접근하는 순간
저장되는가메모리에 값이 있음값이 없고 계산식만 있음
축 2로 보면정적동적

함수(Function / 기능) 이 '수'들을 입력받아 정해진 로직으로 연산하고 처리하는 기능적 단위입니다. 3주차의 뺄셈식(함수 = 객체 − 속성)이 여기 4층 맨 끝에 붙습니다.

7.3 데이터의 네 가지 측정 척도

같은 '숫자'라도 성질이 다릅니다. 이걸 모르면 전처리에서 반드시 사고가 납니다.

대분류척도뜻타이타닉 예가능한 연산
Categorical(범주형)명목 (nominal)순서 없음. 이름표일 뿐Sex, Embarked, Name같다/다르다
Categorical서열 (ordinal)서열은 있으나 간격은 불명Pclass (1등실 > 2등실 > 3등실)크다/작다
Quantitative(양적)등간 (interval)일정한 간격은 있으나 절대 원점이 없음시각, 온도, pH더하기/빼기
Quantitative비율 (ratio)절대 0(원점) 이 있어 비율이 성립나이, 돈, 몸무게곱하기/나누기

⭐ 등간과 비율을 가르는 것은 "10배라는 말이 성립하는가"입니다. 섭씨 20도는 섭씨 10도의 2배가 아닙니다. 0도가 "온도가 없음"을 뜻하지 않기 때문입니다. → 등간 20세는 10세의 2배가 맞습니다. 0세가 "나이가 없음"을 뜻하기 때문입니다. → 비율

왜 실무에서 중요한가: Pclass는 서열형입니다. 1, 2, 3 이라는 숫자로 저장돼 있지만, "3등실은 1등실의 3배"라는 뜻이 아닙니다. 그런데 모델은 숫자를 보면 크기로 해석합니다. 그래서 서열형과 명목형은 원-핫 인코딩 같은 별도 처리를 고민해야 합니다. Sex를 남→0, 여→1로 바꾸는 것도, 두 개뿐이라 문제가 안 될 뿐 원래는 위험한 변환입니다.

7.4 체(體) — 구조체·자료구조 레이어

단독으로 존재하는 '수'들을 특정한 규칙으로 묶어 물리적·논리적 덩어리로 만든 단계입니다.

자료구조(Data Structure) — 데이터를 메모리 내에서 효율적으로 관리하기 위한 논리적 뼈대입니다.

구조기호순서변경성격특징
List[ ]O (인덱스)가능 (Mutable)변수 성격의 체순서가 존재하며 내부 값을 언제든 바꿀 수 있음
Tuple( )O (인덱스)불가 (Immutable)상수 성격의 체한 번 생성하면 절대 값을 바꿀 수 없음
Dictionary{ }키로 접근가능이름표가 붙은 체키(Key)와 값(Value)의 쌍

⭐ List는 변수, Tuple은 상수. 이 대응이 4층(수)과 3층(체)을 하나로 이어줍니다. 4층에서 변수와 상수를 갈랐던 축이, 3층에서 List와 Tuple을 가릅니다. 같은 축이 층만 바꿔 반복됩니다.

Dictionary가 빠른 이유 숫자는 인덱스를 가지고, 문자는 내부적으로 해시(Hash) 함수를 통해 고유한 숫자로 변환하여 저장하므로 초고속 검색이 가능합니다. 리스트에서 "Braund" 를 찾으려면 처음부터 하나씩 비교해야 하지만, 딕셔너리는 "Braund" 를 해시로 바꿔 위치를 곧바로 계산합니다.

Data Frame (표) 자료구조를 분석하기 좋게 2차원 행(Row)과 열(Column)의 구조로 구체화한 형식입니다. 구조적으로는 딕셔너리들을 담은 하나의 거대한 리스트입니다.

[
  {"PassengerId": 1, "Pclass": 3, "Sex": "male",   "Age": 22.0},   # ← 딕셔너리 = 한 행
  {"PassengerId": 2, "Pclass": 1, "Sex": "female", "Age": 38.0},
]
#  ↑ 리스트가 이 딕셔너리들을 순서대로 담고 있다 = DataFrame

Data Set (집합) 특정 목적(분석, 모델 학습 등)을 위해 데이터 프레임이나 파일들을 모아놓은 데이터의 집합체입니다. train.csv 와 test.csv 두 개를 합쳐 부르는 이름이 바로 데이터셋입니다.

3주차 끝에서 본 차원 확장과 정확히 겹칩니다.

3주차 용어이번 주 용어차원
스칼라 (Scalar)값 하나 — 수(數)0차원
벡터 (Vector)List / Tuple1차원
매트릭스 (Matrix)DataFrame2차원
텐서 (Tensor)Dataset (여러 표의 묶음)3차원 이상

7.5 DB — 저장소·엔터프라이즈 레이어

'체' 형태로 구조화된 대규모 데이터셋들이 영구적으로 저장되고 관리되는 시스템 인프라입니다.

이름정체성격비유
Data Base (저장소)실시간으로 발생하는 서비스 데이터(트랜잭션)를 영구 저장하고 즉각적으로 읽고 쓰는 기저 저장소OLTP매장의 계산대
Data Warehouse (창고)여러 DB의 데이터를 통합하고 추출·변환·적재(ETL)하여 장기 축적한 대규모 분석 전용 창고OLAP본사 물류창고
Data Mart (마트)DW에서 특정 부서(마케팅, 영업 등)나 목적의 분석을 위해 필요한 데이터만 소규모로 쪼개놓은 서브 창고OLAP부서별 소분 창고
Data Center (센터)위의 모든 연산·소프트웨어·저장소(서버 및 스토리지)가 실제로 전력을 소비하며 물리적으로 작동하는 거대한 빌딩 인프라물리건물 그 자체

OLTP와 OLAP

OLTP (Online Transaction Processing)OLAP (Online Analytical Processing)
목적처리 — 주문 받기, 회원 가입분석 — 지난 분기 매출 추이
쿼리 성격짧고 많음길고 무거움
데이터 시점지금쌓인 과거 전체
이 교재에서Neon(PostgreSQL)아직 안 다룸

ETL — 3주차에서 데이터 마이그레이션을 배울 때 나온 그 절차입니다. Extract(추출) → Transform(변환) → Load(적재). DB에서 DW로 데이터가 건너갈 때 반드시 거칩니다.

⚠️ "데이터베이스와 데이터 웨어하우스는 크기 차이" 라는 설명은 틀렸습니다. 크기가 아니라 목적이 다릅니다. DB는 쓰기에 최적화되고, DW는 읽기(집계) 에 최적화됩니다.

7.6 종합 아키텍처 흐름도

graph TD
    subgraph 수직_구조 [수직 레이어: 규모 및 추상도]
        direction TB
        L4["[4층: 수(數)] Constant / Enum ➔ Variable / Attribute / Property ➔ Function(연산)"]
        L3["[3층: 체(體)] Data Structure (List / Tuple / Dictionary) ➔ Data Frame ➔ Data Set"]
        L2["[2층: DB] Data Base ➔ (ETL) ➔ Data Warehouse ➔ (Sub-sampling) ➔ Data Mart"]
        L1["[1층: 물리 인프라] Data Center (물리 서버 빌딩)"]

        L4 -->|"변수/상수들을 덩어리로 조직화"| L3
        L3 -->|"대규모 데이터셋의 영구 저장 및 분석"| L2
        L2 -->|"모든 소프트웨어가 상주하는 물리 공간"| L1
    end

    subgraph 수평_구조 [수평 레이어: 가치화 흐름]
        direction LR
        H1["Data (자료)"] -->|"가공 / 정제 / 연산"| H2["Information (정보)"]
        H2 -->|"의사결정 및 활용"| H3["Value (가치)"]
    end

이 지도를 읽는 법: 세로축은 규모와 추상도, 가로축은 가치화 흐름입니다. 세로로 내려갈수록 덩어리가 커지고 물리적이 되며, 가로로 갈수록 같은 데이터가 더 쓸모 있어집니다. 두 축은 독립입니다. 4층에서도 가치화는 일어나고, 2층에서도 일어납니다.