Record에 대해서

ideafy·2025년 5월 26일

CS

목록 보기
11/17

Record 키워드는 자바 14 이후 추가된 키워드이고, DTO를 만들 때 record를 사용하면 불필요한 보일러플레이트 코드를 줄이고 불변성을 가질 수 있다.

아래는 제가 API의 응답에 메세지를 추가하고 싶어서 record를 사용해 DTO를 만들어본 예시입니다.

public record ApiResponseBody(String message, Object data) {}

기존 DTO 생성 방식으로라면 멤버변수가 됐어야 할 message와 data가 record의 매개변수로 선언됐다.
Record에서는 message와 data를 record component라고 한다.

게다가 별도의 접근제어자나 키워드 없이도 private final인 변수로 취급된다. 그렇기 때문에 중간에 데이터가 바뀔 소지 없이 데이터를 단순 전달하는 용도의 DTO로 사용되기 좋다는 것이다.

별도의 lombok 없이도 canonical constructor, getter, toString, equals, hashCode를 자동으로 제공한다.

그런데

ApiResponseBody가 메세지를 전달할 필요 없이 데이터만 보내고 싶을 때는 어떻게 할까?
record의 바디 부분에 생성자를 오버로딩하면 된다.

public record ApiResponseBody(String message, Object data) {
    public ApiResponseBody(Object data) {
        this(null, data);
    }
}

이로써 message 부분을 null로 채우지 않고도 ApiResponseBody의 생성자를 호출할 수 있다.

하지만 주의해야할 점이 있다. record의 필드는 암묵적으로 private final이라고 했으므로, 필드를 직접 할당할 수 없다.

public record ApiResponseBody(String message, Object data) {
    public ApiResponseBody(Object data) {
        this.message = null; // 컴파일 에러: final 필드는 직접 할당 불가
        this.data = data;
    }
}

와 그러면 Record는 완전 만능이네요

그것은 아닙니다. 단순 데이터 전달 용도의 수많은 DTO를 작성할 때 record로 보일러 플레이트 코드를 줄일 수 있다는 것은 분명한 단점이지만 모든 경우에 적용되는 것은 아닙니다.

아래 제가 겪은 실제 예시를 통해 알아보겠습니다.

public record TagDto(Long tagId, String name) {}
DB에서 Tag 데이터를 꺼낼 때 TagDto를 record로 만들었습니다.
Tag 데이터를 꺼낼 때 다른 데이터도 복잡하게 얽혀서 꺼내야 되기 때문에 QueryDsl을 사용했습니다.

그러면 projection으로 QueryDsl에서 TagDto의 값을 채워 넣어야 하겠군요.
@QueryProjection 어노테이션을 레코드에 붙여서 사용해보겠습니다.

@QueryProjection
public record TagDto(Long tagId, String name) {}

이렇게 작성하니 오류가 뜹니다. @QueryProjection은 생성자에만 사용할 수 있는 어노테이션이기 때문에 record에서는 사용이 불가능합니다.

억지로 생성자에 어노테이션을 붙이게 되면 계속 되는 재귀호출로 오류가 발생하기 때문에 이도 사용할 수 없는 방법입니다.

public record TagDto(Long tagId, String name) {
    @QueryProjection
    public TagDto(Long tagId, String name) { 
        this(tagId, name);  // stackOverFlow
    }

record를 유지하면서Projections.constructor()를 사용해 DTO에 데이터를 매핑할 수 있지만 QueryDsl을 사용한 코드가 기본적으로 복잡하게 보여 @QueryProjection 사용을 선호하는 편입니다.
그래서 저는 기존 record에서 class로 DTO를 만드는 것으로 구조를 변경했습니다.

다른 경우

이외에도 record를 단순 데이터 전달 용도의 DTO로 사용하는 것이 아닌 비즈니스 로직을 포함하는 VO로 사용할 수는 있겠지만 암묵적인 final 컴포넌트로 인해 데이터의 수정이 불가능합니다. (애초에 불변을 목적으로 설계됐기 때문)

profile
재밌게 공부하고 싶어요

0개의 댓글