개요
현재 회사에서는 하나의 객체를 만들어 모든 레이어에서 사용하거나 Controller Layer에서 HashMap으로 받고 있습니다.
추가적으로 제약조건으로 Post Method만 허용합니다.
회원 (코멘트) API를 예로 들어 DTO로 분리해 보겠습니다.
AS-IS
1. 회사에서 사용하는 VO (모든 레이어에서 사용)
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonNaming(UpperSnakeCaseStrategy.class)
public class UserVO {
private Long USER_ID;
private String NAME;
private String COMMENT;
private String EMAIL;
private String PHONE_NUMBER;
private String LOGIN_FAIL_COUNT;
private List<UserVo> EDIT_USER_LIST;
private List<UserVo> ADD_USER_LIST;
}
TO-BE
- 위 상황이라면 요청과 응답은 DTO로 분리합니다. RequestDTO, ResponseDTO, MapperDTO
- DTO는 목적에 따라 각기 정의해서 사용하는 것이 좋습니다.
- DTO 이름에서 이 DTO가 어떤 용도로 사용되는지 드러나야 합니다.
1. [요청] UserRequest DTO (Controller → Service 전달)
- 사용자 코멘트만 입력받는 요청
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@JsonNaming(UpperSnakeCaseStrategy.class)
public class UserRequest {
@NotNull
private Long userId;
@NotBlank
private String comment;
@Builder
public UserRequest(Long userId, String comment) {
this.userId = userId;
this.comment = comment;
}
}
2. [Mapper DTO] User Mapper DTO (Service <-> Mapper 사용)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@JsonNaming(UpperSnakeCaseStrategy.class)
public class User {
private Long userId;
private String name;
private String comment;
private String email;
private String phoneNumber;
private int loginFailCount;
@Builder
public User(String name, String comment, String email, String phoneNumber, int loginFailCount ) {
this.name = name;
this.comment = comment;
... 중략
}
public static User createUserComment(UserRequest userRequest) {
return User.builder()
.comment(userRequest.getComment())
.build()
}
public static UserResponse toUserCommentDTO(UserResponse userResponse) {
... 중략
}
}
3. [응답] User Response DTO (Service <-> Controller 전달)
- 사용자 코멘트만 반환하는 응답
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@JsonNaming(UpperSnakeCaseStrategy.class)
public class UserResponse {
@NotNull
private Long userId;
@NotBlank
private String comment;
}
정리
1. DTO를 왜 사용할까요 ?
- 캡슐화 : 필요한 정보만을 노출하고자 사용합니다.
- UI에서도 필요하지 않은 정보를 담고 있어 도메인 모델의 모든 property가 외부로 드러납니다.
- 관심사 분리
- MVC패턴의 각 Layer별로 관심사가 다르며, 분리하지 않으면 모든 Layer를 가로지르는 god class가 됩니다.
- 모든 Layer에서 도메인 모델을 사용하면 결합도가 높아집니다.
- 결국 여러 Layer를 거치는 god class는 변경이 생긴다면 거치는 Layer에 모두 영향을 주게 됩니다.
- SRP(단일책임원칙) 위반
📘 왜 DTO를 사용해야 하는지 추가로 읽어보면 좋을 내용
- 한 번의 호출로 해당 관련된 모든 데이터를 담은 객체를 리턴 받아 사용하는 것입니다.
- 즉, api 호출하는 네트워크 비용이 비싸므로 DTO를 통해 Controller에서 데이터를 조합 해옵니다.
- 설령, Mybatis Mapper 내에서 Join 통해 Domain Model에 모두 담아서 API를 한 번에 처리한다 할 지라도
결국 null인 필드는 존재할 것이고 이는 유지보수하기 어려워집니다.
안녕하세요. 별님 좋은 질문입니다.
항상 유지보수에서 가장 문제가 되는 것이 바로 애매한 것입니다. 특히 같은 필드인데, 어떤 경우에는 null이고 어떤 경우에는 값이 있고 이렇게 모호하면 정말 유지보수가 어려워집니다.
그래서 API 응답 스펙이 정해지면 그 필드에 값은 항상 같은 원칙으로 반환되도록 명확하게 설계하는 것이 중요합니다.
클래스를 여러게 만들더라도, 코드가 중복되는 것 처럼 보일지라도, 명확한 것이 훨씬 더 나은 선택이라는 것이지요.
다만 API를 제공할 때 또 모든 케이스에 대응해서 만들면 API 자체가 너무 많아집니다. 그래서 고민하신 내용에서 null 값 대신에 실제 값을 채워서 반환하는 API를 제공하는 것이 좋습니다.
예를 들어서 username, age 둘다 제공하는 공통 API 하나를 여러곳에서 사용하도록 제공하는 것이지요.
실무에서 API를 설계할 때 진짜 고민은, 생각보다 너무 복잡하다는 것입니다.
어떻게 보면 제공 단위를 크게 만들어서 모든 데이터를 다 반환하는 API 하나를 만들면 될 것 같지만, 이렇게 너무 공통화해도 유지보수가 어렵고, 성능 이슈가 있습니다. 반대로 너무 각각의 케이스를 대응하도록 만들어도 API 자체가 많아져서 유지보수가 어렵습니다.
이 사이에서 적절한 단위로 API를 설계하고 제공하는 것이 묘미이지요^^!
제가 선호하는 방법은 기본 공통 API를 제공하고, 이 기본 공통 API로 해결이 안되는 특수한 경우에 한해서 별도의 API를 제공하는 방법을 선호합니다.
도움이 되셨길 바래요^^
출처: [인프런] 김영한 님, https://www.inflearn.com/questions/72423/dto
2. DTO 장점
- 도메인 모델을 캡슐화 하여 UI에 필요한 property만 제공할 수 있습니다.
- 즉, Frontend 입장에서도 어떤 property를 담아야 하고 어떤 응답을 줘야하는지 알 수 있습니다.
- 뷰와 모델 사이의 결합도를 느슨하게 만듭니다.
- Spring의 경우 RequestDTO로 분리하면 Spring Validation API를 사용하기 쉽습니다. (Validation Check)
3. DTO <-> Domain Model 변환 시 static 함수를 사용했는데,
별도의 변환(Convert) 클래스 모아서 사용하는 방법은 ?
- 지양, 따로 모아서 클래스를 작성하면 common 패키지에 들어가야할 것처럼 보이지만,
변환 로직 자체만으로는 각 도메인에 있어야 할 것 같고, 모호함이 발생합니다. - 정답은 없으니 보통 팀 컨벤션에 따릅니다.
- 마찬가지로 static도 의견 차이가 있습니다.
- static 쓰지 말자 : static 사용함으로써 오는 메모리 부담이 있다. 인스턴스의 메서드 호출하자
- static 쓰자 : new 없이도 바로 호출이 가능하기 때문에 빠르다.
참고 글
- [1] DTO는 왜, 언제 사용할까?
- [2-댓글참고] 어설픈 객체지향은 안티패턴을 만들어낸다.
비즈니스 로직에 DTO ~ Entity 변환로직을 분리해야하는 문제제기에는 동의합니다. (물론 modelmapper로 스트림 사용하여 간단하게 변환할때가 태반이지만요)
그러나 작성하신 해결책은 동의하기 어렵습니다.
DTO는 pure한 데이터를 소유한 객체입니다. (DTO is an object without methods, it is pure data.)
여기에 entity 변환 로직이 들어가는 순간 글쎄요.. 적어도 우리 회사 개발자들에게 DTO에 엔티티 변환 로직이 있을거라고 생각하는 사람은 없을것이며 상당히 어색한 코드입니다. DTO 의미를 보아도 레이어간 데이터 전달을 위한 오브젝트에 엔티티 변환 역할이있다라고 하기에는 역할 책임 관점으로도 받아들이기 어렵습니다.
또한 엔티티가 변경된 경우 관련 DTO를 전부 변경해야하는 Shotgun Surgery가 발생합니다.
만약 객체지향 의미를 살린다면 transter object 혹은 mapper 클래스에서 entity와 DTO간의 변환 로직을 위임할 것입니다. 그렇다면 DTO 혹은 엔티티가 변경되더라도 오직 변환 클래스만 변경하면 되겠죠.
감사합니다.
'Spring' 카테고리의 다른 글
스프링 시큐리티 - Method Security (0) | 2023.09.15 |
---|---|
Spring Boot + Nuxt.js 환경에서의 FCM 웹 푸시 구현 (1/2) - Spring Boot편 (0) | 2023.04.15 |
@Transactional Annotation 정리 (0) | 2022.09.13 |
빈 등록 어노테이션 @Configuration, @Component, @Bean에 대해서 (2) | 2022.08.27 |
세션 vs JWT (0) | 2022.06.21 |