▶ DTO(Data Transfer Object)란?
DTO(Data Transfer Object, 데이터 전송 객체)는 프로세스 간에 데이터를 전달하는 객체이다. 원격 인터페이스로 작업을 할 때, 호출에 따른 비용이 비싸기 때문에 요청의 횟수를 줄여야 하고, 이를 위해 한 번의 요청에 더 많은 데이터를 전송해야 한다. 외부와 통신하는 프로그램에게 있어 호출은 큰 비용이며, 이를 줄이고 더욱 효율적으로 값을 전달할 필요가 있다. 이를 위해 데이터를 모아 한번에 전달하는 클래스를 DTO라고 한다.
🔔 API 사용을 할 때 DTO를 만들어서 받는 이유
API 스펙에 맞춰서 @ResponseBody Entity를 사용하는 것이 아니라 DTO를 만들어 사용해야한다.
Entity를 사용하게 되면 어디까지 API에서 받고 Binding 되는지, 추가적으로 다른 코드에서 Binding 했는지 모를 수 있다.
따라서 DTO에 해당 API를 맞춰서 받는 스펙을 알 수 있다.
또한 외부에 Entity를 보여줘서도 안되기 때문이다.
🔔 Entity 사용 시 문제점
- 도메인 Model의 모든 속성이 외부에 노출된다.
UI 화면마다 사용하는 Model의 정보는 상이하지만, Model 객체는 UI에서 사용하지 않을 불필요한 데이터까지 보유하고 있다. 비즈니스 로직 등 User의 민감한 정보가 외부에 노출되는 보안 문제와도 직결된다. - UI 계층에서 Model의 메서드를 호출하거나 상태를 변경시킬 위험이 존재한다.
- Model과 View가 강하게 결합되어, View의 요구사항 변화가 Model에 영향을 끼치기 쉽다.
또한 User Entity의 속성이 변경되면, View가 전달받을 JSON 및 프론트엔드 Js 코드에도 변경을 유발하기 때문에 상호 간 강하게 결합된다.
그래서 실무에서 이와 같이 Entity를 그대로 사용하면 안 된다고 하여 Spring Data JPA의 JpaRepository<>의 타입을 DTO로 주었다. 하지만 JpaRepository<> 에서 기본으로 제공하는 findAll() 등의 함수의 경우 return type이 List<T>로 되어 해당 엔티티 타입의 리스트로 반환이 되지 않았다.
Entity를 노출시키지 말라고 강조를 받은 나로서는 어떻게 반환을 해야 하는지 의문이 생겼다.
이 부분에 대한 질문과 답변을 찾아서 아래에 공유한다.
💡💡 엔티티를 노출하지 말라고 하는 것은 컨트롤러에서 API로 데이터를 반환할 때 Entity를 직접 반환하지 말라는 의미이다. 해당 프로젝트 안에서는 엔티티를 자유롭게 사용해도 된다. 즉, controller에서 service나 repository를 호출해서 Entity를 반환받는 것은 괜찮다고 한다.
🔔 Entity - Dto 변환 방법
- Mapper - modelMapper
- Mapper - mapStruct
- 수동 변환
※ Mapper의 기능
- Mapper(매퍼) 클래스는 DTO 클래스와 엔티티(Entity) 클래스를 서로 변환해 준다.
- Mapper(매퍼)에게 DTO 클래스 → 엔티티(Entity) 클래스로 변환하는 작업을 위임함으로써 Controller는 더 이상 두 클래스의 변환 작업을 신경 쓰지 않아도 된다. 역할 분리로 코드가 깔끔해진다.
※ MapStruct
- 어떤 도메인 업무 기능이 늘어날 때마다 개발자가 일일이 수작업으로 매퍼(Mapper) 클래스를 만드는 것은 비효율적이므로, MapStruct를 사용한다. MapStruct는 DTO 클래스처럼 Java Bean 규약을 지키는 객체들 간의 변환 기능을 제공하는 Mapper 구현 클래스를 자동으로 생성해 주는 코드 자동 생성기이다.
※ ModelMapper
- ModelMapper는 Runtime시 Java의 리플렉션 API를 이용해서 매핑을 진행하기 때문에 컴파일 타임에 이미 Mapper가 모두 생성되는 MapStruct보다 성능면에서 월등히 떨어진다.
Entity - Dto 사이에서의 변환을 찾다가 위 세 가지 방법이 가장 대중적인 방법이라는 것을 보았다.
그렇다면 실무에서는 어떤 방법으로 바인딩을 많이 하는지 궁금해졌다.
이 부분에 대해서도 검색을 하다가 같은 질문을 올려주신 분이 있어서 해당 답변을 공유한다.
💡💡 실무에서 다양하게 경험한 개인적인 의견을 드리겠습니다.
저도 주위 개발자 분들과 이것 가지고 옥신각신? 하는데요.
결국 코드량을 줄여준다는 장점이 있지요.
그럼 제가 생각하는 단점을 쭉 늘어보겠습니다.
1. 모델이 단순하면 상관이 없는데, 매핑해야 하는 모델이 복잡하거나 서로 차이가 많이 나면 머릿속으로 생각을 좀 많이 해야 합니다. 이게 어느 정도 복잡해지면 생각하시는 시간 때문에 비용이 더 들어가더라고요.
2. 직접 수동으로 할 때는 컴파일 시점에 오류를 잡을 수 있는데, 이건 실행을 해봐야 오류를 찾을 수 있습니다.
3. ModelMapper는 동시성 성능 이슈가 있습니다. 수천 TPS의 리엑티브 모델에서는 이 부분이 명확하게 병목으로 나왔습니다. 물론 수천 TPS가 안 되는 상황에서는 상관이 없습니다. (MapStruct는 모르겠네요)
결국 장단점이 있는데요. 저는 사용을 안 합니다. ㅎㅎ(사용하다가 사용을 안 하게 되었으니, 언젠가는 바뀔지도 모르겠습니다.)
복잡한 실무에서 엔티티를 DTO로 변경하는 게 이상적으로 딱딱 맞아떨어지는 경우만 있는 것도 아니고, 수동으로 작업하면 결국 컴파일 시점에 오류를 잡을 수 있다는 장점도 있고요. 그리고 무엇보다! 수동으로 해도 손가락만 약간 힘들지 몇 분 안 걸립니다.
주위에 개발 잘하시는 분들 중에 정말 선호하는 분들도 있고, 정말 싫어하는 분들도 있습니다.
따라서 해당 부분에 대해서 수동으로 하는 방법을 택하였다.
아래 코드에서 Stram 사용방법이 익숙지 않으면 참고
2022.08.31 - [Java] - [Java] 자바 Stream (map, filter, sorted, collect)
🔔 Entity - Dto 수동 변환 방법 (예시)
OrderSimpleApiController - 추가
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
// ORDER 2개
// N + 1 -> 1 + 회원 N + 배송 N
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return result;
}
@Data
static class SimpleOrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
public SimpleOrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName(); // LAZY 초기화
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); // LAZY 초기화
}
}
▶ 참고
'Java' 카테고리의 다른 글
[Spring] Front에서 보낸 데이터를 서버에서 받는 몇 가지 방법 (0) | 2022.09.27 |
---|---|
[React] react-datepicker 라이브러리 (캘린더, 달력) (0) | 2022.09.02 |
[Java] 자바 Stream (map, filter, sorted, collect) (0) | 2022.08.31 |
[QueryDSL] DATE_FORMAT 사용하기 (0) | 2022.08.30 |
[React] 비동기로 동작하는 setState에 대한 (0) | 2022.08.30 |