Service-Layer Pattern
Service-Layer Pattern는 비즈니스 로직의 분리를 목적으로 한다.
다른 계층에 영향을 주지 않고, 특정 계층만 수정하고 확장할 수 있기 때문에, 새로운 기능을 개발하면서 확장하거나 유지 보수할 때 유리하다는 장점이 있다.
Controller
- 클라이언트의 요청을 수신하여 (request body) 비즈니스 로직이 구현된 서비스 계층을 호출한다.
- 서비스에서 반환된 데이터를 받아 클라이언트에게 응답한다.
- 단지 요청을 받아 클라이언트에게 HTTP 응답 처리를 진행한다. (routing)
Service Layer
- 비즈니스 로직, 캡슐화 및 추상화가 이루어지는 곳이다.
- Controller로 부터 전달된 **요청에 로직을 적용하는 계층이다.**
- Repository 계층을 호출하여 통해 데이터 CRUD 작업을 설정하는 작업을 위임한다.
- Service 계층에서 에러를 던지는 것이 보편적이다.
Repository Layer (Data Access Layer)
- 쿼리를 수행하면서 실제로 DB와 상호작용 한다.
- Service 계층에서 데이터베이스 접근이 필요한 경우 발생하는데, 데이터에 대한 직접적인 코드가 작성된다.
- Node.js의 경우 Prisma와 같은 ORM을 통해 DB 작업을 수행할 수 있다.
어떻게 통신할까?
클라이언트에서 요청을 하면 -> Controller가 그 요청을 받는다. 그 요청을 Service로 전달
-> Service는 DB가 필요한 경우, Repository를 통해 DB와 상호작용해 얻어낸 결과로 비즈니스 로직을 처리
-> 그 후 Service가 그 결과를 Controller에게 전달하고 응답을 클라이언트에게 내려준다.
에러와 관련된 데이터의 처리 흐름 (내 생각..)
- 커스텀 에러 객체 작성 (error.js)
- 커스텀 에러 클래스를 만들어서 애플리케이션에서 발생 가능한 오류를 HTTP 상태코드와 함께 정의해둔다.
- 서비스 계층에서 에러 객체를 던지기
- 서비스 계층에서 특정 상황들
- e.g) 비즈니스 로직 오류 (duplicateemailerror)
- DB 관련 오류 (레포지토리에서 데이터를 조회할 수 없거나 유효하지 않은 값이 들어오는 경우)
- 유효성 검사 실패
- 이 발생 했을 때, 서비스 계층에서 에러 객체를 throw한다.
- 이렇게 던진 에러들은 컨트롤러에서 next(error)를 통해 전달되어서, 공통 처리 미들웨어로 넘어간다.
- 서비스 계층에서 특정 상황들
- 컨트롤러에서 에러를 던지기
- HTTP 요청, 응답에 관련된 오류를 처리한다. 즉, 클라이언트의 요청에 문제가 있을 경우.
- 컨트롤러에서 에러 객체를 next(error)로 넘기면, index.js의 공통 에러 처리 미들웨어로 전달된다.
- 공통 응답 미들웨어, 에러 처리 미들웨어
- 공통 응답 미들웨어는 모든 API응답을 통일된 형식으로 반환한다.
- 에러 처리 미들웨어는 애플리케이션에서 발생한 모든 오류를 처리하는데, 오류가 발생하면 res.error()를 호출해 에러코드와 메세지를 반환한다.
- 에러가 커스텀 에러일 경우, 해당 오류에 맞는 errorCode, reason, data를 포함해 응답을 보낸다.
DTO (Data Transfer Object)
DTO는 "데이터를 옮기는 객체" 역할을 한다.
계층 사이 사이 간 데이터를 전송할 때 사용된다.
또한, 클라이언트로부터 전송 받은 데이터를 객체로 변환할 때도 사용된다.
말 그대로 데이터를 전송하기 위해 사용하는 객체여서 그 안에 비즈니스 로직과 같은 복잡한 코드는 없고, 순수하게 전달하고 싶은 데이터만 담겨있다.
DTO를 사용해야 하는 이유
- 도메인 model을 캡슐화 하고, UI 화면에서 사용하는 데이터만 선택적으로 보낼 수 있다.
- DTO는 클라이언트의 요청에 포함된 데이터를 담아 서버 측에 전달하고, 서버 측의 응답 데이터를 담아서 클라이언트에 전달하는 계층간 전달자 역할을 한다.
- 데이터를 받아오는 과정에서 데이터 하나가 빠질 수도 있고, 원하는 형태로 전달되지 않을 수도 있는데 그럴 때 유효성 검사를 통해 오류가 바생하지 않도록 할 수도 있다. 외부에서 전달되는 데이터가 달라질 경우 DTO의 내용만 수정하면 Service를 수정하지 않아도 되어 편리하다.
- Service에서 req.body의 json 객체를 바로 사용하는 대신, Controller에서 req.body를 dto 형태로 받아 service에게 dto로 값을 전달한다 라고 생각하면 된다.
DTO를 사용하는 목적
- 불필요한 정보 제공 방지
- DB에서 직접 가져온 엔티티 객체를 외부 그대로 노출한다면 민감하거나 불필요한 정보까지 노출될 수 있다.
- DTO를 사용하면, 외부에 노출할 즉, 필요한 데이터만 포함하도록 할 수 있다.
- 무한 루프 방지
- 엔티티에는 관계형 데이터베이스의 특성 상 서로 참조하는 관계가 있을 수 있다,
(A 객체가 B 객체를 참조하고, B 객체가 다시 A 객체를 참조) - 이런 관계를 그대로 직렬화 하여 전송하려고 하면, 순환 참조로 인해 무한 루프에 빠질 수도 있다.
- 따라서 DTO를 사용해 필요한 정보만 분리해 전송하면 이런 무한 루프 문제를 방지할 수 있다.
- 엔티티에는 관계형 데이터베이스의 특성 상 서로 참조하는 관계가 있을 수 있다,
DTO를 어디서 만들어야 할까?
- Service에서 DTO 생성
- Service에서 DTO를 만들어 내는 경우 (Service->Controller 일 때 Controller는 DTO를 받아 사용)
- 이 경우, DTO로 데이터를 걸러 Controller로 전달하여 Controller에서 작업할 때 DB의 일부 민감한 정보를 숨길 수 있다.
- DTO를 Service에서 생성함으로써 Controller에서 클라이언트로 일관된 데이터를 반환할 수 있어, Controller는 요청 데이터 전달과 응답 전송에만 집중할 수 있다.
- DTO 생성 로직을 독립된 함수로 작성해서 여러 서비스 로직에서 재사용 가능하다.
- Controller에서 DTO 생성
- Controller에서 DTO를 만드는 경우 (Controller->Service 일 때, Service는 DTO를 받아 사용)
- 이 경우, 서비스 함수의 범용성이 늘어나서 유지 보수에 용이하다.
Controller는 단순히 요청 데이터를 전달하고 응답을 반환하는 역할에 집중하고,
Service는 비즈니스 로직과 데이터 조합, 변환으로 작업을 분리하는것이 유용한것 같다.