보일러플레이트를 없앤 Java의 세 가지 타입 — record, sealed, enum
Point 클래스를 만든다고 하자. x, y 필드, 생성자, getter, equals, hashCode, toString — Java에서 불변 데이터 객체 하나를 제대로 만들려면 50줄이 필요했다. Lombok @Value로 줄여봤지만, 그것은 서드파티 라이브러리에 대한 의존이었다.
Java 16에 record가 들어왔다. public record Point(double x, double y) {} — 한 줄. 컴파일러가 나머지를 다 만들어 준다. (JEP-395) 왜 Java는 30년이나 지나서야 이 한 줄을 가능하게 했을까? 이 글은 record, sealed, enum 세 가지 현대 타입이 기존 코드를 어떻게 바꾸는지 풀어간다.
record — 불변 데이터 객체의 한 줄 선언
record는 불변 데이터를 간결하게 표현하기 위한 Java 16+(정식) 타입이다. (JEP-395) (JLS §8.10)
// Java 25 — record 선언
public record Point(double x, double y) {}
이 한 줄이 컴파일러에 의해 다음으로 확장된다:
// Java 25 — 컴파일러가 생성하는 코드 (개념적)
public final class Point extends Record {
private final double x;
private final double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double x() { return x; } // accessor (getX() 아님!)
public double y() { return y; }
@Override
public boolean equals(Object o) { ... } // 모든 필드 비교
@Override
public int hashCode() { ... } // 모든 필드 기반
@Override
public String toString() { ... } // "Point[x=1.0, y=2.0]"
}
자동 생성되는 것:
- 모든 필드를 받는 canonical constructor (표준 생성자)
- 각 필드의 accessor —
x(),y()(JavaBeans 스타일getX()가 아님) - 모든 필드를 기반으로 한
equals,hashCode,toString
자동 생성되지 않는 것:
Comparable구현 (필요하면 직접)Serializable구현 (필요하면 직접)- 유효성 검증 (직접 추가)
compact constructor — 검증 추가
record의 생성자에 검증 로직을 넣으려면 compact constructor 형식을 쓴다:
// Java 25 — compact constructor with validation
public record EmailAddress(String localPart, String domain) {
public EmailAddress { // 매개변수 목록 없음 (compact)
if (localPart == null || domain == null) {
throw new NullPointerException("null 불가");
}
if (localPart.isEmpty() || domain.isEmpty()) {
throw new IllegalArgumentException("빈 문자열 불가");
}
// this.localPart = localPart; 는 컴파일러가 마지막에 자동 삽입
}
}
compact constructor에서는 매개변수(이름만)를 명시하지 않는다 — 컴파일러가 자동으로 할당(this.x = x)한다. 개발자는 검증만 작성하면 된다.
record의 제약
final— 상속 불가. record는 암묵적으로final.- 필드는 암묵적으로
private final— 불변. - 추가 인스턴스 필드 불가 — 헤더에 선언된 필드만 가짐.
extends불가 —java.lang.Record를 암묵적으로 상속.
record에 추가 메서드를 정의할 수 있다 — 필드가 아니라 메서드는 자유롭게 추가 가능. 단, 이 메서드는
equals/hashCode계산에 포함되지 않는다.
// Java 25 — record에 메서드 추가
public record Money(long amount, String currency) {
public Money plus(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("통화 불일치");
}
return new Money(amount + other.amount, currency);
}
// static factory
public static Money usd(long amount) {
return new Money(amount, "USD");
}
}
sealed — 닫힌 상속 계층 (Java 17+, JEP-409)
sealed는 상속을 명시적으로 허용된 타입으로만 제한한다. (JEP-409) (JLS §8.1.2)
// Java 25
public sealed abstract class Shape
permits Circle, Rectangle, Triangle {}
public final class Circle extends Shape { ... }
public final class Rectangle extends Shape { ... }
public final class Triangle extends Shape { ... }
// class Hexagon extends Shape { ... } // 컴파일 에러: permits에 없음
sealed의 세 가지 하위 타입 선언 옵션:
| 키워드 | 의미 |
|---|---|
final |
더 이상 상속 불가 |
sealed |
또 다른 sealed 계층 (permits 명시) |
non-sealed |
열림 — 이 트리 아래는 누구나 상속 가능 |
// Java 25 — sealed 계층의 혼합
sealed abstract class Event permits LoginEvent, LogoutEvent, SystemEvent {}
final class LoginEvent extends Event { ... }
final class LogoutEvent extends Event { ... }
non-sealed class SystemEvent extends Event { } // SystemEvent는 열림
class DiskFullEvent extends SystemEvent { } // OK: SystemEvent가 non-sealed
sealed의 가치는 switch expression의 exhaustiveness 검사와 결합될 때 발휘된다. 모든 하위 타입을 알고 있으므로,
default없이도 컴파일러가 완전성을 검증한다. 새 하위 타입을 추가하면 관련 switch가 컴파일 에러로 알려준다.
enum — 타입 안전 열거 (Java 5+)
enum은 미리 정의된 상수 인스턴스의 집합이다. (JLS §8.9) C/C++의 단순한 정수 매크로가 아니라, 각 상수가 싱글톤 객체다.
// Java 25 — 기본 enum
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
Day today = Day.WEDNESDAY;
System.out.println(today.name()); // WEDNESDAY
System.out.println(today.ordinal()); // 2 (선언 순서, 0부터)
System.out.println(Day.valueOf("FRIDAY")); // Day.FRIDAY
System.out.println(Arrays.toString(Day.values())); // 모든 상수
필드와 메서드를 가진 enum
// Java 25 — enum with fields and methods
public enum HttpStatus {
OK(200, "성공"),
NOT_FOUND(404, "찾을 수 없음"),
INTERNAL_ERROR(500, "서버 오류");
private final int code;
private final String message;
// 생성자 (private이 유일 — 외부에서 인스턴스화 불가)
HttpStatus(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() { return code; }
public String getMessage() { return message; }
public boolean isError() {
return code >= 400;
}
}
// Java 25
HttpStatus status = HttpStatus.NOT_FOUND;
System.out.println(status.getCode()); // 404
System.out.println(status.isError()); // true
enum에 추상 메서드 — 상수별 구현
// Java 25 — 상수마다 다른 동작
public enum Operation {
PLUS {
@Override public double apply(double a, double b) { return a + b; }
},
MINUS {
@Override public double apply(double a, double b) { return a - b; }
},
TIMES {
@Override public double apply(double a, double b) { return a * b; }
},
DIVIDE {
@Override public double apply(double a, double b) { return a / b; }
};
public abstract double apply(double a, double b);
}
// Java 25
double result = Operation.PLUS.apply(3, 4); // 7.0
enum의 상수별 메서드 구현은 각 상수가 익명 하위 클래스의 인스턴스가 되는 것으로 구현된다.
Operation.PLUS.getClass()는Operation$1같은 이름을 반환한다. (JLS §8.9.1)
enum과 switch
// Java 25 — enum switch expression
String type = switch (today) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "평일";
case SATURDAY, SUNDAY -> "주말";
// default 불필요 — enum의 모든 값을 처리했으므로
};
enum은 컴파일 타임에 모든 상수를 알 수 있으므로, switch expression에서 default 없이도 exhaustiveness가 보장된다.
단, enum 상수가 나중에 추가되면 컴파일 에러가 아니라 런타임 에러(
IncompatibleClassChangeError또는 default로 빠짐)가 발생할 수 있다. enum 정의와 switch가 같은 컴파일 단위에 있으면 컴파일 에러로 잡히지만, 다른 모듈이면 런타임에 발견된다.
EnumSet과 EnumMap — enum에 최적화된 컬렉션
enum 상수를 모을 때 HashSet<Day> 대신 EnumSet<Day>를 쓰면 성능이 극적으로 좋아진다. (EnumSet API)
// Java 25 — EnumSet (비트 벡터 기반, 극히 빠름)
import java.util.EnumSet;
EnumSet<Day> weekdays = EnumSet.of(
Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY, Day.THURSDAY, Day.FRIDAY
);
EnumSet<Day> weekend = EnumSet.complementOf(weekdays); // 나머지 상수
System.out.println(weekend); // [SATURDAY, SUNDAY]
EnumSet은 내부적으로 비트 벡터(bit vector)로 구현된다 — enum 상수의 ordinal()을 비트 위치로 사용한다. 집합 연산(합집합, 교집합, 차집합)이 비트 연산으로 수행되어 HashSet보다 수십 배 빠르다.
// Java 25 — EnumMap (배열 기반, enum 키 전용)
import java.util.EnumMap;
EnumMap<Day, String> schedule = new EnumMap<>(Day.class);
schedule.put(Day.MONDAY, "스터디");
schedule.put(Day.FRIDAY, "배포");
EnumMap은 키의 ordinal()을 배열 인덱스로 사용한다 — 해시 계산이나 충돌 해결이 필요 없다. HashMap<Day, String>보다 빠르고 메모리도 적게 쓴다.
enum을 키나 요소로 사용하는 컬렉션에서는 항상
EnumSet/EnumMap을 우선한다. 성능 차이가 유의미하며, API도 더 간결하다(EnumSet.range(),EnumSet.complementOf()등).
record + sealed + switch — 현대 Java의 타입 설계
세 가지를 조합하면 ADT(Algebraic Data Type)에 가까운 설계가 가능하다:
// Java 25 — ADT 스타일 타입 설계
sealed interface Result<T> permits Result.Success, Result.Failure {}
record Success<T>(T value) implements Result<T> {}
record Failure<T>(String error) implements Result<T> {}
// 패턴 매칭으로 안전하게 분기
static String describe(Result<?> result) {
return switch (result) {
case Success<?> s -> "성공: " + s.value();
case Failure<?> f -> "실패: " + f.error();
// default 불필요 — sealed로 모든 하위 타입을 처리했음이 보장됨
};
}
이 패턴이 Scala, Rust, Haskell의 ADT와 본질적으로 같다 — Java가 함수형 언어의 타입 설계를 수용한 결과다.
요약 — 이 글의 결론
- record는 불변 데이터 객체의 보일러플레이트를 제거한다. 생성자, accessor, equals, hashCode, toString을 자동 생성. compact constructor로 검증 추가 가능. 단,
final이고 추가 필드를 가질 수 없다. - sealed는 상속 계층을 닫는다.
permits로 허용된 타입만 하위 타입 가능. switch expression의 exhaustiveness 검사와 결합하여 "새 타입 추가 → 컴파일 에러" 패턴을 만든다. - enum은 타입 안전 상수다. 단순 정수가 아니라 싱글톤 객체. 필드, 메서드, 상수별 구현을 가질 수 있다.
equals/hashCode가 자동으로 올바르다(참조 동일성 = 값 동일성). - record + sealed + switch pattern matching = ADT. 현대 Java에서 함수형 언어 스타일의 타입 안전한 설계가 가능하다. 이것이 이후 장(람다, Stream, 동시성)의 타입적 기반이다.
생각해 볼 문제
- record의 accessor가
getX()가 아니라x()인 이유는 무엇인가? JavaBeans 규약을 따르지 않으면 어떤 도구가 호환되지 않는가? record Money(long amount, String currency)에서equals는 어떤 필드를 기반으로 동작하는가?amount만으로 equals를 정의하고 싶다면 어떻게 해야 하는가?- enum 상수에
==를 쓰는 것이 안전한 이유를 설명하라.equals()와의 차이는? sealed interface를 쓰지 않고 일반interface에서 switch expression을 쓸 때default가 필요한 이유는 무엇인가?record에 mutable 상태를 추가하는 것이 가능한가? (예:AtomicInteger필드) 이것이 record의 "불변" 약속과 충돌하는가?
참고
- JLS §8.10 - Record Classes - 접근 2026-07-10
- JLS §8.9 - Enum Types - 접근 2026-07-10
- JLS §8.1.2 - sealed Classes - 접근 2026-07-10
- JEP-395: Records - 접근 2026-07-10
- JEP-409: Sealed Classes - 접근 2026-07-10
'Develop Artifacts > Java' 카테고리의 다른 글
| Java - 10. collections (0) | 2026.07.12 |
|---|---|
| Java - 09. exception (0) | 2026.07.12 |
| Java - 07. interface and abstract (0) | 2026.07.12 |
| Java - 06. inheritance and polymorphism (0) | 2026.07.12 |
| Java - 05. class and object (0) | 2026.07.12 |
