본문으로 건너뛰기
원칙에도 담당 구역이 있다 — 데이터에는 LSP, 제어에는 OCP

생성 수정

IBM * 레드햇 AX 과정 · 13강

원칙에도 담당 구역이 있다 — 데이터에는 LSP, 제어에는 OCP

2026년 6월 8일(월) ~ 6월 12일(금)

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

이 장을 다 읽으면

  1. SOLID의 다섯 원칙이 아무 코드에나 똑같이 붙는 것이 아니라 원칙마다 담당 구역이 있다는 것을 설명할 수 있다.
  2. LSP(리스코프 치환 원칙)가 왜 Schema / DTO / VO / Entity / ORM 같은 데이터 객체의 문제인지, 각각이 LSP를 어떻게 지키는지 말할 수 있다.
  3. OCP(개방-폐쇄 원칙)가 왜 Router / Use Case / Repository 같은 제어 컴포넌트의 문제인지 설명하고, pg_repository 를 mongo_repository 로 갈아끼우는 과정을 그림으로 그릴 수 있다.
  4. '러닝(Learning)'의 본질이 규칙을 누가 만드는가에 있음을 말하고, 피처(Feature)와 피처 엔지니어링 네 가지 작업을 설명할 수 있다.
  5. 머신러닝과 딥러닝을 특징 추출 주체·데이터 양·데이터 형태·해석 가능성 네 축으로 구별할 수 있다.
  6. CPU와 GPU의 차이를 '대학교수 1명'과 '초등학생 3,000명'으로 설명하고, 딥러닝에 왜 GPU가 필수인지 말할 수 있다.
  7. Dataset과 Database가 정적/동적 축으로 갈린다는 것을 알고, Ground Truth의 어원과 두 단계에서의 역할을 설명할 수 있다.
  8. X_train / y_train / X_test / y_test 4분할을 그리고, y_test 를 모델에게 보여주면 안 되는 이유(데이터 누수)를 말할 수 있다.
  9. IaaS·PaaS·SaaS를 9층 책임 공유 모델 표로 구분하고, 우리가 쓰는 서비스가 각각 어디에 속하는지 분류할 수 있다.
  10. 프롬프트·커맨드·서치를 자유도·입력 형태·오류 시 반응으로 구별할 수 있다.

0. 이 장의 판별 축

앞 장들과 같은 방식입니다. 새 용어를 만나면 비유를 찾기 전에 아래 축 중 어디에 꽂히는지부터 정하세요.

#축왼쪽 (0)오른쪽 (1)이 축으로 갈리는 것
1치환 가능한가 / 확장 가능한가치환 (LSP) — 자리를 바꿔 끼워도 안전한가확장 (OCP) — 안 고치고 늘릴 수 있는가데이터 객체(Schema·DTO·VO·Entity·ORM) ↔ 제어 컴포넌트(Router·Use Case·Repository)
2정적인가 동적인가정적 — 특정 시점에 박제됨동적 — 실시간으로 바뀜Dataset ↔ Database
3누가 규칙을 만드는가사람이 정의기계가 발견전통 프로그래밍 ↔ 러닝 / 머신러닝 ↔ 딥러닝
4순차인가 병렬인가순차 — 하나씩 깊게병렬 — 동시에 넓게CPU ↔ GPU
5어디까지 내가 관리하는가전부 내가전부 남이On-Premise ↔ IaaS ↔ PaaS ↔ SaaS

축 1이 이 장의 중심입니다. 5주차에서 SOLID 다섯 원칙을 배웠고, 6주차에서 SRP·OCP·DIP를 실제 파일과 폴더로 내려앉혔습니다. 이 장은 거기서 한 걸음 더 들어갑니다. 지금까지가 "SOLID를 지켜야 한다"였다면, 이제부터는 "어떤 원칙이 어떤 종류의 코드에 붙는가" 입니다.

⭐ 이 장 전체를 한 문장으로 줄이면 — LSP는 데이터 객체에, OCP는 제어 컴포넌트에 붙는다. 원칙은 다섯 개지만, 내 코드 앞에서 물어야 할 질문은 그 코드가 데이터냐 제어냐에 따라 달라집니다.

앞 장에서 이어집니다 — 6주차 11.2절 끝에서 이렇게 예고했습니다. "LSP는 Schema / DTO / ORM / VO / Entity 가 같은 계약을 지키며 서로 교체 가능한가의 문제로 다시 등장합니다." 지금이 그 자리입니다.


1. 원칙에도 담당 구역이 있다

1.1 지금까지의 SOLID, 그리고 이 장에서 달라지는 것

5주차에서 다섯 원칙을 이름과 정의로 배웠습니다.

약자이름한 줄 정의
S단일 책임 원칙 (Single Responsibility Principle)한 클래스는 바뀔 이유가 하나여야 한다
O개방-폐쇄 원칙 (Open-Closed Principle)확장에는 열고, 변경에는 닫는다
L리스코프 치환 원칙 (Liskov Substitution Principle)하위 타입은 상위 타입 자리에 들어가도 안전해야 한다
I인터페이스 분리 원칙 (Interface Segregation Principle)안 쓰는 메서드를 억지로 구현하게 하지 마라
D의존성 역전 원칙 (Dependency Inversion Principle)둘 다 추상에 의존하게 하라

6주차에서는 이 중 셋(SRP·OCP·DIP)을 실제 코드에서 위반 사례로 잡아냈습니다. 그러면 자연스럽게 이런 질문이 생깁니다.

"다섯 개를 전부, 모든 코드에 다 적용하는 건가요?"

아닙니다. 원칙마다 잘 맞는 코드의 종류가 다릅니다. 망치로 나사를 박을 수 없듯, 데이터 그릇에 OCP를 들이대면 할 말이 별로 없고 라우터에 LSP를 들이대도 마찬가지입니다.

1.2 코드는 크게 두 종류다

6주차에서 만든 헥사고날 구조를 다시 펼쳐 보면, 파일들이 두 부류로 갈립니다.

① 데이터 객체 (Data Object)② 제어 컴포넌트 (Control Component)
누가 여기 속하나Schema · DTO · VO · Entity · ORMRouter · Use Case(Interactor) · Repository
하는 일값을 담고 계층 사이를 이동한다흐름을 만들고 다른 것을 호출한다
로직거의 없다 (값에 붙은 규칙 정도)로직 그 자체다
3주차 뺄셈식객체 − 기능 (개체 쪽)객체 − 속성 (함수 쪽)
다형성을 쓰는 순간상속으로 확장된 하위 타입을 대입할 때구현체를 통째로 갈아끼울 때
바뀌는 이유필드가 추가·변경될 때기술이나 정책이 바뀔 때
위험한 순간하위 타입을 상위 타입 자리에 넣었을 때새 요구사항이 왔을 때
담당 원칙⭐ LSP⭐ OCP

이 구분은 3주차의 뺄셈 정의와 그대로 이어집니다. 개체(Entity) = 객체 − 기능 은 저장만 되고, 함수(Function) = 객체 − 속성 은 변화만 일으킵니다. ① 은 개체 쪽에, ② 는 함수 쪽에 가깝습니다. 성격이 다르니 지켜야 할 원칙도 다릅니다.

1.3 LSP — 정의

LSP (리스코프 치환 원칙, Liskov Substitution Principle) 서브타입은 언제나 자신의 기반 타입(Base Type)으로 교체될 수 있어야 한다. 자식 클래스는 부모 클래스의 계약(인터페이스 및 규약) 을 위반하지 않아야 한다.

  • LSP — [객체지향 설계 원칙]인데, [상속 관계에서 부모의 자리에 자식을 넣어도 프로그램이 여전히 옳게 동작할 것]을 요구한다는 점에서 다른 원칙과 구별된다.

이름의 유래는 사람 이름입니다. 바바라 리스코프(Barbara Liskov) 라는 컴퓨터과학자가 정식화한 원칙이라 그의 이름이 붙었습니다. 그러므로 '리스코프'는 뜻이 있는 단어가 아니라 성(姓) 입니다. 뜻을 찾으려 하지 마세요.

⚠️ 한국어 번역어의 함정 — "치환"은 수학의 치환(substitution)과 같은 말입니다. x 자리에 3 을 넣어도 식이 성립하듯, 상위 타입 자리에 하위 타입을 넣어도 코드가 여전히 성립해야 한다는 뜻입니다.

LSP가 묻는 질문은 하나뿐입니다. 이 자리에 자식을 넣어도 부르는 쪽이 아무것도 몰라도 되는가?

1.4 OCP — 정의

OCP (개방-폐쇄 원칙, Open-Closed Principle) 소프트웨어 요소는 확장에는 열려 있고, 변경에는 닫혀 있어야 한다. 즉, 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 한다.

  • OCP — [객체지향 설계 원칙]인데, [기능을 늘릴 때 기존 파일을 열지 않아도 되게 만들 것]을 요구한다는 점에서 다른 원칙과 구별된다.

⚠️ "열려 있다"와 "닫혀 있다"가 헷갈리는 이유는 두 단어가 서로 다른 대상에 붙기 때문입니다.

문구무엇에 대해뜻
확장에 열려 있다 (Open for extension)새 기능에 대해새 파일을 추가하면 기능이 늘어난다
변경에 닫혀 있다 (Closed for modification)기존 코드에 대해기존 파일을 열 필요가 없다

두 문장은 모순이 아니라 하나의 상태를 두 방향에서 본 것입니다. "새 파일은 늘어나되, 옛 파일은 그대로"가 OCP가 만들어내는 그림입니다.

OCP가 묻는 질문도 하나뿐입니다. 요구사항이 하나 더 늘면, 나는 기존 파일을 여는가, 새 파일을 만드는가?

1.5 왜 데이터에는 LSP, 제어에는 OCP인가 ⭐

두 원칙 다 결국 다형성(Polymorphism) 을 쓰는 이야기입니다. 그런데 쓰는 목적이 다릅니다.

LSPOCP
다형성을 쓰는 목적같은 것으로 취급하기 위해다른 것으로 갈아끼우기 위해
바라보는 방향위 → 아래 (부모 자리에 자식)안 → 밖 (추상 뒤에 구현체)
잘못됐을 때 증상런타임에 조용히 틀린 값이 나온다파일을 열 때마다 사고가 난다
손해의 성격정확성(Correctness)의 손해유지보수성(Maintainability)의 손해
잘 맞는 코드데이터 객체제어 컴포넌트

데이터 객체는 '같은 것으로 취급'되는 일이 잦습니다. UserDTO 를 받는 함수에 AdminUserDTO 를 넣는 일이 자연스럽게 생깁니다. 이때 필요한 건 갈아끼우기가 아니라 "똑같이 취급해도 안전한가" 입니다. 그게 LSP입니다.

제어 컴포넌트는 '통째로 갈아끼우는' 일이 잦습니다. DB를 PostgreSQL에서 MongoDB로 바꾸는 일이 생깁니다. 이때 필요한 건 똑같이 취급이 아니라 "바꿔도 위쪽이 안 열려야 한다" 입니다. 그게 OCP입니다.

                LSP (데이터)                        OCP (제어)

           ┌──────────────────┐              ┌──────────────────┐
           │  Parent Concept  │              │  Core Component  │
           │   (상위 타입)     │              │   (핵심 로직)     │
           └────────┬─────────┘              └────────┬─────────┘
                    │  ← 이 자리에                     │  ← 이 안쪽은
         ┌──────────┼──────────┐                      │     절대 안 열림
         ▼          ▼          ▼             ┌────────┴────────┐
      Schema  DTO  VO  Entity  ORM           │ Common Interface│
                                             └────┬───────┬────┘
      ✅ 동일한 인터페이스·계약 준수                 ▼       ▼
      ✅ 부모를 자식으로 안전하게 교체              구현체A   구현체B
      ✅ 예측 가능한 동작 유지                     ← 새 파일 추가로만 확장

2. LSP — 데이터 객체 다섯 형제

2.1 다섯이 어디에 있는가

먼저 위치부터 잡습니다. 6주차 5.3절의 세 개의 그릇에 두 개를 더한 형태입니다.

브라우저 ──JSON──▶ Router ──Schema──▶ Interactor ──DTO──▶ Repository ──ORM Entity──▶ DB
                                          │
                                     Domain Entity · Value Object  (도메인 안쪽에서만 삶)
이름영문사는 곳존재 이유
SchemaSchema어댑터(인바운드) 경계밖에서 들어온 것이 규격에 맞는지 검사
DTOData Transfer Object애플리케이션 계층계층 사이를 건너다니는 순수 값 꾸러미
VOValue Object도메인 안쪽식별자 없이 값 자체로 정의되는 불변 객체
EntityEntity도메인 안쪽식별자(ID) 를 가진 비즈니스 개념
ORMObject-Relational Mapping어댑터(아웃바운드) 경계객체와 DB 테이블 행을 서로 변환

⚠️ Entity라는 단어가 두 번 나옵니다. 3주차의 혼란이 여기서 다시 나오니 못을 박아 둡니다.

어느 Entity어디에무엇인가
도메인 Entitydomain/entities/식별자와 비즈니스 규칙을 가진 순수 파이썬 객체. DB를 모릅니다
ORM Entityadapter/outbound/orm/SQLAlchemy 클래스. 테이블 한 행에 대응합니다

이름이 같다고 같은 것이 아닙니다. 한쪽은 규칙을 알고, 한쪽은 테이블을 압니다.

2.2 다섯이 각각 LSP를 어떻게 지키는가 ⭐

이 장에서 가장 중요한 표입니다.

개념주요 역할LSP 적용 방식 및 핵심 가치
Schema데이터의 구조·타입·제약조건을 정의하는 명세상위 스키마를 확장해 하위 스키마를 정의할 때, 상위 스키마의 필수 필드를 없애거나 타입을 호환되지 않게 바꿔 상위 규약을 깨뜨리지 않아야 합니다
DTO<br>(Data Transfer Object)계층 간 데이터 전송용 객체 (로직 제외)공통 기반 DTO(예: UserDTO)를 상속한 하위 DTO(예: AdminUserDTO)가 있을 때, 상위 DTO를 기대하는 모든 곳에 하위 DTO를 대입해도 오류 없이 동작해야 합니다
VO<br>(Value Object)도메인 고유의 값을 표현 (불변성·값 동등성)두 VO의 값이 같으면 동등하다고 판단하는 규약(__eq__, __hash__)을 상속 구조에서도 일관되게 유지해야 치환 시 논리적 오류가 생기지 않습니다
Entity식별자(ID)를 가진 도메인의 핵심 개념비즈니스 규칙을 내포하므로, 상속 관계에서 부모 엔티티의 식별자 규칙과 도메인 제약을 자식이 그대로 상속·보존해야 합니다
ORM<br>(Object-Relational Mapping)객체와 관계형 DB 테이블의 매핑상속 구조를 가진 엔티티를 DB에 매핑할 때(Single Table, Joined 전략 등), 부모 타입 레퍼런스로 자식을 다루어도 영속성 컨텍스트가 깨지지 않고 다형성을 올바르게 지원해야 합니다

이 표를 외우려 하지 말고, 다섯 줄이 전부 같은 말을 하고 있다는 것을 보세요.

부모 자리에 자식을 넣었을 때, 부르는 쪽이 자식임을 몰라도 되는가?

Schema는 "필수 필드"로, DTO는 "대입 가능성"으로, VO는 "동등성 규약"으로, Entity는 "식별자 규칙"으로, ORM은 "영속성 컨텍스트"로 — 각자 자기 세계의 언어로 같은 질문에 답하고 있을 뿐입니다.

2.3 Schema — 문지기의 계약

6주차에서 Pydantic으로 만들었던 그 Schema입니다. 밖에서 들어온 JSON이 규격에 맞는지 검사하는 문지기입니다.

# adapter/inbound/api/schemas/user_schema.py
from pydantic import BaseModel

class UserSchema(BaseModel):
    name: str
    email: str

class AdminUserSchema(UserSchema):        # 확장
    permission_level: int                 # 필드를 '추가'만 했다 → 안전

LSP를 지키는 확장은 위처럼 더하기만 하는 것입니다. UserSchema 를 받도록 만들어진 함수는 AdminUserSchema 를 받아도 name 과 email 을 정상적으로 찾습니다.

LSP를 깨는 확장은 이렇습니다.

# ❌ LSP 위반 — 부모의 필수 필드를 선택으로 낮춰버림
class BrokenUserSchema(UserSchema):
    email: str | None = None      # 부모는 반드시 있다고 약속했는데 없어도 되게 만듦

UserSchema 를 받는 쪽은 user.email 이 항상 문자열이라고 믿고 코드를 짰습니다. 그런데 BrokenUserSchema 가 들어오면 None 이 나옵니다. 부모의 계약을 자식이 깬 것입니다.

⭐ LSP 위반의 전형적인 두 가지 형태 ① 입력을 더 까다롭게 만든다 (부모는 받아주던 값을 자식은 거부한다) ② 출력을 더 헐겁게 만든다 (부모는 반드시 준다던 값을 자식은 안 줄 수도 있다) 외우는 문구: "받는 건 넓게, 주는 건 좁게." 반대로 하면 LSP가 깨집니다.

2.4 DTO — 계층을 건너는 값 꾸러미

6주차 5.7절에서 DTO는 @dataclass 가 정석이라고 배웠습니다. 로직이 없는 순수 값 꾸러미입니다.

# app/dtos/user_dto.py
from dataclasses import dataclass

@dataclass(frozen=True)
class UserDTO:
    name: str
    email: str

@dataclass(frozen=True)
class AdminUserDTO(UserDTO):
    permission_level: int

DTO의 LSP는 "대입 가능성" 으로 나타납니다.

def send_welcome_mail(user: UserDTO) -> None:
    print(f"{user.name} 님께 {user.email} 로 발송")

send_welcome_mail(UserDTO("월터", "walter@example.com"))          # ✅
send_welcome_mail(AdminUserDTO("로즈", "rose@example.com", 9))    # ✅ 이것도 되어야 한다

두 번째 줄이 오류 없이 동작하는 것이 곧 LSP를 지킨 상태입니다. send_welcome_mail 안에 if isinstance(user, AdminUserDTO): 같은 분기가 생기는 순간 그건 LSP가 깨졌다는 신호입니다.

💡 판별 팁: isinstance 나 type(x) == ... 로 하위 타입을 골라내는 분기를 보면, 그 자리에는 거의 항상 LSP 위반 또는 잘못된 상속이 숨어 있습니다.

2.5 VO — 값이 곧 정체성인 객체

VO(Value Object, 값 객체) — [도메인 객체]인데, [식별자가 없고 값의 조합 자체가 정체성]이라는 점에서 Entity와 구별된다.

EntityVO
식별자(ID)있다없다
같다고 판단하는 기준ID가 같으면 같다모든 값이 같으면 같다
변경가능 (같은 ID로 상태가 변함)불변(immutable) — 바꾸려면 새로 만든다
예승객(Passenger), 예약(Booking)금액(Money), 좌표(Point), 이메일주소(Email)

10,000원짜리 지폐 두 장은 구별할 필요가 없습니다. 액수가 같으면 같은 것입니다. 그게 VO입니다. 반면 승객 두 명은 이름이 같아도 다른 사람이라 ID로 구별해야 합니다. 그게 Entity입니다.

@dataclass(frozen=True)
class Money:
    amount: int
    currency: str = "KRW"

Money(10000) == Money(10000)     # ✅ True — 값이 같으면 같다

파이썬의 @dataclass 는 __eq__ 를 자동 생성하고, frozen=True 는 불변성을 강제합니다. VO를 상속으로 확장하면서 이 동등성 규약을 자식이 다르게 바꿔버리면 — 예를 들어 자식만 통화(currency)를 무시하고 비교한다면 — Money 로 취급하는 코드에서 "같은데 다르다고 나오는" 논리적 오류가 생깁니다. 그게 VO의 LSP 위반입니다.

2.6 Entity — 식별자와 규칙을 물려받기

도메인 Entity는 ID와 비즈니스 규칙을 함께 가집니다. 그래서 상속할 때 물려받아야 할 것도 두 가지입니다.

# domain/entities/passenger.py
class Passenger:
    def __init__(self, passenger_id: int, age: int):
        if age < 0:
            raise ValueError("나이는 음수일 수 없다")   # ← 도메인 제약
        self.passenger_id = passenger_id             # ← 식별자 규칙
        self.age = age

Passenger 를 상속한 CrewMember 가 있다면, 이 두 가지를 그대로 보존해야 합니다.

  • 식별자 규칙: passenger_id 로 동일성을 판단한다는 약속을 자식이 깨면 안 됩니다.
  • 도메인 제약: 부모가 "나이는 음수 불가"라고 했으면 자식이 그 제약을 느슨하게 풀어도 안 됩니다.

⚠️ 부모보다 더 엄격한 제약을 자식이 추가하는 것도 위험합니다. 부모를 기대하는 코드가 정상적으로 넣던 값을 자식이 거부하면, 그것도 2.3절의 "받는 건 넓게" 규칙 위반입니다.

2.7 ORM — 상속을 테이블로 내리는 세 가지 전략

ORM은 객체 세계의 상속을 테이블 세계로 옮겨야 합니다. 그런데 관계형 DB에는 상속이 없습니다. 그래서 전략이 필요합니다.

전략방식장점단점
Single Table부모·자식 전부 한 테이블에 몰고 구분 컬럼(discriminator)으로 나눔조인이 없어 빠름자식 전용 컬럼이 전부 NULL 허용이 됨
Joined부모 테이블 + 자식 테이블을 나누고 조인해서 복원정규화가 깨끗함조회할 때마다 조인 비용
Concrete Table자식마다 완전한 독립 테이블조인 없음부모 타입으로 전체 조회가 어려움

여기서 LSP는 이렇게 요구합니다.

부모 타입으로 조회했는데 자식 인스턴스가 돌아와도, 부르는 쪽 코드가 그대로 동작해야 한다.

즉 session.get(Passenger, 1) 이 실제로는 CrewMember 를 돌려주더라도 passenger.age 같은 접근이 전부 정상이어야 하고, 영속성 컨텍스트(변경 추적)가 깨지지 않아야 합니다. ORM이 다형성을 제대로 지원한다는 건 정확히 이 뜻입니다.

2.8 __abstract__ = True — 설계만 남기고 테이블은 안 만들기

실무에서 자주 생기는 상황이 있습니다.

"공통 컬럼(생성일시, 수정일시 등)을 부모 클래스에 모아두고 싶다. 그런데 그 부모는 실제 테이블로 만들고 싶지 않다."

SQLAlchemy에서는 한 줄로 해결합니다.

# adapter/outbound/orm/base_orm.py
from datetime import datetime
from sqlalchemy.orm import Mapped, mapped_column
from tailor.core.database import Base

class TimeStampedORM(Base):
    __abstract__ = True                 # ⭐ 이 한 줄

    created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
    updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow,
                                                 onupdate=datetime.utcnow)

__abstract__ = True 를 붙이면 SQLAlchemy는 이 클래스를 매핑 대상에서 제외합니다. 즉 테이블은 안 만들고, 컬럼 정의는 자식에게 물려줍니다.

class PassengerORM(TimeStampedORM):
    __tablename__ = "passengers"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    # created_at, updated_at 은 자동으로 따라온다

이게 왜 LSP 절에 있는가: __abstract__ 는 "상속은 하되 실체는 없다" 를 선언하는 장치입니다. 6주차 1장의 추상과 구상 구분이 ORM 레이어에도 그대로 있다는 뜻입니다. 추상 클래스는 계약(공통 컬럼)만 정의하고 실제 테이블(구상)은 자식이 만들며, 그 계약이 자식에서도 지켜지는 것 — 그게 LSP입니다.

💡 Alembic으로 마이그레이션을 만들 때, __abstract__ 가 빠져 있으면 원치 않는 테이블이 생성됩니다. "왜 timestampedorm 이라는 테이블이 생겼지?" 싶으면 이 줄을 확인하세요.

2.9 LSP 자가 진단 다섯 문항

#질문아니오/예라면
1부모를 기대하는 함수에 자식을 넣어도 그대로 동작하는가?아니오 → 상속이 잘못됐습니다
2자식이 부모의 필수 필드를 없애거나 선택으로 낮췄는가?예 → 계약 위반입니다 (2.3)
3자식이 부모보다 입력 조건을 더 까다롭게 만들었는가?예 → "받는 건 넓게" 위반입니다
4부르는 쪽에 isinstance 분기가 생겼는가?예 → LSP가 이미 깨졌습니다 (2.4)
5자식이 오버라이드하면서 예외를 새로 던지는가?예 → 예상 못 한 실패가 생깁니다

⚠️ 가장 유명한 LSP 반례 — 정사각형은 직사각형이 아니다 수학에서는 정사각형이 직사각형의 한 종류라 Square(Rectangle) 로 상속하고 싶어집니다. 그런데 Rectangle 에는 set_width() 와 set_height() 가 따로 있고, Square 는 하나를 바꾸면 다른 하나도 바뀝니다. "가로만 3으로 바꾸고 넓이를 재면 세로×3이 나올 것"이라 믿은 코드가 조용히 틀린 값을 받습니다. 수학적으로 IS-A인 것이 프로그래밍에서 IS-A가 아닐 수 있습니다. 기준은 계약이지 분류학이 아닙니다.


3. OCP — 제어 컴포넌트 세 형제

3.1 전체 그림

       [ Client ]
           │
           ▼
      ┌─────────┐
      │ Router  │  (요청 분기: 기존 라우터 수정 없이 새 경로 추가)
      └────┬────┘
           ▼
   ┌───────────────┐
   │   Use Case    │  (핵심 비즈니스 로직: 외부 인프라 변경에 영향 없음)
   │ (Interactor)  │
   └───────┬───────┘
           ▼
 ┌───────────────────┐
 │ Repository (IF)   │  (추상화 인터페이스: 변경에 닫힘)
 └─────────┬─────────┘
     ┌─────┴─────┐
     ▼           ▼
┌───────────┐ ┌─────────────┐
│    pg     │ │    mongo    │  (구체적인 구현체: 확장에 열림)
│Repository │ │ Repository  │
└───────────┘ └─────────────┘

이 그림에서 가로선 하나가 이 장의 핵심입니다. Repository (IF) 라는 추상 인터페이스 위쪽은 절대 안 바뀌고(변경에 닫힘), 아래쪽은 얼마든지 늘어납니다(확장에 열림).

OCP는 결국 "선을 어디에 긋는가"의 문제입니다. 그 선의 이름이 인터페이스입니다.

3.2 Router — 경로가 늘어나도 분기문이 안 늘어난다

Router의 OCP 메커니즘: 클라이언트 요청을 알맞은 컨트롤러나 유스케이스로 분기합니다. 라우팅 테이블 구조나 미들웨어 패턴을 체계화하면, 기존 라우팅 제어 로직을 변경하지 않고도(변경 닫힘) 새로운 엔드포인트나 플러그인을 쉽게 추가(확장 열림)할 수 있습니다.

OCP를 안 지킨 라우팅은 이렇게 생겼습니다.

# ❌ 경로가 하나 늘 때마다 이 함수를 연다
def handle(path: str, body: dict):
    if path == "/james":
        return james_logic(body)
    elif path == "/rose":
        return rose_logic(body)
    elif path == "/walter":          # ← 새 기능마다 여기가 늘어난다
        return walter_logic(body)

FastAPI의 APIRouter 는 이 문제를 등록(registration) 으로 바꿔 해결합니다.

# ✅ adapter/inbound/api/v1/__init__.py — 6주차 3.4절의 gateway
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(prefix="/titanic")
titanic_router.include_router(james_router)
titanic_router.include_router(rose_router)
titanic_router.include_router(walter_router)     # ← 새 라우터는 '한 줄 추가'

if 문을 고치는 것과 include_router 한 줄을 더하는 것은 결과는 같아 보여도 성격이 다릅니다. 전자는 기존 함수를 열어 다른 분기를 건드릴 위험을 지고 함수 전체를 다시 검증해야 하지만(OCP 위반), 후자는 줄 하나를 더할 뿐 기존 줄은 손대지 않고 새 라우터만 검증하면 됩니다(OCP 준수).

3.3 Use Case / Interactor — 바깥이 바뀌어도 안쪽은 그대로

Use Case의 OCP 메커니즘: 애플리케이션 고유의 비즈니스 규칙을 구현한 영역입니다. 데이터베이스 벤더가 바뀌거나 UI 프레임워크가 바뀌어도 내부 로직은 수정할 필요가 없도록 격리됩니다(변경 닫힘). 새 요구사항이 생기면 기존 유스케이스를 수정하는 것이 아니라 새 유스케이스 객체를 추가해 확장합니다(확장 열림).

방향무엇이 바뀌어도왜 안 바뀌는가
아래쪽(인프라)이 바뀔 때DB를 PG → Mongo로 교체유스케이스는 Repository 라는 추상만 알기 때문
위쪽(요구사항)이 늘 때"선박 예약 취소" 기능 추가기존 유스케이스를 고치는 게 아니라 새 파일을 만들기 때문

⚠️ 가장 흔한 실수 — 기존 유스케이스에 if mode == "cancel": 을 추가하는 것입니다. 이건 확장이 아니라 변경입니다. 새 유스케이스 파일을 만드세요. 6주차 4.2절의 "정책이 늘어날 때마다 이 함수를 연다" 문제와 정확히 같은 함정입니다.

3.4 Repository — 인터페이스가 방패다

Repository의 OCP 메커니즘: 데이터 액세스 레이어를 추상화한 인터페이스(또는 추상 클래스)입니다. 고차원의 비즈니스 로직(Use Case)은 이 Repository 인터페이스에만 의존하므로, 인프라가 어떻게 바뀌든 비즈니스 로직 코드는 완벽히 보호됩니다(변경 닫힘).

# app/ports/output/passenger_repository.py   ← 추상 (포트)
from abc import ABC, abstractmethod
from tailor.app.dtos.passenger_dto import PassengerDTO

class PassengerRepository(ABC):
    @abstractmethod
    async def save(self, dto: PassengerDTO) -> None: ...

    @abstractmethod
    async def find_by_id(self, passenger_id: int) -> PassengerDTO | None: ...

이 파일은 DB라는 단어를 한 번도 쓰지 않습니다. SQL도, 컬렉션도, 커넥션도 없습니다. "무엇을 할 수 있는가"만 있고 "어떻게 하는가"는 없습니다. 그게 포트(Port)입니다.

3.5 pg_repository ↔ mongo_repository — 갈아끼우기 실전 ⭐

이 장에서 가장 자주 인용되는 OCP 사례입니다.

① PostgreSQL 구현체

# adapter/outbound/pg/passenger_pg_repository.py
from sqlalchemy.ext.asyncio import AsyncSession
from tailor.app.ports.output.passenger_repository import PassengerRepository

class PassengerPgRepository(PassengerRepository):
    def __init__(self, session: AsyncSession):
        self.session = session

    async def save(self, dto):
        self.session.add(PassengerORM(**dto.__dict__))
        await self.session.commit()

    async def find_by_id(self, passenger_id):
        orm = await self.session.get(PassengerORM, passenger_id)
        return PassengerDTO(**orm.__dict__) if orm else None

② MongoDB로 바꿔야 할 때 — 기존 파일은 한 줄도 안 건드립니다

# adapter/outbound/mongo/passenger_mongo_repository.py   ← 새 파일 '추가'
from tailor.app.ports.output.passenger_repository import PassengerRepository

class PassengerMongoRepository(PassengerRepository):
    def __init__(self, collection):
        self.collection = collection

    async def save(self, dto):
        await self.collection.insert_one(dto.__dict__)

    async def find_by_id(self, passenger_id):
        doc = await self.collection.find_one({"_id": passenger_id})
        return PassengerDTO(**doc) if doc else None

③ 조립소에서 한 줄만 바꾼다

6주차 9.6절에서 만든 의존성 조립소(DIP 팩토리) 가 유일하게 바뀌는 자리입니다.

# dependencies/passenger_dependency.py
def get_passenger_use_case(session = Depends(get_db)) -> PassengerUseCase:
    # repo = PassengerPgRepository(session)          # ← 이 줄을
    repo = PassengerMongoRepository(mongo_collection) # ← 이 줄로 교체
    return PassengerInteractor(repo)

바뀐 것과 안 바뀐 것을 정리하면 이렇습니다.

파일DB 교체 시이유
passenger_router.py✅ 안 바뀜유스케이스 추상만 안다
passenger_interactor.py✅ 안 바뀜레포지토리 추상만 안다
passenger_repository.py (포트)✅ 안 바뀜계약은 그대로다
passenger_pg_repository.py✅ 안 바뀜지우지도 않는다. 나중에 되돌릴 수도 있다
passenger_mongo_repository.py🆕 추가됨확장에 열려 있다
passenger_dependency.py⚠️ 한 줄 바뀜조립소는 원래 바뀌라고 있는 곳이다

⭐ OCP는 "아무것도 안 바뀐다"가 아니라 바뀌는 자리를 한 곳으로 몰아넣는 것입니다. 그 한 곳이 조립소(dependencies)입니다. 나머지는 전부 닫혀 있습니다.

3.6 OCP 자가 진단 다섯 문항

#질문예라면
1기능을 하나 더할 때 기존 파일을 여는가?OCP 위반 신호
2if type == "..." 같은 분기가 늘고 있는가?다형성으로 바꿀 자리
3유스케이스가 import 하는 것 중에 구현체 이름이 있는가?DIP·OCP 동시 위반 (6주차 9.2)
4새 구현체를 추가할 때 조립소 말고 다른 곳도 열어야 하는가?추상이 새고 있습니다
5추상에 특정 기술 용어(session, query, collection)가 들어 있는가?포트가 아니라 어댑터입니다

4. 두 원칙이 한 요청 안에서 만나는 곳

4.1 한 번의 요청, 두 개의 원칙

브라우저에서 요청 하나가 들어와 DB까지 갔다 돌아오는 동안, 두 원칙이 번갈아 작동합니다.

[Client] ──JSON──▶ Router ──Schema──▶ Interactor ──DTO──▶ Repository ──ORM──▶ DB
                      │         │          │        │         │         │
                    (OCP)     (LSP)      (OCP)    (LSP)     (OCP)     (LSP)
                   경로 추가    치환      유스케이스  치환    구현체 교체   매핑
                    로 확장    가능성     추가로 확장 가능성   로 확장     다형성

상자에는 OCP, 화살표에는 LSP. 이 한 문장이 이 장의 요약입니다.

4.2 대조표

LSPOCP
붙는 대상Schema · DTO · VO · Entity · ORMRouter · Use Case · Repository
그림에서화살표 위를 흐르는 것화살표 사이의 상자
핵심 질문자식을 넣어도 안전한가안 고치고 늘릴 수 있는가
쓰는 도구상속 + 계약 준수인터페이스(ABC) + 의존성 주입
깨졌을 때값이 조용히 틀린다파일을 계속 열게 된다
진단 신호isinstance 분기if type == "..." 분기
이 장의 절2장3장

4.3 그렇다면 나머지 세 원칙은 어디에 붙는가

이 장의 분류를 나머지 원칙으로 확장하면 SOLID 전체 지도가 완성됩니다.

원칙주 담당 구역묻는 질문이 교재의 자리
S (SRP)모든 코드이 파일이 바뀔 이유가 하나인가6주차 4.3
O (OCP)제어 컴포넌트안 고치고 늘릴 수 있는가7주차 3장
L (LSP)데이터 객체자식을 넣어도 안전한가7주차 2장
I (ISP)포트(인터페이스)안 쓰는 메서드를 강요하는가5주차
D (DIP)제어 컴포넌트의 연결부둘 다 추상을 보는가6주차 9.2

⚠️ "주 담당"이지 "전용"이 아닙니다. 데이터 객체에도 SRP는 필요하고, 포트에도 OCP는 작동합니다. 다만 어느 원칙부터 의심할지를 정할 때 이 표가 시간을 아껴 줍니다.


5. 스키마와 인스턴스 — 같은 단어, 다른 세계

5.1 DB 세계의 schema / instance

Schema 라는 단어를 2장에서 Pydantic 모델로 썼습니다. 그런데 데이터베이스 이론에서 이 단어는 다른 것을 가리킵니다. 같은 주에 이 이야기가 나온 이유가 있습니다. 두 세계의 단어가 겹치기 때문입니다.

구분스키마 (Schema)인스턴스 (Instance)
무엇인가DB의 구조 정의 — 테이블·컬럼·타입·제약조건특정 시점에 그 구조 안에 실제로 들어 있는 데이터
언제 정해지는가설계 시점매 순간
얼마나 자주 바뀌는가거의 안 바뀜 (정적)계속 바뀜 (동적)
비유표의 머리글 행표의 나머지 모든 행

🔗 https://computer-science-student.tistory.com/478

5.2 세 세계에서 반복되는 같은 축

3주차와 4주차에서 이미 이 축을 만났습니다. 이번이 세 번째입니다.

세계정적 (설계·틀)동적 (실체·상태)
OOP (3주차)클래스 (Class)인스턴스 (Instance)
웹/메모리 (4주차 9장)Attribute / 클래스 변수 / 상수Property / 인스턴스 변수 / 변수
DB (이 장)스키마 (Schema)인스턴스 (Instance)
데이터 분석 (이 장 9장)DatasetDatabase

⭐ '인스턴스'라는 단어는 세 세계에서 전부 같은 뜻입니다. "틀에 실제 내용이 채워진 것." 틀이 클래스면 객체 인스턴스, 틀이 스키마면 DB 인스턴스입니다.

⚠️ 한편 Schema 라는 단어는 세 곳에서 다르게 쓰입니다. 이건 반드시 구별해야 합니다.

어디서무엇을 가리키나
Pydantic Schema (2.3절)API 경계에서 요청/응답의 규격을 정의한 클래스
DB Schema (5.1절)데이터베이스 전체의 테이블 구조 정의
PostgreSQL의 schema하나의 DB 안에서 테이블을 묶는 네임스페이스 (public.users 의 public)

셋 다 어원은 같습니다. 그리스어 skhēma = 모양, 형태. "내용이 아니라 틀" 이라는 공통점만 잡으면 됩니다.


6. 러닝(Learning)의 본질 — 규칙을 누가 만드는가

6.1 전통 프로그래밍과 러닝의 갈림길

1주차에서 AI ⊃ 머신러닝 ⊃ 딥러닝 ⊃ 생성형 AI 라는 포함 관계를 배웠습니다. 이제 그 안쪽으로 한 층 더 들어갑니다. 전통적인 프로그래밍과 AI의 가장 큰 차이는 규칙(공식)을 누가 만드느냐에 있습니다.

전통적 프로그래밍머신러닝 / 딥러닝
사람이 주는 것규칙 + 데이터데이터 + 정답
컴퓨터가 내놓는 것결과규칙(공식)
컴퓨터가 하는 일시킨 계산을 한다경험을 통해 스스로 규칙을 찾아낸다
우리가 짜는 코드if 나이 < 15: 생존확률 += 0.3model.fit(X_train, y_train)
[ 전통적 프로그래밍 ]                    [ 러닝 ]
  규칙 ──┐                             데이터 ──┐
         ├──▶ [컴퓨터] ──▶ 결과                  ├──▶ [컴퓨터] ──▶ 규칙
  데이터 ─┘                             정답  ──┘

화살표의 출력이 다릅니다. 왼쪽은 답이 나오고, 오른쪽은 공식이 나옵니다. 그 공식(= 학습된 모델)에 새 데이터를 넣으면 그때 비로소 답이 나옵니다.

⭐ 이것이 '러닝(Learning, 학습)' 이라는 단어를 쓰는 이유입니다. 사람이 공부해 공식을 익히듯, 기계가 데이터에서 공식을 익힙니다. 이 장 판별 축 3(사람이 정의 ↔ 기계가 발견)이 여기서 처음 등장합니다.

⚠️ 흔한 오해: "AI는 데이터만 주면 알아서 다 한다." 아닙니다. 러닝은 문제(Feature)와 정답(Label)을 함께 줘야 시작됩니다. 정답 없이 하는 학습(비지도 학습)도 있지만, 이 장에서 다루는 것은 정답이 있는 지도 학습(Supervised Learning) 입니다.

6.2 피처(Feature) — 컴퓨터가 보는 단서

피처(Feature, 특징) — [데이터의 한 항목]인데, [모델이 예측·분류할 때 근거로 삼는 단서]라는 점에서 그냥 컬럼과 구별된다. 과일을 분류한다면 색상·모양·무게·질감이, 타이타닉이라면 나이·성별·객실 등급·요금이 피처입니다.

데이터 세계 용어통계 용어머신러닝 용어표에서
컬럼 (Column)독립변수 (X)피처 (Feature)세로 한 줄
정답 컬럼종속변수 (Y)라벨 (Label) / 타깃 (Target)맞혀야 하는 한 줄
행 (Row)관측치샘플 (Sample) / 인스턴스가로 한 줄

⚠️ 여기서도 인스턴스가 나옵니다. 5.2절의 정의 그대로 "틀에 실제 값이 채워진 것" — 데이터 한 행이 곧 인스턴스입니다.

6.3 피처 엔지니어링 — 재료 손질

피처 엔지니어링(Feature Engineering) — AI 모델의 성능을 높이기 위해 원시 데이터를 컴퓨터가 이해하기 좋은 형태로 가공·변환하는 과정입니다. 요리로 치면 재료 손질 단계입니다.

작업무엇을 하는가왜 필요한가타이타닉 예
스케일링 (Scaling)데이터의 단위를 맞춘다나이(080)와 요금(0512)이 섞이면 큰 숫자가 결과를 지배함Age, Fare 를 0~1로
인코딩 (Encoding)문자를 숫자로 바꾼다모델은 숫자만 계산할 수 있음Sex: male→0, female→1
결측치 처리빈칸을 채우거나 지운다빈칸이 있으면 계산이 멈춤Age 빈칸을 중앙값으로
피처 생성기존 지표를 결합해 새 단서를 만든다사람만 아는 의미를 넣어줌SibSp + Parch + 1 = FamilySize

⭐ 머신러닝에서는 이 작업의 품질이 모델 성능의 절반 이상을 결정합니다. 알고리즘을 바꾸는 것보다 피처를 잘 만드는 것이 효과가 큰 경우가 대부분입니다. "쓰레기를 넣으면 쓰레기가 나온다(Garbage In, Garbage Out)"는 말이 나온 자리가 여기입니다.

앞으로 다시 만납니다 — 피처 엔지니어링은 딥러닝에서 사라집니다. 정확히는 사람이 하지 않고 모델이 대신합니다. 그 차이가 다음 절의 주제입니다.


7. 머신러닝 vs 딥러닝

7.1 포함 관계부터 다시

1주차의 그림을 다시 꺼냅니다. 둘은 대등한 두 기술이 아닙니다.

AI (인공지능)
└── 머신러닝 (Machine Learning)          ← 데이터에서 규칙을 찾는 AI
    └── 딥러닝 (Deep Learning)           ← 그중 인공신경망을 여러 층 쌓은 것
        └── 생성형 AI (Generative AI)

모든 딥러닝은 머신러닝입니다. 하지만 모든 머신러닝이 딥러닝은 아닙니다. 그러므로 "머신러닝 vs 딥러닝"이라는 비교는 정확히는 "딥러닝이 아닌 머신러닝 vs 딥러닝" 의 비교입니다. 이 장에서 '머신러닝'이라고 쓰면 앞의 뜻입니다.

7.2 핵심 차이점 다섯 줄 ⭐

구분머신러닝 (Machine Learning)딥러닝 (Deep Learning)
개념 관계딥러닝을 포함하는 더 큰 개념머신러닝의 한 종류 (인공신경망 기반)
특징(Feature) 추출사람이 개입하여 직접 특징을 가이드해 줌컴퓨터가 스스로 데이터의 본질적 특징을 찾음
데이터 양비교적 적은 데이터로도 학습 가능성능을 내려면 방대한 양의 데이터 필수
학습 데이터 형태엑셀 표처럼 정돈된 데이터(정형 데이터)사진·영상·음성·자연어 등 복잡한 데이터
해석 가능성결론을 도출한 판단 근거를 사람이 확인할 수 있음내부 연산이 너무 복잡해 판단 근거 확인이 어려움

7.3 특징 추출의 주체 — 이 장 축 3의 핵심

[ 머신러닝 — 가이드된 입력 ]
  사람이 지시 ──▶ [ SIZE ] [ COLOR ] [ WEIGHT ] ──▶ 알고리즘 ──▶ 규칙
       ▲  피처 엔지니어링(6.3절) = 재료를 손질하는 요리사

[ 딥러닝 — 원시 데이터 입력 ]
  Raw image ──▶ ┌ 1층: 선(Curves) ─ 2층: 질감(Textures) ─ 3층: 물체 부위 ┐ ──▶ 규칙
  Text/Audio    └        심층 신경망 — 레이어별 자동 탐지 · No Human      ┘

머신러닝은 "고양이는 귀가 뾰족하고 수염이 있다" 를 사람이 알려줘야 합니다. 딥러닝은 사진 수십만 장을 보고 "귀"와 "수염"에 해당하는 패턴을 스스로 만들어냅니다. 심지어 그 패턴에는 사람이 이름 붙일 수 없는 것도 많습니다.

⭐ 한 줄 요약 — 머신러닝은 인간의 특징 정의가 필요하지만, 딥러닝은 특징 추출까지 자동으로 수행합니다.

7.4 해석 가능성이라는 대가

딥러닝이 특징 추출을 자동화한 대신 잃은 것이 해석 가능성(Interpretability) 입니다.

머신러닝 (예: 결정트리)딥러닝 (예: 신경망)
"왜 그렇게 판단했나?"답할 수 있다 — "나이 < 15 이고 여성이므로 생존 예측"답하기 어렵다 — 수백만 개 가중치의 조합
별명화이트박스 (White Box)블랙박스 (Black Box)
규제 산업(금융·의료)에서쓰기 쉽다설명 책임 문제가 생긴다

3주차에서 결정트리(Decision Tree) 로 첫 모델을 만든 이유가 여기 있습니다. 학습 결과가 if 문의 나무로 나와 눈으로 읽을 수 있기 때문입니다.

⚠️ 트레이드오프입니다. 성능이 높다고 항상 좋은 선택이 아닙니다. "왜 그렇게 판단했는지 설명해야 하는가?" 를 먼저 물으세요.


8. CPU vs GPU — 순차와 병렬

8.1 두 개의 비유

CPU (중앙처리장치)GPU (그래픽처리장치)
비유대학교수 1명초등학생 3,000명
잘하는 것복잡하고 정교한 계산을 순서대로 잘 푼다단순한 사칙연산을 동시에 수천 개 푼다
코어적고(수 개~수십 개) 각각이 매우 똑똑함많고(수천 개) 각각은 단순함
어울리는 일조건 분기, 논리 판단, 순차 처리행렬 곱셈, 픽셀 연산, 대규모 반복
어울리는 학습머신러닝딥러닝

교수 한 명에게 두 자리 덧셈 10만 개를 시키면 오래 걸립니다. 초등학생 3,000명에게 나눠 주면 순식간에 끝납니다. 반대로 초등학생 3,000명에게 박사 논문 심사를 시킬 수는 없습니다.

⭐ GPU가 CPU보다 좋은 게 아닙니다. 잘하는 일이 다릅니다.

8.2 왜 딥러닝에는 GPU가 필수인가

딥러닝은 수많은 인공 뇌세포(신경망)를 거치며 무지막지한 양의 '행렬 곱셈(단순 반복 연산)' 이 발생합니다. 이를 대규모 병렬로 한 번에 처리해야만 학습이 가능합니다.

신경망 한 층의 계산 = 입력 벡터 × 가중치 행렬
  ┌ x1 x2 x3 ┐   ┌ w11 w12 ┐     각 칸의 곱셈은 서로 아무 관계가 없다
  └          ┘ × │ w21 w22 │  →  = 동시에 계산해도 된다
                 └ w31 w32 ┘

핵심은 "각 곱셈이 서로 독립적" 이라는 점입니다. 1번 곱셈의 결과를 2번 곱셈이 기다릴 필요가 없으므로 나눠서 동시에 할 수 있습니다. 이런 계산을 병렬화 가능(parallelizable) 하다고 합니다.

3주차에서 배운 데이터 차원이 여기서 다시 나옵니다. 0차원 스칼라 5, 1차원 벡터 [5, 3, 1], 2차원 매트릭스(표 한 장 — 행렬 곱셈의 대상), 3차원 이상 텐서. 딥러닝 프레임워크 이름에 'Tensor'가 들어가는 이유가 이것입니다. 다차원 배열 연산이 곧 딥러닝이기 때문입니다.

8.3 왜 머신러닝은 CPU로 충분한가

머신러닝은 데이터 양이 상대적으로 적고, 복잡한 논리 구조(조건문 등)를 차근차근 직렬로 처리하는 것이 효율적입니다. 결정트리를 생각해 보세요. if 나이 < 15 의 결과를 알아야 다음 분기를 결정할 수 있습니다. 앞 계산이 끝나야 뒤 계산을 시작할 수 있으므로 나눠서 동시에 할 수가 없습니다. 이런 계산에는 교수 한 명이 빠릅니다.

계산의 성격병렬화어울리는 하드웨어
앞 결과가 뒤 계산의 조건이 된다❌ 불가CPU
각 계산이 서로 독립적이다✅ 가능GPU

8.4 ⚠️ 3주차의 동기/비동기와 헷갈리지 마세요

축무엇에 대한 이야기인가왼쪽오른쪽
3주차 축 4기다림 — I/O 대기 시간을 놀리지 않는 법동기 (기다림)비동기 (안 기다림)
이 장 축 4일꾼 수 — 계산을 몇 개의 연산 유닛이 나눠 하는가순차 (CPU)병렬 (GPU)

3주차에서 "비동기는 CPU 연산에는 도움이 안 된다" 고 배웠습니다. 비동기는 기다리는 시간을 활용하는 기술이라 계산 자체를 빠르게 하지 않기 때문입니다. 계산 자체를 빠르게 하는 것이 병렬화이고, 그 하드웨어가 GPU입니다. 완전히 다른 층위의 이야기입니다.


9. Dataset vs Database — 정적과 동적

9.1 비교표

구분Dataset (데이터셋)Database (데이터베이스)
개념특정 목적을 위해 모아둔 데이터의 집합(묶음)데이터를 통합·관리하는 구조화된 시스템
상태정적 (Static) — 특정 시점에 추출된 데이터동적 (Dynamic) — 실시간으로 추가·수정·삭제됨
목적데이터 분석, AI 모델 학습, 통계 처리데이터의 지속적 저장, 안전한 관리, 실시간 조회
형태CSV, JSON, XML, Excel 파일 등Oracle, MySQL, PostgreSQL, MongoDB 등
접근 방식파일 전체를 불러와 메모리에 올린 후 사용SQL 등 질의어로 필요한 데이터만 요청
저장 위치기본적으로 디스크에 저장, 분석 시 메모리에 통째로 로드기본적으로 디스크에 저장, 빠른 조회를 위해 상시 메모리(캐시/버퍼) 활용

9.2 메타포로 이해하기

  • Dataset은 '밀키트(Meal Kit)' 혹은 '사진 앨범'입니다. 특정 요리(목적)를 위해 재료를 딱 맞춰 손질해 둔 정적인 묶음이거나, 과거의 특정 순간을 박제해 놓은 기록물과 같습니다. 스스로 내용이 변하지 않습니다.

  • Database는 '대형마트' 혹은 'CCTV 모니터링 시스템'입니다. 수많은 식재료가 구역별로 정리되어 있고 끊임없이 채워지거나 비워지며(CRUD), 실시간으로 현장이 녹화되고 상시 운영되는 동적인 생태계입니다.

9.3 왜 이 구분이 중요한가

머신러닝을 하려면 반드시 Dataset이 필요합니다. Database에 직접 학습을 걸 수 없습니다. 이유는 정적/동적 축 그 자체입니다.

학습 중에 데이터가 바뀌면, 무엇을 학습했는지 알 수 없게 됩니다. 그래서 특정 시점의 스냅샷을 떠서 박제합니다. 그 박제물이 Dataset입니다.

Database (동적) ──[특정 시점 추출·SELECT]──▶ Dataset (정적) ──[학습]──▶ 모델
    ▲                                                                    │
    └──────────────[예측 결과를 다시 저장·서비스]────────────────────────────┘

4주차 9장에서 배운 정적/동적 3계층(웹의 Attribute↔Property, OOP의 클래스 변수↔인스턴스 변수, 메모리의 상수↔변수)에 데이터 층(Dataset ↔ Database) 이 네 번째로 추가된 셈입니다.

⚠️ 혼동 주의: "Dataset은 파일이고 Database는 서버다"라고 외우면 반쯤만 맞습니다. 둘 다 디스크에 저장됩니다. 갈리는 것은 저장 위치가 아니라 변하는가 여부입니다. CSV를 매일 덮어써도 Dataset이고, 매 순간 CRUD가 일어나야 Database입니다.


10. Ground Truth — 우리가 진실이라고 믿기로 한 것

10.1 정의와 어원

Ground Truth (그라운드 트루스) — [머신러닝의 기준 데이터]인데, [모델의 예측이 아니라 우리가 참이라고 합의한 정답]이라는 점에서 예측값(prediction)과 구별된다.

어원이 이 개념을 가장 잘 설명합니다.

기상학이나 위성 사진 분석에서, 컴퓨터의 예측이 맞는지 확인하는 방법은 하나뿐이었습니다. 직접 땅(Ground)을 밟고 현장 조사를 해서 진실(Truth)을 확인하는 것. 위성이 "여기는 밀밭"이라고 판독했으면, 사람이 그 좌표로 가서 정말 밀밭인지 눈으로 봤습니다. 그렇게 확인한 값이 Ground Truth 입니다.

여기서 두 가지가 따라 나옵니다. ① Ground Truth는 모델 바깥에서 옵니다. ② 얻으려면 비용이 듭니다. 실제로 땅을 밟아야 하니까요.

⚠️ 한국어 번역이 어려운 단어입니다. '정답', '실측값', '참값' 어느 것도 "직접 가서 확인했다"는 어감을 담지 못해 원어를 그대로 쓰는 경우가 많습니다.

10.2 두 단계에서의 역할

Ground Truth는 학습과 평가, 두 단계에서 완전히 다른 일을 합니다.

단계역할비유
학습 (Training)모델에게 문제와 정답을 같이 보여주며 규칙을 학습시킴 (지도학습)교과서와 해설지를 함께 펴놓고 공부
평가 (Evaluation)모델이 새로 예측한 값과 Ground Truth를 비교해 정확도·오차를 계산(채점)시험지를 제출한 뒤 채점

같은 데이터인데 보여주느냐 숨기느냐가 다릅니다. 이 구분이 다음 장(11장)의 4분할 구조를 만들어냅니다.

10.3 ⚠️ 라벨링 노이즈 — Ground Truth도 틀릴 수 있다

현실 세계의 Ground Truth는 대개 '인간이 그렇다고 합의하고 라벨링한 데이터' 입니다. 그러므로 라벨링 노이즈(오류) 가 없도록 품질을 관리하는 것이 매우 중요합니다.

  • 사진 10만 장에 "고양이/개"를 붙인 사람이 졸다가 몇 장을 잘못 눌렀다면, 그게 그대로 '진실'이 됩니다.
  • 의료 영상 판독처럼 전문가끼리도 의견이 갈리는 영역에서는 애초에 하나의 정답이 없습니다.
  • 모델은 주어진 정답을 의심하지 않습니다. 틀린 정답을 주면 틀린 규칙을 성실하게 배웁니다.

⭐ 그래서 "AI가 틀렸다"의 상당수는 "데이터가 틀렸다"입니다. 모델을 바꾸기 전에 라벨을 의심하는 습관이 실무의 시작입니다.

앞으로 다시 만납니다 — 이 이야기는 LLM의 할루시네이션(Hallucination) 문제와 이어집니다. "모델이 사실을 지어낸다"의 상당 부분은 "모델이 학습한 근거가 부실했다" 입니다. 이 주 마지막 날의 RAG 이야기(14.13절)가 그 대응책입니다.