Response DTO에 대해 찾아보다가 이 주제에 관한 블로그 글들을 접하게 되었다. 생각하지못했던 부분이라서 자세히 읽어보았고, 참고한 블로그들을 바탕으로 정리하였다.
WEB 응답
1. Service에서 Response Dto 생성 후 Controller에서 반환
2. Service에서 도메인 자체를 반환하여 Controller에서 Response Dto로 변환 후 사용
나는 주로 1번으로 코드를 짰는데, 2번으로 코드를 짜는 이유는 무엇일까?
비즈니스 로직을 담당하는 Service 계층에서 View에 종속적인 Response Dto를 반환하는 것은 Service의 책임이 아니라 Controller의 책임이다.
이러한 이유로, 요청 Format이 변할 때 Service 계층까지 변경 영향이 미치는 것이 좋지 않다.
그래서 2번 방식으로 코드를 짜는 사람들이 있는데, 2번 방식에도 단점이 존재한다.
컨트롤러에 도메인이 노출된다는 점과 View에 반환할 필요가 없는 데이터까지 Domain 객체에 포함되어 Controller까지 넘어온다는 것이다.
WEB 요청
1. Controller에서 Request Dto를 그대로 Service에 전달하여 사용
2. Controller에서 Dto를 Entity로 변환 후, Service에 전달하여 사용
3. Service Dto를 따로 만들고, Controller에서 Request Dto를 Service Dto로 변환 후 전달하여 사용
이것도 마찬가지로 나는 1번의 방식을 주로 사용한다.
1번의 방식 같은 경우, 해당 Service
의 메소드는 해당 Controller
만 사용가능하다는 단점이 있다.
정해진 DTO만을 받기 때문에 Service
의 메소드를 활용하고 싶어도 불가능하다.
2번의 방식의 경우, 문제점이 존재한다.
복잡한 어플리케이션의 경우 Controller가 View에서 전달받은 DTO만으로 Entity를 구성하기는 어렵다.
Repository를 통해 여러 부수적인 정보들을 조회하여 Domain 객체를 구성할 수 있는 경우도 존재하기 때문이다.
그렇기 때문에 Controller에서 DTO를 완벽하게 Domain 객체로 구성한 뒤 Service에 넘겨주려면, 복잡한 경우 Controller가 여러 Service(혹은 Repository)에 의존하게 된다.
이러한 경우 DTO를 Service에게 넘겨주어 Service가 Entity로 변환시키도록 하는 것이 더 나은 방안이라 사료된다.
3번의 방식을 사용할 경우, Service Dto를 생성해야하므로 Dto 관리 범위가 더 커진다.
배달의 민족 블로그에서 사용하는 이유를 알 수 있었다.
외부 API가 컨트롤러 단에 존재할 때, Web에서 들어온 Request Dto
와 외부 API 호출 후에 Service로 전달될 Dto
의 포맷이 달라진다는 상황일 경우, 필요하다는 것이다.
즉, Controller에서 원하는 포맷과 Service에서 원하는 포맷이 다를때 쓰는 것이 좋다.
→ 결국 정답은 없기에, 프로젝트의 규모와 아키텍처 등을 고려해서 결정하는 것이 좋다!
현업에서는 보통 서비스에 DTO 진입을 허용하되 서비스 메소드 상위에서 DTO 체크 및 도메인 변환을 하고, 변환 후에는 DTO를 사용하지않고, 도메인만 사용하도록 구현한다고 한다.
초기에 도메인
으로 변환하지않으면 repository까지 진입하는 경우도 있다고 한다.
2번 아님 3번의 방식을 주로 사용하지만, 3번의 방식을 사용한다. 규모가 커질수록 Controller에서 원하는 포맷과 Service에서 원하는 포맷이 다를 때가 많기 때문이다.
application과 web layer는 양방향 의존을 해서는 안된다.
즉, web은 application을 알아도 되지만, application은 web을 알아서는 안된다.
쉽게 아는 방법은 import한 것을 보면 된다. web에 있는 request, response가 application에 있으면 안된다. 순수하게 격리되어 있어야한다.
가장 간단한 방법은 필드를 다 풀어서 적는 것이다. 너무 필드가 많으면 serviceDTO로 묶자!
serviceDTO를 따로 만드는 것이 아니라면, request와 response를 applicatin layer로 올리는 것이 좋다.
그래서 serviceDTO를 사용해서 리팩토링을 해보았다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
public static class PointCommand {
private Long partnerId;
private String barcodeNumber;
private Integer amount;
@Builder
public PointCommand(Long partnerId, String barcodeNumber, Integer amount) {
this.partnerId = partnerId;
this.barcodeNumber = barcodeNumber;
this.amount = amount;
}
}
근데 여기서 의문점은 규모가 작아서 그런지 request, response와 serviceDTO의 필드값이 모두 동일할 수 밖에 없었다. 솔직히 다른 경우를 체감하지 못하겠다..!
그럼 이런 경우에는 그냥 request와 response를 applicatin layer
로 올리는 것이 나을까?
멘토님의 코멘트가 필요하다! (추후 업데이트 예정)
처음 스프링을 배울 때 프로젝트로 바로 들어가서 개발을 시작했었는데, 이때 다른 여러 사람들의 코드를 보고 참고했었다.
DTO를 toEntity
로 변환하고, Entity를 toDto
로 변환하는 로직도 봤었다.
그러나 그때는 이걸 왜 이렇게 써야하는지 몰랐고, 그래서 지금까지 코드에서도 사용하지 않았다.
앞에서 말한 내용을 바탕으로 사용하는 이유를 알 수 있었다.
public static User toEntity(UserRegistrationRequest request) {
return User.builder()
.username(request.getUsername())
.password(request.getPassword())
.age(request.getAge())
.build()
}
앞서 말했듯이 대부분 예시와 같이 DTO 클래스 안에 toEntity
라는 staic Mapper 메소드를 만들어서 사용하는 것을 볼 수 있다.
하지만, DTO는 특별한 로직을 가지지 않는 것이 좋다!
예시처럼 코드를 짤 경우, DTO안에 toEntity 메소드를 만들거나 Entity안에 toDto 메서드를 만들어서 변환을 한다면, 둘 중 하나가 바뀌어도 코드를 수정해야한다.
이는 Controller
와 Service
의 의존을 만들게 된다.
그럼 어떻게 하는 것이 좋을까?
@Component
public class UserMapper {
public CreateUserResponse toDto(User user) {
return CreateUserResponse.builder().userName(user.getUserName()).build();
}
public User toEntity(CreateUserRequest dto) {
User user =
User.builder()
.userName(dto.getStudyName())
.build();
return user;
}
}
→ 다음과 같이 Mapper Class를 따로 만들어서 사용하자!
Mapper를 사용하면 DTO와 Entity 사이의 의존관계를 줄일 수 있고, 한 쪽에 수정이 발생해도 Mapper만 수정하면 된다!
mapper같은 경우는 각 dto에 toModel() 만드는 것이 낫다. 쉽게하는 방법도 있지만, mapper를 따로 만들면 복잡하다.
근데, 이게 toDTO()
을 만들면 결국, request가 entity안에서 import되게 된다.
그래서 mapper
를 사용해보기로 했다.
MapStruct를 사용해보자!
반복되는 객체 매핑에서 발생할 수 있는 오류를 줄일 수 있으며, 구현 코드를 자동으로 만들어주기 때문에 사용이 쉽다.
다음 블로그를 참고해서 작성했다.
lombok 라이브러리와 충돌이 있을 수도 있어서 lombok 설정 후에 mapconstruct 라이브러리를 설정해줘야한다.
그리고 @getter
, @builder
, @NoArgsConstructor
설정을 잘해줘야 자동생성된다.
API가 새로 만들어질때마다 각 API의 요청, 응답에 맞게 DTO를 새로 만들경우, 너무 많은 DTO가 생기는 것을 볼 수 있다.
이러한 문제를 해결하기 위해서 DTO를 Inner Class로 관리
하는 방법이 있다!
ublic class UserDto {
@Getter
@AllArgsConstructor
@Builder
public static class Info {
private int id;
private String name;
private int age;
}
@Getter
@Setter
public static class Request {
private String name;
private int age;
}
@Getter
@AllArgsConstructor
public static class Response {
private Info info;
private int returnCode;
private String returnMessage;
}
}
위와 같이 도메인 별로 관련되는 DTO들을 Class 하나에 묶게되면 클래스의 수도 줄어들고, DTO의 ClassName을 정하는 것도 수월해진다.
Controller와 Service 레이어 간 요청 & 응답 Dto 사용에 관하여