의존성는 무엇을 말하는 것일까?
A가 B를 의존한다는 뜻은
B가 변하면, A에게 영향을 끼친다는 것이다.
코드 관점에서 의존관계 라는 것은 다음과 같은 뜻이다.
A 클래스 안에서 B 클래스 객체를 사용한다
다음 코드는 Car
클래스가 Engine
클래스를 의존하고 있는 예시이다.
class Engine {
public void start() {
System.out.println("엔진이 시작됩니다.");
}
}
class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = new Engine(); // Car 객체 내에서 직접 Engine 객체를 생성
}
public void drive() {
engine.start();
System.out.println("차가 운전됩니다.");
}
}
//...
Car myCar = new Car(); // Car 객체 생성
myCar.drive();
Car
클래스 내부에서 Engine
객체를 직접 생성하고 있다. 강하게 의존하고 있다.
이런 경우, Engine의 구현이 변경되거나, 다른 종류의 Engine으로 대체하고 싶으면 Car 클래스도 함께 수정해야 한다.
다음은, Engine을 인터페이스로 구현해서 다른 Engine으로 대체가 쉽도록 확장 가능성을 열어둔 코드이다.
interface Engine {
void start();
}
// 가솔린 엔진 구현체
class GasolineEngine implements Engine {
@Override
public void start() {
System.out.println("가솔린 엔진이 시작됩니다.");
}
}
// 전기 엔진 구현체
class ElectricEngine implements Engine {
@Override
public void start() {
System.out.println("전기 엔진이 시작됩니다.");
}
}
class Car {
private Engine engine;
public Car() {
this.engine = new GasolineEngine(); // Car 객체 내에서 특정 Engine 구현체를 직접 생성
// this.engine = new ElectricEngine(); // 또는 ElectricEngine 생성
}
public void drive() {
engine.start();
System.out.println("차가 운전됩니다.");
}
}
Car myCar = new Car();
myCar.drive();
위 예시에서 Car
클래스는 구체적인 엔진 클래스(가솔린, 전기)를 Engine
인터페이스에만 의존하게 된다.
이는 나중에 새로운 엔진 등을 추가하고자 할 때 Car
클래스의 로직은 변경할 필요가 없어지므로 확장성 측면에서 나아졌다.
하지만 여전히 Car
클래스에서 Engine
구현체인 GasolineEngine
를 생성하므로 구현체 간의 결합도가 높아져서 의존성이 발생하는건 마찬가지이다.
의존성 주입이란 정확히 뭘 말하는걸까?
객체를 필요로 하는 클래스 내에서 직접 객체를 생성하는 것이 아닌,
객체 외부에서 의존성(서비스)를 객체(클라이언트) 에게 직접 제공(주입) 하는 것을 말한다.
즉, 객체는 전달받는 객체가 어떻게 구성되어있는지 모르게 되는 것이다.
그래서 위에서 예시로 보여준 코드에 의존성 주입을 적용해보겠다.
interface Engine {
void start();
}
// 가솔린 엔진 구현체
class GasolineEngine implements Engine {
@Override
public void start() {
System.out.println("가솔린 엔진이 시작됩니다.");
}
}
// 전기 엔진 구현체
class ElectricEngine implements Engine {
@Override
public void start() {
System.out.println("전기 엔진이 시작됩니다.");
}
}
class Car {
private Engine engine;
// 의존성 주입: 생성자를 통해 구현체를 주입받는다.
public Car(Engine engine) {
this.engine = engine;
}
public void drive() {
engine.start();
System.out.println("차가 운전됩니다.");
}
}
//...
// 가솔린 엔진을 사용하는 차 생성
Car gasolineCar = new Car(new GasolineEngine());
gasolineCar.drive();
// 전기 엔진을 사용하는 차 생성
Car electricCar = new Car(new ElectricEngine());
electricCar.drive();
직전 예시 코드와 바뀐것은 Car 객체를 생성하는 부분밖에 없다.
여전히 Car
클래스는 구체적인 엔진 클래스 (GasolineEngine
, ElectricEngine
) 가 아닌 Engine 인터페이스에만 의존하고 있어서 확장 가능성이 좋고,
Car
의 생성자를 통해 Engine
구현체를 주입받기 때문에, Car
는 Engine
의 구체적인 구현에 대해 알 필요가 없어서 주입받은 엔진을 사용하여 작동한다. 또한, 다른 Engine
구현체로 쉽게 교체가 가능하기 때문에 클래스 간의 결합도가 낮아져서 유연성과 확장성이 향상된다.
이는 객체지향의 SOLID 원칙 중 의존관계 역전 원칙(Dependency Inversion Principle) 에 부합하므로, 좋은 객체지향 설계의 원칙을 따르고 있다.