IT동아리 Mash-up에서 서버팀 사이드 프로젝트로 Nest를 사용해 Discord 봇을 만들고 있었습니다.
디스코드 봇에 내릴 수 있는 커맨드들을 정의하고 그에 대한 응답을 보내주는 로직을 작성하는데,
이런 커맨드 하나를 추가하기 위해 최소 4개의 파일을 수정해야하는 상황이였습니다.
아래는 각 파일의 수정이 필요한 부분들입니다.
1. 커맨드들에 대한 정보를 정의해야했습니다.
2. 각 커맨드들에 대한 응답을 매핑해서 리턴해주는 Factory Provider에서 각각의 Reply Provider 들을 주입받아 처리했어야했습니다.
3. Nest의 DI를 사용하기 위해 Module에 각 Reply Provider들을 넣어주어야 했습니다.
4. 각 응답에 대한 Reply Provider 파일을 작성해야했습니다.
위의 과정 중에서 3번과 4번은 새로운 커맨드를 정의할 때 필수적으로 해야하는 과정입니다.
1번과 2번의 과정 또한 당장은 필요한 과정이지만 우아하게 느껴지지는 않는 것 같습니다.
왜일까요?
먼저 1번의 경우, 커맨드를 추가하기 위해서 논리적으로 관련이 없어보이는 파일에 커맨드에 대한 정보를 입력해야합니다. 이러한 코드의 분산은 사람들이 4번 그림에서 보이는 Reply 파일을 보고 이 Reply가 어떤 커맨드에 관한 응답인지 알 수 없게 할 것입니다. 데이터는 관련이 있는 녀석들끼리 모여있는 게 보기 좋을 것 같습니다.
두번째로, Reply Factory에서 1에서 적은 command명을 그대로 다시 적어주며 switch문 만으로 커맨드에 대한 Reply를 정의해주고 있습니다. 같은 데이터(커맨드 이름)이 이렇게 단순 문자열로 분산되어 있는 것이 보기 좋지 않았고, 단순히 switch문을 통해 인스턴스를 전달만 하는 목적을 가진 Factory가 각 Reply에 대한 의존성을 갖고 있는 것으며 이를 관리해줘야하는 부분에서 개선의 여지가 있다고 판단하였습니다.
그렇다면 관련 있는 데이터를 한 곳으로 모으고, 중복 없이 다루면서 커맨드들을 쉽게 추가할 수 있을까요?
저는 이에 대한 해답으로 Decorator와 Nest의 DiscoveryService를 활용해보기로 했습니다.
먼저 Decorator입니다.
데코레이팅할 클래스에 전달받은 값을 `COMMAND`라는 metadata의 키값으로 넣어주는 Class Decorator Factory를 만들었습니다.
인자로는 SlashCommandBuilder를 받아 각 응답에 대한 class들은 자신이 어떤 명령에 대한 응답인지를 metadata로 갖게 될 것입니다.
각 Reply들에 위처럼 Command 데코레이터로 로 자신이 응답할 커맨드에 대한 정보를 넣어주면 관련있는 데이터들을 모으는 작업은 마무리된 것 같습니다.
이제 두 작업이 남았습니다.
1-1사진처럼 명령어를 Discord에 등록해주고, 1-2처럼 각 응답에 대한 Reply를 정의해주어야합니다.
이 두 작업은 Nest의 DiscoveryService를 사용했습니다.
DiscoveryService를 사용하면 런타임 상에서 Nest IoC컨테이너에 등록된 프로바이더에 대한 정보에 접근할 수 있습니다.
우리는 이미 각각의 응답 클래스가 어떤 커맨드에 매칭되는 것인지 메타데이터 태깅을 해놨기 때문에 프로바이더를 가져오면 이 메타데이터 를 얻을 수 있게 됩니다.
런타임에 등록된 프로바이더들을 모두 가져온 후 아래의 과정을 통해 디스코드 봇에 저장될 커맨드 정보를 불러옵니다.
- `isDependencyTreeStatic`을 통해 프로바이더 중에서 싱글턴으로 선언된 프로바이더들을 추립니다.
- prototype이 있는 instance들만을 추립니다. 우리는 Command로 데코레이팅 된 Reply클래스를 찾는 것이 목적이기 때문에 prototype이 없는 값은 걸러줍시다.
- reflector를 통해 prototype에서 `COMMAND`심볼을 키 값으로 메타데이터를 가져옵니다.
위 과정을 거치게 되면 우리가 Command 데코레이터에 넣은 명령어 정보를 런타임 상에서 한 번에 가져올 수 있습니다. 그 후 원래와 마찬가지로 디스코드 봇에 명령어를 넣어줍니다.
마지막으로 커맨드에 대한 알맞은 Reply를 Factory에서 리턴해주도록 해보겠습니다.
위와 비슷한 과정을 거친 후 command의 이름과 해당 메타데이터가 마킹된 클래스 인스턴스를 Map에 저장합니다.
이때, 해당 인스턴스는 Nest에서 의존성을 해결해둔 상태여야 하므로 `OnModuleInit` 인터페이스를 구현하여 모듈이 초기화 되고 난 후 실행 되도록 해줍니다.
짜잔~ 이제 새로운 커맨드와 응답을 만들 때 더 이상 여러 파일을 수정하지 않아도 됩니다!
Reply클래스를 만들고 Command로 데코레이팅 해준 후 Module에 등록만 해주면 새로운 명령어가 디스코드 봇에 추가되게 됩니다!
추가하려는 Command와 관련없는 파일을 수정하지 않아도 되게 되었고, Command와 관련된 코드들이 한 곳에 모이게 되었습니다. 이로서 코드의 응집도는 올라가고 확장성도 나아진 것 같습니다.
이상, Metadata와 DiscoveryModule을 사용하여 디스코드 봇 어플리케이션의 개발 환경을 개선해봤습니다.
이 방법이 Best는 아닐 수 있지만 과거의 구조보다는 나아간 거 같다는 생각이 듭니다.
이처럼 메타데이터와 Nest의 DiscoveryModule에서 제공해주는 기능을 이용하면 런타임 상에서 유연하고 다양하게 기능과 환경을 개선할 수 있습니다. 토스에서 만든 nestjs-aop 도 이 두 기능을 적극적으로 이용하여 횡단 관심사의 분리를 라이브러리화한 좋은 예시인 것 같습니다. 단, 무분별한 사용은 어플리케이션의 복잡도를 증가시킬 수 있으므로 해결하고자 하는 문제의 해결책으로 이 방법지 `적절`한지는 충분히 고민해보면 좋을 것 같습니다.
이만 글을 마치며, 다음에는 디스코드 봇 대신 좀 더 범용적으로 적용가능한 예시와 함께 찾아와보겠습니다~(언제일진 모르겠지만!)
'Node.js > Nest.js' 카테고리의 다른 글
[TypeORM] Active Recore & Data Mapper Pattern (0) | 2021.08.30 |
---|---|
NestJS Provider(프로바이더) (0) | 2021.08.19 |
NestJS Lazy-loading Modules (0) | 2021.08.08 |