Record는 Java 14에서 추가된 불변 데이터 클래스로, 그 자체로 '값'을 나타내는 클래스(즉, Value Object)를 좀 더 쉽게 생성 및 수정할 수 있게 해준다. Record 도입 이전의 VO를 보고 어떤 문제를 해결하기 위해 Record가 도입되었는지를 확인해보자
Record 이전의 VO
VO는 다음의 특징들을 가지고 있는 클래스를 말한다
- DTO처럼 단순한 데이터들의 집합이 아닌, 객체 그 자체가 '값'을 나타낸다
- 반드시 불변 객체(immutable Object)로 사용해야 하기에 한번 생성한 후 수정할 수 없고, 항상 새로운 객체를 만들어야 한다
- 비즈니스 로직을 포함할 수 있다 (Setter와 같이 값을 수정할 수 있는 로직은 제외)
VO 예시를 보자
@Getter
public class Person {
// 필드 선언
private final String name;
private final String address;
// 생성자
public Person(String name, String address) {
this.name = name;
this.address = address;
}
// hashCode와 equals
@Override
public int hashCode() {
return Objects.hash(name, address);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (!(obj instanceof Person)) {
return false;
} else {
Person other = (Person) obj;
return Objects.equals(name, other.name)
&& Objects.equals(address, other.address);
}
}
// toString
@Override
public String toString() {
return "Person [name=" + name + ", address=" + address + "]";
}
}
먼저 불변을 위해 final로 필드가 선언되어있고, 객체 비교를 위해 hashCode와 equals가 오버라이딩 되어있다. 언뜻 보면 간단하고 필요한것만 들어가있는 것 같지만 다음과 같은 문제가 있다
- boilerplate code가 많다
- 새로운 VO 클래스를 만들때마다 constructor, hashCode, equals, 그리고 toString 등의 보일러 플레이트 코드를 똑같이 매번 작성해야하는 수고로움이 있다.
- 또한 새로운 필드를 추가하거나 기존의 필드를 삭제한다면, 이에 맞춰서 코드를 업데이트 해주어야 한다
- '값' 그 자체를 나타낸다는 VO의 목적이 모호해진다
- 필드가 아닌, 추가적인 코드들에 의해서 단순히 '값'만을 표현한다는 VO라는 사실이 모호해진다
Record를 사용한 VO
그렇다면 위와 같은 문제점을 어떻게 Record를 통해서 해결할 수 있는지를 알아보자
Record
- 생성
- Record Keyword(필드)를 파라미터로 넘기기만 하면 된다
- equals(), hashCode(), toString() 메서드 및 private, final field, public constructor는 Java 컴파일러에 의해 자동으로 생성된다
- Constructor
- 모든 필드가 포함된 public AllArgs Constructor가 생성된다
- Getter
- 필드이름과 동일한 getter 메서드가 생성된다
- ex) record.필드명()
- 필드이름과 동일한 getter 메서드가 생성된다
-
- 'get필드명' 으로 getter를 사용하고 싶다면, record keyword 앞에 lombok의 @Getter 어노테이션을 붙여주면 된다
- ex) public record RecordClass(@Getter String field) {}
- 사용 -> getField();
- 'get필드명' 으로 getter를 사용하고 싶다면, record keyword 앞에 lombok의 @Getter 어노테이션을 붙여주면 된다
- equals(), hashCode()
- equals()는, 두 객체의 유형이 동일하고 모든 필드값이 일치하는 경우 true를 반환하도록 override 되어있음
- hashCode()는, 두 객체의 필드값이 모두 일치하는 경우 동일한 값을 반환하도록 override 되어있음
- toString()
- Record 이름 + 각 필드의 name과 value가 "[]" 안에 포함된 문자열을 반환
- ex) Record이름[필드name1=필드value1, 필드name2=필드value2, ...]
// 생성
public record RecordTest(String name, int value, @Getter String address) {}
// 생성자로 생성
RecordTest recordTest = new RecordTest("kdh", 28, "Busan");
// Getter
recordTest.name(); // 결과: kdh
recordTest.value(); // 결과: 28
recordTest.address(); // 결과: Busan
recordTest.getAddress(); // 결과: Busan
// toString()
System.out.println(recordTest); // 결과: RecordTest[name=kdh, value=28, address=Busan]
// equals
recordTest.equals(new RecordTest("kdh", 28, "Busan")); // 결과: true
// hashCode
recordTest.hashCode() == new RecordTest("kdh", 28, "Busan").hashCode(); // 결과: true
위와같이 개발자가 Object에 필요한 메서드를 일일이 오버라이딩 할 필요가 없고, 레코드의 필드 값만 명시함으로써 boilerplate code를 줄이고 클래스의 목적을 명확하게 나타낼 수 있다
Static Variables & Method
- 정적 변수와 메서드는, 일반적인 클래스에서 선언하는 것과 동일한 방식으로 선언한다
- 선언 이후에는 다른 필드와 동일한 방식으로 참조할 수 있다
// 정적 변수 & 메서드 선언
public record RecordTest(String name, int value) {
public static String staticName = "staticName";
public static void staticMethod(String name, int value) {
System.out.println("Name field is :" + name);
System.out.println("Value field is :" + value);
}
}
// 정적 변수 & 메서드 사용
System.out.println("staticName = " + RecordTest.staticName); // 결과: staticName
RecordTest.staticMethod("kdh", 28); // 결과: Name field is :kdh, Value field is :28
제약사항
- Record는 다른 클래스를 상속받을 수 없다
- private final 이외의 인스턴스 필드를 선언할 수 없다
- 직접 선언하는 다른 모든 필드는 static이어야 한다
- Record는 암시적으로 final이며, abstract 일 수 없다
위의 제약사항을 통해 레코드의 API가 상태 설명에 의해서만 정의되며, 나중에 다른 클래스나 레코드에 의해 변경될 수 없음을 강조한다
Reference
- Java14 Record Keyword: https://www.baeldung.com/java-record-keyword
- Java14 레코드(Record)를 알아보자: https://colevelup.tistory.com/28