의존성 역전 원칙(Dependency Inversion Principle, DIP)은 객체 지향 설계의 중요한 원칙 중 하나로, 로버트 C. 마틴이 제안했습니다. 이 원칙은 고수준 모듈이 저수준 모듈에 의존해서는 안 되며, 이 둘 다 추상화에 의존해야 한다는 것을 의미합니다. 구체적으로 말하면, 구현이 아닌 추상화에 의존하도록 코드를 작성해야 합니다.
의존성 역전 원칙(DIP)은 고수준 모듈과 저수준 모듈이 추상화에 의존하게 하여 유연성과 유지보수성을 향상시키는 중요한 원칙입니다. 이를 통해 코드를 더 모듈화하고, 변경에 대한 영향을 최소화하며, 테스트 용이성을 높일 수 있습니다. DIP를 적용하여 설계하면, 더 견고하고 확장 가능한 소프트웨어 시스템을 구축할 수 있습니다.
고수준 모듈과 저수준 모듈:
고수준 모듈은 시스템의 주요 정책이나 비즈니스 로직을 포함하는 모듈이며, 저수준 모듈은 세부적인 구현을 포함하는 모듈입니다. DIP는 이 둘 간의 의존성을 역전시켜야 한다고 주장합니다.
추상화에 의존:
구체적인 클래스가 아닌 인터페이스나 추상 클래스에 의존하도록 코드를 작성합니다. 이를 통해 구현의 변경에 대한 영향을 최소화할 수 있습니다.
의존성 주입:
DIP를 구현하는 한 가지 방법은 의존성 주입(Dependency Injection)을 사용하는 것입니다. 이를 통해 객체 간의 의존성을 외부에서 주입할 수 있습니다.
유연성과 확장성 향상:
DIP를 준수하면 코드가 특정 구현에 의존하지 않기 때문에 유연성과 확장성이 향상됩니다. 새로운 구현체를 추가하거나 기존 구현체를 변경하더라도 고수준 모듈에 영향을 미치지 않습니다.
테스트 용이성:
DIP를 통해 의존성을 주입하면, 모듈을 독립적으로 테스트할 수 있습니다. 모의 객체(Mock Object)를 사용하여 테스트할 수 있기 때문에 단위 테스트가 용이해집니다.
유지보수성 향상:
구체적인 구현이 아닌 추상화에 의존하기 때문에, 코드의 유지보수성이 향상됩니다. 변경이 필요한 경우 추상화 계층만 수정하면 됩니다.
인터페이스와 추상 클래스 사용:
클래스 간의 의존성을 줄이기 위해 인터페이스와 추상 클래스를 사용합니다.
의존성 주입 사용:
생성자 주입(Constructor Injection), 세터 주입(Setter Injection), 인터페이스 주입(Interface Injection) 등의 의존성 주입 방법을 사용하여 객체 간의 의존성을 외부에서 설정합니다.
예를 들어, DIP를 위반하는 코드와 DIP를 준수하는 코드를 비교해보겠습니다.
DIP를 위반하는 코드:
class Light {
public void turnOn() {
System.out.println("Light turned on");
}
public void turnOff() {
System.out.println("Light turned off");
}
}
class Switch {
private Light light;
public Switch() {
this.light = new Light(); // 직접 인스턴스 생성
}
public void operate() {
// 스위치를 조작하는 코드
light.turnOn();
}
}
이 예시에서 Switch 클래스는 Light 클래스에 직접 의존하고 있습니다. 만약 Light 클래스의 구현을 변경해야 한다면 Switch 클래스도 수정해야 합니다.
DIP를 준수하는 코드:
interface Switchable {
void turnOn();
void turnOff();
}
class Light implements Switchable {
@Override
public void turnOn() {
System.out.println("Light turned on");
}
@Override
public void turnOff() {
System.out.println("Light turned off");
}
}
class Switch {
private Switchable device;
public Switch(Switchable device) {
this.device = device; // 의존성 주입
}
public void operate() {
// 스위치를 조작하는 코드
device.turnOn();
}
}
이 예시에서는 Switchable 인터페이스를 도입하여 Light 클래스가 이를 구현하도록 합니다. Switch 클래스는 Switchable 인터페이스에 의존하게 하고, 구체적인 구현은 생성자 주입을 통해 설정합니다. 이렇게 하면 Switch 클래스는 Light 클래스의 구체적인 구현에 의존하지 않게 되어, 다른 Switchable 구현체로 쉽게 교체할 수 있습니다.