[Nest.js] 제어의 역전(Ioc), 의존성 주입(DI)

장성우·2023년 3월 28일
0
post-thumbnail

제어의 역전 - Ioc(Inversion of Control)

IoC는 프로그래머가 작성한 프로그램이 재사용 라이브러리의 흐름 제어를 받게 되는 소프트웨어 디자인 패턴을 말한다. 전통적인 프로그래밍에서 흐름은 프로그래머가 작성한 프로그램이 외부 라이브러리의 코드를 호출해 이용한다. 하지만 제어 반전이 적용된 구조에서는 외부 라이브러리의 코드가 프로그래머가 작성한 코드를 호출한다. 설계 목적상 제어 반전의 목적은 다음과 같다.

  • 작업을 구현하는 방식과 작업 수행 자체를 분리한다.
  • 모듈을 제작할 때, 모듈과 외부 프로그램의 결합에 대해 고민할 필요 없이 모듈의 목적에 집중할 수 있다.
  • 모듈을 바꾸어도 다른 시스템에 부작용을 일으키지 않는다.

DI(Dependency injection)

IoC 컨테이너가 직접 객체의 생명주기를 관리하는 방식이다.

위 사진의 모듈에 클래스들 사이엔 명확한 종속성이나 계층 구조가 있다. 서비스가 올바르게 작동하기 위해서 저장소에 의존하고, 컨트롤러가 올바르게 작동하기 위해서는 서비스에 의존한다.

export class UsersController {
	constructor(private readonly usersService: UsersService) {}
		...
}

UsersController는 UsersService에 의존하고 있지만 UsersService 객체의 라이프 사이클에는 전혀 관여하지 않고 있다. 어디선가 자신의 생성자에 주어지는 객체를 가져다 쓰고 있을 뿐이다. 이 역할을 하는것이 IoC 다.

DI는 이렇게 IoC 컨테이너가 직접 객체의 생명주기를 관리하는 방식이다. 예를 들어 A 객체에서 B객체가 필요하다고 할 때, A 클래스에는 B 클래스를 직접 생성하여 사용할 수 있다. 여기서 B의 구현체가 변경되면 문제가 발생한다. A는 B를 직접 참조하고 있으므로 B가 변경될 때마다 컴파일러는 A를 다시 컴파일 해야한다.

이를 해결하려면 B에 대한 인터페이스 IB를 정의하고, A에서는 IB 타입을 이용하면 된다. 하지만 IB의 구현체 B1, B2 등을 직접 생성해야 하는 것은 여전하다. 여기서 IoC의 강력함이 발휘되는 것이다.

IoC를 사용하지 않은 코드와 사용한 코드를 비교해보자

IoC사용 X

export interface Person {
  getName: () => string;
}

@Injectable()
export class Dexter implements Person {
  getName() {
    return 'Dexter';
  }
}

@Injectable()
export class Jane implements Person {
  getName() {
    return 'Jane';
  }
}

class MyApp {
    constructor(private person: Person) {}
}

L1~17: Person 인터페이스를 구현하는 2개의 클래스 Dexter, Jane이 있다. 각 클래스는 getName 함수의 구현체가 다르다.

L19~24: MyApp 클래스는 Person 타입의 멤버 변수를 가지고 생성자에서 구현체를 생성.

IoC 사용 O

class MyApp {
    constructor(@Inject('Person') private person: Person) { }
}

이제 Person 객체의 관리는 IoC가 담당한다. Person은 interface인데 Person을 실제로 구현한 클래스를 어디선가 정의를 해두어야 객체를 생성할 수가 있을 것이다. 이는 모듈에서 선언한다.

@Module({
  controllers: [UsersController],
  providers: [
		UsersService,
    {
      provide: 'Person',
      useClass:Dexter
    }
  ]
})
export class UserssModule {}

객체로 선언할 때 provide 속성에 토큰을 'Person'으로 주고 있다. 이 토큰은 프로바이더를 가져다 쓸 때 @Inject 데코레이터의 인자로 넘겨준 것과 같다. 만약 Dexter 객체가 아니라 Jane으로 구현을 바꾸고자 한다면 useClass 속성의 클래스 이름만 바꾸면 된다.

@Module({
  controllers: [UsersController],
  providers: [
		UsersService,
    {
      provide: 'Person',
      useClass:Jane
    }
  ]
})
export class UserssModule {}

DI 기능 비유

고문관한테 아 제발 넌 아무것도 하지마. 그냥 뭐 필요한 거 있으면 얘기해 내가 도와줄테니까. 너는 뭐 하려는 능력을 키우지마, 그냥 부탁해 니 능력 내가 대신 해줄게. 그러니까 DI는 어떤 것을 필요할 때 필요하다고 말만 하면 프레임워크가 알잘딱해주는 것이다. 이렇게 프로그램에게 뭐 필요하다 요청을 통해 객체 간의 의존성을 낮출 수 있다.

0개의 댓글