URL: https://www.youtube.com/watch?v=lM7zWJRrRtg 날짜: 2026-07-31 채널: ArjanCodes
📌 핵심 질문 / 이 영상이 다루는 핵심 논점
dict[str, Any]나 **kwargs 같은 유연한 자료구조는 시스템 경계(edge)에서는 유용하지만, ==도메인 비즈니스 로직 내부에서 쓰면 중요한 개념을 숨기고 타입 안정성을 파괴한다==. 이 영상은 항공권 예약 시스템 예제를 실시간으로 리팩토링하며, 느슨한 딕셔너리 기반 코드를 dataclass·Enum·Value Object로 이루어진 엄격한 도메인 모델로 단계적으로 전환하는 전 과정을 보여준다.
- 딕셔너리에는 승객 이름, 항공편 번호, 가격, 상태, 로열티 등 중요한 도메인 개념이 들어있지만 문자열·불리언 등 임의의 타입으로만 취급된다
status가scheduled인지cancelled인지,price가 양수여야 하는지 등 아무런 제약도 코드에 드러나지 않는다create_booking함수는**kwargs로seat값을 몰래 받고 있어 함수 시그니처만 봐서는 실제로 무엇이 필요한지 알 수 없다- 도메인 함수가 형태가 정해지지 않은 데이터를 받으면, 개발자는 매번 수동으로 방어 코드를 추가해야 하고 AI 코딩 어시스턴트도 기댈 구조가 없어 더 부서지기 쉬운 코드를 만들게 된다
이 영상은 "유연성은 시스템의 경계(JSON, config, API payload, 프레임워크 훅)에서는 유용하지만, 도메인 내부 비즈니스 로직에서는 엄격해야 한다"는 원칙을 예약 시스템 리팩토링을 통해 실증적으로 보여준다.
1. 문제: 유연한 자료구조가 숨기는 것
원본 코드는 동작은 하지만, 딕셔너리와 키워드 인자 뒤에 도메인 지식을 숨기고 있어 유지보수를 어렵게 만든다.
1.1. 원본 코드의 구조와 문제점
- 딕셔너리 기반 예약 데이터
- 내용물:
passenger_name,flight_number,price,status,loyalty등 항공권 예약에 필요한 핵심 필드가 모두 일반 딕셔너리 안에 문자열·불리언 값으로 뒤섞여 존재한다 - 숨겨진 제약:
status가scheduled또는cancelled외의 값이 될 수 없다는 규칙,price가 음수가 될 수 없다는 규칙 등이 코드 어디에도 명시돼 있지 않다
- 내용물:
create_booking함수의 키워드 인자 문제- 숨은 인터페이스:
seat값이**kwargs로 넘어오기 때문에, 함수 시그니처만 봐서는 이 함수가 실제로 무엇을 필요로 하는지 전혀 알 수 없다 - 함수 이름과 동작의 괴리: 함수 이름은 단순히 "예약 생성"이지만 실제로는 취소된 항공편을 거부하고, 로열티 할인을 적용하고, 결과를 출력하는 등 여러 책임을 암묵적으로 수행한다
- 숨은 인터페이스:
1.2. 유연성이 필요한 곳과 필요 없는 곳
- 유연성이 유용한 지점 (시스템의 경계)
- 예시: JSON 데이터, config 파일, API payload, 프레임워크 훅(hook)처럼 애초에 구조가 정해져 있지 않거나 외부에서 결정되는 데이터
- 이유: 이런 데이터는 애초에 유연해야만 다양한 입력을 수용할 수 있다
- 엄격함이 필요한 지점 (도메인 내부)
- 비즈니스 로직: 도메인 함수가 잘못된 형태의 데이터를 받으면 코드 곳곳에 방어적 체크를 추가해야 하고, 개발자가 암묵적 구조를 계속 기억해야 하는 부담이 생긴다
- AI 어시스턴트와의 관계: 구조가 없으면 AI 코딩 도구도 기댈 컨텍스트가 부족해져 더 깨지기 쉬운 코드를 생성하게 된다
2. 리팩토링 여정: 딕셔너리에서 엄격한 도메인 모델로
발표자는 하나의 예약 생성 함수를 단계별로 리팩토링하며 점점 더 엄격한 타입 구조를 도입한다.
2.1. 1단계 — dataclass로 BookingRequest 도입
- 임의 딕셔너리를 클래스로 치환
@dataclass(frozen=True)사용:BookingRequest라는 불변(frozen) 데이터 클래스를 만들어passenger_name(str),flight_number(str),price(Decimal),status,loyalty필드를 명시적으로 선언seat를 kwargs에서 필드로 승격: 기존에**kwargs로 숨겨져 있던seat값을BookingRequest의 정식 필드로 옮기고 기본값을standard로 지정
create_booking함수 시그니처 정리- 키워드 인자 제거: 함수가 더 이상
**kwargs를 받지 않고 하나의request: BookingRequest객체만 받도록 변경 - 결과: 딕셔너리 인덱싱(
data["price"])이 인스턴스 속성 접근(request.price)으로 바뀌어, 이제create_booking이 정확히 무엇을 요구하는지 타입 시그니처만으로 알 수 있게 됨
- 키워드 인자 제거: 함수가 더 이상
2.2. 2단계 — 도메인 개념 분리: Passenger, Flight 클래스 도입
Passenger클래스- 필드:
name과loyalty_status(기본값 지정 가능)를 갖는 별도 dataclass로 승객 개념을 명시적으로 모델링 create_booking내부 사용:request.passenger를 통해loyalty와name에 접근하도록 변경
- 필드:
Flight클래스- 필드:
number,price,status를 가지며, "모든 승객에게 항공권 가격이 동일하다"는 단순화 가정을 전제로 함 (실제 항공사는 가격이 승객마다 다를 수 있음을 발표자가 직접 언급) - 효과:
BookingRequest,Passenger,Flight세 개의 도메인 객체가 갖춰지면서, 코드가 시스템 확장 시 새로운 도메인 객체를 추가해 나갈 수 있는 안정적인 기반이 마련됨
- 필드:
2.3. 3단계 — Enum으로 값의 범위를 제한
FlightStatusEnum- 값:
scheduled,cancelled두 가지로 제한하여, 문자열 오타나 정의되지 않은 상태값이 시스템에 유입되는 것을 원천 차단 - 효과: 잘못된 값을 넣으면 타입 에러가 발생해 조용히 시스템을 통과하는 대신 즉시 거부됨
- 값:
SeatTypeEnum- 값:
standard,extra_legroom두 가지로 좌석 타입을 제한 - 확장성: 항공사가 나중에 새로운 좌석 타입(발표자의 농담: "키 큰 네덜란드 사람용 좌석")을 도입하면 Enum에 값을 하나 추가하는 것만으로 명시적이고 안전하게 확장 가능
- 값:
2.4. 4단계 — Value Object 도입: Money 클래스
Moneydataclass와__post_init__검증- 불변 규칙:
amount가 항상 0 이상이어야 한다는 규칙을__post_init__메서드에서 검증하고, 위반 시ValueError를 발생시킴 - Value Object 개념:
Money는 단순히 값을 담는 것을 넘어, "음수 금액은 존재할 수 없다"는 검증 규칙까지 함께 캡슐화한 값 객체(value object)로 정의됨
- 불변 규칙:
Money에 행동(behavior) 이전discounted(percentage)메서드: 할인율을 받아amount * (1 - percentage)를 계산한 새Money객체를 반환 — 할인 로직을create_booking함수 밖으로 이동시켜Money개념 자체에 귀속시킴__str__메서드: 통화 표시를 위해 소수점 둘째 자리까지 포맷팅하는 문자열 표현을 추가 (통화 기호는Money객체가 아니라 호출부에서 처리하도록 분리 — 발표자가 실시간으로 이 설계 결정을 재고하는 과정을 보여줌)
2.5. 5단계 — 도메인 행동을 관련 객체로 이동
Flight.ensure_bookable()메서드- 캡슐화: 기존에
create_booking함수 안에 흩어져 있던 "항공편이 취소되지 않았는지 확인"하는 로직을Flight클래스의 메서드로 이동 - 검증: 취소된 항공편으로 테스트했을 때 실제로 에러가 발생함을 라이브 코딩으로 확인
- 캡슐화: 기존에
- 효과:
create_booking함수가 짧아지고 명확해짐- 책임 분리:
create_booking은 이제 항공편 상태 판단 같은 구현 세부사항을 담지 않고, 오직 "예약을 생성한다"는 본연의 역할만 수행 - 가독성: 기존의 딕셔너리 인덱싱, 키워드 인자 매직, 매직 스트링 비교로 뒤엉켜 있던 코드에 비해 함수가 훨씬 명확해지고 작업하기 쉬워짐
- 책임 분리:
2.6. 6단계 — 경계(boundary)에서 Pydantic으로 변환
- 문제: 엄격한 도메인 모델은 생성이 번거로워짐
- 트레이드오프:
Passenger,Flight,BookingRequest를 모두 직접 생성해야 하므로, 예를 들어 외부 API에서 데이터를 받아오는 상황에서는 번거로운 작업이 늘어남 - 해결 방향: API 등 경계(boundary)와 실제 도메인 로직 사이에 "변환 단계(translation step)"가 필요함
- 트레이드오프:
BookingInput(Pydantic 모델) +to_domain()메서드- 구조: API에서 넘어오는 원본 데이터를 그대로 받는
BookingInput이라는 Pydantic 모델을 별도로 정의하고, 여기에 가격이 음수가 될 수 없다는 등의 검증 규칙을 그대로 적용 - 변환:
BookingInput에to_domain()메서드를 추가해 검증이 끝난 원본 데이터를 엄격한 도메인 모델(BookingRequest등)로 변환 — API 레이어에서는 여전히 유연한 원본 딕셔너리를 받되, 도메인에 진입하기 전 한 번에 검증 및 변환이 이루어짐
- 구조: API에서 넘어오는 원본 데이터를 그대로 받는
3. 설계 철학: 모든 것에 타입을 씌우는 게 아니라, 어디에 구조가 필요한지 판단하는 것
3.1. Primitive Obsession의 함정을 피하기 — flight_number는 문자열로 남긴다
- 의도적으로 타입을 만들지 않은 사례
flight_number: 발표자는 항공편 번호에 대해 별도의 value object나 Enum을 만들 수도 있었지만, 실제로는 단순 문자열로 남겨둠- 이유: 항공편 번호는 단지 표시되고 전달될 뿐, 코드 내에서 그 구조를 검사하거나 검증하는 의미 있는 규칙이 전혀 없기 때문에 문자열로도 충분함
- 좋은 도메인 모델의 기준
- 선택적 엄격함: 좋은 도메인 모델은 모든 필드에 클래스를 씌우는 것이 아니라, 실제로 보장이 필요한 지점(예:
Money의 음수 방지,FlightStatus의 값 제한)에만 구조를 추가하는 것 - 원시 타입(primitive)도 괜찮다: 문자열이나 정수 같은 원시 타입을 그대로 쓰는 것 자체는 문제가 아니며, 핵심은 "어디에 안전장치를 둘 것인가"를 판단하는 능력
- 선택적 엄격함: 좋은 도메인 모델은 모든 필드에 클래스를 씌우는 것이 아니라, 실제로 보장이 필요한 지점(예:
3.2. 엄격한 도메인 모델이 주는 실질적 이점
- 명시성과 안전성
- 잘못된 값의 원천 차단: 유효하지 않은 상태값, 좌석 타입, 음수 가격 등이 설계상(by design) 거부됨
- 명확한 계약(contract):
create_booking함수가 정확히 무엇을 기대하는지 시그니처만으로 알 수 있음
- 개발 생산성 측면의 이점
- IDE 자동완성: 필드가 명시적 속성으로 존재하므로 IDE가 필드를 자동완성해줄 수 있음
- AI 코딩 어시스턴트 품질 향상: 구조화된 컨텍스트가 있으면 AI 코딩 도구가 더 높은 품질의 코드를 생성할 가능성이 높아짐
- 더 안전한 리팩토링: 모든 필드가 명시적 속성(named attribute)이므로 리팩토링 시 실수할 위험이 줄어듦
- 암묵적 구조를 기억할 필요 없음: 가장 중요한 이점으로, 개발자가 숨겨진 데이터 구조를 계속 머릿속에 담아두지 않아도 됨
주요 발언 모음
"This line here looks very flexible, but actually this hides a lot."
"Inside of the domain, business logic should work on very strict meaningful data. If your domain function gets fake data, you need to add a ton of checks everywhere."
"If seat in this case is important to creating a booking, it should be part of the model. It should not be put in keyword arguments."
"A good domain model doesn't mean that we provide types and classes for everything. Primitives are still okay to use like strings or integers, but it just means that we think about where we're going to add some guarantees where they're actually helpful."
"Flexible structures are useful at the edges of your system, their infrastructure, but with business logic, be very strict. We don't want any crap inside the domain."
핵심 데이터 & 수치
- 영상 길이: 약 20분 47초(1247초), 실시간 라이브 코딩 리팩토링 형식으로 진행
- 리팩토링 단계 수: 딕셔너리 →
BookingRequest(dataclass) →Passenger/Flight분리 →FlightStatus/SeatTypeEnum →Moneyvalue object →Flight.ensure_bookable()행동 이전 →BookingInput(Pydantic) 경계 검증까지 총 6단계 - 도메인 객체 수: 최종적으로
BookingRequest,Passenger,Flight,Money,FlightStatus(Enum),SeatType(Enum),BookingInput(Pydantic) 등 7개의 명시적 타입으로 구성됨 - 예외적으로 타입화하지 않은 필드:
flight_number— 검증 규칙이 없다는 이유로 의도적으로 단순 문자열로 유지
결론 및 시사점
dict[str, Any]와**kwargs는 도구이지 악(惡)이 아니다. 문제는 이들을 "시스템의 경계"가 아니라 "도메인 비즈니스 로직 내부"에서 쓸 때 발생한다.- 도메인 모델링은 단계적으로 접근할 수 있다. 딕셔너리 → dataclass → 개념 분리 → Enum → value object → 행동 이전 → 경계 검증(Pydantic)까지, 한 번에 완벽한 모델을 만들 필요 없이 점진적으로 엄격함을 더해갈 수 있다.
- 경계(boundary)와 도메인 사이에는 명시적인 변환 단계가 필요하다. API처럼 유연한 데이터가 들어오는 지점에서는 Pydantic 같은 도구로 검증한 뒤,
to_domain()같은 메서드로 엄격한 도메인 타입으로 변환해 넘기는 패턴이 실용적이다. - 모든 필드를 타입화할 필요는 없다. 실제 검증 규칙이나 제약이 존재하는 곳(가격, 상태값 등)에만 구조를 추가하고, 의미 있는 규칙이 없는 필드(항공편 번호 등)는 원시 타입으로 남겨두는 것이 Primitive Obsession을 피하는 균형점이다.
- 엄격한 도메인 모델은 AI 코딩 어시스턴트 시대에 더욱 중요해진다. 구조화된 타입은 IDE 자동완성뿐 아니라 AI가 더 높은 품질의 코드를 생성하도록 돕는 컨텍스트 역할을 한다.
