DTO란?
Data Tranfer Object의 약자, 계층 간 데이터를 전송 시 사용하는 객체입니다.
왜 사용할까?
데이터를 전송하게 될때 하나의 값을 따로 보내게 되면 메서드 호출이 많아지는데, 이러한 문제점을 해결하기 위한 다른 방법으로 파라미터의 수를 늘리는 것이죠, 하지만 파라미터 수를 늘리게 되면 두 메서드 간 결합도가 높아져 확장 및 사용하기가 쉽지 않습니다. 따라서 하나의 객체로 보내기 위해 DTO를 사용하게 됩니다.
어떻게 사용하는데?

위와 같은 layer를 기준으로 회원을 처리하는 서비스를 하고 있고,
public class User {
private String id;
private String name;
private String password;
private List<Role> roles;
public User(String name, String password, List<Role> roles) {
this.name = name;
this.password = encrypt(password);
this.roles = roles;
}
private String encrypt(String password) {
// logi
return null;
}
}
enum Role {
FMAILY,
SILVER,
GOLD,
PLATINUM,
DIAMOND
}
User domain이 위와 같이 구성되어 있습니다.
UI에서 User의 정보를 가져온다고 하면 어떻게 가져올 수 있을까요?
user의 정보는 user domain이 가지고 있고, 여러 데이터를 가져오게 되므로 객체를 통해 가져오게 됩니다.
자 그럼, Domain Object를 사용하면 되는 거 아냐?
즉, User 객체를 직접 전달 받아서 사용하면 되는거 아니야?라고 생각할 수 있는데요.
public User createUser(User user) {
// logic
}
이렇게 받게 되면 약간의 문제가 생기게 됩니다.
1. 불필요한 데이터
반환할 때부터 봐보겠습니다. 과연 비밀번호를 다시 제공해 줘야 할까요? 비밀번호와 같은 데이터가 들어가 있게 되면 UI에서 접근이 가능하기 때문에 보안상 위험이 있게 됩니다.
입력의 경우, 입력에 대한 값이 domain 속성 값들에 저장하지 않는 값이라면?
예를 들어, 점심 메뉴를 랜덤하게 추천받기 위해서 추천받을 메뉴의 수와 종류를 받게 된다고 하면, domain에 종류를 넣겠지만 메뉴의 수까지 가지고 있을 필요가 있을까요?
즉 입력 요구 사항과 domain의 속성 값들이 다른 경우가 있습니다.
아니 그럼 보안상의 위험이 있으면 반환해 줄 때 값을 처리해서 보내주면 되는 거 아냐?
-> 가능은 하겠다만, 사람이 하는 일이고 다른 사람이 내가 했던 작업을 수행하게 되었을 때 전부 처리할 수 있다?
이거는 좀 힘들다고 보기 때문에 아예 분리하는 게 더 안전하다 봅니다.
2. UI와 Presentation layer간 강결합
입력 요구 사항과 domain이 유사한 경우도 많을 텐데, 이때 presentation layer와 UI간 결합이 강해지는 문제가 발생합니다. 이는 UI의 요구 사항이 변경되게 되면 domain Object또한 변경이 되는 문제가 생깁니다.
자 이제 이러한 이유 때문에 DTO를 사용한다고 가정해 보겠습니다.
어떻게 만들 수 있을까요?
위의 예시를 생각해 보면,
요청의 경우: 사용자의 이름과 비밀번호, 어떤 유저로 가입하는지에 대해 필요하고
반환의 경우: 사용자의 이름과 역할만 필요하다면
public class UserRequestDto {
private final String name;
private final String password;
private final List<Role> roles;
public UserRequestDto(String name, String password, List<Role> roles) {
this.name = name;
this.password = password;
this.roles = roles;
}
}
public class UserResponseDto {
private final String name;
private final List<Role> roles;
public UserResponseDto(String name, List<Role> roles) {
this.name = name;
this.roles = roles;
}
}
이 처럼 받을 수 있겠습니다.
그런데 여기서 의문이 생길 수 있습니다.
어 근데 데이터를 받아오게 되면 예외처리는 어디서 하지?
이를 위해서 먼저 DTO의 역할에 대해 다시 상기시켜 보겠습니다.
DTO의 역할
- 데이터를 전달시켜야 한다.
- 필요한 데이터만 감싸고, 불필요한 데이터는 삭제한다.
결국 UI에서 Presentation layer의 상호 데이터 전달을 위해서 DTO를 사용했기 때문에 전달
이라는 역할만 하는구나라고 생각할 수 있습니다.
그럼 당연히 비지니스 로직은 들어갈 수 없겠네?라고 생각할 수 있죠. 그렇기 때문에 데이터 자체가 변경될 일이 없으므로 final을 통해 데이터를 불변으로 만들었습니다.
DTO에서 예외처리를?
그다음으로 생각해야 할 것은 예외처리가 비지니스 로직인가?입니다.
예외도 예외 방식에 따라 여러 개가 있을 수 있다고 생각합니다. 예를 들어 password를 입력받게 되면, password에 대한 예외로는 값이 제대로 들어왔는가, password 형식이 맞는가 등이 있겠죠, 근데 사람마다의 관점은 다르겠지만, 값이 제대로 들어왔는가는 데이터에 대한 예외이고, password 형식은 User라는 객체가 담당해야 하는 예외가 아닐까 싶네요.
즉, DTO는 dto가 할 수 있는 예외를 처리를 하고 domain 객체도 domain 객체가 할 수 있는 예외를 처리한다 생각하면, 상황에 따라 다르긴 하겠지만 DTO와 Domain 두 곳에서 전부 하는게 맞지 않을까?라는 생각을 했습니다.
사실 여기까지만 생각했었습니다. 김영한님의 답변을 보고 더 찾아본 결과, 흥미로운 글을 발견했습니다.
요점은, 위에 역할은 사실 당연한거지만 이는 layered architecture를 만족하는 경우에 대한 것이지 다른 상황도 고려해야 한다 였습니다.
RequestDto를 통해 데이터를 받고 비지니스 로직에서 Dto를 domain object로 변경을 할 때, 사전 작업을 하는 경우가 생기는데 이때까지는 requestDto의 값에 올바른 값이 들어와 있는지 확인할 겨를이 없다는 것입니다.
예시를 생각해 봤습니다.
만약 사용자의 email을 통해 db에 이메일이 있는지 확인하고 없으면 새로운 사용자를 만들어 줄 수있다는 상황이라고 하면,
public User createuser(UserRequestDto userRequestDto) {
Optional<User> user = repository.findByEmail(userRequestDto);
if (user.isPresent()) {
throw new Exception();
}
User creatUser = userRequestDto.toUser();
repository.save(creatUser);
return creatUser;
}
위와 같이 간단하게 나타낼 수 있는데, 이때 email에 들어가서는 안될 데이터가 들어가거나 잘못된 데이터가 들갈 수 있기 대문에 안전하지 못하다는 것입니다. 이러한 경우 먼저 변환하면 되는것 아니냐 할 수 있지만, 이러한 가능성 자체가 나중에 오류를 발생시킬 수 있다고 생각을 하기때문에 이러한 예외를 아예 없애려면 DTO에서 검증을 해야 한다는 생각도 이렇게 본다면 좋을 거 같습니다.
이러한 관점에서 본다면 그럼 domain object에서는 필요 없는거 아냐? 라고 생각할 수 있지만, 그건 또 아닌게 이 부분은 입력 데이터에 대해 안전한 비지니스 로직을 동작하기 위해서이고, dto를 통해서만 domain을 생성하는 것은 아니기 때문에 둘다 해줘야 한다고 생각합니다. 이렇게되면 중복 로직이 발생하게 되는데, 하나의 클래스에서 관리한다 던지, 아님 상속을 통해 부분 override한다던지 추가적인 처리를 해서 사용해야할 것 같습니다.
*추가: 생성자에서 예외처리를 해야 하는가?
생성자는 생성의 역할을 가지고 있는데, 어떻게 보면 데이터가 가져와서 그 데이터를 생성하기 위해 예외를 처리하는 것이니까 예외 처리를 할 수 있다고 생각합니다. 하지만 반대로 생성자는 생성 자체만을 가지고 있다고 생각한다면 of()함수등을 통해 예외처리를 한 후 생성을 하고, 생성자는 private으로 접근을 막아야 하는 게 아닌가라고 생각을 하는데, 현재는 생성자에서는 생성만 하고, of함수를 통해 예외 처리 후 생성을 하는 것이 맞다고 생각합니다.
변환은 누구의 책임?
이제 DTO를 통해 데이터를 받고 예외처리까지 하였습니다. 그러면 이제 생각해야 할 것은 변환을 하는 메서드는 누가 가지고 있어야 하는가?에 대해서 생각해 볼 수 있습니다.
크게 4가지를 고민해 볼 수 있을 거 같습니다.
- Domain Object
- to DTO와 to Entity의 분리
- DTO
- 새로운 Mapper 클래스
[Domain Object]
domain Object는 domain에 대한 처리나 하는 곳이고 비지니스 로직을 처리하기 때문에 dto를 알아야 하나?라고 생각을 할 수 있으며 관심사 분리가 제대로 일어나지 않았다고 생각합니다.
또한 역할적으로만 봐도 DTO는 필요한 데이터만 뽑아서 wrap해주는 역할을 했는데 domain에서 dto에 대해 알아야 하는 것이 맞는가?라고 생각을 해봐도 아니라고 생각을 하며
domain에 여러 dto가 존재하게 된다면 코드 자체도 보기 어려워질 수 있다고 생각합니다.
따라서 Domain Object 내에서 처리하는 것은 지양해야 한다고 생각합니다.
그럼 DTO에서 처리하는 것과, 새로운 Mapper 클래스가 남았습니다.
[DTO]
dto의 역할은 domain object 자체를 감싸는 역할을 하긴 하지만, 걸리는 것은 변환이 비지니스 로직인가?와 데이터 전송만을 한다는 역할을 부수는 것은 아닐까? 라는 생각을 할 수 있습니다.
데이터를 전송하는 과정에서 변환이 필요하다는 생각을 하면 가질 수 있다고 생각하며, 하나로 관리했을 때보다 크기가 작기 때문에 해당 DTO에서만 변경을 하면 되겠지만, 많은 DTO가 생기게 되면 domain이 변경될 때 마다 dto들에서 변경을 해 줘야 한다는 단점이 있을 것 같습니다.
public class UserRequestDto {
// code ...
public User toUser() {
return new User(id, name, password, roles);
}
}
public class UserResponseDto {
// code ...
public static UserResponseDto toDto(User user) {
return new UserResponseDto(user.name(), user.roles());
}
}
[Mapper]
반면 mapper의 경우 하나의 클래스에서 관리하게 되므로 하나의 클래스에서 모든 dto를 관리할 수 있고, dto들이 많아지더라도 한 곳에서 관리할 수 있어 좋다고 생각합니다. 다만 util 클래스나 혹은 spring을 사용한다면 빈을 통해 싱글톤으로 관리해야 하지 않을까 싶습니다.
이 부분에 대해서는 아직 저만의 결론을 내리지는 못했지만, DTO나 Mapper중 사용할 것 같습니다.
거의 다 왔습니다.
그럼 어디에서 변환을 해줄 것인가가 또 다른 쟁점이 될 것 같습니다.
변환은 어디서?
우리가 데이터를 변환할 수 있다고 생각하는 곳은 크게 presentation layer
, business layer
, persistence layer
가 있다고 생각합니다.
근데 DAO에서 이걸 변환한다고 생각하기엔 domain의 영속성을 관리하는 layer라 변환하기도 하고, presentation layer에 대해 알게 되는 것이라 생각하기 때문에 알맞지 않다고 생각합니다.
[Presentation Layer]
presentation layer의 경우 controller와 같은 UI와 Business layer를 연결해 주는 통로라고 생각하기 때문에, 통로의 역할만 한다고 하면 presentation layer에서 하는 것은 적절치 않다고 생각합니다.
비슷한 동작을 하는 DTO가 여러 개 일 경우 하나의 DTO로 묶어서 사용할 수 있고 이때 필요한 데이터가 다르므로 presentation layer에서 분리를 한 후 service layer로 보내면 되는 거 아니냐? 라고 생각할 수도 있습니다.
여기에 대해서는 서로 필요한 데이터가 완전히 다른 경우에는 애초에 분리하는 것이 맞다고 생각하며, 데이터가 비슷한 경우 또한 controller에서는 연결해 주는 역할만하고 service에서 필요한 데이터를 뽑아서 사용하면 되는 것이 아닐까라고 생각합니다.
물론 DTO가 필요한 데이터만 필터링하는 역할을 하니까 presentation에서 나누고 좀 더 정확히 필요한 데이터만 service로 보내야 한다는 관점에서도 생각을 해보면, 파라미터로 보내게 되면 나중에 길어지게 되면 너무 많은 파라미터가 필요로 하게 된다는 생각을 할 수 있고, 객체로 보내게 된다고 하면, 또 다른 이번에는 presentation과 business layer를 연결해 주는 dto를 만들게 되는데, 오버엔지니어링이 아닐까 라는 생각을 합니다.
[Business layer]
따라서 business layer에서 dto를 domain object로 변환하여 사용하는 것이 더 좋지 않을까 생각합니다.
마지막으로 생각해 볼 문제는 dto를 사용자의 입력마다 만들어야 할까? 에 대한 것입니다.
이렇게 되면 dto에 대한 클래스와 mapper의 수가 늘어날 뿐 아니라 재사용 및 데이터 변경에 취약하게 된다고 생각하기에 기존의 것을 재사용하는 것이 좋다고 생각합니다.
또 다른 접근
지금까지 위에서 설명했던 것은 어떻게 보면, 입력의 관점에서 봤는데 사실 중요한 것은 도메인 관점이 아니였을까 에서 시작합니다. 도메인 관점에서 보면 위의 설명과 같은 말이지만 반대로 보면 내 도메인을 어디까지 알려줄 것인가?라고도 생각해볼 수 있을 거 같습니다.
제가 구현하면서 생각해 보았던 점들을 주위 분들과 얘기를 나누고 정립한 제 생각을 작성해 보았습니다. 결국 정답은 없다고 생각을 하고, 이 글을 보시는 분들도 생각하시는 점을 서로 공유하면 좋을 것 같습니다!!
결론
- DTO는 데이터를 전송 및 데이터를 감싸 필요한 데이터만 전달하기 위해 사용한다.
- DTO에서 비지니스 로직을 처리하는 것이 아니기 때문에 변환될 필요가 없다 따라서 fianl을 통해 불변을 만들었다.
- 각 객체에 맞는 예외처리가 있다고 생각하기에, 데이터 자체에 대한 예외, 즉 값이 null인지 등은 DTO에서 하는 것이 맞지 않을까 생각한다.
- 또한 중요한 도메인의 경우나 비지니스 로직에서 dto에서 domain으로의 변환 과정전에 추가적인 작업이 있다하면 requestDto에서 domain에 대한 검증 로직을 추가하여 예외 발생 가능성을 없애는 것도 좋지 않을까?
- 중복된 예외 로직은 하나의 클래스에서 관리하여 같이 사용 or 상속을 통한 알맞는 데이터 검사
- 변환하는 메서드는 DTO나 Mapper가 더 좋다 생각한다.
- 변환하는 과정은 businsess layer
- dto가 많아질 대는 재사용하는 것은 어떨까?
참고 링크
검증 로직 Entity는 어떻게 사용하면 좋을까요? - 인프런 | 질문 & 답변 (inflearn.com)
계층화 아키텍처 (Layered Architecture) (hudi.blog)
Gunther Popp의 블로그 : DTO로 또는 DTO로 ...
P of EAA: Data Transfer Object (martinfowler.com)
DTO의 사용 범위에 대하여 (techcourse.co.kr)
'잡다한것' 카테고리의 다른 글
TestContainer (0) | 2023.07.07 |
---|