Contents
see List개요
Java 14의 Record와 Java 17의 Sealed Class는 불변 데이터 모델링과 타입 안전성을 획기적으로 개선한 기능입니다. 이 두 기능을 조합하면 대수적 데이터 타입(Algebraic Data Types)을 자연스럽게 표현할 수 있어, 함수형 프로그래밍 패러다임을 Java에 도입할 수 있습니다.
이 글에서는 Record와 Sealed Class를 활용한 실전 디자인 패턴과 엔터프라이즈 애플리케이션 적용 사례를 다룹니다.
Record 핵심 개념
Record는 불변 데이터 운반 객체를 간결하게 정의하는 특수한 클래스입니다. 생성자, getter, equals, hashCode, toString이 자동 생성되며, 모든 필드는 final입니다.
기본 Record 정의
// 전통적인 방식 (50줄 이상)
public final class PersonOld {
private final String name;
private final int age;
public PersonOld(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* 10줄 */ }
@Override
public int hashCode() { /* 5줄 */ }
@Override
public String toString() { /* 3줄 */ }
}
// Record 방식 (1줄!)
public record Person(String name, int age) {}
// 사용 예제
var person = new Person("김철수", 30);
System.out.println(person.name()); // getter는 필드명()으로 호출
System.out.println(person); // Person[name=김철수, age=30]
커스텀 생성자와 검증
public record Email(String address) {
// Compact Constructor: 검증 로직 추가
public Email {
if (address == null || !address.contains("@")) {
throw new IllegalArgumentException("Invalid email: " + address);
}
address = address.toLowerCase().trim(); // 정규화
}
}
public record Range(int start, int end) {
public Range {
if (start > end) {
throw new IllegalArgumentException("start must be <= end");
}
}
// 커스텀 메서드 추가 가능
public boolean contains(int value) {
return value >= start && value <= end;
}
public int length() {
return end - start;
}
}
Sealed Class 실전 패턴
Sealed Class는 상속 계층을 제한하여 타입 안전성을 보장합니다. 컴파일러가 모든 하위 타입을 알기 때문에 switch 표현식에서 exhaustiveness 검사가 가능합니다.
결제 시스템 모델링
public sealed interface Payment
permits CreditCardPayment, BankTransferPayment, CryptoPayment {
String transactionId();
double amount();
}
public record CreditCardPayment(
String transactionId,
double amount,
String cardNumber,
String cvv
) implements Payment {}
public record BankTransferPayment(
String transactionId,
double amount,
String bankCode,
String accountNumber
) implements Payment {}
public record CryptoPayment(
String transactionId,
double amount,
String walletAddress,
String blockchain
) implements Payment {}
// 완전한 패턴 매칭 (모든 케이스 처리 보장)
public class PaymentProcessor {
public String processPayment(Payment payment) {
return switch (payment) {
case CreditCardPayment cc ->
"Processing credit card ending in " +
cc.cardNumber().substring(cc.cardNumber().length() - 4);
case BankTransferPayment bt ->
"Processing bank transfer from " + bt.bankCode();
case CryptoPayment crypto ->
"Processing crypto payment on " + crypto.blockchain();
// default 절 불필요! 컴파일러가 모든 케이스 확인
};
}
}
API 응답 모델링
public sealed interface ApiResponse
permits Success, ClientError, ServerError {
// 공통 메서드
default boolean isSuccess() {
return this instanceof Success;
}
}
public record Success(
T data,
String message
) implements ApiResponse {}
public record ClientError(
int statusCode,
String errorMessage,
Map fieldErrors
) implements ApiResponse {}
public record ServerError(
String errorMessage,
String traceId
) implements ApiResponse {}
// 사용 예제
public class UserService {
public ApiResponse getUser(String userId) {
try {
User user = fetchUserFromDb(userId);
if (user == null) {
return new ClientError<>(404, "User not found", Map.of());
}
return new Success<>(user, "User retrieved successfully");
} catch (Exception e) {
return new ServerError<>(e.getMessage(), generateTraceId());
}
}
// 클라이언트 코드
public void displayUser(String userId) {
var response = getUser(userId);
switch (response) {
case Success s ->
System.out.println("User: " + s.data().name());
case ClientError ce ->
System.err.println("Client error: " + ce.errorMessage());
case ServerError se ->
System.err.println("Server error (trace: " + se.traceId() + ")");
}
}
private User fetchUserFromDb(String userId) { return null; }
private String generateTraceId() { return "trace-123"; }
}
고급 패턴: 상태 머신
public sealed interface OrderState
permits OrderCreated, OrderPaid, OrderShipped, OrderDelivered, OrderCancelled {}
public record OrderCreated(String orderId, LocalDateTime createdAt)
implements OrderState {}
public record OrderPaid(String orderId, LocalDateTime paidAt, String paymentId)
implements OrderState {}
public record OrderShipped(String orderId, LocalDateTime shippedAt, String trackingNumber)
implements OrderState {}
public record OrderDelivered(String orderId, LocalDateTime deliveredAt, String signature)
implements OrderState {}
public record OrderCancelled(String orderId, LocalDateTime cancelledAt, String reason)
implements OrderState {}
public class OrderStateMachine {
public OrderState transition(OrderState current, OrderEvent event) {
return switch (current) {
case OrderCreated created -> switch (event) {
case PaymentReceived(var paymentId) ->
new OrderPaid(created.orderId(), LocalDateTime.now(), paymentId);
case OrderCancelRequested(var reason) ->
new OrderCancelled(created.orderId(), LocalDateTime.now(), reason);
default -> throw new IllegalStateException("Invalid transition");
};
case OrderPaid paid -> switch (event) {
case ShippingStarted(var trackingNumber) ->
new OrderShipped(paid.orderId(), LocalDateTime.now(), trackingNumber);
default -> throw new IllegalStateException("Invalid transition");
};
case OrderShipped shipped -> switch (event) {
case DeliveryConfirmed(var signature) ->
new OrderDelivered(shipped.orderId(), LocalDateTime.now(), signature);
default -> throw new IllegalStateException("Invalid transition");
};
case OrderDelivered delivered -> delivered; // 최종 상태
case OrderCancelled cancelled -> cancelled; // 최종 상태
};
}
}
sealed interface OrderEvent
permits PaymentReceived, OrderCancelRequested, ShippingStarted, DeliveryConfirmed {}
record PaymentReceived(String paymentId) implements OrderEvent {}
record OrderCancelRequested(String reason) implements OrderEvent {}
record ShippingStarted(String trackingNumber) implements OrderEvent {}
record DeliveryConfirmed(String signature) implements OrderEvent {}
활용 팁
- DTO로 완벽: Record는 API 요청/응답 객체로 이상적입니다. JSON 직렬화 라이브러리(Jackson, Gson)와 호환됩니다.
- 패턴 매칭 활용: Sealed Class + switch 표현식으로 타입 안전한 분기 처리가 가능합니다.
- 불변성 보장: Record의 모든 필드는 final이므로 스레드 안전합니다.
- 성능 최적화: Record는 내부적으로 최적화되어 있어 일반 클래스보다 메모리 효율적입니다.
- 상속 제한: Record는 다른 클래스를 상속할 수 없지만 인터페이스 구현은 가능합니다.
- 검증 로직: Compact Constructor에서 필드 검증과 정규화를 수행하세요.
- Sealed 계층 설계: 가능한 모든 상태를 명시적으로 모델링하면 버그가 줄어듭니다.
마무리
Record와 Sealed Class는 Java를 더 안전하고 간결한 언어로 만들어줍니다. 불변 데이터 모델링, 도메인 주도 설계, 함수형 프로그래밍 패턴을 자연스럽게 표현할 수 있습니다.
특히 API 설계, 상태 머신, 이벤트 소싱 시스템에서 타입 안전성과 코드 가독성이 크게 향상됩니다. Kotlin의 data class와 sealed class에 익숙하다면 즉시 활용할 수 있는 기능입니다.
레거시 코드를 리팩토링할 때 DTO부터 Record로 전환하고, 복잡한 상태 분기를 Sealed Class로 재설계해보세요. 코드 품질이 눈에 띄게 개선될 것입니다.