NestJs

NestJS - @Module(), @Injectable(), @InjectRepository()

hs-archive 2023. 3. 17. 18:55

@Module(), @Injectable(), @InjectRepository()

모듈

NestJS는 여러 모듈로 이루어집니다. NestJS에서 @Module() 데코레이터는 그러한 모듈을 만들 때 사용합니다. 모듈은 providers, controllers, impoers, exports를 정의하며 최소 하나 이상의 @Conroller()나 @Provider()를 포함합니다.

@Injectable 데코레이터는 클래스를 Injectable 하게 만드는 데 필요한 메타데이터를 NestJS에게 제공합니다. NestJS에서 해당 클래스를 DI 하고자 할 때 @Injectable 데코레이터가 제공하는 메타데이터가 필요합니다. @Injectable()은 아래와 같이 사용합니다.

@Injectable()
export class SomeClass { ... }

 

TypeORM의 데코레이터인 @InjectRepository()는 일반적인 @Injectable() 데코레이터와 비슷하게 의존성 주입을 위한 데코레이터입니다. 하지만 그 둘의 목적이 조금 다릅니다. @Injectable()은 NestJS에서 만든 클래스를 의존성 주입 가능하게 만드는 데코레이터입니다. 반면, @InjectRepository()는 NestJS가 작성한 것이 아닌 외부라이브러리 TypeORM 레포지토리에 의존성을 주입하는 데코레이터입니다. 즉, @Injectable() 데코레이터는 NestJS에서 일반적인 클래스에 사용되고, @InjectRepository() 데코레이터는 TypeORM에서 제공하는 레포지토리 클래스를 NestJS에서 사용하기 위해 사용됩니다.

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) { ... }
}

 

@Module()에 들어가는 옵션들

@Module()에는 imports, controllers, providers, exports 총 네 가지 옵션이 있습니다. import는 해당 모듈에서 사용할 외부 모듈을 정의합니다. 예를 들어 해당 모듈에서 다른 모듈인 B 모듈을 사용하려면 imports에 B를 추가해야 합니다.

@Module({
  imports: [BModule],
})
export class SomeModule { }

 

controllers는 라우터를 정의할 때 사용됩니다. @Controller() 데코레이터를 사용하여 컨트롤러를 정의하고, 이를 @Module()에서 controllers 배열에 추가하여 사용합니다.

<some.controller.ts>
@Controller('somepath')
export class SomeController { ... }


<some.module.ts>
@Module({
    controllers: [SomeController],
})
export class SomeModule { }

 

providers는 해당 모듈에서 자동 주입되어야 하는 컴포넌트들을 등록하는 데 사용됩니다. 여기에 적힌 컴포넌트들은 NestJS가 자동으로 생성하고 관리하는 대상이 되며 다른 클래스에서 주입받을 수 있게 됩니다. 단, providers는 클래스를 injectable 하게 만들기 위해서 메타데이터가 필요합니다. 해당 메타데이터는 @Injectable()가 제공합니다. 따라서 providers에 추가할 컴포넌트는 사전에 @Injectable() 데코레이터를 붙여줘야 합니다.

<some.service.ts>
@Injectable()
export class SomeService { ... }


<some.module.ts>
@Module({
    controllers: [SomeController],
    providers: [SomeService]
})
export class SomeModule { }


<some.controller>
@Controller('somepath')
export class AuthController {
	// @Injectable() 붙었고
    // 해당 클래스를 providers 배열에
    // 추가하였으니 에러 없이 동작합니다.
    constructor(private readonly someService: SomeService) { }
}


exports는 해당 모듈의 providers와 controllers에 정의된 컴포넌트를 다른 모듈에서 사용할 수 있도록 내보냅니다. 즉, 다른 모듈에서 해당 providers와 controllers에 적힌 컴포넌트를 주입받을 수 있게 됩니다.

<some.service.ts>
@Injectable()
export class SomeService { ... }


<some.module.ts>
@Module({
    controllers: [SomeController],
    providers: [SomeService],
    exports: [SomeService]
})
export class SomeModule { }


<some.controller>
@Controller('somepath')
export class AuthController {
    constructor(private readonly someService: SomeService) { }
}

/////////////////////////////////////

<other.module.ts>
@Module({
    // 이렇게 작성하면
    // 해당 모듈에 등록된 모든 컴포넌트에서
    // SomeModule의 exports에 등록된
    // SomeService를 사용할 수 있습니다.
    imports: [SomeModule]
})
export class OtherModule { }

 

참고로, 어떤 클래스(SomeClass)가 이미 어느 모듈(SomeModule)의 providers 배열에 추가되었다면, 위 코드처럼 그 모듈(SomeModule)의 exports 배열에 해당 클래스를 추가하는 방식(위에 적힌 방식)으로 코드를 작성하는 것이 좋습니다. 만약 이러한 방식을 따르지 않고 다른 모듈(OtherModule)에서도 해당 클래스(SomeClass)를 자신의 providers에 추가한다면 해당 클래스(SomeClass)는 전체 애플리케이션에서 총 두 개 생성됩니다.

// 서로 다른 모듈에서 같은 SomeService 클래스를 각자의 
// providers 배열에 추가하였습니다. 
// 따라서, 전체 애플리케이션에서 SomeService는 두 개가 생성됩니다.

[방법 1]
<some.service.ts>
export class SomeService { ... }


<some.module.ts>
@Module({
	providers: [SomeService],
})
export class SomeModule


<other.module.ts>
@Module({
	providers: [SomeService],
})
export class OtherModule
// SomeModule에서 SomeService를 providers에 추가한 뒤,
// SomeService를 exports로 내보내고 있습니다.
// OtherModule에서는 imports로 SomeModule을 받고 있습니다.
// 따라서, 전체 애플리케이션에서 SomeService는 단 한 개만 생성됩니다.

[방법 2]
<some.service.ts>
export class SomeService { ... }


<some.module.ts>
@Module({
	providers: [SomeService],
    exports: [SomeService]
})
export class SomeModule


<other.module.ts>
@Module({
	imports: [SomeModule],
})
export class OtherModule

 

 

@Module()에 들어가는 providers는 다른 방식으로도 인자를 제공할 수 있습니다.

@Module()의 providers 배열에는 useFactory, useClass, useValue를 사용하여 인자를 제공할 수도 있습니다. 이 셋에 대해 배워봅시다.

 

useFactory는 Factory 함수를 사용하여 객체 인스턴스를 동적으로 생성합니다. 주로 다른 클래스를 주입하거나 복잡한 로직을 수행해야 하는 경우에 사용됩니다.

@Module({
  providers: [
    {
      provide: 'MyService',
      useFactory: () => {
        const myService = new MyService();
        myService.setup();
        return myService;
      }
    }
  ]
})

 

useClass는 다른 클래스를 사용하여 객체 인스턴스를 생성합니다.

@Module({
  providers: [
    {
      provide: 'MyService',
      useClass: MyService
    }
  ]
})

 

마지막으로 useValue는 정적인 값을 사용하여 객체 인스턴스를 생성할 때 사용합니다.

@Module({
  providers: [
    {
      provide: 'Config',
      useValue: {
        port: 3000,
        database: 'mongodb://localhost/nestjs'
      }
    }
  ]
})

 

useFactory의 경우 인스턴스 생성 전/후에 해야 할 작업이 있을 때 사용하는 것이고, useValue는 인스턴스에 정적인 값을 전달해야 할 때 사용하는 것을 알겠습니다. 하지만 useClass는 위 예시에서 단지 이름만 바꾸는 것 같은데 왜 사용할까요? 우선 코드 먼저 적겠습니다.

@Injectable()
export class SomeService {
  // X 이렇게 하는 것보다 X
  // constructor(private readonly service: OldService) {}
  
  // O 이렇게 하는 것이 더 좋음 O
  // SomeService 코드를 변경하지 않아도 됨!
  constructor(private readonly service: MyInterface) {}
}


@Module({
  imports: [SomeOtherModule],
  providers: [
    {
      provide: MyInterface,
      // useClass: OldService, -> useClass: NewService
      useClass: NewService,
    },
  ],
})
export class AppModule {}

 

OldService와 NewService가 인터페이스 MyInterface를 구현한다고 생각해 봅시다. 어떤 경우 우리는 클래스에서 구체적인 OldService를 주입받는 것보다, MyInterface를 주입받는 것이 더 좋을 수 있습니다. OldService를 대체할 NewService를 만들어서 SomeService에서 사용하고자 할 때, 해당 클래스가 MyInterface를 주입받고 있으면, useClass를 사용하여 해당 클래스에 주입되는 서비스를 SomeService의 코드를 변경하지 않고도 OldService를 NewService로 교체할 수 있습니다. 만약 useClass를 사용하지 않았다면 이런 방식의 교체는 불가능합니다. 이처럼 useClass를 사용하면 의존성 역전 원칙을 준수하고 코드의 유연성과 유지보수성을 높일 수 있습니다.

 

더보기

위 useFactory, useClass, useValue의 예시에서 provide는 해당 객체를 식별하는 데 사용되는 토큰을 의미합니다. 이렇게 생성된 객체는 의존성 주입 시에 해당 토큰을 사용하여 주입됩니다.

 

예를 들어, 아래와 같이 지정할 수 있습니다.(@Inject("SOME_NAME")을 하지 않아도 되지만 @Inject를 사용하여 명시적으로 표현하는 것이 더 좋아 보입니다.)

 

@Module({
  providers: [
    {
      provide: 'SOME_NAME',
      useFactory: () => new Logger('app'),
    },
  ],
  exports: ['SOME_NAME'],
})
export class SomeModule {}


@Injectable()
export class SomeService {
  constructor(
    @Inject('SOME_NAME') private readonly logger: Logger,
  ) {}
}

 

 

 

 


https://openai.com/blog/chatgpt

 

Introducing ChatGPT

We’ve trained a model called ChatGPT which interacts in a conversational way. The dialogue format makes it possible for ChatGPT to answer followup questions, admit its mistakes, challenge incorrect premises, and reject inappropriate requests.

openai.com

'NestJs' 카테고리의 다른 글

service에서의 repository 의존 역전하기 - 클린 아키텍처 적용하기  (0) 2023.03.28
Entity -> DTO 변환  (0) 2023.03.23
NestJS - passport  (0) 2023.03.13
NestJs - Guard  (0) 2023.03.09
NestJS 소개  (0) 2022.10.23