최근에 유튜브를 보다가 흥미로운 영상을 보게 되었습니다. DTO를 자바의 record라는 것을 이용해서 구현하는 영상이었는데요, DTO를 많이 만드는 저에게는 매우 흥미로운 영상이었습니다.
(저는 모든 요청과 응답이 원하는 데이터가 다르기 때문에 각각 DTO를 만들어야 한다고 생각해서 이런 식으로 구현합니다)
기존에 진행했던 프로젝트가 record를 지원하는 자바 17이기도 하고, 알아두면 앞으로도 종종 쓸 일이 있을 것 같아서 기존 프로젝트에 적용해 보기로 했어요. 우선 Record가 뭔지 알아보겠습니다.
제가 공부할 때 자주 이용하는 Baeldung이라는 사이트에서는 다음과 같이 설명하고 있어요.
Passing immutable data between objects is one of the most common, but mundane tasks in many Java applications. Prior to Java 14, this required the creation of a class with boilerplate fields and methods, which were susceptible to trivial mistakes and muddled intentions.
With the release of Java 14, we can now use records to remedy these problems.
짧게 설명하면, Java 14부터 도입된 Record 클래스는 불변 데이터를 객체 간에 전달하는 작업을 간단하게 만들어준다. Record 클래스를 사용하면 불필요한 코드를 제거할 수 있고, 적은 코드로도 명확한 의도를 표현할 수 있다라고 합니다.
Record의 특징으로는
- 멤버변수는 private final로 선언된다
- 필드별 getter가 자동으로 생성된다
- 모든 멤버변수를 인자로 하는 public 생성자를 자동으로 생성한다
—> (@allargsconstructor와 유사하지만, record는 불변 데이터를 다루므로 생성자가 실행될 때 인스턴트 필드를 수정할 수 없다) - equals, hashcode, toString을 자동으로 생성한다
- 기본생성자는 제공하지 않으므로 필요한 경우 직접 생성해야 한다
와 같은 점이 있습니다. 이제 기존에 DTO에 적용해 보면서 알아보겠습니다.
기존의 제 DTO 클래스입니다.
package com.codelion.animalcare.domain.doctorqna.dto.response;
import com.codelion.animalcare.domain.doctorqna.entity.Answer;
import com.codelion.animalcare.domain.doctorqna.entity.Question;
import com.codelion.animalcare.domain.user.entity.Member;
import lombok.Getter;
import java.time.LocalDateTime;
import java.util.List;
@Getter
public class QuestionResponseDto {
private Long id;
private String title;
private String content;
private LocalDateTime createdAt;
private int view;
private List<Answer> answerList;
private Member member;
private int likeCount;
public QuestionResponseDto(Question entity){
this.id = entity.getId();
this.title = entity.getTitle();
this.content = entity.getContent();
this.view = entity.getView();
this.createdAt = entity.getCreatedAt();
this.answerList = entity.getAnswerList();
this.member = entity.getMember();
this.likeCount = entity.getLikeCount();
}
}
일반적인 응답용 DTO 클래스입니다. 이를 record로 바꿔볼게요.
package com.codelion.animalcare.domain.doctorqna.dto.response;
import com.codelion.animalcare.domain.doctorqna.entity.Answer;
import com.codelion.animalcare.domain.doctorqna.entity.Question;
import com.codelion.animalcare.domain.user.entity.Member;
import java.time.LocalDateTime;
import java.util.List;
public record QuestionResponseDto (
Long id,
String title,
String content,
LocalDateTime createdAt,
int view,
List<Answer> answerList,
Member member,
int likeCount
) {
public QuestionResponseDto(Question entity){
this(entity.getId(), entity.getTitle(), entity.getContent(),
entity.getCreatedAt(), entity.getView(),
entity.getAnswerList(), entity.getMember(), entity.getLikeCount());
}
}
record로 변환하면 이렇게 바꿀 수 있습니다.
예제가 단순해서 그런지 크게 와닿지는 않을 수도 있을 것 같아요. 달라진 점을 알아보자면
- Getter를 자동으로 생성해 주므로 @Getter가 제거되었습니다.
- 소괄호 안에 멤버변수를 포함합니다.
- 불변객체이기 때문에 생성자를 통해 값을 초기화해 줬습니다.
- 기본적으로 private으로 선언되기 때문에, 접근 제어자를 명시해주지 않았습니다.
이전에 작성한 간단한 테스트코드를 통해 잘 동작하는지도 알아보겠습니다.
컴파일 에러가 발생했어요. 분명 자동으로 getter가 생성된다고 했는데 getter가 동작하지 않는 모습입니다.
getter를 만들지 않은걸까요? 한번 디컴파일된 코드를 확인해 보겠습니다.
public record QuestionResponseDto(Long id, String title, String content, LocalDateTime createdAt, int view, List<Answer> answerList, Member member, int likeCount) {
public QuestionResponseDto(Question entity) {
this(entity.getId(), entity.getTitle(), entity.getContent(), entity.getCreatedAt(), entity.getView(), entity.getAnswerList(), entity.getMember(), entity.getLikeCount());
}
public QuestionResponseDto(Long id, String title, String content, LocalDateTime createdAt, int view, List<Answer> answerList, Member member, int likeCount) {
this.id = id;
this.title = title;
this.content = content;
this.createdAt = createdAt;
this.view = view;
this.answerList = answerList;
this.member = member;
this.likeCount = likeCount;
}
public Long id() {
return this.id;
}
public String title() {
return this.title;
}
public String content() {
return this.content;
}
public LocalDateTime createdAt() {
return this.createdAt;
}
public int view() {
return this.view;
}
public List<Answer> answerList() {
return this.answerList;
}
public Member member() {
return this.member;
}
public int likeCount() {
return this.likeCount;
}
}
디컴파일된 코드의 전문이에요. get을 통해서 getter를 구현한 것이 아니라 멤버변수의 이름으로 메소드를 만들었네요. 수정해 보겠습니다.
컴파일 에러도 해결되었고, 테스트도 모두 정상적으로 동작합니다.
한번 더 연습해 보겠습니다.
@Getter
@Setter
@NoArgsConstructor
public class QuestionSaveRequestDto {
@NotBlank(message = "제목은 필수 입력 사항입니다.")
@Size(max = 200, message = "제목이 너무 길어요.")
private String title;
@NotBlank(message = "내용은 필수 입력 사항입니다.")
private String content;
@Builder
public QuestionSaveRequestDto(String title, String content) {
this.title = title;
this.content = content;
}
public Question toEntity(Member member) {
return Question.builder()
.title(title)
.content(content)
.member(member)
.build();
}
}
이 클래스를 record로 바꿔볼게요. 바꾸는 김에 setter도 빼보겠습니다.
public record QuestionSaveRequestDto(
@NotBlank(message = "제목은 필수 입력 사항입니다.")
@Size(max = 200, message = "제목이 너무 길어요.")
String title,
@NotBlank(message = "내용은 필수 입력 사항입니다.")
String content
) {
public Question toEntity(Member member) {
return Question.builder()
.title(title())
.content(content())
.member(member)
.build();
}
}
@valid 어노테이션 또한 동작하므로, 위와 같이 바꿔주었습니다.
@DisplayName("질문_작성된다")
@Test
void t1() {
//given
QuestionSaveRequestDto question = QuestionSaveRequestDto.builder()
.title("title10")
.content("content10")
.build();
Principal principal = () -> "member1@test.com";
//when
questionCommandService.save(question, principal);
//Testinitdata -> 3 question
QuestionResponseDto savedQuestion = questionQueryService.findById(4L);
//then
assertEquals(savedQuestion.title(), "title10");
assertEquals(savedQuestion.content(), "content10");
assertEquals(savedQuestion.member().getEmail(), "member1@test.com");
}
이전의 테스트코드입니다. 기존의 builder대신 생성자로 만들겠습니다.
@DisplayName("질문_작성된다")
@Test
void t1() {
//given
QuestionSaveRequestDto question = new QuestionSaveRequestDto("title10", "content10");
Principal principal = () -> "member1@test.com";
//when
questionCommandService.save(question, principal);
//Testinitdata -> 3 question
QuestionResponseDto savedQuestion = questionQueryService.findById(4L);
//then
assertEquals(savedQuestion.title(), "title10");
assertEquals(savedQuestion.content(), "content10");
assertEquals(savedQuestion.member().getEmail(), "member1@test.com");
}
정상적으로 동작하네요!
이상으로 Record에 대해서 알아보았는데요
- 불필요한 코드를 제거할 수 있다.
- 멤버변수는 private final로 선언된다
- 모든 멤버변수를 인수로 가지는 생성자를 자동으로 생성해 준다
- getter, equals, hashcode, toString과 같은 기본 메소드를 제공한다
의 특징으로 간단하게 요약할 수 있을 것 같습니다.
실제로 사용해 보니 간단하고 편해서 저는 종종 사용하게 될 것 같습니다.
+ 2023-08-10 실제 프로젝트에 도입해보면서 느낀 장점, 주의점, 소감도 작성해 보았습니다!
블로그에 올리는 첫 글인데요, 부족한 코드를 같이 올리려고 하니 많이 부끄럽네요 ㅎㅎ
잘못된 정보에 대한 정정, 피드백은 언제나 환영입니다!
'JAVA' 카테고리의 다른 글
자바의 Record로 DTO를 만들어보자 - 2 (0) | 2023.08.10 |
---|---|
Stream API의 map에 대해서 코드로 이해해보자 (2) | 2023.05.12 |
[JAVA] Garbage Collection (GC)에 대해 알아보자 (0) | 2023.04.23 |