트러블 슈팅
[Nest.js] HTTP query에 따라 Service가 동적으로 할당되어야 한다.
임채성
2024. 5. 7. 17:33
프로젝트 소개 및 요구사항
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를 의존하는 클래스가 없으므로 클라이언트 추가/삭제에 자유로워짐.