oop: 의존성 주입

hwisaac·2023년 2월 27일
0

oop

목록 보기
5/5

Youtube clone

개념

의존성 주입(Dependency Injection)은 객체 지향 프로그래밍에서 사용되는 설계 패턴 중 하나로, 클래스간의 의존 관계를 느슨하게 만드는 방법입니다.

의존성 주입은 클래스에서 사용하는 객체를 직접 생성하거나 참조하지 않고, 외부에서 생성된 객체를 주입받아 사용합니다.

이렇게 함으로써, 클래스와 객체 사이의 의존 관계를 느슨하게 만들 수 있습니다.

패턴에서 '주입'(Injection) 이라는 단어는 외부에서 의존성 객체를 제공하는 것을 의미합니다.

의존성 객체를 객체 내부에서 생성하는 것이 아니라 외부에서 제공받아 '주입'받는 것과 같은 개념이기 때문입니다.

의존성 주입은 클래스를 테스트하기 쉽게 만들어주고, 유지 보수성을 높여줍니다.

특히, 객체지향 프로그래밍에서는 클래스간의 의존 관계가 복잡해지는 경우가 많은데, 이런 경우에 의존성 주입을 사용하면 코드의 유연성과 확장성을 높일 수 있습니다.

의존성 주입은 다음과 같은 방법으로 구현됩니다.

  1. 생성자 주입(Constructor Injection)은 객체를 생성할 때 생성자를 통해 외부에서 객체를 주입받습니다.
  2. 필드 주입(Field Injection)은 객체를 생성한 후에 필드를 통해 객체를 주입받습니다.
  3. 프로퍼티 주입(Property Injection)은 객체 생성 이후에 의존성을 주입하는 방식입니다. 주로 Setter 메서드를 통해 의존성 객체를 주입합니다.
  4. 메서드 주입(Method Injection)은 객체를 생성한 후에 메서드를 통해 객체를 주입받습니다.

의존성 주입은 Spring Framework, Angular, React 등의 프레임워크에서 널리 사용되는 개념이며, 객체 지향 프로그래밍에서 중요한 개념 중 하나입니다.

생성자 주입

생성자 주입(Constructor Injection)은 객체를 생성할 때 생성자를 통해 외부에서 객체를 주입받는 방법입니다. 이 방법은 객체 생성 시점에 모든 의존성을 주입받기 때문에 안전하고, 의존성이 변하지 않는 경우에 유용합니다.

생성자 주입을 적용하지 않은 경우:

class Car {
  private engine: Engine;

  constructor() {
    this.engine = new Engine();
  }

  public start(): void {
    this.engine.start();
  }
}

위 코드에서는 Car 클래스의 생성자에서 Engine 클래스의 인스턴스를 직접 생성하여 의존성을 주입하고 있지 않습니다. 따라서, Car 클래스를 사용하려면, Engine 클래스의 인스턴스를 직접 생성해야 합니다. 이는 의존성 주입을 적용하지 않은 경우에 해당하며, 다음과 같은 문제점이 있습니다.

  • Car 클래스와 Engine 클래스가 강하게 결합되어 있기 때문에, Engine 클래스의 구현이 변경되면 Car 클래스의 코드도 변경해야 합니다. 이는 코드 유지보수를 어렵게 만들고, 유연성을 저하시킵니다.
  • Car 클래스를 테스트하기 어려울 수 있습니다. Engine 클래스의 인스턴스를 직접 생성하기 때문에, Engine 클래스의 동작에 따라 Car 클래스의 테스트 결과가 영향을 받을 수 있습니다.

생성자 주입을 적용한 경우:

class Car {
  private engine: Engine;

  constructor(engine: Engine) {
    this.engine = engine;
  }

  public start(): void {
    this.engine.start();
  }
}

위 코드에서는 Car 클래스의 생성자에서 Engine 클래스의 인스턴스를 외부에서 주입받고 있습니다. 따라서, Car 클래스를 사용하려면, 외부에서 Engine 클래스의 인스턴스를 주입해야 합니다. 이는 생성자 주입을 적용한 경우에 해당하며, 다음과 같은 이점이 있습니다.

Car 클래스와 Engine 클래스가 느슨하게 결합되어 있기 때문에, Engine 클래스의 구현이 변경되어도 Car 클래스의 코드를 변경할 필요가 없습니다. 이는 코드 유지보수를 쉽게 만들고, 유연성을 높입니다.
Car 클래스를 테스트하기 쉬워집니다. Engine 클래스의 인스턴스를 외부에서 주입받기 때문에, Engine 클래스의 동작과 Car 클래스의 테스트 결과가 분리되어 있습니다.

따라서, 생성자 주입을 적용하는 것이 의존성 주입의 권장사항입니다. 생성자 주입을 사용하면, 코드의 유지보수성과 테스트 용이성이 개선됩니다.

생성자 주입의 장단점

장점:

  • 안정성이 높다: 생성자 주입은 객체를 생성할 때 모든 의존성을 주입받으므로, 의존성이 변하지 않는 경우에 안전합니다.
  • 명시적인 의존성 관리: 생성자 주입을 사용하면 클래스의 의존성을 명시적으로 관리할 수 있습니다. 즉, 클래스 내에서 필요한 의존성을 직접 생성하지 않고, 외부에서 주입받아 사용하기 때문에 코드의 복잡도를 줄일 수 있습니다.
  • 테스트 용이성: 의존성을 주입받기 때문에 테스트할 때 Mock 객체를 생성하여 주입할 수 있습니다.

단점:

  • 코드가 길어진다: 생성자 주입을 사용하면 객체 생성 코드가 길어지기 때문에 코드의 가독성이 떨어질 수 있습니다.
  • 의존성이 많아질 경우 처리하기 어려울 수 있다: 만약 클래스가 의존성이 많은 경우, 생성자 인자가 길어지고 복잡해질 수 있습니다. 이 경우에는 인자를 일일히 넘기는 것이 번거로울 수 있습니다.
  • 의존성 순환문제 발생 가능성: 의존성이 순환적일 때, 생성자 주입을 사용하면 의존성 순환문제가 발생할 수 있습니다.

따라서, 생성자 주입은 안정적이고 명시적인 의존성 관리가 가능하지만, 코드가 길어질 수 있고 의존성이 많을 때 처리하기 어려울 수 있습니다. 클래스의 의존성이 간단하고 명확할 경우에 적합합니다.

필드 주입

필드 주입(Field Injection)은 객체 생성 후, 필드에 의존성을 주입하는 방식입니다.

다음과 같이 작성할 수 있습니다.

class Car {
  private engine!: Engine;

  constructor(private engineService: EngineService) {}

  public start(): void {
    this.engine = this.engineService.getEngine();
    this.engine.start();
  }
}

위 코드에서는 Car 클래스의 생성자에서 EngineService 클래스의 인스턴스를 주입받고 있습니다. 따라서, Car 클래스를 사용하려면, 외부에서 EngineService 클래스의 인스턴스를 주입해야 합니다. 이는 필드 주입을 적용한 경우에 해당하며, 다음과 같은 이점이 있습니다.

  • Car 클래스와 EngineService 클래스가 느슨하게 결합되어 있기 때문에, EngineService 클래스의 구현이 변경되어도 Car 클래스의 코드를 변경할 필요가 없습니다. 이는 코드 유지보수를 쉽게 만들고, 유연성을 높입니다.
  • Car 클래스를 테스트하기 쉬워집니다. EngineService 클래스의 인스턴스를 외부에서 주입받기 때문에, Engine 클래스의 동작과 Car 클래스의 테스트 결과가 분리되어 있습니다.

위 코드에서는 Car 클래스의 필드 engine을 느낌표(!)로 선언하여, 필드 주입이 발생하면 TypeScript 컴파일러에서 오류를 무시하도록 설정하고 있습니다. 이는 필드 주입이 초기화되지 않은 상태에서 사용되는 것을 허용하기 위한 방법입니다

필드주입 장단점

장점:

  • 코드가 간결해진다: 필드 주입은 객체 생성 후 필드에 의존성을 주입하기 때문에, 생성자 인자를 따로 받을 필요가 없습니다. 따라서 코드가 간결해질 수 있습니다.
  • 의존성 주입의 편리성: 필드 주입을 사용하면 객체 생성 후에도 의존성을 주입할 수 있기 때문에, 객체를 생성하고 나서도 의존성을 쉽게 변경할 수 있습니다.

단점:

  • 의존성이 숨겨질 수 있다: 필드 주입을 사용하면, 객체 생성 시점에 의존성이 명시되지 않기 때문에, 코드의 의존성이 숨겨질 수 있습니다. 이러한 경우에는 코드의 유지보수가 어렵고, 테스트가 어렵습니다.
  • 테스트 용이성: 필드 주입을 사용하면 테스트할 때, 의존성을 목 객체로 대체하기 어렵습니다.
  • 결합도 증가: 필드 주입은 클래스와 의존성 간의 결합도를 증가시킬 수 있습니다. 즉, 의존성이 많아질수록 객체 간의 결합도가 높아지기 때문에, 객체의 유지보수성이 떨어질 수 있습니다.

필드 주입은 코드가 간결해지며, 의존성 주입의 편리성이 높아지지만, 의존성이 숨겨질 수 있고, 테스트가 어렵며, 결합도가 증가할 수 있습니다. 클래스의 의존성이 단순하고 명확할 경우에 적합합니다.

프로퍼티 주입

프로퍼티 주입은 생성자 주입(Constructor Injection)과 달리 선택적인 의존성 주입에 많이 사용됩니다.

프로퍼티 주입(Property Injection)을 적용하기 전과 후를 비교하며 설명해보겠습니다. TypeScript로 작성된 예시 코드를 사용하겠습니다.

프로퍼티 주입을 적용하지 않은 경우

class Car {
  private engine: Engine;

  constructor() {
    this.engine = new Engine();
  }

  public setEngine(engine: Engine): void {
    this.engine = engine;
  }

  public start(): void {
    this.engine.start();
  }
}

위 코드에서는 Car 클래스의 생성자에서 Engine 클래스의 인스턴스를 직접 생성하여 의존성을 주입하고 있지 않습니다. 대신, setEngine 메서드를 통해 프로퍼티(engine)에 의존성을 주입하고 있습니다.

이 경우, 다음과 같은 문제점이 있습니다.

  • 의존성 주입이 생성자에서 이루어지지 않기 때문에, 객체의 생성과 동시에 의존성이 설정되지 않습니다. 이는 객체의 상태를 불안정하게 만들 수 있습니다.
  • setEngine 메서드에서 의존성을 주입하므로, 의존성이 없는 상태로 Car 클래스의 인스턴스가 생성되어도 start 메서드를 호출할 수 있습니다. 이는 객체의 상태를 불안정하게 만들 수 있습니다.
  • setEngine 메서드를 호출하지 않고, start 메서드를 바로 호출하면, engine 프로퍼티가 초기화되지 않아 런타임 오류가 발생할 수 있습니다.

프로퍼티 주입을 적용한 경우

class Car {
  private engine!: Engine;

  @Inject(Engine)
  public set engineService(engineService: EngineService) {
    this.engine = engineService.getEngine();
  }

  public start(): void {
    this.engine.start();
  }
}

위 코드에서는 Car 클래스의 engineService 프로퍼티에 @Inject 어노테이션을 사용하여 Engine 클래스의 의존성을 주입하고 있습니다. 이는 프로퍼티 주입을 적용한 경우에 해당하며, 다음과 같은 이점이 있습니다.

Car 클래스와 EngineService 클래스가 느슨하게 결합되어 있기 때문에, EngineService 클래스의 구현이 변경되어도 Car 클래스의 코드를 변경할 필요가 없습니다. 이는 코드 유지보수를 쉽게 만들고, 유연성을 높입니다.
Car 클래스를 테스트하기 쉬워집니다. EngineService 클래스의 인스턴스를 외부에서 주입받기 때문에, Engine 클래스의 동작과 Car 클래스의 테스트 결과가 분리되어 있습니다.

위 코드에서는 Car 클래스의 engine 필드를 느낌표(!)로 선언하여, 필드 주입이 발생하면 TypeScript 컴파일러에서 오류를 무시하도록 설정하고 있습니다. 이는 필드 주입이 초기화되지 않은 상태에서 사용되는 것을 허용하기 위한 방법입니다.

또한, @Inject 어노테이션을 사용하여 의존성 주입을 수행하는데, 이는 일반적으로 reflect-metadata 패키지와 함께 사용됩니다. reflect-metadata 패키지는 TypeScript에서 런타임에 타입 정보를 사용할 수 있도록 도와주는 패키지입니다.

위 코드에서는 engineService 프로퍼티가 의존성 주입을 위한 setter 메서드로 사용되고 있습니다. engineService 프로퍼티에 @Inject 어노테이션을 적용하면, engineService 프로퍼티에 주입되는 값의 타입 정보를 기반으로 해당 타입의 의존성을 주입하게 됩니다.

즉, engineService 프로퍼티가 설정될 때, EngineService 클래스의 인스턴스를 주입받아 getEngine 메서드를 호출하여 engine 필드에 값을 설정합니다. 이렇게 함으로써, Car 클래스의 생성자에서 의존성을 주입하지 않아도 engine 필드에 값이 설정됩니다. 따라서, Car 클래스를 사용할 때 engineService 프로퍼티에 EngineService 클래스의 인스턴스를 주입해주면 됩니다.

프로퍼티 주입 장단점

프로퍼티 주입(Property Injection)은 객체 생성 이후, Setter 메서드를 통해 의존성 객체를 주입하는 방식입니다. 이 방식의 장단점은 다음과 같습니다.

장점:

  • 선택적 의존성 주입: 프로퍼티 주입은 객체 생성 시점에서 모든 의존성을 주입받지 않아도 됩니다. 따라서, 선택적으로 필요한 의존성만 주입받을 수 있어 코드의 유연성이 높아집니다.
  • 코드의 가독성: 프로퍼티 주입은 Setter 메서드를 통해 의존성을 주입하기 때문에, 클래스 내부에서 어떤 의존성이 주입되는지 명확하게 파악할 수 있습니다.
  • 테스트 용이성: 프로퍼티 주입을 사용하면, 필요한 의존성을 모킹(mocking)하여 테스트할 수 있습니다. 이는 테스트 코드 작성을 용이하게 만들어줍니다.

단점:

  • 코드의 안전성: 프로퍼티 주입은 Setter 메서드를 통해 의존성을 주입하기 때문에, 클래스 내부에서 의존성이 변경될 가능성이 있습니다. 이는 코드의 안전성을 떨어뜨릴 수 있습니다.
  • 가독성 저하: 프로퍼티 주입은 Setter 메서드를 사용하기 때문에, 코드의 가독성이 떨어질 수 있습니다. Setter 메서드의 이름이나 파라미터명을 잘 지어야 하며, 필요한 Setter 메서드가 많아지면 가독성이 더욱 저하될 수 있습니다.

이처럼 프로퍼티 주입은 선택적 의존성 주입이 가능하고, 코드의 가독성이 좋다는 장점이 있지만, 코드의 안전성과 가독성 저하라는 단점이 있습니다.

메서드 주입

메서드 주입을 적용하지 않은 경우

class Car {
  private engine: Engine;

  constructor() {
    this.engine = new Engine();
  }

  public start(): void {
    this.engine.start();
  }

  public setEngine(engine: Engine): void {
    this.engine = engine;
  }
}

위 코드에서는 Car 클래스의 생성자에서 Engine 클래스의 인스턴스를 직접 생성하여 의존성을 주입하고 있지 않습니다. 대신, setEngine 메서드를 통해 메서드 인자로 의존성을 주입하고 있습니다.

이 경우, 다음과 같은 문제점이 있습니다.

객체 생성과 의존성 주입이 분리되어 있기 때문에, 객체의 생성과 동시에 의존성이 설정되지 않습니다. 이는 객체의 상태를 불안정하게 만들 수 있습니다.
start 메서드를 호출하기 전에 setEngine 메서드를 반드시 호출해야 하므로, 사용법이 불편할 수 있습니다.

메서드 주입을 적용한 경우

class Car {
  private engine!: Engine;

  public start(): void {
    this.engine.start();
  }

  public setEngine(engine: Engine): void {
    this.engine = engine;
  }

  @Inject(Engine)
  public set engineService(engineService: EngineService): void {
    this.engine = engineService.getEngine();
  }
}

위 코드에서는 Car 클래스의 engineService 메서드에 @Inject 어노테이션을 사용하여 Engine 클래스의 의존성을 주입하고 있습니다. 이는 메서드 주입을 적용한 경우에 해당하며, 다음과 같은 이점이 있습니다.

객체 생성과 의존성 주입이 한번에 이루어지므로, 객체의 상태를 안정적으로 만듭니다.
setEngine 메서드를 호출하지 않고, engineService 메서드만 호출해도 engine 필드에 값을 설정할 수 있습니다.
위 코드에서는 engineService 메서드가 의존성 주입을 위한 setter 메서드로 사용되고 있습니다. engineService 메서드에 @Inject 어노테이션을 적용하면, engineService 메서드에 주입되는 값의 타입 정보를 기반으로 해당 타입의 의존성을 주입하게 됩니다.

즉, engineService 메서드가 호출될 때, EngineService 클래스의 인스턴스를 주입받아 getEngine 메서드를 호출하여 engine 필드에 값을 설정합니다. 이렇게 함으로써, Car 클래스를 사용할 때 engineService 메서드에 EngineService 클래스의 인스턴스를 주입해주면 됩니다.

메서드 주입 장단점

메서드 주입의 장단점은 다음과 같습니다.

장점:

  • 코드의 유연성: 메서드 주입을 사용하면 필요한 의존성을 메서드 파라미터로 전달할 수 있기 때문에, 해당 메서드를 사용하는 클래스에서는 의존성 객체를 직접 생성할 필요가 없습니다. 이는 코드를 더 유연하게 만들어줍니다.

  • 테스트 용이성: 메서드 주입을 사용하면 의존성 객체를 모킹(mocking)하여 테스트할 수 있습니다. 이는 테스트 코드 작성을 용이하게 만들어줍니다.

  • 컴파일 시점 의존성 검사: TypeScript에서 메서드 주입을 사용하면, 컴파일 시점에서 의존성이 잘못 주입되는 경우 에러를 발생시킬 수 있습니다. 이는 코드의 안정성을 높여줍니다.
    단점:

  • 코드의 복잡성: 메서드 주입을 사용하면, 의존성을 전달하는 메서드가 많아질 수 있습니다. 이는 코드의 복잡성을 높일 수 있습니다.

  • 가독성 저하: 메서드 주입을 사용하면, 코드를 이해하기 어려울 수 있습니다. 메서드가 많아지고, 메서드 파라미터가 많아지면 가독성이 저하될 수 있습니다.

이처럼 메서드 주입은 의존성 주입 패턴에서 유연성과 테스트 용이성을 높이는 장점이 있지만, 코드의 복잡성과 가독성 저하라는 단점이 있습니다. 따라서, 프로젝트의 크기와 복잡도, 개발자들의 스킬 레벨 등을 고려하여 적절한 DI 패턴을 선택해야 합니다.

0개의 댓글