이번 포스팅에서는 최근 작업을 하면서 느꼈던 점을 정리해보려 합니다.
현재 프로젝트에서는 서버를 계층형 구조로 개발을 진행하고있는데,
다들 아시는것처럼 (모두 그런건 아니지만) 컨트롤러, 서비스, 레포지토리로 계층을 나눠서 각각의 역할에 맞게 개발하는 것을 말합니다.
이는 결합도와 응집도에 관한 문제인데, 그 부분 이야기는 다음에 하도록 하겠습니다.
우선 저는 작업을 하던 도중 하나의 의문이 들었습니다.
DTO 즉, 계층간에 데이터를 전달해 주기 위한 객체인 DTO의 사용범위에 대한 것이었는데요.
예를 들어, 클라이언트 <-> 컨트롤러 <-> 서비스 <-> 레포지토리 각 계층간에 데이터 전달을 위해 필요한 것입니다.
좀더 구체적으로 말하면, DTO와 엔티티간의 변환 위치에 관한 것 이었습니다.
여기에서 기존에 작성했던 코드를 보면
유저를 생성하는 작업을 생각했을 때,
클라이언트 -CreateUserDto-> 컨트롤러 -CreateUserDto-> 서비스
위와 같은 방식이었습니다.
클라이언트에서 컨트롤러로 DTO를 전달하고, 컨트롤러에서 그걸 그대로 서비스 함수를 호출할 때 전달만 해주는 방식인데,
여기서 의문점이 시작되었습니다.
첫번째로 컨트롤러가 서비스로 값을 그냥 전달만 하고 응답 객체 또한 서비스에서 반환받아 클라이언트로 보내주기만 한다면 컨트롤러를 굳이 분리하는 이유에 대한 의문이 들었고
두번째는 재사용성에 관한 것이었는데요, 사실 현재의 서비스에서는 대부분의 컨트롤러와 서비스가 1:1관계이기 때문에 크게 상관은 없지만, 확장성과 재활용성을 고려 해봤을 때, DTO를 서비스에서 전달받고, 다시 전달만 하게 되면 하나의 서비스는 하나의 컨트롤러에만 종속이 된다는 문제가 있습니다.
서비스에서 DTO <-> 엔티티 변환을 하는 경우
user.controller.ts
@Post()
async create(){
const result = await this.userService.create(createUserDto);
return HttpResponse.ok(res, result);
}
user.service.ts
async create(createUserDto: CreateUserDto){
const user = User.from(createUserDto);
const result = await this.userRepository.save(user);
return {
name: result.name,
email: result.email,
phone: result.phone
};
}
컨트롤러에서 DTO <-> 엔티티 변환을 하는 경우
user.controller.ts
@Post()
async create(@Body() createUserDto: CreateUserDto){
const user = User.from(createUserDto);
const result = await this.userService.create(user);
return HttpResponse.ok(res, new ResponseUserDto(result));
}
user.service.ts
async create(user: User){
return await this.userRepository.save(user);
}
물론 위의 예시코드는 너무 간략해서 겉보기에는 별차이가 없어보이긴 하지만,
컨트롤러는 클라이언트에서 받은 값을 그대로 전달만 하는게 아니라, 해당 서비스의 함수에서 필요한 엔티티 혹은 값들을 객체로 재구성해서 전달하고, 서비스는 엔티티에 값을 할당 혹은 저장하는 로직이 들어가는 것이 적절하지 않나하는 고민을 하고있습니다..
그리고 반환값도 해당 컨트롤러에서 응답해야 하는값 자체를 구성해서 반환해버리면 해당 컨트롤러에서 밖에 사용할 수 없기때문에 엔티티(도메인)자체를 반환하고, 컨트롤러에서는 받은 값을 그대로 보내는게 아닌, 위의 코드에 작성해놓은 것 처럼 리스폰스 클래스의 생성자를 사용해 재구성하여 반환하는것이 더 역할이 분리 되어있는게 아닐까 하는 고민을 요즘 하고 있습니다.
하나의 걸림돌은 서비스에서 엔티티를 파라미터로 받게되면 DTO클래스와는 다르게 엔티티 클래스안의 프로퍼티를 활용하는 경우에 그 값의 존재 유무를 코드만 보고 확신할 수 없게 되고, 이는 에러로 이어질 수 있다는 고민이 있긴합니다..
최종적으로 현재 프로젝트에서는 아래와 같은 형태로 타협하여 사용할 것 같습니다.
- 컨트롤러 -> 서비스: DTO 전달
- 서비스 -> 컨트롤러: 엔티티 반환
- 컨트롤러 -> 클라이언트: DTO 변환
작업을 하면서 고민되었던 부분이라 정답은 없고, 결론도 없이 글을 마치지만(개인적으로는 후자를 선호하지만)
비슷한 고민을 하셨거나, 어느정도 고민이 정리가 되신분들은 댓글로 마구 남겨주시면 감사하겠습니다.
'JavaScript & TypeScript' 카테고리의 다른 글
[NestJS] iterable 객체 병렬처리 하기(feat.Bluebird JS) (1) | 2022.10.09 |
---|---|
[NestJS] class-validator를 사용한 필수값 체크(feat.다음 우편번호 API) (0) | 2022.10.08 |
[NestJS] 이벤트 드리븐으로 기능 구현하기(feat.Event Emitter) (0) | 2022.09.18 |
[NestJS] 쿼리빌더를 사용한 코드 리팩토링 (feat.TypeORM) (0) | 2022.08.23 |
[NestJS] Many-to-Many 생성시에 할당하는 코드 리팩토링 (feat.TypeORM) (0) | 2022.07.12 |