Contents
see List작성일 2025. 11. 30.
Java Record와 Sealed Class 활용
Java 14에서 도입된 Record와 Java 15에서 도입된 Sealed Class는 불변 데이터 모델과 제한된 상속 구조를 간결하게 표현합니다. DTO, Value Object 작성 시 보일러플레이트 코드를 크게 줄여줍니다.
언제 사용하나요?
- DTO(Data Transfer Object) 정의
- 불변 값 객체 생성
- API 응답/요청 모델
- 상속 계층 제한이 필요한 도메인 모델
Record 기본 사용
// 기존 방식 - 많은 보일러플레이트 코드
public class User {
private final Long id;
private final String name;
private final String email;
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public Long getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
@Override
public String toString() { ... }
}
// Record 사용 - 한 줄로 끝!
public record User(Long id, String name, String email) {}Record 특징
public record User(Long id, String name, String email) {
// 자동 생성되는 것들:
// - private final 필드
// - 생성자
// - getter (id(), name(), email() - get 접두사 없음)
// - equals(), hashCode(), toString()
// 추가 메서드 정의 가능
public String displayName() {
return name + " (" + email + ")";
}
// 정적 팩토리 메서드
public static User of(String name, String email) {
return new User(null, name, email);
}
}
// 사용
User user = new User(1L, "홍길동", "[email protected]");
System.out.println(user.name()); // 홍길동
System.out.println(user.displayName()); // 홍길동 ([email protected])Record 유효성 검사
public record User(Long id, String name, String email) {
// Compact Constructor - 유효성 검사
public User {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("이름은 필수입니다");
}
if (email != null && !email.contains("@")) {
throw new IllegalArgumentException("유효한 이메일이 아닙니다");
}
// 값 변환도 가능
name = name.trim();
}
}
// 또는 명시적 생성자
public record User(Long id, String name, String email) {
public User(Long id, String name, String email) {
this.id = id;
this.name = Objects.requireNonNull(name, "이름 필수");
this.email = email;
}
}Record + JPA
// Record는 Entity로 직접 사용 불가 (불변이므로)
// DTO로 활용
public record UserDto(Long id, String name, String email) {
public static UserDto from(User entity) {
return new UserDto(
entity.getId(),
entity.getName(),
entity.getEmail()
);
}
public User toEntity() {
return new User(id, name, email);
}
}
// JPQL에서 직접 매핑
@Query("SELECT new com.example.UserDto(u.id, u.name, u.email) FROM User u")
List<UserDto> findAllAsDto();Sealed Class 기본
// 허용된 서브클래스만 상속 가능
public sealed class Shape
permits Circle, Rectangle, Triangle {
public abstract double area();
}
// 서브클래스 정의
public final class Circle extends Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public final class Rectangle extends Shape {
private final double width, height;
@Override
public double area() {
return width * height;
}
}
// non-sealed로 추가 상속 허용
public non-sealed class Triangle extends Shape {
// 다른 클래스가 Triangle 상속 가능
}Sealed + Pattern Matching
// Java 21+ switch 패턴 매칭
public double calculateArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> t.base() * t.height() / 2;
// 모든 케이스 커버됨 - default 불필요!
};
}
// Sealed + Record 조합
public sealed interface Result<T> {
record Success<T>(T value) implements Result<T> {}
record Failure<T>(String error) implements Result<T> {}
}
public void handleResult(Result<User> result) {
switch (result) {
case Result.Success<User> s -> processUser(s.value());
case Result.Failure<User> f -> handleError(f.error());
}
}Record vs Lombok
| 항목 | Record | Lombok @Data |
|---|---|---|
| 불변성 | 강제 | 선택 |
| 의존성 | 없음 | 필요 |
| 상속 | 불가 | 가능 |
| Java 버전 | 14+ | 제한 없음 |
주의사항
- Record는 필드 변경 불가 (불변)
- Record는 다른 클래스 상속 불가
- Sealed Class는 같은 모듈/패키지에서만 서브클래스 정의 가능
- JPA Entity로는 Record 직접 사용 불가