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, "홍길동", "hong@test.com");
System.out.println(user.name()); // 홍길동
System.out.println(user.displayName()); // 홍길동 (hong@test.com)


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








항목RecordLombok @Data
불변성강제선택
의존성없음필요
상속불가가능
Java 버전14+제한 없음


주의사항



  • Record는 필드 변경 불가 (불변)

  • Record는 다른 클래스 상속 불가

  • Sealed Class는 같은 모듈/패키지에서만 서브클래스 정의 가능

  • JPA Entity로는 Record 직접 사용 불가