데이터를 담는 것이 목적인 클래스의 보일러플레이트를 없앤 타입(Java 16 정식).
record Point(int x, int y) { }
// 자동으로 생기는 것
// · private final 필드 x, y
// · 표준 생성자
// · 접근자 x() · y() (getX() 가 아니다)
// · equals · hashCode · toString
검증은 컴팩트 생성자에서
record Point(int x, int y) {
Point { // 파라미터 목록도 대입도 없다
if (x < 0 || y < 0) throw new IllegalArgumentException();
x = Math.min(x, 100); // 값을 보정할 수도 있다
}
}
얕은 불변이다
record Order(Long id, List<String> items) { }
var list = new ArrayList<String>();
var o = new Order(1L, list);
list.add("변경"); // o.items() 가 바뀐다
// 방어적 복사를 직접 해야 한다
record Order(Long id, List<String> items) {
Order { items = List.copyOf(items); }
}
sealed — 닫힌 계층
sealed interface Result permits Success, Failure { }
record Success(String data) implements Result { }
record Failure(String reason) implements Result { }
"이 인터페이스를 구현할 수 있는 건 이 둘뿐" 을 컴파일러에게 알려 준다
- switch 에서 default 없이 망라 검사가 가능해진다
- 새 구현을 추가하면 모든 switch 가 컴파일 에러로 알려 준다 ← 결정적 이득
패턴 매칭 switch
String describe(Result r) {
return switch (r) {
case Success(String data) -> "성공: " + data; // 레코드 분해(deconstruction)
case Failure(String reason) -> "실패: " + reason;
// default 가 필요 없다 — sealed 라 컴파일러가 전부임을 안다
};
}
// instanceof 패턴 — 캐스팅이 사라진다
if (obj instanceof Member m && m.age() > 20) { ... } // m 을 바로 쓴다
// 조건을 덧붙이는 when 절
case Success(String d) when d.isEmpty() -> "빈 성공";
왜 강력한가 — 대수적 데이터 타입
-
sealed(선택지가 닫혀 있다) + record(각 선택지의 데이터)
- = 상태를 타입으로 표현한다
-
예전: — status 필드 + if-else 체인 + "이 상태에서 이 필드는 null" 이라는 암묵적 규칙
-
지금: — 각 상태가 자기 데이터를 갖는 별도 타입 → 컴파일러가 누락을 잡아 준다
record가 안 맞는 곳
-
JPA 엔티티 — 기본 생성자와 가변 필드가 필요하다 (프록시·변경 감지)
-
상속이 필요한 경우 — record 는 final 이고 확장할 수 없다
-
필드가 많고 일부만 채우는 경우 — 빌더가 더 낫다
-
잘 맞는 곳: DTO · 응답 모델 · 값 객체 · 다중 반환 · 맵의 복합 키
면접 함정
- ❌ "record는 불변이다" → 얕은 불변이다. 가변 필드를 담으면 깨진다.
- ❌ "record의 게터는 getX()" → 필드명 그대로
x()다.