방탈출 예약 관리 미션을 진행하면서 아래와 같은 리뷰를 받았다.
이 리뷰를 계기로 Controller에서 받은 요청 DTO를 어디 계층까지 넘길 수 있는 가에 대해 고민하였다.
이 고민을 브리 코치님과 이야기하였더니 DTO가 무엇인가에 대해서 질문을 해주셨다.
그래서 이번 포스팅에서는 DTO가 무엇인지, 어떤 이유로 생기게 되었는 지, 그리고 어떻게 사용할 수 있을 지 알아보려고 한다.
🙋♀️ DTO란? (feat. Martin Fowler)
DTO는 Data Transfer Object의 줄임말로 계층 간 데이터 전달을 위해 쓰는 객체이다.
The solution is to create a Data Transfer Object that can hold all the data for the call.
...
Although the main reason for using a Data Transfer Object is to batch up what would be multiple remote calls into a single call, it's worth mentioning that another advantage is to encapsulate the serialization mechanism for transferring data over the wire.
마틴 파울러의 글을 보면 위와 같이 정의되어있다.
간단하게 정리하면 한 번의 네트워크 상 데이터 전달에서 효율적인 전송을 위해 데이터를 하나로 묶을 수 있는 객체를 사용하는 것이다.
따라서 DTO를 사용해서 데이터 전달의 비용 절감 효과를 볼 수 있다. 또 다른 장점은 직렬화 매커니즘을 단순화할 수 있다는 것이다. 이를 통해 DTO는 외부로 전송하고 싶은 데이터를 손쉽게 정의하고 직렬화 할 수 있다.
즉 DTO의 탄생 이유는 효율적인 데이터 전달에 있다. 필요한 데이터만 군집화하여 다른 프로세스로 전달할 수 있기 때문에 직렬화의 간편함도 덩달아 따라올 수 있는 것이다.
It also decouples the domain models from the presentation layer, allowing both to change independently.
Baeldung을 보면 이러한 문구도 추가로 있다. 표현 계층 (일반적으로 말하는 컨트롤러 레이어) 으로부터 도메인을 분리하여 도메인과 DTO 객체를 독립적으로 유지할 수 있게 한다라는 의미인 것 같다.
직역이 맞는 지는 정확히 모르겠습니다 😥 지적 부탁드립니다.
데이터를 전송하기 위해 설계된 이 객체는 데이터 전송을 위한 직렬화 혹은 역직렬화 로직이나 정보 조회 로직을 제외하고 어떠한 비즈니스 로직도 포함하지 않는다.
👩💻 사용하는 방법
언제 사용할 수 있는가?
- 도메인 모델의 여러 가지 필드를 한 번에 전달해야할 때
- 클라이언트와 서버 단의 데이터 전송 횟수를 줄이고 싶을 때
- 클라이언트에 요구에 따라 유연하게 필드를 추가하거나 변경하는 일이 잦을 때
이렇게 DTO는 어플리케이션 내에서 표현 계층이 외부로 값을 전달할 때 효율적인 전달을 위해 사용할 수 있다. 이에 따라 외부에게 보여줘야 하는 정보가 달라지게 되면 DTO만 수정해주면 되므로 유지보수가 쉬워지는 장점이 있다.
DTO 만들어보기
DTO는 POJO 클래스를 기반으로 한 객체이다.
즉, 비즈니스 로직을 포함하지 않고 직렬화나 Parsing의 기능을 제공하는 메서드만을 포함한 객체인 것이다.
간단하게 위와 같은 예약 페이지에서 예약자 이름, 날짜, 시간을 입력하면 예약 정보를 받아 도메인으로 바꾸는 것을 실습해보겠다.
public class Reservation {
private final long id;
private final String name;
private final LocalDate date;
private final LocalTime time;
public Reservationn(final long id, final String name, final LocalDate date, final LocalTime time) {
validateName(name);
this.id = id;
this.name = name;
this.date = date;
this.time = time;
}
private void validateName(final String name) {
if(name.length() < 0 || name.length() >= 10) {
throw new IllegalArgumentException("[ERROR] 잘못된 이름 입력입니다.");
}
}
}
먼저 도메인을 정의해보았다. 필요한 정보를 인스턴스 변수로 선언하였고 생성자에서 이름 입력을 검증해주었다. 이러한 이름 길이 검증과 같은 로직이 비즈니스 로직에 해당된다.
클라이언트에서 들어오는 정보는 어떻게 담아 가져올 수 있을까? 요청을 받는 DTO를 정의해보았다.
public record ReservationRequest(
String name,
LocalDate date,
LocalTime time
) {
public Reservation toEntity() {
return Reservation.of(name, date, time);
}
}
Java 17의 Record 기능을 사용해서 입력 받은 이름과 날짜, 시간을 객체로 정의하였다.
DTO를 Entity로 중간 변환하기 위한 toEntity() 메서드도 정의해주었다.
스프링 프레임워크를 사용해 클라이언트에서 받아온 정보를 자동으로 DTO로 변환해보자.
@PostMapping("/reservations")
public ReservationReseponse save(@RequestBody ReservationRequest reservationRequest) {
Reservation reservation = reservationService.save(reservationRequest.toEntity());
return ReservationResponse.from(reservation);
}
컨트롤러 메서드에 다음과 같이 API를 작성하였다.
애플리케이션을 실행하고 해당 API로 JSON 형태의 요청을 담아 보내면 DTO에 값이 담기게 된다. 그리고 외부로 값을 반환하는 응답 용 DTO를 생성하여 반환하면 클라이언트에서 요청에 대한 응답 결괏값을 확인할 수 있다.
public record ReservationResponse(
long id,
String name,
LocalDate date,
ReservationTime time
) {
public static ReservationResponse from(final Reservation reservation) {
return new ReservationResponse(
reservation.getId(),
reservation.getName(),
reservation.getDate(),
reservation.getTime()
);
}
}
이런 식으로 포스트맨에서 객체가 JSON의 형태로 잘 넘어온 것을 확인할 수 있다 😊
주의할 점
- DTO 개수가 증가할 수록 클래스 개수나 이를 변환하는 매핑 로직이 증가한다. 따라서 이러한 트레이드 오프를 고려하여 DTO를 재사용하거나 최소화하는 방안을 고려하자.
- DTO 클래스에 비즈니스 로직을 넣지 말자. DTO의 역할은 데이터의 전송이며 비즈니스 로직은 도메인 레이어에 있어야 한다.
참고
https://www.baeldung.com/java-dto-pattern
'우아한테크코스 > 레벨2' 카테고리의 다른 글
[OOP] POJO (Plain Old Java Objects) 란? (feat. 스프링의 등장 배경) (0) | 2024.04.27 |
---|---|
[Spring] 직렬화 (Serialization)와 역직렬화 (Deserialization) 의 정의 및 스프링에서 사용하는 방법 (4) | 2024.04.27 |
[회고] 우아한테크코스 6기 백엔드 레벨2 1주차 회고 (6) | 2024.04.21 |
[Spring] 문자열 경로를 반환하는 API를 호출하면 웹 페이지가 렌더링되는 이유는 뭘까? (feat. ViewResolver) (7) | 2024.04.18 |
[Spring] @ResponseBody와 ResponseEntity의 차이점과 동작 원리 (2) | 2024.04.18 |