Nest 아키텍처 : 모듈

종속성 주입(DI) 이해해보기

종속성 주입 이해를 위한 소규모 프로젝트

소규모 프로젝트 개요

3개의 계층으로 이루어진 모듈을 만들 것이다. 최상층에는 Computer Module이 있고 내부에는 Computer Controller가 run이라는 메서드를 가진다. 이 내부에는 CPU, Disk Module이 있는데 각각 CPU Service, Disk Service를 가지고 있으며 이는 또 compute와 getData라는 메서드를 가진다. 최하층에는 Power Module이 있으며 Power Service를 가진다. 메서드는 supplyPower()이다.

생성은 당연히 최하층 Power Module이 전원을 제공하면 CPU와 Disk가 작동하고 Computer가 작동하는 형태이다.

기존과는 다른 새로운 디렉토리에서 Nest 프로젝트를 시작하자.

터미널에 nest new di를 입력하고 npm을 선택하여 설치하자. di는 우리 프로젝트의 이름이다.

파일 생성

설치가 완료되면 src 내의 파일들을 main.ts를 제외하곤 모두 삭제하자. 이제 내 목적에 맞도록 다시 만들면 된다.

터미널에서 위치를 di로 옮긴

nest g module computer

nest g module cpu

nest g module disk

nest g module power

를 차례로 입력하여 모듈 4개를 만든다. src 내에 각 이름으로 된 폴더가 생성될 것이다.

이제 터미널에

nest g module cpu

nest g module power

nest g module disk

를 입력해 3개의 서비스를 만든다.

nest g controller computer를 입력하여 1개의 컨트롤러를 만든다.

삭제하지 않은 main.ts에서 AppModule을 import하여 NestFactory.create에 전달하는데 이를 ComputerModule로 변경한다. 이로써 기본 모듈을 설정해준 것.

이 사이드프로젝트는 cpu와 disk에 전원을 공급하여 이 둘이 컴퓨터에 제공되도록 해야한다. 따라서 가장 먼저 해야할 것은 전원을 공급하는 것이다. src내의 power디렉토리에서 power service로 가서 메서드를 정의하자.

1
2
3
4
5
6
7
8
9
10
// power.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class PowerService {
  supplyPower(energy: number) {
    console.log(`Supplying ${energy} worth of power.`);
  }
}

이제 이 서비스가 디스크와 CPU 모듈에 접근할 수 있어야 한다. 그래야 CPU 서비스와 Disk 서비스가 전력을 제공받아 작동할 것이다.

Disk는 잠시 제껴두고, CPU와 Power모듈만 생각해보자.

Cpu 서비스는 PowerService의 인스턴스가 있어야 작동할 수 있다. 이는 다시 말해 CPU와 Power라는 두 모듈이 코드를 공유한다는 뜻!


예시 : 종속성 주입

여기서 이해를 위해 하나의 모듈 혹은 모듈간 DI(Dependency Injection, 종속성 주입)을 생각해보자.

가정을 하나 만들어서, 파워 모듈안에 Power 서비스와 Regulator 서비스 가 있다고 가정해보자. 그리고 regulator 서비스는 Power 서비스의 인스턴스가 있어야 제대로 작동할 수 있다고 가정하자. 이떄 하나의 Power module 안에 있는 두 서비스간의 DI 과정은 3단계로 나눠진다.

  1. PowerService에 @injectable() decorator 추가하기
  2. PowerService를 PowerModule의 providers 리스트에 추가하기
  3. RegulatorService에 contructor 메서드를 정의하고 PowerService를 추가해주기

우리의 사이드 프로젝트로 돌아오자. Power Module에는 Power 서비스 하나만 있다. Cpu Module은 Cpu 서비스를 가지고 있다. 이 Cpu 서비스는 Power 서비스의 인스턴스가 필요하다. 별개의 모듈에 DI를 해야한다는 의미이다. 이때는 다음과 같은 3단계가 이뤄진다.

  1. PowerService를 PowerModule의 export 리스트에 추가하기
  2. CpuModule에서 PowerModule를 Import 하기
  3. CpuService에 constructor 메서드 정의하고 PowerService를 추가해주기

비슷한듯 다르다. 실제 코드로 알아보자.

1단계

src의 power 디렉토리의 모듈 파일을 보자

1
2
3
4
5
6
7
8
9
// power.module.ts
import { Module } from '@nestjs/common';
import { PowerService } from './power.service';

@Module({
  providers: [PowerService],
})
export class PowerModule {}

Powerservice가 자동으로 추가되어있는데, 이는 기본적으로 Private이다. 다른 모듈에서 사용하기 위핸 export를 추가해준다.

1
2
3
4
5
6
7
8
9
10
// power.module.ts
import { Module } from '@nestjs/common';
import { PowerService } from './power.service';

@Module({
  providers: [PowerService],
  exports: [PowerService],
})
export class PowerModule {}

2단계

이제 Cpu 모듈에서 import 해야한다. Cpu 모듈 파일로 가서 추가하자.

1
2
3
4
5
6
7
8
9
10
11
// cpu.module.ts
import { Module } from '@nestjs/common';
import { CpuService } from './cpu.service';
import { PowerModule } from '../power/power.module'; // 추가한 코드 1

@Module({
  imports: [PowerModule], // 추가한 코드 2
  providers: [CpuService],
})
export class CpuModule {}

3단계

이제 CPU서비스에서 생산자를 정의하자. CPU의 서비스 파일을 수정한다.

1
2
3
4
5
6
7
8
9
// cpu.service.ts
import { Injectable } from '@nestjs/common';
import { PowerService } from '../power/power.service'; // 추가한 코드 1

@Injectable()
export class CpuService {
  constructor(private powerService: PowerService) {} // 추가한 코드 2
}

위 방법이 서로 다른 모듈 사이에 종속성 주입을 설정하고 서비스를 공유하는 방법이다.


DI 적용해보기

다시 원래 사이드 프로젝트로 돌아와서 cpu 서비스에 compute 라는 메서드를 추가하자. 그냥 두 숫자를 받아서 더해주는 것 뿐이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// cpu.service.ts
import { Injectable } from '@nestjs/common';
import { PowerService } from '../power/power.service';

@Injectable()
export class CpuService {
  constructor(private powerService: PowerService) {}

  computer(a: number, b: number) {
    console.log('Drawing 10 energy of power from Power Service');
    this.powerService.supplyPower(10);
    return a + b;
  }
}

Power 모듈은 CPU와 Disk 모듈 두 곳에 전원을 공급해줬다. CPU를 마쳤으니 Disk에도 똑같이 적용해주자.

위에서 말했듯

  1. Power모듈에서 export 해주고
  2. Disk모듈에서 Import 한다.
  3. Disk서비스에서 생성자 메서드를 정의하고 여기에 Power서비스를 추가해준다.
1
2
3
4
5
6
7
8
9
10
11
// disk.module.ts
import { Module } from '@nestjs/common';
import { DiskService } from './disk.service';
import { PowerModule } from '../power/power.module';

@Module({
  imports: [PowerModule], // import 아니라 imports다.
  providers: [DiskService], // 여기 끝에 , 없으면 오류뜬다.
})
export class DiskModule {}

이제 Disk서비스에 import하고 getData() 메서드를 정의하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// disk.service.ts
import { Injectable } from '@nestjs/common';
import { PowerService } from '../power/power.service';

@Injectable()
export class DiskService {
  constructor(private powerService: PowerService) {}
    
  getData() {
      console.log('Drawing 20 energy of power from PowerService');
      this.powerService.supplyPower(20);
      return 'data!';
  }
}

이젠 CPU 모듈과 Disk모듈을 최상위 계층인 Computer모듈에게 공유해야 한다. 중요하니 한번 더 설명하는 3단계.

  1. export하고
  2. import하고
  3. constructor 정의한 후 제공한다.

disk와 Cpu 모듈에 각각

exports: [DiskService],

exports: [CpuService],

를 추가한다. 이제 컴퓨터 모듈에서 import 한다.

1
2
3
4
5
6
7
8
9
10
11
12
//computer.module.ts
import { Module } from '@nestjs/common';
import { ComputerController } from './computer.controller';
import { CpuModule } from '../cpu/cpu.module';
import { DiskModule } from '../disk/disk.module';

@Module({
  controllers: [ComputerController],
  imports: [DiskModule, CpuModule],
})
export class ComputerModule {}

computer 컨트롤러로 가서 생성자 메서드를 정의하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
//computer.controller.ts
import { Controller } from '@nestjs/common';
import { CpuService } from '../cpu/cpu.service';
import { DiskService } from '../disk/disk.service';

@Controller('computer')
export class ComputerController {
  constructor(
    private cpuService: CpuService,
    private diskService: DiskService,
  ) {}
}

컨트롤러에 Get decorator와 run 메서드를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// computer.controller.ts
import { Controller, Get } from '@nestjs/common';
import { CpuService } from '../cpu/cpu.service';
import { DiskService } from '../disk/disk.service';

@Controller('computer')
export class ComputerController {
  constructor(
    private cpuService: CpuService,
    private diskService: DiskService,
  ) {}

  @Get()
  run() {
    return [this.cpuService.computer(1, 2), this.diskService.getData()];
  }
}

npm run start:dev 로 서버를 실행해보자.

GET 요청을 했기 떄문에 별도로 API Client 이용안하고 로컬호스트 링크 로 들어가면 [3,”data!”] 가 뜬다!

혹시 안된다면 import, export에서 모듈과 서비스를 헷갈리지 않았는지 확인해보고 오타도 확인해보자..

정리해보겠다.

  1. power 모듈은 provider로 power service를, export로 power service를 가진다. (power 디렉토리의 모듈 파일에 나와있다.)
  2. power 모듈의 DI container에는 클래스와 종속성이 나와있을 것이다. 여기에는 Power Service 뿐이다.(power 모듈은 아무것도 import 하지 않았으니까)
  3. Cpu 모듈은 provider로 Cpu service를 가지고 Power Module(서비스 아님) 을 import한다. export는 지금은 잠시 제쳐두자.
  4. Cpu 모듈의 DI container는 Cpu Service를 가지고 있다. 이것을 작동시키기 위해 Power Service가 필요하다. Power 모듈에서 Power Service를 export 했고, Cpu 모듈에서 Power Module를 import 했기 떄문에 Power Service를 사용할 수 있다.
  5. power 모듈의 종속성을 Cpu 모듈에 주입하여 사용한 것이다.

import할 때 module, service가 헷갈린다.

Power 모듈 파일을 보자

1
2
3
4
5
6
7
8
9
// power.module.ts
import { Module } from '@nestjs/common';
import { PowerService } from './power.service';

@Module({
  providers: [PowerService],
  exports: [PowerService],
})
export class PowerModule {}

PowerModule 클래스를 export하고 그 모듈 내에서 PowerService도 export하여 사용할 수 있도록 한 것이다.

PowerService를 사용하기 위해 import 할 땐 **모듈**, 사용할 땐 **서비스**라고 생각하자.

이 사이드 프로젝트가 DI를 이해하는데 도움이 됐길!