API를 만들다 보면, 정말 필수적으로 만들게 되는게 Request / Response 를 위한 객체이다.
이를 우린 흔히 'DTO' 라고 부르게 된다.
현재 진행중인 프로젝트의 Request의 일부를 가져와봤다.
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
public class RegisterRequest {
@NotBlank(message = "이메일은 필수 항목입니다.")
@Email
private String email;
@NotBlank(message = "비밀번호는 필수 항목입니다.")
private String password;
@NotBlank(message = "닉네임은 필수 항목입니다.")
private String nickname;
@NotNull(message = "나이는 필수 항목입니다.")
private Long age;
// 생성자
public RegisterRequest(String email, String password, String nickname, Long age) {
this.email = email;
this.password = password;
this.nickname = nickname;
this.age = age;
}
// getter
public String getEmail() {
return email;
}
public String getPassword() {
return password;
}
public String getNickname() {
return nickname;
}
public Long getAge() {
return age;
}
// setter
public void setEmail(String email) {
this.email = email;
}
public void setPassword(String password) {
this.password = password;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public void setAge(Long age) {
this.age = age;
}
// toString
@Override
public String toString() {
return "RegisterRequest{" +
"email='" + email + '\'' +
", password='" + password + '\'' +
", nickname='" + nickname + '\'' +
", age=" + age +
'}';
}
public User toEntity() {
return User.builder()
...
.build();
}
}
조금 극적인 차이를 보이기 위해, Lombok을 사용하지 않았다.
위 DTO의 아쉬운 점은 무엇이 있을까?
- 보일러플레이트 코드가 많다.
보일러 플레이트 코드는, getter, setter, toString과 같은 코드들을 말한다. 수정을 거치지 않고 여러 부분에 사용되는 반복되는 코드들이다. 이런 코드는 가독성을 저해하고, 해당 클래스가 데이터 클래스임을 알아보기 어렵게 한다.- (만약) setter가 존재하지 않더라도, 해당 클래스 객체가 불변(immutable) 객체인지 바로 파악하기 어렵다.
이런 점들을 보완하기 위해, Java14에서 처음 도입된 Record를 도입해보자.
record는 불변 객체를, 특히 데이터 객체를 쉽게 만들 수 있는 클래스의 신유형이다. JDK16부터 정식 지원한다고 한다.
특징은 다음과 같다.
- getter, toString, equals, hashCode와 같은 보일러 플레이트 코드를 자동 생성해준다. 불변 객체기에 setter는 없다!
- 모든 필드는 자동 private final로 선언된다. 그게 아니라면, static이여야 한다.
- 생성자를 자동으로 만들어준다.
- 다른 클래스를 상속 할 수 없다. (레코드가 상속하거나, 다른 클래스가 레코드를 상속하는 행위)
위 DTO는 다음과 같은 Record로 정의 할 수 있다.
public record RegisterRequest(@NotBlank(message = "이메일은 필수 항목입니다.") @Email String email,
@NotBlank(message = "비밀번호는 필수 항목입니다.") String password,
@NotBlank(message = "닉네임은 필수 항목입니다.") String nickname,
@NotNull(message = "나이는 필수 항목입니다.") Long age, // 출생 년도임
) {
public User toEntity() {
return User.builder()
...
.build();
}
이게 끝이다!
getter, 생성자, toString과 같은 코드들은 자동 생성된다.
public record 레코드이름(필드들...){ } 의 꼴로 선언하면 된다.
Record를 사용하면 해당 클래스가 데이터 클래스이자, 불변 객체임을 보장한다. 추가적으로 코드의 가독성도 굉장히 많이 향상되었음을 알 수 있다.
RegisterRequest request = new RegisterRequest("email@exam.com", "password", "nickname", "2024");
의 꼴로 사용하면 된다. setter는 없으니, 생성자를 통해 값을 바인딩하면 된다.
헷갈릴 수 있는 점은, 기존에 getter가 있을때는 request.getEmail() 과 같은 형태로 getter를 통해 값을 받았지만
record의 경우엔 record.email(); 과 같이 get이 빠진 형태로 getter가 구현된다. 해당 부분만 적응하면 record의 장점만 느껴지게 될 것이다.
이 부분은 스프링부트에 해당하는 내용으로 , 내가 처음에 헷갈렸던 부분이다.
우리가 Json 형태로 Request를 받고자 하면, @RequestBody를 사용한다. 근데 Record엔 Setter가 없는데, 스프링은 어떻게 값을 바인딩해주지?
처음엔 생성자를 쓰나? 라고 생각했지만, @RequestBody는 생성자를 사용하지 않는다.
정답은 @RequestBody는 Reflection을 이용해 값을 넣어준다.
(참고로, @ModelAttribute는 생성자를 통해 첫 바인딩을 하고, 바인딩 되지 않은 값들은 Setter를 통해 바인딩한다.)
Reflection은 클래스 타입같은걸 모르더라도, 런타임에 클래스의 메서드, 필드등에 접근 가능하도록하는 자바의 API다.
즉, 런타임에 동적으로 클래스에 접근 및 수정 할 때 사용된다. 우리가 쓰는 Spring의 Annotation들 (@Component를 붙이면 컴포넌트 스캔이 이루어지는 등..)에서도 사용한다. 실제 코드 레벨에서 우리가 사용할 일은 거의 없는 것 같다.