record로 값 모델링하기
좌표, 요청, 결과처럼 구성 값이 곧 정체성인 타입은 필드와 생성자, 접근자, 동등성 코드가 반복됩니다.
record는 컴포넌트를 선언하면 이 값 중심 계약을 간결하게 만들어 줍니다.
record는 암묵적으로 final이며 다른 클래스를 상속할 수 없지만 인터페이스는 구현할 수 있습니다.
정규 생성자나 compact 생성자에서 불변식을 검사할 수 있습니다.
이 절에서는 record가 적합한 값과 일반 클래스가 필요한 객체를 나눕니다.
여기서 불변이라는 말은 record가 가리키는 컴포넌트 참조를 다시 대입할 수 없다는 뜻입니다. 컴포넌트 자체가 가변 목록이나 배열이면 내부 내용까지 자동으로 얼어붙지는 않습니다. 값 객체의 경계를 설계할 때는 이 얕은 불변성을 먼저 구분해야 합니다.
컴포넌트가 상태와 공개 접근자를 정의한다
record Point(int x, int y)는 final 필드와 x(), y() 접근자, 생성자를 제공합니다.
JavaBean getter 이름과 다르다는 점을 기억합니다.
값 기반 equals와 hashCode가 함께 생성된다
같은 컴포넌트 값을 가진 두 record는 동등하게 비교됩니다.
컬렉션 키나 메시지 값으로 쓰기 좋습니다.
예제는 record가 자동으로 제공하는 값 동등성과, 개발자가 생성자에 직접 넣어야 하는 불변식 검사를 한 파일에서 비교합니다.
import java.util.List;
public class RecordValues {
record Range(int start, int end) {
Range {
if (start > end) throw new IllegalArgumentException("start > end");
}
int length() { return end - start; }
}
record Tags(List<String> values) {
Tags { values = List.copyOf(values); }
}
public static void main(String[] args) {
Range first = new Range(2, 7);
Range second = new Range(2, 7);
System.out.println(first.length());
System.out.println(first.equals(second));
System.out.println(new Tags(List.of("java", "jdk")));
}
}5는 Range가 정의한 도메인 계산이고 true는 두 컴포넌트로 생성된 값 동등성입니다. new Range(7, 2)를 추가하면 객체가 만들어진 뒤가 아니라 compact 생성자 안에서 즉시 실패합니다.
5
true
Tags[values=[java, jdk]]Range의 구성 값이 같아 equals가 true이고 compact 생성자가 잘못된 구간을 막습니다.
생성 순서를 하나로 묶어 보면 record의 자동 영역과 직접 설계할 영역이 갈립니다. 호출자가 컴포넌트 값을 넘기면 compact 생성자가 먼저 불변식을 확인하고, 통과한 값으로 필드와 값 기반 메서드의 계약이 완성됩니다. 컴파일러가 코드를 만들어 주더라도 어떤 값이 유효한지는 타입 작성자가 정해야 합니다.
compact 생성자에서 값을 검증한다
매개변수 목록 없이 생성자 본문을 선언하고 컴포넌트에 대입되기 전에 정규화하거나 거부할 수 있습니다.
필드 대입은 컴파일러가 마무리합니다.
다음 세 조각은 record가 줄여 주는 코드와 여전히 남아 있는 타입 제약을 나눠 보여 줍니다.
record Point(int x, int y) implements java.io.Serializable { }Point point = new Point(3, 4);
System.out.println(point.x()); // 컴포넌트 접근자는 x()// point.x = 5; // 컴파일 오류: 컴포넌트 필드는 final/private
// class Child extends Point { } // 컴파일 오류: record는 final정체성과 수명 주기가 중요하면 클래스를 쓴다
값이 같아도 서로 다른 계정이나 변경 가능한 생명주기를 가진 엔티티는 일반 클래스가 더 자연스럽습니다.
짧다는 이유만으로 모든 타입을 record로 바꾸지 않습니다.
가변 구성 요소는 여전히 경계 밖에서 바뀔 수 있다
record 컴포넌트가 List나 배열이면 참조만 final입니다.
값 객체로 쓰려면 정규 생성자에서 방어적 복사를 적용합니다.
ArrayList를 만든 뒤 new Tags(source)에 전달하고 원본 목록에 값을 추가해 보세요. 현재 예제의 List.copyOf가 있으면 Tags의 출력은 변하지 않습니다. 복사를 제거했을 때만 함께 바뀐다면, record 문법과 방어적 복사가 서로 다른 책임이라는 사실을 직접 확인한 셈입니다.
record는 불변 값 전달에 강하며 구성 값이 객체의 정체성을 완전히 설명할 때 가장 잘 맞습니다.