[자바] 14. record : 보일러플레이트를 지우다 (Java 16)

자바 버전 여행 14편 - 보일러플레이트를 지우다

작성자

카테고리:

* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

(자바 버전 여행 ⑭)

값을 담기만 하는 클래스(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의 매력이 확 온다.

  1. 이름과 나이를 담는 일반 클래스 User를 getter·equals·toString까지 만든다(길다).
  2. 그걸 record User(String name, int age) { } 한 줄로 바꾼다.
  3. 두 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 생성자에 조건을 적으면 됩니다. 값 대입은 자동으로 이어집니다.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다