서론
안녕하세요😎 백엔드 개발자 제임스입니다 :)
최근 한 커뮤니티에 질문이 올라왔습니다. 이 질문은 DTO와 관련된 내용이었습니다. 이전에 이와 관련해서 고민한 적이 있어서인지 질문에 관심이 갔습니다.
초기에 코드를 작성할 때마다, Controller(API 계층)에서 DTO를 처리하는 것이 클린 한 코드를 작성하는데 방해가 된다고 생각했습니다. 그러면서 'DTO를 Service 계층으로 바로 보내면 안될까?' 라고 고민했습니다. 뿐만 아니라 여러 문제도 겪게 되었습니다. 그래서 [채식이들] 프로젝트에서는 DTO를 Service 계층에서 처리하도록 적용했습니다. 결과적으로는 큰 문제는 없었을 뿐만 아니라, 걱정했던 문제들을 해결했습니다.
그러던 어느날 위 이미지에서 언급된 질문을 보게 되었습니다. 그리고 DTO 처리 범위에 대해 다시 한번 고민하게 되면서, DTO를 API(표현) 계층에서만 처리해야 하는 이유가 궁금해졌습니다.
DTO 클래스는 Controller(API 표현 계층)에서만 처리해야한다.
1. DTO란?
DTO는 Data Transfer Object로, 계층끼리 데이터 교환을 위해 사용하는 객체(Java Beans)를 의미합니다. DTO를 이해하기 위해서는 MVC 패턴에 대해서 알고 있어야 합니다.
MVC에 대해 간단하게 설명하자면, 애플리케이션 개발 시 구성 요소를 Model, View, Controller로 역할을 구분하는 디자인 패턴을 의미합니다. 여기서는 MVC에 대해서 자세하게 다루지 않겠습니다. 여기서 핵심은 Controller는 View의 요청 데이터를 Model로 보내거나, 반대로 Model에서 처리하는 데이터를 View로 다시 보내는 역할을 수행하는 것입니다. 즉, Controller는 View와 Model의 연결고리입니다.
이때 각 계층들이 데이터를 주고받기 위해 사용하는 것이 DTO입니다.
1.1 DTO의 사용
위에서 DTO의 역할을 언급은 했지만, 상황을 통해 조금 더 쉽게 설명하겠습니다.
(1) 회원 도메인
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
@Column(nullable = false)
private String memberName;
@Column
private String password;
@Column
private boolean emailReceivedStatus = false;
}
위와 같이 [회원] 도메인이 있습니다. 해당 도메인은 필드로 <식별자, 이름, 비밀번호, 이메일 수신 여부>를 갖습니다.
DTO를 이해하기 위해서 회원가입 상황을 예로 들겠습니다. 회원가입을 위해서는 회원의 이름과 비밀번호 정보만 필요로 합니다. 즉, 클라이언트는 회원가입 시에 이름과 비밀번호만 입력하면 됩니다. 위 코드에서 memberId(식별자)와 emailReceivedStatus(이메일 수신 여부)는 사용되지 않습니다.
(2) 회원가입 DTO
public class SignUpDto {
@NotEmpty(message = "이름을 공백없이 입력해주세요.")
@Pattern(regexp = "^[ㄱ-ㅎ가-힣]{2,5}$")
private String memberName;
@Nullable
@Length(min = 8, max = 16, message = "비밀번호는 8글자 이상 16 글자 이하로 작성해야 합니다.")
private String password;
따라서 위 코드와 같이, 회원가입 시에만 필요한 필드로 구성하여 새로운 객체를 만들었습니다.
(2) 회원가입 데이터를 주고받을 Controller 메서드
Cotroller에서는 View에서 전달하는 데이터를 JSON형태로 받게 됩니다. 이때 방금 만든 SignUpDto 객체에 맞게 데이터를 받을 수 있습니다.
응답으로 데이터를 전송할 때도 이와 같이 DTO 객체로 View에 전달합니다. 가령 패스워드 같이 중요한 데이터는 Response로 전달할 필요 없기 때문에 DTO를 활용합니다.
2. 꼭, DTO와 Domain 간의 변환 위치는 Controller(API 계층) 여야 하는가?
이제 본론으로 돌아와서 DTO의 처리 범위를 생각해보겠습니다. 사실 MVC를 더 자세하게 알아보면, Controller의 역할이 데이터를 주고받는 것뿐만이 아닌, 각 계층에서 사용할 수 있도록 변환하는 역할도 있습니다. 이에 따르면 DTO를 Controller에서 처리하는 것은 맞습니다.
그럼에도 이유를 분석하기 위해 직접 프로젝트에 적용하여 실험했습니다.
2.1 Controller에서 변환하는 경우
위 경우는 Dto를 컨트롤러에서 처리하는 경우입니다. 위와 같이 했을 때 몇 가지의 문제를 느꼈습니다.
1. Controller의 메서드가 복잡할 경우, 가독성이 떨어지거나 Service 의존도가 높아짐
2. 도메인의 불필요한 데이터들이 표현 계층인 Controller까지 넘어옴
3. 여러 Domain 정보를 조합한 DTO를 생성할 경우, 결국 Controller에 Service 로직이 포함됨
4. 테스트 코드 작성 시 시간비용이 더 들어감
5. Spring Rest Docs를 통해 API 문서를 만들 때 복잡해짐
2.2 Service에서 변환하는 경우
확실히 Controller에서 사용되는 코드는 간략해진 것을 확인할 수 있습니다. 동작에도 문제가 없습니다. 저의 생각으로는 Controller가 데이터 전달 역할만 수행하게 되면서, 단일 책임 원칙(SRP)을 따를 수 있을 것이라 생각했습니다. 즉, 데이터를 가공하는 모든 작업을 Service에 위임한 것인데요. 이로써 Controller는 의존 관계가 단순해졌습니다.
* Repository Layer에서의 DTO는?
Repository 계층은 Entity의 영속성을 관장하는 역할이라고 명시됩니다. 이로 인해, Repository 단에서 도메인의 데이터를 DTO로 변환하는 작업은 지양해야 합니다.
3. 결론
1) DTO는 Controller 계층에서 처리, Service 계층은 Entity 객체만 다룰 것
2) 필요에 경우 유연하게 DTO 사용
위와 같이 고민했을 때, Service Layer에서 DTO를 변환하는 것이 각 계층의 역할을 확실하게 분리할 수 있다고 생각했습니다. 뿐만 아니라 코드들의 가독성이 확실히 좋았습니다.
그럼에도 끝내 Controller에서 DTO를 처리해야 하는 이유를 발견했습니다. 확장 또는 Service를 응용하는 것이 쉽지 않았습니다. 가령 제품 도메인 컨트롤러에서 회원 도메인의 서비스를 필요로 하는 경우 메서드 사용 시 번거로움이 있었습니다. 즉, 한 컨트롤러에서 다른 서비스 로직을 사용할 때 결합하기가 쉽지 않았습니다.
참고 글에 따르면, DTO가 서비스에 들어오다가 자칫하면 Repository까지 진입하는 경우도 발생한다고 합니다.
Reference
위 내용을 분석하기 위해 [채식이들] 프로젝트에서 다양한 전략으로 Service, Controller를 구현했습니다. 초기에는 Service 계층에서 DTO를 모두 처리하도록 했습니다. 하지만 문제를 발견하고, Controller에서 처리하도록 모두 Refactoring 했습니다. 코드 확인은 아래 링크를 참고해주세요.
(1) [Service Layer에서 DTO 처리]
(2) [Controller Layer에서 DTO 처리]
'개발 > Spring&JPA' 카테고리의 다른 글
[스프링 시큐리티] WebSeucrityConfigurerAdapter Deprecated 대처 (2) | 2023.01.25 |
---|---|
[스프링] 스프링(Spring)과 스프링 부트(Spring Boot) (2) | 2023.01.19 |
[JPA] Spring Data JPA의 DB 초기화 (2) | 2022.10.02 |
[스프링] 백엔드 개발자의 필수 과제, '순환 참조(Circular Reference)' 문제 해결 (4) | 2022.09.18 |
[JPA] JPA에서 Spring Data의 Audit 기능 적용하기 (2) | 2022.09.07 |