Spring DTO의 생성 및 레이어 이동

Miz·2021년 5월 4일
2

간단한 API를 제작하며 생각했던 고민들을 정리해보려고 한다.

먼저 Controller는 외부에서 @PathValue나 @Param / @RequestBody / @ModelAttribute등을 통해 데이터를 전달받는다.

여기서 @ModelAttribute는 Http Form(Get/Post) 방식의 전달이나 QueryParameter의 정보들을 선언한 타입의 Object로 변경해준다.

@RequestBody는 Contents-type이 Json에 한해서 선언한 타입의 Object로 컨버팅 해준다.

  • 위의 내용은 추후 다시 정리하겠다.

아키텍쳐나 등등에 정확한 정답은 없을 수도 있기 때문에 좋은 생각있으시면 공유 해주시면 감사하겠습니다.

DTO(Data Transfer Object)

  • 말 그대로 단순히 계층간 데이터 전달할 때 사용한다, 하지만 필수적인 것은 아니다.

  • 가장 중요한 것은 의존 관계라는 점이 중요하다.

  • 실용적인 개발 아키텍처는 컨트롤러, 서비스, 리포지토리 계층이 모두 엔티티 계층에 의존하는 것입니다. 왜냐하면 엔티티라는 것이 우리의 핵심 비즈니스이기 때문에 대부분의 로직은 엔티티가 필요합니다.

위의 내용은 김영한님의 강의에서 질문에 대한 답변을 해주신 바로 정리 했습니다.

참조 : www.inflearn.com/questions/139564

외부에서 입력된 값들을 담은 Dto 를 RequestDto라고 말한다면, 이 dto를 어디까지 전달하는게 맞을까에 대한 고민이 시작이였다.

처음에는 아무 생각없이 그냥 받아온 dto를 Service로 전달하고 그냥 사용하였다.

작은 프로젝트에서는 Controller의 Request를 Service에서 원하는 Request와 크게 다른점이 없다고 생각했지만, Controller와 Service간의 결합이 생긴다.

결합이 강해지면 Controller의 수정이 Service의 수정을 부르고, Service의 수정이 Controller의 수정을 부르게 되면서 유지보수가 어렵게 된다.

그렇기 때문에 Service로 넘길 때 간단한 부분은 파라미터 전달으로 리팩토링하고, 파라미터로 하기에 내용이 많다면 Mapper를 만들던가 직접 Service Layer로 이동하는 DTO를 생성하는 코드를 작성했다.

  1. 파라미터로 직접 넘기는 방법
    @GetMapping("/user/{userId}/orders/between/{from}/and/{to}")
    public ApiResult<List<OrderDto>> findOrders(OrderSearchDto orderSearchDto) {

        return succeed(
                orderService.findOrdersByTime(orderSearchDto.getUserId(),orderSearchDto.getFrom(), orderSearchDto.getTo())
                        .stream()
                .map(OrderDto::new)
                .collect(Collectors.toList())
        );
    }
  1. Service에 필요한 Dto로 변환
    @PostMapping("/user/{userId}/store/{storeId}/order")
    public ApiResult<OrderDto> createOrder(
            @PathVariable Long userId, @PathVariable Long storeId, @Param("time") String time,
            @RequestBody @Valid List<OrderItemRequest> items
    ) {
        if(items.size() <= 0) throw new NoOrderItemException();
        List<CreateOrderDto.CreateOrderItemDto> createItems = items.stream()
                .map(i -> new CreateOrderDto.CreateOrderItemDto(i.getName(), i.getUnitPrice(), i.getUnitCount()))
                .collect(Collectors.toList());

        Order order = orderService.order(new CreateOrderDto(userId, storeId, time, createItems));
        return succeed(
                new OrderDto(order)
        );
    }

여기서 Mapper를 사용하는 것에 대한 포스팅은 공부한 뒤 올리겠다.

추가적으로, DTO Class의 패키지 위치가 중요하다. 결국 중요한것은 의존관계이기 때문이다.

기본적으로 MVC모델에서 연관관계는 Controller -> Service -> repository이다.

만약 Service로 전달하는 DTO가 Controller 패키지단에 있으면 Service에서 Controller패키지를 참조하게 된다.

이러한 문제를 방지하기 위해서 전달 받을 DTO를 Service패키지에 두었다.

이렇게 되면 Controller에서 CreateOrderDto를 보는 것은 Service단을 의존하기 때문에 Controller에서 사용해도 괜찮고, Service단은 같은패키지이기 때문에 상관없다. 하지만 여기서 이 Dto를 가지고 Repository로 이동하는 것은 Repostiory가 Service를 의존하기 때문에 하면 안된다.

Response 관련
이제 응답을 할 때 어떤식으로 dto를 사용할지에 대해 생각해보겠다.

기본적으로 반환할때 Entity를 절대 그대로 반환해서는 안된다.

  1. Entity의 변경이 모든 API spec의 변화를 가져온다.

  2. API마다 필요한 정보들이 다른데 Entity를 반환하면 모든 정보를 보여준다

위의 문제로 반환을 할때는 Dto로 변환 후 반환하는게 무조건 필요하다. (간단한 토이프로젝트라면 상관없을수도..?!)

결국 Entity -> Dto 변환 타이밍이 중요하다.

이 부분에서는 구글검색을 해보니 Service에서 하는 것이 좋다 or Controller에서 하는 것이 좋다 의견이 갈리는 문제가 있다.

여기서 저는 요즘 강의를 듣고있고 위에 질문답변에서 말씀해주신 김영한 개발자님의 의견으로 하고자 한다.

실용적인 개발 아키텍처는 컨트롤러, 서비스, 리포지토리 계층이 모두 엔티티 계층에 의존하는 것입니다. 왜냐하면 엔티티라는 것이 우리의 핵심 비즈니스이기 때문에 대부분의 로직은 엔티티가 필요합니다.

  1. 실용적으로 엔티티를 전체 구조에서 사용하자(물론 아키텍처 방향에 따라서 엔티티 노출은 제약할 수 있다)
    엔티티는 어느 계층에서 의존하는 것이 상관없기 때문에 Service에서도 Entity를 반환하고, 그 후 Controller에서 Entity -> Dto 변환 작업을 하면 된다고 생각한다. 물론 이게 정답은 아니라고 생각한다.

처음 블로그를 쓰게 된거라 잘 못써도 이해부탁드립니다ㅠ.ㅠ

profile
2년차 백엔드 개발자

0개의 댓글