* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
(자바 버전 여행 ⑭)
값을 담기만 하는 클래스(DTO, VO)를 하나 만들 때마다 우리는 같은 것을 반복해 왔다. 필드, 생성자, getter, equals, hashCode, toString. IDE가 만들어 준다지만 코드에는 그대로 남아 화면을 채운다. 자바 16의 record는 이걸 한 줄로 끝낸다.
Before — 40줄짜리 데이터 클래스
public final class Point {
private final int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
@Override public boolean equals(Object o) { /* ... */ }
@Override public int hashCode() { /* ... */ }
@Override public String toString() { /* ... */ }
}
After — 한 줄
record는 불변 데이터를 담는 특별한 클래스다. 괄호 안에 담을 값(컴포넌트)만 적으면, 나머지 여섯 가지를 컴파일러가 만들어 준다.
public record Point(int x, int y) { }
var p = new Point(3, 4);
p.x(); // 3 (getX가 아니라 x())
p.toString(); // Point[x=3, y=4]
p.equals(new Point(3, 4)); // true (값으로 비교)
검증은 compact 생성자로
값이 들어올 때 검사하고 싶으면, 매개변수 없는 compact 생성자에 조건만 적으면 된다. 대입은 자동으로 이어진다.
public record Point(int x, int y) {
public Point { // compact 생성자
if (x < 0 || y < 0)
throw new IllegalArgumentException("음수 좌표 불가");
}
}
주의할 점. record는 불변이라 필드를 나중에 못 바꾸고, 다른 클래스를 extends할 수 없다(상속 대신 조합). 값 객체에는 딱이지만, 상태가 바뀌는 엔티티에는 맞지 않는다.
직접 해보기 — 클래스를 record로 줄이기
직접 줄여 보면 record의 매력이 확 온다.
- 이름과 나이를 담는 일반 클래스
User를 getter·equals·toString까지 만든다(길다). - 그걸
record User(String name, int age) { }한 줄로 바꾼다. - 두
User를 같은 값으로 만들어equals와toString이 알아서 동작하는지 확인한다.
record User(String name, int age) {
public User {
if (age < 0) throw new IllegalArgumentException("나이 음수");
}
}
var a = new User("cozmo", 3);
var b = new User("cozmo", 3);
System.out.println(a); // User[name=cozmo, age=3]
System.out.println(a.equals(b)); // true
System.out.println(a.name()); // cozmo
실행해 보면 이렇게 나온다.
User[name=cozmo, age=3]
true
cozmo
여기서 조금씩 바꿔 보자.
new User("x", -1)로 compact 생성자의 검증이 도는지 본다.- record 안에
static팩토리 메서드나 인스턴스 메서드를 추가할 수 있는지 시도해 본다(된다).
한 걸음 더 — record를 Map의 키로 써 보자. equals·hashCode가 자동이라 그냥 동작한다.
✅ 확인 — record가 무엇을 자동 생성하는지, 왜 불변인지, 언제 쓰면 안 되는지 설명할 수 있으면 통과다.
record가 자동으로 주는 것 / 안 주는 것
record는 반복 코드를 대신 만들어주지만, 그 대가로 몇 가지 제약이 따라온다. 이 둘을 알아야 ‘언제 record를 쓸지’가 보인다.
| 자동으로 주는 것 | 제약 |
|---|---|
| 표준 생성자·접근자·equals·hashCode·toString | 필드가 전부 final — 불변 |
| 접근자는 x() 형태(getX 아님) | 암묵적 final — 상속 못 함 |
| 모든 필드 기준의 값 동등성 | 인터페이스 구현은 가능 |
compact 생성자로 검증·정규화
검증이나 정규화는 compact 생성자에서 한다. 파라미터 목록 없이 이름만 쓰고, 필드 대입은 자동이라 생략한다. 값을 다듬어 저장하고 싶으면 여기서 재대입한다.
record Point(int x, int y) {
Point { // compact 생성자 (파라미터 목록 없음)
if (x < 0 || y < 0)
throw new IllegalArgumentException("음수 불가");
// 필드 대입은 자동 — 여기서 정규화만 하면 됨
}
}
언제 record, 언제 일반 클래스
record는 ‘불변 데이터 운반‘에 최적이다 — DTO·VO·좌표·API 응답 객체·패턴 매칭 대상. 반대로 가변 상태를 갖거나, 상속 계층이 필요하거나, 필드를 감추고 로직이 중심이면 일반 클래스가 맞다. 판단 기준 하나: ‘equals가 모든 필드 기준이어도 되나?‘ — 그래도 되면 record가 어울린다.
record + sealed + 패턴의 조합
record는 혼자보다 sealed·패턴 매칭과 함께일 때 강하다. sealed 인터페이스를 여러 record가 구현하면, switch 패턴으로 타입별 분해가 깔끔해진다. 자바 21이 미는 데이터 중심 스타일의 기본 재료가 record다. 보일러플레이트를 지우는 것에서 시작해, 언어의 새 문법을 여는 열쇠가 된다.
데이터를 record로 깔끔하게 담게 됐다. 다음 편은 그 데이터를 타입으로 분기하는 패턴 매칭과 sealed 클래스다. record와 만나면 진가가 나온다.
자주 묻는 질문 (FAQ)
record와 일반 클래스는 뭐가 다른가요?
record는 불변 데이터 전용으로 생성자·접근자·equals·hashCode·toString을 자동 생성합니다. 대신 상속은 못 합니다.
record에 검증 로직을 넣을 수 있나요?
네, compact 생성자에 조건을 적으면 됩니다. 값 대입은 자동으로 이어집니다.

답글 남기기