의존성 주입(Dependency Injection)은 객체 지향 프로그래밍에서 사용되는 설계 패턴 중 하나로, 클래스간의 의존 관계를 느슨하게 만드는 방법입니다.
의존성 주입은 클래스에서 사용하는 객체를 직접 생성하거나 참조하지 않고, 외부에서 생성된 객체를 주입받아 사용합니다.
이렇게 함으로써, 클래스와 객체 사이의 의존 관계를 느슨하게 만들 수 있습니다.
패턴에서 '주입'(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
클래스의 테스트 결과가 분리되어 있습니다.
따라서, 생성자 주입을 적용하는 것이 의존성 주입의 권장사항입니다. 생성자 주입을 사용하면, 코드의 유지보수성과 테스트 용이성이 개선됩니다.
장점:
단점:
따라서, 생성자 주입은 안정적이고 명시적인 의존성 관리가 가능하지만, 코드가 길어질 수 있고 의존성이 많을 때 처리하기 어려울 수 있습니다. 클래스의 의존성이 간단하고 명확할 경우에 적합합니다.
필드 주입(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
)에 의존성을 주입하고 있습니다.
이 경우, 다음과 같은 문제점이 있습니다.
프로퍼티 주입을 적용한 경우
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
메서드를 통해 의존성을 주입하기 때문에, 클래스 내부에서 어떤 의존성이 주입되는지 명확하게 파악할 수 있습니다.단점:
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 패턴을 선택해야 합니다.