
jdk14 버전에 새롭게 추가된 record class
개인적으로나 포럼의 게시글들에서 DTO를 대신해 record class로 관리하는 방법이 좋을 것 같다는 생각을 했었다.

회사에서 jdk21로 새로운 프로젝트를 시작하게 되었고
"그렇다면 모든 DTO 클래스를 record로 관리해보자!"고 코드를 작성한 이후
현재는 다시 모든 DTO클래스를 record에서 기존의 class로 롤백해서 사용하게 되었다. (내 시간 흑흑..)
그 원인에 대해서 공유하고 싶고 혹시나 더 좋은 방법이 있는지, record 클래스를 좀더 잘 사용할 방법이 있는지 알아보고 싶다.
위 세가지의 특징만으로 DTO클래스의 @Getter, @Builder, @AllArgsConstructor, @ToString 등등..
어노테이션이 주렁주렁 달리는걸 좀더 깔끔하게 관리할 수 있다고 생각했고
실제로 사용해보니 가독성의 부분에선 체감이 될 만큼 깔끔한 코드를 만들 수 있었다.
but.
너무 불변하다..
이 조건이 코드를 작성하는데 있어서 애로사항이 생겼다.
//페이지 유틸
@Getter
@Setter
public class PageUtil {
private Integer page;
private Integer size;
public Pageable getPageable() {
return PageRequest.of(this.page, this.size);
}
}
//회원조회 검색조건
@Getter
@AllArgsConstructor
public class SearchMemberRequest extends PageUtil {
private LocalDate startDate;
private LocalDate endDate;
private String keyword;
private String param;
}
해당 코드처럼 검색조건에 관한 DTO를 받아올 때
SearchMemberRequest, SearchAdminRequest, Search...Request 등 의 여러 다른 조건의 검색조건이 생겼을때
각각의 DTO들의 page와 size는 동일한 필드로써 중복되는 부분이 있어 PageUtil을 만들어 상속하여 관리하도록 하였다.
하지만 해당 코드 Search...Request 시리즈를 class가 아닌 record로 관리하게 된다면 PageUtil을 상속받을 수 없다.
왜냐? 너무나도 불변한 record 클래스는 상속을 허용하지 않기 때문이다.
사실 클래스간의 상속은 객체지향의 특징 중 하나지만 명확한 이유가 없다면 지양해야할 부분이라고 생각한다.
PageUtil은 내부의 필드가 page와 size만 담고 있는 객체이다.(Util이라는 네이밍이 별로지만..)
해당 필드의 값은 흔히 Entity에 들어가는 createdAt, updatedAt 같이 쉽게 변하지 않는 값이라 생각해 검색조건에 중복된 필드를 줄이고자 만든 클래스이기에 다른 dto에 상속해 사용하려 했다.
상속이 없이 해당 부분을 구현하려면
//페이지 유틸
public record SearchRequest(
String ...
...
Integer page;
Integer size;
) {
}
위와 같이 두 필드를 중복해서 적어줘야한다.
이 부분이 record 사용을 망설이게 한 첫번째 이유다.
record 타입은 생성자의 QueryProjection의 사용이 불가능하다.
이게 무슨 소리인가?
코드를 보자
public record DetailMemberResponse(
String email;
String name;
String phone;
String location;
) {
@QueryProjection
public DetailMemberResponse(String email, String name, String phone, String location) {
this.email = email;
this.name = name;
this.phone = phone;
this.location = location;
}
}
해당 클래스는 사용할 수 있을까? 정답은 X
record 타입은 내부의 생성자를 선언하거나 추가할 수 없다.
(물론 canonical constructor(명시적 생성자)를 만들 순 있으나 정말 명시적인 용도의 생성자이지 그 이상 그 이하도 아니다)
그럼 Projections.bean, Projections.field, Projections.constructor는?
copilot의 답변을 보자

이렇기에 github에선 어떤 issue까지 있냐면
https://github.com/querydsl/querydsl/issues/3520

꽤나 재밌는 생각이지만 받아들여지지는 않을 제안이다..
그럼 record 클래스에 Projections.constructor를 사용하면 되지 않겠냐 하신다면
내가 사용하는 경우의 예시를 들어보고 싶다.
//QueryDsl
List<DetailMemberResponse> results = queryFactory
.select(Projections.constructor(DetailMemberResponse.class,
member.email,
member.name,
member.phone,
member.address,
member.addressDetail))
.from(member)
.fetch();
//기존 class라면
@Getter
public class DetailMemberResponse {
private String email;
private String name;
private String phone;
private String location;
public DetailMemberResponse(String email, String name, String phone, String address, String addressDetail) {
this.email = email;
this.name = name;
this.phone = phone;
// 생성자 custom
this.location = address + ", " + addressDetail;
}
}
위와 같이 response dto의 내부 생성자에서 custom하게 조립하는게 가능하다. 하지만 record 클래스에서는?
불가하다.
이유는?
불변하기 때문에
...
결국 record class는 단순한 response 응답객체로만 사용하기로 했다.
어떤 요구사항이 바뀔지 모르는 세상에서 불변하게 유지되는 것은 응답객체 말고는 잘 떠오르지 않았기 때문이다.
만약 이 글을 보고 의견이 있다면 듣고 싶다.