[Nest.js] HTTP query에 따라 Service가 동적으로 할당되어야 한다.트러블 슈팅2024. 5. 7. 17:33
Table of Contents
프로젝트 소개 및 요구사항
waktaverse games라는 약 30개의 팬게임과 협업하는 게임 포럼/배포 서비스입니다.
본 웹 사이트에는 2가지 클라이언트가 존재합니다.
- 웹 브라우저(react)
- 팬게임 개발자용 클라이언트(electron)
각 클라이언트마다 고유의 refresh 토큰을 가지고 있어서 메서드 내부에서 처리해줘야 하는 중복 로직이 많았습니다.
그래서 이번 글에서는 다음과 같은 작업을 수행해보겠습니다.
- 클라이언트가 더 추가되도 코드의 변경이 일어나지 않도록 개선
- request query에 따라 service가 동적으로 할당되도록 개선
Service 동적 주입 위치는?
처음에는 Service 인스턴스를 어디서 결정할 지 고민하였습니다.
아직도 여전히 controller에서 주입해주는 것이 올바르다고 생각하고 있긴하지만, Nest.js의 아키텍처에서는 모듈시스템에서
export type Provider<T = any> = Type<any> | ClassProvider<T> | ValueProvider<T> | FactoryProvider<T> | ExistingProvider<T>;
크게 5가지가 존재합니다.
- Type<any>(General): 우리가 자주 사용하는 @Injectable 데코레이터가 달린 클래스를 바로 사용하는 방식(토큰과 실제 값 동일.)
- ValueProvider<T>: 주입받고자 하는 토큰과 실제로 사용할 값을 입력하여 사용하는 방식입니다. (정적 할당)
- ClassProvider<T>: 주입받고자 하는 토큰과 실제로 사용할 클래스를 입력하여 사용하는 방식입니다. (조건문을 통한 할당)
- FactoryProvider<T>: 주입받고자 하는 토큰과 실제로 사용할 클래스를 return하는 팩터리 메서드를 구현하는 방식입니다.
- ExistProvider<T>: 이미 존재하는 싱글톤 클래스를 사용하는 방식입니다. 새로운 토큰에도 주입되니 싱글톤 클래스에 접근하는 방식을 하나 더 추가하는 행위.
위 예제 중에 FactoryProvider가 가장 적절해보입니다.
현재 Request는 어떻게 가져올까?
nestjs에서 현재 Request를 가져오는 REQUEST identifier를 제공해줍니다. 다음과 같이 사용이 가능합니다.
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Inject(REQUEST) request: Request // current Request
이제 필요한 것은 모두 갖춰졌습니다. useFactory와 REQUEST를 사용하여 다음과 같은 팩터리 메서드를 구현할 수 있습니다.
// auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth-service';
import { AuthWebService } from './auth-web.service';
import { REQUEST } from '@nestjs/core';
import { AuthClientService } from './auth-client.service';
import { Request } from 'express';
// 추상 클래스 AuthClient를 상속받고 있는 클래스.
const AuthClientServices = [AuthClientService, AuthWebService];
@Module({
imports: [], // ...import할 모듈들.
providers: [
...AuthClientServices, // spread 연산자로 펼치기. 펼쳐진 Service들은 Standard providers 방식으로 생성됨.
{ // Factory providers 방식.
provide: AuthService,
inject: [REQUEST, ...AuthClientServices],
useFactory: (req: Request, ...serivces: AuthService[]) => {
let type: string = req.query.clientType.toString(); // request로부터 type 쿼리 가져오기.
const valid = AuthService.isValidClientType(type); // 존재하지 않거나 올바르지 않으면
if (!valid) type = AuthService.defaultClientType; // 기본값(web) 적용.
return serivces.find((service) => { // 상속받은 클래스가 type에 맞는다면 이 클래스 return.
return service.getType() === type;
});
},
},
],
controllers: [AuthController],
export: [AuthWebService]
})
export class AuthModule {}
// auth-web.service.ts
import { Injectable } from '@nestjs/common';
import { AuthService } from './auth-service';
@Injectable()
export class AuthWebService extends AuthService {
async logout(id: number): Promise<void> {
this.usersService.updateRefreshToken(id, '', 'webToken');
}
async login(id: number, encryptedToken: string): Promise<void> {
this.usersService.updateRefreshToken(
id,
encryptedToken,
'webToken',
);
}
getType(): 'web' {
return 'web';
}
}
리팩터링 결과.
- controller에서 매번 web에 대한 요청인지 client에 대한 요청인지 확인할 필요가 없어졌음. 그저 AuthService에게 메세지만 보내면 끝.
- 중복로직을 추상클래스인 AuthService에 분배함으로 중복성을 없앰.
- Web, Client Service를 의존하는 클래스가 없으므로 클라이언트 추가/삭제에 자유로워짐.
'트러블 슈팅' 카테고리의 다른 글
[Node.js] 트랜잭션을 활용한 테스트 격리 환경 구현하기 (1/2) | 솔루션 찾기 (0) | 2024.07.31 |
---|---|
html에서 React로 마이그레이션 시 겪었던 문제점 (0) | 2024.02.05 |
[MySQL 복구 2] binlog를 통해 데이터베이스 복구하기 (0) | 2023.09.08 |
[MySQL 복구 1] .ibd 파일을 이용하여 데이터 복구하기 (0) | 2023.09.08 |
돈이 없는데 서버의 하드디스크가 꽉찼다. (docker chromium) (0) | 2023.06.04 |
@임채성 :: 푸르고 개발 블로그
글 내용 중 잘못되거나 이해되지 않는 부분은 댓글을 달아주세요! 감사합니다! 문의: puleugo@gmail.com