[기본 개념] DI의 완전 기초에 대해 알아보자

찐찐·2022년 5월 6일
1

Nest를 공부할 때 Provider 설명에 나오던 DI 개념이 이해가 안가 이해해보고자 정리했다.
하지만 여전히 제대로 이해하지 못했으며......
오히려 더 헷갈려진 것 같기도 하다....

IoC(Inversion Of Control)

객체의 생성부터 소멸까지 애플리케이션이 제어권을 갖는 것이 아닌 대신 관리해주는 IoC 컨테이너(Nest에서는 Runtime System)에게 넘기는 것을 말한다.

  • 기존의 제어 흐름과 비교해서 전환된 제어 흐름이다.
  • IoC에서 프로그램의 사용자 정의 부분은 일반적으로 프레임워크에게 제어 흐름을 받는다.

IoC container

: IoC를 구현하는 프레임 워크. 객체의 생성, 관리, 의존성의 관리를 책임지는 컨테이너다.

  • 외부에서 객체를 생성하고, 필요한 객체를 주입시키고 관리하는 것이 IoC 컨테이너
  • 모든 의존성을 컨테이너를 통해 받아오게 되니 일관화되게 구현할 수 있다.

기존의 제어 흐름 VS IoC

  • 기존의 제어 흐름: 사용자 코드가 재사용 가능한 라이브러리를 부른다.
  • IoC: 프레임워크가 사용자 코드를 부른다.
    -> 즉, 클라이언트 코드가 모든 제어를 가지던 흐름에서 프레임워크가 제어를 나눠 갖도록 반전하는 것이다.

IoC의 목적

  1. 어떻게?(작업 구현 방식)와 언제?(작업 수행)를 구분한다.
  2. 결합도에 대한 고민을 할 필요 없이 모듈 목적에 집중해 제작할 수 있다.
  3. 다른 시스템의 동작을 고려할 필요 없이 정해진 협약대로 동작하게만 구현하면 된다.
  4. 모듈을 변경해도 다른 시스템에 부작용을 일으키지 않는다.

예시

모니터가 망가졌을 때, 노트북은 해당 노트북 기종에 맞는 모니터만 사용할 수 있지만 데스크탑은 아무 모니터나 가져다 사용할 수 있는 것으로 이해할 수 있다.


DI(Dependency Injection)

객체가 의존성이 있는 다른 객체를 받는 디자인 패턴이다.

  • 즉, 의존관계를 외부에서 결정하고 주입하는 것
  • IoC 구현 기술 중 하나이다.

설명 예시

너가 냉장고에서 스스로 무언가를 꺼낼 때, 문제가 될 수 있단다. 냉장고의 문을 그대로 열어둘 수 있고, 엄마와 아빠가 너가 먹기를 원하지 않는 초콜릿이나 사탕같은것들을 왕창 꺼낼 수도 있지. 또는 엄마와 아빠가 냉장고에 넣어놓지 않은 땅콩버터를 찾을수도있고, 또는 미처 버리지 못해서 유통기한이 지난 땅콩버터를 찾을수도 있지. 그렇기 때문에 냉장고에서 필요한게 있을때 항상 말해주면 좋겠다. “저는 점심에 빵과 함께 먹을 땅콩버터가 필요해요” 이런식으로. 그러면 엄마랑 아빠가 너가 점심을 먹기위해 식탁에 앉았을 때 땅콩버터가 준비될 수 있도록 할게

부모가 냉장고 문을 열 수 있는 권한을 가지고 아이가 먹고 싶은 것을 요구하면 부모가 냉장고에서 음식을 꺼내주는 것

Dependecy

A가 B를 의존한다는 것은, A가 B의 기능을 이용하는 것이다.

  • 즉 B가 변경되면 A는 B를 의존하고 있으므로 영향을 받게 된다.

의존관계를 인터페이스로 추상화

A가 B를 의존하고 있을 때, A가 C(혹은 더 다양한 클래스를)도 의존하고 싶어지는 경우 계속해서 코드를 수정해야 한다.
-> 이러한 문제를 해결하기 위해 인터페이스를 만들어 추상화한다.
-> 이렇게 추상화하면 다양한 의존관계를 맺을 수 있고, 실제 구현 클래스와의 관계가 느슨해져 결합도가 낮아지게 된다.

DIP (Dependency Inversion Principle)

고차원 모듈은 저차원 모듈에 의존하면 안되며, 모듈은 모두 다른 추상화된 것에 의존해야 한다.
추상화 된 것은 구체적인 것에 의존하면 안되며, 구체적인 것이 추상화된 것에 의존해야 한다.
=> 이를 통해 의존 관계를 독립적으로 만들어준다.

DI의 목표

  1. 느슨하게 결합(loosley copuled)된 프로그램을 만든다.
  • copuling: 모듈간의 상호 의존도, 모듈들이 얼마나 서로 관계있는지 그 강도를 나타낸다. copuling이 낮을 수록 잘 설계된 구조이며, 높은 cohesion과 함께 할 때 읽기 쉽고 유지 보수가 용이한 구조가 된다.
  1. 객체가 사용하고자 하는 서비스의 자세한 구성을 알 필요 없이 외부에서 의존성을 주입받아 사용한다.

DI 속 4가지 역할

service and clients

  • 서비스는 유용한 기능을 가진 어떤 클래스
  • 클라이언트는 그 기능(서비스)를 사용하는 어떤 클래스
  • 어떤 객체든 서비스나 클라이언트가 될 수 있으며, injection에서 어떤 역할을 하냐에 따라 달라지는 것. 즉, 동시에 서비스이자 클라이언트 일 수도 있다.

interface

클라이언트가 사용하고, 서비스가 구현하는 인터페이스이다.

  • 클라이언트는 사용할 서비스의 세부 구현을 알 필요가 없다.
    -> 이를 통해 종속성이 변경?(서비스가 변경..?)돼도 클라이언트는 뭘 바꿀 필요가 없다.

injector

클라이언트에게 서비스를 소개시켜주는 역할

  • 클라이언트나 서비스 역할을 갖는 복잡한 객체 그래프를 구성하고, 연결하는 역할을 한다.
  • 인젝터 자체가 함께 동작하는 구성요소 일 수도 있지만, 순환 의존성을 피하기 위해 클라이언트가 돼서는 안된다.

DI 장점

  1. 의존성이 감소한다.
  2. 코드의 재사용성이 높아진다.
  3. 테스트하기 좋은 코드가 된다.
    • 유닛 테스트를 더 쉽게 할 수 있게 된다.
  4. 가독성이 좋아진다.
    • 각 기능을 별도로 분리해 가독성을 높인다.

DI 종류

1. constructor injection

client의 생성자를 통해 의존성을 제공한다.

  • 가장 흔한 방법이며, 클래스가 올바르게 동작하기 위한 의존성을 갖고 있을 때 이 방법을 사용한다.
  • 필요한 의존성을 지정할 수 있고, 의존성이 얻어졌음을 보장할 수 있다.
    -> 필요한 의존성 없이 객체화 되지 않기 때문에 항상 유효한 client를 보장한다.
  • Nest는 이 방법을 권장한다.

2. Property Injection(Setter Injection)

client가 의존성을 받는 setter method를 제공한다.

  • 의존성이 선택적으로 필요할 때,의존성이 인스턴스화 이후 바뀔 가능성이 있을 때 사용한다.
  • 의존성이 property로 제공되기 때문에 원하는 대로 설정해서 사용할 수 있다.
    • 대신 의존성이 없는 경우의 기본 구현을 제공해야한다.
  • 유연성이 높지만 모든 의존성이 주입됐는지, 모든 클라이언트가 유효한지 사용되기 전에 확인하기 어렵다.

3. Method Injection

의존성의 interface가 전달된 클라이언트에 의존성을 주입하는 injector method를 제공한다.

  • 의존성의 구현이 여러 가지 버전이 있을 때 아니면 의존성이 매번 새롭게 바뀌어야 할 때 사용한다.
    • 예를 들어, 차에 색을 칠할 때마다 칠하는 붓을 바꿔야 하는 경우
  • 사용 시점에 의존성을 바로 주입할 수 있다.

Nest에서 제공하는 DI

Nest에서의 DI

  • Angular의 DI를 많이 참고했다고 한다.
  • TypeSciprt를 사용하기 때문에 타입을 통한 쉬운 의존성 관리가 가능하다.
  • NestJS runtime system을 IoC 컨테이너로 사용한다.

Provider

Nest의 기본 개념. 많은 Nest의 기본 class들이 provider로 취급된다.

  • Nest의 거의 모든 데이터 처리 및 비즈니스 로직을 담당한다.
  • provider의 메인 개념은 의존성을 주입하는 것이다.
  • controler가 외부와의 통신을 담당한다면, service는 비즈니스 로직을 구현한다.
    -> 즉, Nest는 controler와 하위 계층(provider)를 구분한다.
    -> 결합도는 낮추고 응집도는 높힌다.

Nest에서 DI 사용하기

클래스에 @Injectable() 데코레이터를 붙여서 사용한다. 데코레이터를 붙이면 Nest IoC 컨테이너에서 해당 클래스를 프로바이더로 인식하고 관리한다.

  • inejctable() 데코레이터가 붙은 클래스는 singleton 객체로 메모리에 존재하게 된다.
    • singleton: 클래스의 인스턴스화를 하나(single)의 인스턴스로 제한하는 것이다.
      여러 인스턴스가 만들어져 메모리가 낭비되는 것을 방지하고, 내부 데이터 값도 공유될 수 있도록 유지된다.
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  findAll(): Cat[] {
    return this.cats;
  }
}
  • provider를 만들었으면, contorller에 의존성을 주입해 사용할 수 있다.
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}
  • provider를 IoC 컨테이너에 등록하고 토큰과 연관시킨다.
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}
  1. Nest IoC 컨테이너가 contorller를 인스턴스화할 때, 종속성을 먼저 찾는다.
  2. 종속성을 찾았으면, 해당 클래스를 반환하는 토큰을 찾는다.
  3. Singleton Scope라는 가정하에, 하나의 인스턴스를 만들고 저장 후 반환한다. 만약 이미 저장됐다면, 존재하고 있는 인스턴스를 반환한다.

내가 이해한 대로 최종 정리 :)

  1. Depdenecy Injection은 의존성 주입으로, IoC를 구현하는 기술 중 하나이다.
  2. 의존성 주입은 의존 관계를 인터페이스로 추상화 한 후, 그것을 외부에서 구체화 해 객체에 넣어주는 것이다.
  3. 의존성 주입을 주입받은 client는 정확한 구현을 몰라도 service의 기능을 사용할 수 있다.
  4. 의존성 주입을 사용하면 결합도가 느슨해져 변경이 용이해지고 테스트가 쉬워지며, 재사용이 용이한 코드가 되는 등 여러 장점을 갖는 코드를 만들 수 있다.
  5. Nest에서는 Provider를 사용해 의존성을 주입할 수 있고, 클래스에 Injectable() 데코레이터를 붙여 IoC 컨테이너에서 해당 컨테이너를 관리할 수 있게 선언하여 Provider로 사용한다.

참고자료

IoC

DI


이해 못해서 보류한 부분들

DI가 해결해주는 것들

  • 어떻게 클래스가 의존하는 객체의 생성으로부터 독립적일 수 있는가?
  • 어떻게 애플리케이션과 객체는 다른 구성을 가져도 지원하는가?
  • 어떻게 직접적인 수정 없이 코드 일부의 행동을 수정할 수 있는가?

DI의 근본적인 구성

method에 parameter로 전달하는 것으로 구성된다

  • 클라이언트가 직접 빌드하거나 서비스를 찾지 않는다.
  • 정확한 구현을 하는 것이 아니라, 사용할 서비스의 인터페이스만 정의하면 된다.

DI가 구현하는 IoC

DI는 IoC를 composition을 통해 구현한다.

  • composition 추가 조사 필요할듯!.!
  • strategy pattern과 비슷하게 취급되는데, 차이점은 strategy pattern은 객체의 생명주기 동안 상호교환이 가능한 의존성을 목적으로 한다면, DI는 일반적으로 단일 족송성에 사용된다.
profile
백엔드 개발자 지망생

0개의 댓글