
레코드~ 레코드~ 끽! 끽! 끽! (죄송합니다 ㅠㅠㅠㅠ)
KakaoPay로 결제수단을 연결 도중, 어떤 블로그에서 DTO를 Class가 아닌 Record로 정의했다.
DTO (Data Transfer Object, 데이터 전송 객체)
프로세스 간에 데이터를 전달하는 객체를 의미
말 그대로 데이터를 전송하기 위해 사용하는 객체
(예시로, Spring에서 React로 Response를 하기 위해 담아서 보내는 데이터 객체)
어? 이게 뭐지 언제 나온거지?
이걸 사용하니까 그 Java의 더러운 BoilerPlate 코드가 확 줄기에 한번 알아보고자 한다.
Record란 자바 14에서 preview로 추가된 이후, 16버전에서 정식 스펙이 됐다.
처음 소개된 새로운 Class 타입이다.
파이썬의 Dictionary, Kotlin의 Data Class처럼 값의 집합으로 이루어진 간단한 객체를 심플하게 개발하기위해 고안되었다고 한다.
그래서 불변 데이터를 다룰 class 구현에 최적화되어있다.
@Getter
public class LoginReqDTO {
private final String email;
private final String password;
}
여기 로그인을 위해 프런트에서 아이디와 비밀번호를 받아오는 DTO가 있다.
Final Class여서 한번 들어온 값은 변할 수 없다. (불변 클래스)
이 Class를 Record로 변환을 시키면...
public record LoginReqDTO(String email, String password) {
}
엄청 코드가 간결해진다.
구조를 살펴보면, class 대신 record를 사용한다.
(Header), {Body}의 구조를 가지는데 Header에 나열되는 필드를 컴포넌트라고 부른다.
컴파일러는 Header를 통해 필드를 추론한다. (String 타입의 email, password 추론)
추론 후, 우리가 많이 쓰는 @Getter, @Setter, toString 등을 선언하지 않아도 자동으로 제공하게 된다.
기본적으로 record는 email과 password 필드를 매개변수로 받는 생성자를 자동으로 제공한다.
만약 다른 Class를 이용해서 저 객체를 초기화해야하는 경우면 어떻게 해야할까?
일단 컴펙트 생성자라는 개념이 있다.
public record LoginReqDTO(String email, String password) {
public LoginReqDTO {
Objects.requireNonNull(email);
Objects.requireNonNull(password);
}
}
Objects.requireNonNull()은 객체가 null이면 NullPointerException을 던지는 메서드다.
개발자가 명시적으로 인스턴스를 초기화 하지 않아도, 컴팩트 생성자의 마지막 부분에 초기화 구문이 자동으로 삽입된다.
일반적으로 사용하는 표준 생성자와는 달리 컴팩트 생성자 내부에서는 인스턴스 필드에 접근할 수 없다.
(아마 Setter로 접근하는 듯 싶다)
그래서 컴팩트 생성자는 null 값이 들어오는 것을 막기 위한 용도인 것 같다.
public record LoginReqDTO(String email, String password) {
public LoginReqDTO (User user) {
user.getEmail(),
user.getPassword()
}
}
이게 지금 적합한 예시는 아니지만, ResponseDto 같은 경우는 Entity로 초기화를 하는 생성자가 있으면 엄청 편하다.
그럴 경우엔 저 예시코드처럼 선언하면 된다.
물론 사용법은 이와 같다.
LoginReqDTO loginReqDTO = new LoginReqDTO("test123@naver.com", "1234");
User testUser = new User("test123@naver.com", "1234"); // 이런 Class가 있다고 가정
LoginReqDTO loginReqDTO = new LoginReqDTO(testUser);
그럼 이 좋은 기능들이 있는 record를 Entity로 사용하면 되겠네????
이유는 다음과 같다.
// record
String email = loginReqDTO.email();
// @Getter
String email = loginReqDTO.getEmail();
record의 getter는 필드명을 그대로 사용하고 있다.
쿼리 사용 후, 가져온 데이터를 접근자(Getter)로 초기화(Setter)를 진행한다.
메소드 이름이 다르니, 쿼리 입장에서는 접근도 초기화도 못하는 아이러니한 상황....;;
JPA를 사용할 때 엔티티 클래스에는 기본 생성자(no-args constructor)가 필수다.
// 기본 생성자와 커스텀 생성자
protected User() {}
JPA가 DB에서 데이터를 조회하여 엔티티를 생성할 때, 리플렉션(Reflection)이라는 기술을 사용하기 때문인데....
결론부터 말하면 record는 기본 생성자(no-args constructor)를 지원하지 않는다.
hibernate와 같은 JPA는 Proxy 생성을 위해 인수 생성자, non-final 필드, setter 및 non-final 클래스가 없는 엔티티에 의존한다.
근데, record는 final 클래스에 setter도 당연히 안되니...
final 클래스인 DTO에만 record를 사용하자!
사용하면서 코틀린쓰는거 같아서 코드 작성할 맛이 났다.
더러운 보일러 코드....