
Pull Request 요청을 받고 코드 리뷰를 하는데
팀원이 Controller에서 반환 타입을 ResponseEntity<ResponseDto>가 아닌 ResponseEntity<ResponseProj>로 작성해 의문이 들었다.
ResponseProj는 아래와 같았다.
public record ResponseProj(
Long id,
String name,
...
) {}
JPA projection 사용하니까 suffix를 Proj로 한 것까진 알겠는데 record는 뭐고 필드가 왜 저기 적혀있지...?
이전 회사에서도 record라는 타입을 본 적이 없었기에 궁금해서 검색해보았다.
우리는 그동안 변경 불가능한 데이터 클래스를 작성하기 위해 아래와 같은 6가지 작업을 반복해왔다.
public class Person {
// 1. private 필드
private final String name;
private final String address;
// 2. public 생성자
public Person(String name, String address) {
this.name = name;
this.address = address;
}
// 3. 각 필드에 대한 getter
public String getName() {
return name;
}
// 4. 모든 필드가 일치할 때 동일한 값을 반환하는 hashCode 메서드
@Override
public int hashCode() {
return Objects.hash(name, address);
}
// 5. 모든 필드가 일치할 때 동일한 클래스의 객체에 대해 true를 반환하는 equals 메서드
@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);
}
}
// 6. 클래스 이름과 각 필드 이름 및 해당 값을 포함하는 toString 메서드
@Override
public String toString() {
return "Person [name=" + name + ", address=" + address + "]";
}
}
이렇게 하면 불변 클래스를 만들 수는 있지만 2개의 문제점이 있다.
equals, hashCode 및 toString 메서드와 생성자 생성 등 각 데이터 클래스에 대해 지루한 동일 과정을 반복해야 한다.
IDE에서 자동 생성 기능을 제공하긴 하지만, 새 필드를 추가할 때 클래스를 자동으로 업데이트 하지는 못 해 equals와 같은 메서드를 수동으로 업데이트 해야 한다.
추가 코드들로 인해 이름과 주소 딱 2개의 String 필드를 갖는 간단한 데이터 클래스라는 점을 모호하게 한다.
이러한 문제들을 해결하기 위해서는 해당 클래스가 데이터 클래스라고 명시적으로 선언해야 한다.
위의 문제를 해결하기 위해 Java 14에서 처음 등장한 타입으로, 필드 타입과 이름만 필요한 불변 데이터 클래스다.
코드는 아래와 같이 작성한다.
public record Person (String name, String address) {}
equals, hashCode, toString 메서드와 private, final 필드, public 생성자는 Java Compiler에 의해 자동으로 생성된다.
필드 이름과 동일한 이름을 가진 getter 메서드도 사용할 수 있다.
@Test
public void givenValidNameAndAddress_whenGetNameAndAddress_thenExpectedValuesReturned() {
String name = "John Doe";
String address = "100 Linda Ln.";
Person person = new Person(name, address);
assertEquals(name, person.name());
assertEquals(address, person.address());
}
기본 public 생성자는 컴파일러가 자동으로 생성해주는데, 필요할 경우 사용자 정의 생성자를 만들 수 있다.
단, 기본 생성자와 동일한 arguments를 갖는 생성자를 만들면 컴파일 오류가 발생한다.
public record Person(String name, String address) {
// Null 체크 생성자
public Person {
Objects.requireNonNull(name);
Objects.requireNonNull(address);
}
}
public record Person(String name, String address) {
// 다른 argument를 갖는 생성자
public Person(String name) {
this(name, "Unknown");
}
}
public record Person(String name, String address) {
public Person {
Objects.requireNonNull(name);
Objects.requireNonNull(address);
}
// 컴파일 오류
public Person(String name, String address) {
this.name = name;
this.address = address;
}
}
일반 클래스와 마찬가지로 정적 변수와 메서드도 생성 가능하다.
public record Person(String name, String address) {
// 정적 변수
public static final String UNKNOWN_ADDRESS = "Unknown";
// 정적 메서드
public static Person unnamed(String address) {
return new Person("Unnamed", address);
}
}
필드가 한 번 설정되면 값을 변경할 수 없다. 이는 불변성을 보장하며, 데이터의 안정성을 높인다.
final 선언을 하지 않아도 컴파일러가 해당 필드를 불변으로 판단하고 자동 final로 처리한다.
다른 클래스를 상속받을 수도, 다른 클래스가 상속할 수도 없다.
선언되는 다른 모든 필드는 static이어야 한다.
응답 값은 불변 데이터이기에 팀원이 record를 사용했다는 것을 알게 되었다.
덕분에 새로운 타입 공부도 했고, 앞으로 불변 데이터 전달 시에는 record를 사용하는 게 더 좋겠다는 생각이 들었다.