자바의 신문물 Record
개요
어느샌가부터 자바에는 Record라는 신기한 타입이 생겼다. 처음에는 무슨 음반(?) 이라고 생각했으나, 다시 생각해보니 Row
의 의미를 가진 타입이라고 생각했다.
컴퓨터 과학에서 레코드(record, struct)는 기본적인 자료 구조이다. 데이터베이스나 스프레드시트)의 레코드는 보통 로우(row)라고 부른다. 레코드는 각기 다른 자료형에 속할 수 있는 필드의 모임이며, 보통 고정 숫자나 시퀀스로 이루어져 있다. 레코드의 필드들은 특히 객체 지향 프로그래밍에서 멤버(member)로도 부른다.
위키백과 레코드
예를 들면, 파이썬에 있는 tuple
과 유사한 자료구조이다. 이름은 record
이지만, 자바이기 때문에 그 기반은 class
이다. record
는 자바 14에서 처음 베타 형태로 공개되었고, 자바 16에서 정식 스펙이 되었다.
어디에 쓰는 물건인고?
개발하다보면 수 많은 DTO와 VO를 만든다. 이 과정에서, 정말 많은 보일러플레이트 코드를 작성하기도 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Person {
private final String name;
private final String address;
private final int age;
public Person(String name, String address, int age) {
this.name = name;
this.address = address;
this.age = age;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
public int getAge() {
return age;
}
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
Person person = (Person) object;
return age == person.age && Objects.equals(name, person.name) && Objects.equals(address, person.address);
}
@Override
public int hashCode() {
return Objects.hash(name, address, age);
}
}
불변의 Person
객체를 만들려면 기본적으로 이만큼의 코드가 필요하다. 물론 Lombok
라이브러리에 의존하면 다음과 같이 줄일 수 있다.
1
2
3
4
5
6
7
8
9
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Person {
private final String name;
private final String address;
private final int age;
}
Lombok
에 의존하여 획기적으로 줄였다. Lombok
에 의존하여 말이다. Lombok
라이브러리에 의존해서 코드를 짜는 것이 너무 당연한 것이 되어버렸고, 코틀린에서는 Data class를 통해 이러한 복잡성, 의존성을 없애버렸다.
코틀린 문법을 구경할 때 부러웠던 것이 Data class였는데, 자바에도 상응하는 문법이 나왔다.
위 Person
을 record
로 똑같은 기능을 하도록 만들면 어떻게 되는 지 코드로 알아보자.
1
public record Person(String name, String address, int age) {}
끝이다.
사용
1
2
public record Person(String name, String address, int age) {
}
코드에는 아무것도 없어보이지만, Lombok
의 Getter
, RequiredArgsConstructor
, EqualsAndHashCode
, ToString
에 해당하는 코드들이 모두 자동으로 작성된다. 즉, 우리 눈에는 보이지 않지만 컴파일 시에 바이트 코드로 만들어준다.
Getter
1
2
3
4
5
6
7
8
9
10
Person person = new Person("leoh", "Seoul", 20);
System.out.println("name = " + person.name());
System.out.println("address = " + person.address());
System.out.println("age = " + person.age());
### 결과
name = leoh
address = Seoul
age = 20
우리는 보통 Getter
를 생성할 떄에는 get변수명
이라는 메서드명을 컨벤션으로 사용한다. record
는 변수명 그 자체를 getter
메서드명으로 사용한다. record
는 불변이고, Setter
는 있을 수 없기 때문에 그렇지 않을까 예상해본다.
Equals, HashCode
효과적인 불변 객체의 비교를 위해, equals
와 hashcode
를 오버라이드 해줘야 한다. record
를 사용하면 이 역시 컴파일할 때 자동으로 생성해준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Person person1 = new Person("leoh", "Seoul", 20);
Person person2 = new Person("leoh", "Seoul", 20);
Person person3 = new Person("nimoh", "Seoul", 21);
System.out.println(person1.equals(person2));
System.out.println(person1.hashCode() == person2.hashCode());
System.out.println(person1.equals(person3));
System.out.println(person1.hashCode() == person3.hashCode());
### 결과
true
true
false
false
record
에 equals
와 hashcode
를 오버라이딩 하지 않았지만, 모든 필드가 동일한 경우 동등하다는 equals
가 오버라이딩 되어있다.
ToString
자바에서 클래스를 만들면, 기본적으로 toString()
이 존재한다. 이 기본 메서드는 인스턴스 그 자체에 대한 정보를 출력한다.
1
2
3
4
5
6
PersonNoRecord personNoRecord = new PersonNoRecord("leoh", "Seoul", 20); // record X
System.out.println(personNoRecord);
### 결과
com.leoh.javaspringpractice.record.pojo.nonrecords.PersonNoRecord@107088f1
record
는 이 toString()
을 오버라이딩 해준다.
1
2
3
4
5
Person person1 = new Person("leoh", "Seoul", 20); // record O
System.out.println(person1);
### 결과
Person[name=leoh, address=Seoul, age=20]
Constructor
Compact Constructor
앞서 얘기했듯이, record
는 모든 필드를 final
로 눈에 보이지 않는 생성자를 통해 매핑해준다. 이 때, 매핑할 생성자는 우리가 자바코드로 존재하지 않기 때문에 값에 대한 유효성 검사를 할 수 없다.
record
는 이를 위해 Compact Constructor
라는 고유 생성자 문법을 사용할 수 있다. Compact Constructor
를 사용해 기존 자바의 생성자와는 다르게, 매개변수를 넣을 수 없다. 이미 매개변수가 들어왔다고 치고, 실제 초기화 로직 수행 전 데이터 전처리를 도와준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public record Person(String name, String address, int age) {
public Person {
if (!StringUtils.hasText(name)) {
name = "unknownName";
}
if (!StringUtils.hasText(address)) {
address = "unknownAddress";
}
if (age < 0) {
age = 0;
}
// 초기화 실행
}
}
Constructor
당연하게도 위와 같이 매개변수를 모두 포함하는 생성자는 만들 수 없다. 눈에 보이지는 않지만, 이미 존재하는 생성자이다. 하지만, 매개변수가 다르다면 생성자를 생성할 수 있다.
1
2
3
public Person(String name) {
this(name, "unknownAddress", 0);
}
Static
record
에서 static
변수와 메서드 역시 사용할 수 있다.
static 변수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public record Person(String name, String address, int age) {
public static final String UNKNOWN_NAME = "unknownName";
public static final String UNKNOWN_ADDRESS = "unknownAddress";
public Person {
if (!StringUtils.hasText(name)) {
name = UNKNOWN_NAME;
}
if (!StringUtils.hasText(address)) {
address = UNKNOWN_ADDRESS;
}
if (age < 0) {
age = 0;
} }
}
static 메서드
1
2
3
public static Person emptyPerson() {
return new Person(UNKNOWN_NAME, UNKNOWN_ADDRESS, 0);
}
메서드
그냥 메서드도 사용할 수 있다.
1
2
3
4
5
6
public PersonEntity toEntity() {
return PersonEntity.builder()
.name(name)
.address(address)
.age(age).build();
}
스프링에서 record
나는 주로 DTO를 requestDTO
와 responseDto
의 용도로 사용한다. record
를 처음 접하고 걱정했던 것이, 컨트롤러에서 record
의 직렬화, 역직렬화를 잘 호환해줄까? 라는 걱정이었다.
1
public record MemberRequest(String name, String email, String userId, int age) {}
1
2
3
4
5
6
7
8
9
@GetMapping("/basic")
public MemberResponse getRecord(MemberRequest memberRequest) {
return new MemberResponse(memberRequest.name(), memberRequest.userId(), memberRequest.age());
}
@PostMapping("/basic")
public MemberResponse postRecord(@RequestBody MemberRequest memberRequest) {
return new MemberResponse(memberRequest.name(), memberRequest.userId(), memberRequest.age());
}
잘 호환된다.
결론
record
는 이전부터 존재에 대해 알고는 있었지만, 크게 관심을 가지지 않아 뭔 지도 잘 몰랐고 사용해본 적이 없었다. 이번에 오프라인에서 모각코를 진행하면서 같이 한 분이 record
에 대해 간략하게 알려주셨다. 내 사이드 프로젝트에서 DTO를 만들 때마다 Lombok
으로 붙일 애노테이션을 생각하는 게 참 귀찮았는데, record
를 알게되어서 참 다행이라 생각했다.
조금 아쉬운 점은 record
는 상속이 안된다. 또, 불변객체이기 때문에 당연할 수도 있는데 빌더패턴을 사용할 수 없다.
record
를 알게된 이상 굳이 기존 자바 클래스로 DTO를 만들 필요가 전혀 없어졌다.자바 17이상을 사용하는 코드에서는 웬만하면 DTO에 record
를 사용하려고 한다. 내가 사용하는 기술이 업데이트되고, 버전이 올라갔을 때 무엇이 추가되었고 뭐가 좋아졌는 지 확인해보는 습관을 가질 필요성을 느꼈다.
Reference
- https://www.baeldung.com/java-record-keyword
- https://www.baeldung.com/java-record-vs-lombok