최근 JPA를 사용해서 만든 게시판 프로젝트를 리팩토링 하면서 여러가지를 적용하고 있다.
그 중 큰 흥미와 도움이 됐던 record에 대해서 글을 써보려고한다.
record는 자바 14, 15에서 preview로 추가된 이후 16버전에서 정식 스펙이 됐다.
파이썬의 dictionary, C#의 record, kotlin의 data class처럼 특정 값 타입을 담는데 특화되었다는 record, 보면 볼수록 VO와 DTO로 쓰기 좋아 보인다.
본격적으로 들어가기에 앞서 DTO를 생각해보자.
DTO를 구현하기 위해서는 getter, setter, equals, hashCode, toString 같은 데이터 처리 혹은 특정 연산을 수행하기 위해 오버라이드된 메소드를 반복해서 작성하게 된다.
public class Address {
private String city;
private String street;
private String zipCode;
public Address() {
}
public Address(String city, String street, String zipCode) {
this.city = city;
this.street = street;
this.zipCode = zipCode;
}
public String getCity() {
return city;
}
public String getStreet() {
return street;
}
public String getZipCode() {
return zipCode;
}
public void setCity(String city) {
this.city = city;
}
public void setStreet(String street) {
this.street = street;
}
public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Address)) return false;
Address address = (Address) o;
return Objects.equals(city, address.city) &&
Objects.equals(street, address.street) &&
Objects.equals(zipCode, address.zipCode);
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipCode);
}
}
address라는 주소에 해당하는 정보를 가지고 있는 단순한 DTO를 만든 코드이다. 굳이 자세히 보지 않아도 보일러 플레이트 코드가 불필요하게 큰 것을 알 수 있다.
보일러 플레이트 코드
최소한의 변경(인자, 혹은 결과 타입)으로 여러 곳에서 재사용 되면 반복적으로 비슷한 형태를 가지고 있는 코드
→ getter, setter, equals, hashCode, toString 등이 여기에 해당
일반적으로 이러한 보일러 플레이트 코드는 lombok 또는 kotlin(보다 정확하게는 data class) 사용해 중복으로 발생하는 코드의 양을 줄인다.
이렇게 비대해진 보일러 플레이트 코드는 자바가 가지고 있는 단점으로 작용한다. lombok이나 IDE의 도움을 받아 코드를 간결하게 만들 수 있지만, 이는 근본적으로 자바가 가지고 있는 한계를 해결하지 못한다.
자바가 가지고 있는 한계를 극복하기 위해 자바를 업그레이드하면서 다양한 기능들을 추가하게 됐는데 그 기능들 중 하나가 바로 record이다.
record의 목표
record는 불변 객체로 abstract로 선언할 수 없으며 암시적으로 final로 선언된다.
한 번 값이 정해지면 setter를 통해 값을 변경할 수 없으며 상속을 할 수 없다.
record 내 각 필드(헤더에 나열한 컴포넌트)는 private final로 정의된다.
다른 클래스를 상속 받을 수 없지만, 인터페이스로는 구현이 가능하다. (extends : X, implements : O)
레코드 내부에 멤버 변수(인스턴스 필드)를 선언할 수 없다. 그러나 static 변수는 생성이 가능하다. 이는 헤더에서 정의한 멤버만을 record에서 관리하기 위함이다.
위의 4가지 주요 특징을 제외하고는 자바의 클래스 개발과 동일하게 사용할 수 있다.
public record Gundam(String name, String pilot, String series) {}
record의 구조를 살펴보면 class 선언 시 들어가는 class 대신 record를 사용한다.
레코드명(헤더), {바디}
의 구조를 가지는데 헤더에 나열되는 필드를 컴포넌트라고 부른다. 위의 예제 코드로 보면 이름은 Gundam, private final 필드를 name, pilot, series를 가진 record라고 볼 수 있다.
컴파일러는 헤더를 통해 내부 필드를 추론하는데, 이 때 String 타입의 name, pilot, series가 있다는 것을 인식하게 된다.
이후 코드에 명시적으로 접근자와 생성자, toString, equals, hashCode를 선언하지 않아도 이에 대한 구현을 자동으로 제공한다.
컴팩트 생성자는 생성자 매개 변수를 받는 부분이 사라진 형태이다.
개발자가 명시적으로 인스턴스 필드를 초기화 하지 않아도 컴팩트 생성자의 마지막 부분에 초기화 구문이 자동으로 삽입된다. 일반적으로 사용하는 표준 생성자와는 달리 컴팩트 생성자 내부에서는 인스턴스 필드에 접근할 수 없다.
이러한 이유로 컴팩트 생성자에는 컴포넌트로 들어온 값을 불변으로 만들거나 불변식이 만족하는지(유효성 체크, 예를 들어 null check) 등의 작업을 하기에 적합하다.
public record Gundam(String name, String pilot, String series) {
public Gundam {
Objects.requireNonNull(name);
Objects.requireNonNull(pilot);
Objects.requireNonNull(series);
}
}
이렇게 선언한 컴팩트 생성자는 일반 생성자 쓰듯이 똑같이 사용하면 된다.
Gundam freedom = new Gundam("freedom_gunDam", "kira yamato", "SEED");
여기까지 보면 이 record라는 녀석은 jpa의 entity로 쓸 수 있지 않을까? 라는 생각이 든다.
데이터 전송 객체를 만들 때 나오는 자바의 한계를 극복하는 게 목적인 record를 jpa의 entity로 사용하면 entity의 구조를 단순화 하면서 직관적으로 쓸 수 있지 않을까?
record는 엔티티가 될 수 없는 이유는 다음과 같다.
위의 이유로 record는 엔티티가 되기 위한 조건을 만족하지 않는다. 결국 entity는 표준 java 클래스로 구현해야 해당 기준을 쉽게 충족할 수 있습니다.
record는 목적에 맞게 쓸 수 있는 곳이 없는 걸까?
이 글을 시작하기에 앞서 살펴본 DTO가 있다. DB에 저장된 데이터의 전송 객체로서 record는 아주 적합한 형태를 가지고 있다.
읽어온 정보를 변경하지 않으려면 record 타입의 DTO가 적격이다. record는 한 번 값이 정해지고 나면 setter를 통해 값을 변경할 수 없는데, 자바 내부에서 데이터 가공 시 중간에 변질될 우려가 없다.
일단 DTO를 사용하면 API에서 도메인 모델을 분리할 수 있는데 백엔드 내부의 entity를 외부에 공개할 필요가 없게 된다.
jetBrain 자바 보고서 - 전세계 자바 버전 점유율
하지만, 여전히 한국의 자바 환경은 8버전이 주를 이루고 있으며 나머지 파이는 11버전이 차지하고 있다. record를 제대로 된 스펙으로 사용하려면 16이상의 버전을 사용해야 하는데 버전을 올렸을 때 겪을 리스크가 있다. 그러나 세계적인 추이가 점점 새로운 자바 버전의 사용이 늘고 있는 추이기 때문에 훗날을 위해서라도 record를 DTO로 사용하는 것에 대해 친숙해질 필요는 있다고 생각한다.
출처
https://openjdk.org/jeps/395
https://mostadmired.tistory.com/133
https://scshim.tistory.com/372
https://blog.hexabrain.net/399
https://stackoverflow.com/questions/70601508/can-i-use-java-16-record-with-jpa-entity
https://thorben-janssen.com/java-records-hibernate-jpa/
https://thorben-janssen.com/dto-projections/
https://wkorando.github.io/sip-of-java/015.html
https://medium.com/javarevisited/javas-records-vs-kotlin-s-data-classes-b3fef851155a
https://www.reddit.com/r/java/comments/fzuex7/will_records_become_the_standard_way_to_create/