유동
[NodeJS]효율적인 Express 프로젝트 구조(TypeDI와 레이어드 아키텍처) 본문
이번 프로젝트에서 typedi를 이용해서 express/prisma 환경에서 레이어드 아키텍처를 적용했습니다.
레이어드 아키텍쳐가 무엇이고, 왜사용하는지는 저번 글에서 다루었습니다만 [NodeJS] Express + Inversify DI (3 layerd architecture)
예전 글이기도 하고, 부족한 부분이 많은것같아 다시한번 간단하게 설명하겠습니다.
- 간단하게 말하자면 한 함수 (또는 메서드)에서 모든 로직을 처리하는것이 아니라 (app.use에서 모든로직 처리하는 그거) 역할별로 관심사를 나누자는 모토로 나온 개념입니다.
- 웬만한 REST-API 백엔드 어플리케이션에서의 요청 흐름을 간단하게 그려보았습니다.
서버에서는 클라이언트의 요청을 받아 적절한 응답을 내려줍니다. 응답을 내려주기까지 수많은 작업들이 존재하겠죠. 일반적인 어플리케이션 서버는 거의 할일이 정해져있습니다.
- 클라이언트로부터 온 값 검증(validation)
- 비즈니스 로직을 돌리기에 적합하게 값을 변형(transform)
- 비즈니스 로직 수행 (데이터베이스 쿼리 포함)
- 요청 반환
이걸 코드로 보면 다음과 같습니다
한 라우터(함수)에서 위에 나열한 절차들을 전부 수행하고 있습니다. 소규모 어플리케이션 개발에는 좋은 선택지일수도 있으나. 기능이 점점 많아질수록 수직적 확장이되며 중복 코드가 생기고 결국엔 유지보수하기 힘들어지는 코드가 만들어질겁니다.
레이어드 아키텍쳐를 적용한다면 다음과 같이 코드가 바뀔수 있습니다
- controller의 역할을 하는 클래스 혹은 함수를 만듭니다. 이 클래스 혹은 함수는 다음과 같은 일을 처리합니다
- 클라이언트로부터 온 값을 검증합니다.
- 쿠키 또는 헤더에서 값을 추출해 인가&인증을 수행합니다
- service의 역할을 하는 클래스 혹은 함수를 만듭니다. 이 클래스 혹은 함수는 다음과 같은 일을 처리합니다
- 비즈니스로직을 수행합니다.
- 레포지토리 레이어에 데이터베이스 접근 로직이 매핑되어있는 메서드들을 조합하여 비즈니스 로직을 완성합니다
- 이후 클라이언트에 응답을 내려주기 위해 가공을 합니다 (repsonse dto같은)
- repository의 역할을 하는 클래스 혹은 함수를 만듭니다. 이 클래스 혹은 함수는 다음과 같은 일을 처리합니다
- 데이터베이스 접근 로직을 수행합니다.(sql or transaction)
이런 식으로 진행이 될겁니다. 여기서 저는 몇가지 특이한점이 보입니다.
- 모든 클라이언트 요청은 동일한 작업을 거치네? (validation같은) (AOP)
- 각 레이어는 자기 할일이 정해져있구나. 다른 레이어에서 무슨일이 일어나는지 관심도 없고 관심을 가지지도 않는구나? (SOC)
여기서 AOP나, SOC와 같은 개념이 나옵니다.
AOP (Aspect Oriented Programming) 관점지향 프로그래밍
- 여기서 나오는 관점(Aspect)이라는 단어는 약간 생소합니다.
- 1번에서 말한 동일한 작업(validation)은 모든 api요청에 대해서 controller단에서 이루어집니다.
- controller레이어의 관심사 중 하나는 http요청에 대한 validation입니다.
- 하지만 controller는 한개가 아니다. (user, post, comment) 등 여러개 생길 수도 있습니다.
- 고로 해당 관심사를 처리하는 한 개의 모듈을 만들어 각 컨트롤러에서 호출한다면?
Controller의 주요 관심사
- 인증 & 인가
- 유효성 검사 (validation)
- routing
* NestJS에서는 interceptor라는 모듈을 제공합니다. express의 미들웨어에 순서를 붙인 느낌?
Express로 개발을 해보신분이라면 아래와같은 router함수를 사용해보셨을겁니다.
const router = Router();
router.get('/', authMiddleware(), someMiddleware(), async (req, res, next) => {
// ...some logic
return res.send();
})
해당 라우팅 경로에 해당하는 요청들은 이러한 미들웨어들을 거쳐 마지막 미들웨어의 res.send를 만날때까지 로직을 수행합니다. 해당 콜백을 수행하기 전에 필요한 동작들을 미들웨어로 추상화시켜 필요한 라우터에 적용시켜줬습니다. 이것도 관심사의 분리의 일종이라고 볼수 있습니다.
Express는 본질적으로 일련의 미들웨어들의 집합이라고 볼수있습니다. 공식문서에 따르면 다음과 같이 적혀있습니다. Express 공식문서
Express는 최소한의 자체 기능만 갖춘 라우팅 및 미들웨어 웹 프레임워크입니다. Express 애플리케이션은 본질적으로 일련의 미들웨어 함수 호출입니다.
- 각 미들웨어는 요청-응답을 가지고 있고, 각 미들웨어는 자신의 역할을 다하면 다음 미들웨어를 호출하거나 클라이언트에 응답을 보냅니다.
- 이처럼 Express의 기본 철학은 함수형 프로그래밍이라는것을 알수있습니다.
본론으로 돌아가봅시다.
- 이제부터는 제가 어떻게 express환경에서 레이어드 아키텍쳐를 적용했고, 어떤 기준으로 추상화를 하였고 그에따른 결과가 어떻게 되었는지 설명하겠습니다.
Controller (router)
path parameter로 classIdx를 받아 해당 반의 커리큘럼 리스트를 가져오는 api입니다
- 레이어드 아키텍쳐에서의 presentation layer입니다. 클라이언트로부터 오는 요청을 가공하여 서비스 레이어에 넘겨주는 역할을 합니다.
- 이 라우터는 콜백 로직을 수행하기 이전에 두 가지 미들웨어를 통과합니다.
- 요청 경로 라우팅
- path-parameter로 들어온 classIdx validation
- 저 validate 미들웨어는 따로 포스팅을 하였습니다 express-validator사용해서 선언형으로 validation하기
꼭 controller를 클래스로 구성하고싶다! 하시면 routing-controller라는 대안도 존재합니다. 이 라이브러리를 이용하면 객체지향적으로 Express서버를 구축할 수 있습니다.
하지만 이거까지쓸정도면 그냥 Nest쓰지않을까..
Service (Repository)
classIdx를 받아 prismaService에 접근하여 결과값을 반환합니다.
- 보시다시피 해당 서비스에서는 this.prismaService를 통해 데이터베이스 접근 로직을 작성하였습니다. 여기서 두가지 의문이 있을것 같습니다.
- 라우터는 함수로 했으면서 왜 서비스는 클래스로 작성함?
- 우리가 레이어를 구성하는 이유는 같은 관심사를 가진 기능들의 집합을 만들어 관리하고자 함입니다. 이 집합은 클래스, 함수 혹은 네임스페이스가 될수도 있습니다. 개발자의 취향 또는 팀의 컨벤션에 따라가면 됩니다. 서비스를 클래스로 만든 이유는 prismaClient를 가지고 있는 PrismaService 클래스의 인스턴스를 주입받기 위해 클래스로 구성하였습니다.
- 위에서 레이어드 아키텍쳐는 repository레이어가 존재한다고 했는데 왜 service에서 db접근로직을 수행함?
- Prisma ORM은 PrismaClient라는 친구가 존재합니다 이 PrismaClient는 우리가 정의한 schema.prisma파일을 읽어 데이터베이스 접근 로직을 이미 추상화를 하고있기 때문에 굳이 레포지토리 레이어를 도입할 필요가 적다고 생각했습니다.
- 프로젝트 규모가 웬만큼 커지지 않거나, 여러 명이서 작업하지 않는다면 repository layer를 도입하는것은 복잡도를 증가시킨다고 생각했습니다.
의존성 주입과 Typedi
의존성주입은 너무 유명하기때문에 따로 설명하지 않겠습니다. 다만 적용한다면 어떤 이로운 효과를 가져오는지 잠깐 알아봅시다..
- 레이어드 아키텍쳐의 가장 중요한 요소인 독립성에 관한 부분입니다. 각 레이어는 독립적으로 자신의 역할을 수행합니다. 서비스는 컨트롤러와 같은 다른 레이어에 의존을 받아야 하지 말아야합니다. DI를 통해 느슨한 결합을 적용시켜 하나의 레이어가 독립적으로 이루어질수 있게 도와줍니다.
- 객체간의 의존성을 외부에서 주입받기 때문에, 의존성을 쉽게 변경할 수 있습니다. 다양한 구현체를 바꿔가며 사용할수 있는 유연성을 제공합니다.
- DI를 사용하면 테스트 시 Mock객체를 주입하여 외부에 의존하지 않은 독립적인 테스트 환경을 구성할 수 있습니다. 단위 테스트 시 유용합니다.
드이어 Typedi 사용법입니다..
일단 서비스에서 PrismaClient를 주입받는거부터 시작하겠습니다
PrismaClient를 상속받은 PrismaService 클래스를 생성해 TypeDI의 Service데코레이터를 똭 붙여주면 끝입니다
이처럼 주입받고싶은 클래스를 생성자에 적어주면 자동으로 주입됩니다
굳이 클래스로 의존성을 등록하고싶지 않으시다 하면
- Containter.set(의존성 이름, 의존성) 으로 등록해주시고, 필요할때 Container.get(등록한 의존성 이름)으로 가져와서 사용하시면 됩니다.
개인프로젝트로 Kysely를 적용해보고있는데 적합한 예시인것같아 첨부하겠습니다
요렇게 등록해 주시고
저는 편의를 위해 컨테이너 유틸을 만들어 프로젝트의 의존성을 따로 관리해주었습니다.
- 우리가 Service 데코레이터를 붙이거나 Conatiner.set한 의존성들은 전부 런타임 시 Typedi의 Container에 (의존성 이름, 의존성) 들어가게됩니다.
- @Service데코레이터에 첫번째 매개변수에 이름을 지정해주지 않았으면 자동으로 해당 클래스의 이름으로 의존성이 등록됩니다.
등록한 의존성을 가져오고 싶으면 등록한 의존성의 키값을 Inject데코레이터에 붙여주면 런타임 시에 자동으로 Typedi가 의존성을 주입해줍니다.
'node.js > ExpressJS' 카테고리의 다른 글
[NodeJS] express-validator사용해서 선언형으로 validation하기 (0) | 2024.06.26 |
---|---|
[NodeJS] Express + Inversify DI (3 layerd architecture) (0) | 2024.03.05 |
[Express] 비동기함수에 반복되는 try-catch 없애기 (0) | 2024.01.05 |
[Express] 요청 객체를 dto로 변환하기 (0) | 2023.12.13 |
[Express] jwt로 로그인 유지시켜주기 (0) | 2023.11.04 |