nestjs singleton

Jae Min·2023년 11월 14일
0

✅ 용어설명

singleton
객체의 인스턴스가 오직 1개만 생성되는 패턴
메모리에 인스턴스를 한번만 만들어서 올려놓고 필요할 때 마다 생성하는 패턴이다.

IOC(inversion of control)
제어의 권한을 바꾼다
인스턴스의 생성 및 할당과 해제를 개발자가 하는 것이 아니고, 프레임워크가 이를 맡아주는 것을 말한다.

DI(dependency of injection)
의존성 주입
A가 B를 사용하기 위해 직접 B를 생성하는 것이 아니라, 외부로부터 가져와서 사용한다는 것을 말한다.

❗️ 의존성 주입을 사용하지 않을 경우 단점 ❗️

  1. A를 B에서도 사용하고 C에서도 사용을 하는데 둘다 의존성 주입이 아니라 직접 인스턴스를 생성해서 사용한다고 가정을 해보자. (A->B / A->C)
    이때 A가 변하면 B, C에서도 수정된 A를 다시 생성해야 한다.
    이를 강한 결합 이라고 한다.
  2. 또한, A의 인스턴스들의 생명주기를 B, C 가 직접 관리를 해주면 메모리 관리를 해야한다. 이때 memory leak 가 발생할 수 있다.

여기서 잠깐! 근본적으로 생각해보자. 이런 의존성이니 주입이니 이런말이 왜 나온걸까? 아키텍처를 구성할때, 도메인에 따라서 서비스 레이어를 구분하도록 구성하면 구현체의 로직이 변경되도 호출부에서는 해당 내용을 알 필요가 없다. 이로써, 생산성과 재사용성이 좋아지게 된다.

nest는 내부적으로 IOC container 를 이용해서 DI를 관리해준다
ioc container는 @Injectable(), @Module() 데코레이터가 달린 클래스들을 DI대상으로 관리한다. nest 는 typescript 이기 때문에, 타입기준으로 DI를 관리하기 수월하다.

✅ nestjs 싱글톤 패턴의 특징

  • 공유된 자원을 사용하기 위해.
    - 하나의 인스턴스를 여러 곳에 사용했을 경우 동시성 이슈와 데드락 같은 문제를 방지할 수 있다.
  • 인스턴스를 필요할 때 마다 생성하고 필요없을 때, 제거 하면서 까지 메모리 관리를 신경 쓰지 않아도 된다.
  • 하나의 인스턴스를 global 하게 사용할 수도 집약적으로 사용할 수 있다.
    - global 하게 사용한다면 A클래스에서 B클래스에서 사용하는 인스턴스에 접근을 방지 하기 위해 private 을 선언해주는 방법도 있다.

✅ 구현방법

nest는 애초에 싱글스레드이기 때문에, 인스턴스간의 동시성이 발생하지 않는다고 한다.
nestjs 에서는 docs 에서 알 수 있듯이, singleton 을 지향하고 있다. 하지만 무조건 싱글톤으로 구현되는 건 아니다. 아래서 다뤄보겠다.

싱글톤을 구현하기 위해서는 간단하게 설명을 하자면, @Injectable(), @Module() 데코레이터를 사용하면 된다.
@Injectable(), @Module() 데코레이터를 선언하면, 해당 인스턴스를 nest 내장 IOC container 가 관리한다
(데코레이터가 달린 클래스는 타입스크립트가 컴파일시 메타데이터로 어떤 서비스에 의존하고 있는지 명시를 해준다. 그러면 nestjs 가 어떤 의존관계가 있는지 알 수 있다.)

👉 nest 내부의 IOC container 가 @Injectable(), @Module() 데코레이터 클래스를 싱글톤으로 생성하여 DI의 대상으로 관리한다.

✅ 뭐야 두개 생기는데?

socket adapter 를 공부하다가 소켓에 연결하면 즉각 실행하는 afterInit() 이라는 함수를 구현하다가 뭔가 이상함을 봤다.

// ws.gateway.ts
@WebSocketGateway(80)
export class WsKorGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
  constructor(private readonly logger: Logger){
  }
  @WebSocketServer()
  wsServer: Server;
  /**
     * 
     * @param server 해당 gateway 가 실행되면 가장 먼저 실행되는 함수 -> handleConnection 보다 먼저 실행된다
     */
  afterInit(server: any) {
    this.logger.log('kor ws gateway started!') 
    // 터미널을 보면 이게 두번 실행됨을 알 수 있다.
  }
}



// ws.module.ts
@Module({
    providers: [WsKorGateway, Logger]
})
export class WsKorModule {
}



// app.module.ts
@Module({
  imports: [WsKorModule, WsUsaModule],
  controllers: [AppController],
  providers: [AppService, WsKorGateway, Logger],
})
export class AppModule {}

위와 같이 소스코드를 구현해 놓고, ws server 를 구동하면 다음과 같은 결과가 나온다.

위 사진을 보면 kor connected! 라는 로그가 두번찍혔는데 이로써 afterInit() 함수가 두번 실행됐음을 알 수 있다.

app module 에서 import 에서 한번 providers 에서 한번, 총 두번 주입을 해줬기 때문에, WsKorGateway 가 두번 실행되서 로그가 두번찍히는건 알겠는데

app module 에서 import 에서 한번, providers 에서 한번, 총 두번 주입을 하지만 한개의 인스턴스를 그냥 두번 주입하는게 아닌가?

nest 는 인스턴스를 싱글톤으로 만든다고 했는데..?

여기서 그냥 아무 decorator 로 싱글톤을 구현하는것이 아닌것을 알 수 있다.
@injectable() @Module() 이 두개로만 싱글톤이 적용되는 것으로 추측된다.

✅ 주입된 놈이 수정되면 나도 컴파일 해야 하나?

@Controller('payment')
export class PaymentController extends CommonController {
  constructor(
    @Inject('PAYMENT_SERVICE')
    private readonly paymentService: PaymentService,
  ) {
    super();
  }

위 같은 경우는 PaymentService 가 변하면 PaymentController 도 같이 컴파일을 다시 해야한다.

@Controller('talk')
export class ChatController extends BaseController {
  constructor(
    @Inject(CHAT_SERVICE)
    private readonly chatService: IChatService,
  ) {
    super();
  }

하지만 다음과 같은 경우는 Interface 를 주입받았기 때문에, ChatService의 구현체가 변해도 ChatController 는 컴파일 하지 않아도 된다.
이게 interface 를 사용하는 중요한 이유다 (일종의 추상화)


References
reference 1
reference 2

profile
자유로워지고 싶다면 기록하라.

0개의 댓글