
소프트웨어 개발의 핵심은 "복잡성 관리"입니다.
그중에서도 DDD는 비즈니스 로직을 어떻게 안전하게 보호할 것인가를 고민하며,
대수적 타입은 그 로직을 '런타임 에러'가 아닌 '컴파일 타임의 구조'로 강제하는 강력한 도구를 제공합니다.
오늘은 이 두 개념을 연결하여 "말이 안 되는 상태"를 원천 차단하는 설계 기법을 알아보겠습니다.
도메인을 설계할 때 우리는 흔히 축구 경기에 비유하곤 합니다.
여기서 가장 중요한 것이 불변식입니다.
불변식은 "언제나 참이어야 하는 조건"입니다.
이 불변식을 유지하기 위해 우리는 관련 객체들을 애그리게이트라는 컨테이너에 가둡니다.
설계 팁: 엔티티인가 VO인가?
- 데이터가 무한히 늘어나는가? → 엔티티
- 동시에 여러 곳에서 수정되는가? → 엔티티
- 정합성이 깨지면 시스템이 망하는가? → VO로 묶어서 원자적으로 처리
타입은 수학적으로 집합입니다.
타입을 어떻게 조합하느냐에 따라 우리가 관리해야 할 '경우의 수'가 결정됩니다.
AND클래스나 인터페이스처럼 속성을 나열하는 방식입니다.
class User(name: String, active: Boolean)String × Boolean = 무한ORenum이나 Union Type처럼 "이것 아니면 저것"인 방식입니다.
type Result = Success | FailureSuccess + Failure = 2잘못된 곱타입 설계와 올바른 합타입 설계를 비교해 봅시다.
class Payment {
status: 'paid' | 'pending';
paidDate?: Date; // 결제 전에는 null, 결제 후에는 Date
}
2 * (Date + 1)pending이면서 paidDate가 존재하는 "불가능한 상태"가 표현 가능합니다. 이를 막기 위해 런타임에 if 문으로 체크하는 코드가 도처에 깔리게 됩니다.type Payment = PendingPayment | PaidPayment;
interface PendingPayment {
status: 'pending';
}
interface PaidPayment {
status: 'paid';
paidDate: Date; // 이제 date는 필수입니다.
}
1 + DatePending 상태일 때는 아예 paidDate에 접근조차 할 수 없습니다. 불변식이 구조적으로 보장됩니다.타입스크립트는 '구조적 타이핑'을 따릅니다. 즉, number 타입은 어디서든 number입니다. 하지만 UserId와 OrderId가 같은 number라고 해서 서로 섞여도 될까요?
이를 방지하기 위해 브랜드 타입 기법을 사용합니다.
// 공용 브랜드 유틸리티
declare const __brand: unique symbol;
type Brand<T, ID> = T & { readonly [__brand]: ID };
type OrderId = Brand<number, "OrderId">;
type UserId = Brand<number, "UserId">;
// 컴파일 타임에만 존재하는 캐스팅 함수
function createOrderId(id: number): OrderId {
return id as OrderId;
}
const oid = createOrderId(123);
const uid: UserId = 123; // Error!
이 기법을 사용하면 원시 타입에도 비즈니스적 의미를 부여하고, 컴파일 타임에 실수를 완벽히 차단할 수 있습니다.
모든 불변식을 타입으로 다 표현할 수는 없습니다. 이때는 애그리게이트 내부에서 명시적으로 체크해야 합니다.
abstract class Aggregate<T : Aggregate<T>> {
protected var invariants = listOf<(T) -> Boolean>()
protected fun checkInvariants() {
if (!invariants.all { it(this as T) }) throw IllegalStateException("불변식 위반!")
}
abstract fun clone(): T
}
class Order(val status: Status, val price: Long) : Aggregate<Order>() {
init {
invariants = listOf({ it.price > 0 }) // 불변식 정의
checkInvariants()
}
fun applyDiscount(amount: Long): Order {
val next = this.clone(price = this.price - amount)
next.checkInvariants() // 상태 변경 후 즉시 검증
return next
}
}
도메인 설계의 핵심은 "불변식을 어디에 가둘 것인가"이며, 대수적 타입은 그 불변식을 "런타임이 아닌 컴파일 타임으로 끌어올리는 도구"입니다.
이러한 습관이 모여 테스트 코드가 줄어들고, 배포가 두렵지 않은 견고한 시스템을 만듭니다.