
SOFTMOA TECHNOLOGY
Record와 Sealed Class로 견고한 도메인 모델링
소프트모아가 정리한 기술 기록입니다.
Contents
작성일 2026. 02. 11.
개요
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와 상태 모델링에 적극 활용하기를 권장합니다.
Share this
Java
Showing 1 to 10 of 68 entries
