Contents
see List개요
Java 16에서 도입된 Record와 Java 17에서 정식 출시된 Sealed Class는 도메인 모델링의 표현력을 비약적으로 높여주는 기능입니다. 불변 데이터를 간결하게 정의하고, 타입 계층의 범위를 컴파일 타임에 제한함으로써 더 안전하고 예측 가능한 코드를 작성할 수 있습니다. 이 글에서는 두 기능을 결합한 실전 도메인 모델링 패턴을 소개합니다.
핵심 개념
Record와 Sealed Class의 핵심 특성을 정리합니다.
- Record: 불변 데이터 캐리어로, equals/hashCode/toString이 자동 생성됩니다. 모든 필드가 final이며, 생성자와 접근자가 자동으로 제공됩니다.
- Sealed Class:
permits절로 허용된 하위 타입만 상속 가능합니다. 컴파일러가 모든 하위 타입을 알고 있어 switch에서 완전성(exhaustiveness) 검사가 가능합니다. - 결합 활용: Sealed Interface + Record 조합으로 대수적 데이터 타입(Algebraic Data Type)을 구현할 수 있습니다.
- 패턴 매칭 연동: Sealed Class와 Record Pattern을 함께 사용하면 타입 안전한 분기 처리가 가능합니다.
실전 예제
결제 시스템의 도메인 모델을 Record와 Sealed Class로 설계하는 예제입니다.
// 결제 수단을 Sealed Interface로 정의
public sealed interface PaymentMethod
permits CreditCard, BankTransfer, DigitalWallet {
}
// 각 결제 수단을 Record로 구현
public record CreditCard(
String cardNumber,
String holderName,
YearMonth expiryDate,
String cvv
) implements PaymentMethod {
// Compact Constructor로 유효성 검사
public CreditCard {
if (cardNumber == null || cardNumber.length() != 16) {
throw new IllegalArgumentException("카드번호는 16자리여야 합니다");
}
// 마스킹 처리
cardNumber = cardNumber.substring(0, 4) + "****" + cardNumber.substring(12);
}
}
public record BankTransfer(
String bankCode,
String accountNumber,
String accountHolder
) implements PaymentMethod {}
public record DigitalWallet(
String provider, // "kakao", "naver", "toss"
String walletId
) implements PaymentMethod {}
// 주문 상태를 Sealed Class로 모델링
public sealed interface OrderStatus {
record Pending(LocalDateTime createdAt) implements OrderStatus {}
record Confirmed(LocalDateTime confirmedAt, String orderId) implements OrderStatus {}
record Shipped(String trackingNumber, LocalDateTime shippedAt) implements OrderStatus {}
record Delivered(LocalDateTime deliveredAt) implements OrderStatus {}
record Cancelled(String reason, LocalDateTime cancelledAt) implements OrderStatus {}
}
패턴 매칭과 결합한 비즈니스 로직 예제입니다.
// 결제 처리 - 패턴 매칭으로 분기
public PaymentResult processPayment(PaymentMethod method, Money amount) {
return switch (method) {
case CreditCard card -> processCreditCard(card, amount);
case BankTransfer transfer -> processBankTransfer(transfer, amount);
case DigitalWallet wallet -> processDigitalWallet(wallet, amount);
// sealed이므로 default 불필요 - 컴파일러가 완전성 보장
};
}
// 주문 상태 전이 검증
public OrderStatus transition(OrderStatus current, String action) {
return switch (current) {
case OrderStatus.Pending p when "confirm".equals(action)
-> new OrderStatus.Confirmed(LocalDateTime.now(), generateOrderId());
case OrderStatus.Confirmed c when "ship".equals(action)
-> new OrderStatus.Shipped(generateTracking(), LocalDateTime.now());
case OrderStatus.Shipped s when "deliver".equals(action)
-> new OrderStatus.Delivered(LocalDateTime.now());
case OrderStatus.Pending p when "cancel".equals(action)
-> new OrderStatus.Cancelled("고객 요청", LocalDateTime.now());
default -> throw new IllegalStateException(
"잘못된 상태 전이: " + current + " -> " + action);
};
}
활용 팁
- Record의 Compact Constructor를 활용하면 유효성 검사를 생성 시점에 강제할 수 있어, 항상 유효한 객체만 존재하게 됩니다.
- Sealed Interface 내부에 Record를 중첩 선언하면 관련 타입을 하나의 파일에 모아 응집도를 높일 수 있습니다.
- DTO(Data Transfer Object)를 Record로 대체하면 보일러플레이트 코드가 극적으로 줄어듭니다.
- Record는 직렬화에 특별한 지원을 받아 Jackson, Gson 등에서 자연스럽게 매핑됩니다.
- Sealed Class의
permits목록은 같은 모듈 또는 같은 패키지 내에서만 지정할 수 있음을 유의하세요.
마무리
Record와 Sealed Class의 조합은 함수형 언어에서 흔히 볼 수 있는 대수적 데이터 타입을 Java에서 구현하는 강력한 도구입니다. 불변성과 타입 안전성을 동시에 확보하며, 패턴 매칭과 결합하면 비즈니스 로직을 명확하고 간결하게 표현할 수 있습니다. 특히 DDD(Domain-Driven Design) 환경에서 Value Object와 상태 모델링에 적극 활용하기를 권장합니다.