의존성 역전 원칙 (Dependency Inversion Principle, DIP)
고수준 모듈은 저수준 모듈에 의존해서는 안 된다 둘 다 추상화에 의존해야 한다
추상화는 세부 사항에 의존해서는 안 된다. 세부 사항은 추상화에 의존해야 한다
DIP의 장점
- 유연성 향상: 시스템의 한 부분을 변경해도 다른 부분에 영향을 주지 않으므로 시스템의 유연성이 높아다.
- 테스트 용이성: 각 모듈이 추상화에 의존하기 때문에 단위 테스트가 용이하다.
- 유지보수성 향상: 코드의 변경이 적고 재사용성이 높아져 유지보수성이 향상된다.
다른 원칙과의 관계:
- SRP: SRP를 지키면 각 클래스가 단일 책임을 가지므로, 의존성 주입 시 명확한 역할을 가진 객체들 간의 관계를 설정할 수 있습니다.
- OCP: DIP를 지키면 구체적인 구현체 대신 인터페이스나 추상 클래스에 의존하게 되어, 기존 코드를 수정하지 않고도 확장이 가능합니다.
- LSP: DIP를 적용하여 고수준 모듈이 추상화에 의존하게 되면, 서브 클래스들이 기반 클래스를 대체할 수 있어 LSP를 지킬 수 있습니다.
- ISP: ISP를 통해 인터페이스가 잘 분리되면, DIP를 적용하여 필요한 인터페이스에만 의존하게 할 수 있어, 불필요한 의존성을 줄이고 코드의 유연성을 높입니다.
의존성 역전원칙은 코드를 보면서 무엇이 고수준 모듈인지, 저수준 모듈인지 이해 할 수 있다
DIP 원칙을 따르지 않은 코드
// 저수준 모듈
class LightBulb {
public void turnOn() {
System.out.println("LightBulb: Bulb turned on...");
}
public void turnOff() {
System.out.println("LightBulb: Bulb turned off...");
}
}
// 고수준 모듈
class Switch {
private LightBulb lightBulb;
public Switch() {
this.lightBulb = new LightBulb(); // 직접 생성
}
public void operate(String command) {
if (command.equals("ON")) {
lightBulb.turnOn();
} else if (command.equals("OFF")) {
lightBulb.turnOff();
}
}
}
public class Main {
public static void main(String[] args) {
Switch mySwitch = new Switch();
mySwitch.operate("ON");
mySwitch.operate("OFF");
}
}
위의 코드를 보면 Switch에서 사용해야 하는 LightBulb를 직접 클래스 내에서 생성하는 것을 볼 수 있다
이렇게 되면 LightBulb의 인스턴스 생성이 바뀐다던지, 메소드가 추가 또는 삭제가 되면 Switch 내부의 LightBulb 메서드를 관리하기 어렵기 때문에 고수준 모듈인 Switch가 저수준 모듈 LightBulb를 의존하게 된다
DIP 원칙을 따르는 코드
// 추상화된 인터페이스
interface Switchable {
void turnOn();
void turnOff();
}
// 저수준 모듈
class LightBulb implements Switchable {
@Override
public void turnOn() {
System.out.println("LightBulb: Bulb turned on...");
}
@Override
public void turnOff() {
System.out.println("LightBulb: Bulb turned off...");
}
}
// 고수준 모듈
class Switch {
private Switchable device;
public Switch(Switchable device) {
this.device = device; // 추상화로 받아와서 저장
}
public void operate(String command) {
if (command.equals("ON")) {
device.turnOn();
} else if (command.equals("OFF")) {
device.turnOff();
}
}
}
public class Main {
public static void main(String[] args) {
Switchable bulb = new LightBulb();
Switch mySwitch = new Switch(bulb);
mySwitch.operate("ON");
mySwitch.operate("OFF");
}
}
의존성 역전 원칙을 따르게 되면 Switch는 직접 LightBulb를 생성하지 않고 Main 클래스에서 추상화로 LightBulb를 할당받아 사용한다
또한 LightBulb는 자기 자신의 특징을 설명한 인터페이스를 생성하여 Switch가 인터페이스만 보고 사용할 수 있도록 한다
예시
의존성 원칙을 더 쉽게 이해하기 위해 앞의 원칙에서 이전의 OCP에서 사용한 예제를 참고한다
기존의 코드는 구체적인 구현에 의존하고 있다
바리스타 클래스 내에서 직접적으로 커피의 클래스들을 구현하고 있기 때문이다
이는 DIP의 핵심 개념인 고수준 모듈이 저수준 모듈에 의존해서는 안 된다는 원칙을 위반한다
수정
Barista 클래스가 CoffeeFactory 인터페이스에만 의존하도록 변경하고 CoffeeFactory 구현 클래스의 인스턴스를 외부에서 주입한다
main 클래스에서 커피 팩토리 패턴을 구현하여 map으로 저장을 한다. 이 저장된 커피 데이터들을 바리스타에게 전달하여 주입받게 하면 고사향 모듈은 저수준 모듈에 의존하지 않게 된다
팩토리 클래스
// coffeeFactory 인터페이스
public interface CoffeeFactory {
String makeCoffee();
}
// 구체적인 커피 클래스들
public class EspressoFactory implements CoffeeFactory {
@Override
public String makeCoffee() {
return "Espresso is ready!";
}
}
public class LatteFactory implements CoffeeFactory {
@Override
public String makeCoffee() {
return "Latte is ready!";
}
}
public class CappuccinoFactory implements CoffeeFactory {
@Override
public String makeCoffee() {
return "Cappuccino is ready!";
}
}
바리스타 클래스
// 바리스타 클래스
import java.util.HashMap;
import java.util.Map;
public class Barista {
private Map<String, CoffeeFactory> coffeeFactories;
// 외부에서 CoffeeFactory 주입 받음
public Barista(Map<String, CoffeeFactory> coffeeFactories) {
this.coffeeFactories = coffeeFactories;
}
public String giveCoffee(String coffeeType) {
CoffeeFactory factory = coffeeFactories.get(coffeeType.toLowerCase());
if (factory == null) {
throw new IllegalArgumentException("Unknown coffee type: " + coffeeType);
}
return factory.makeCoffee();
}
}
캐셔 클래스
// 캐셔 클래스
public class Cashier {
private Barista barista;
public Cashier(Barista barista) {
this.barista = barista;
}
public String orderCoffee(String coffeeType) {
return barista.giveCoffee(coffeeType);
}
}
메인 사용 클래스
// 사용 예시
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, CoffeeFactory> coffeeFactories = new HashMap<>();
coffeeFactories.put("espresso", new EspressoFactory());
coffeeFactories.put("latte", new LatteFactory());
coffeeFactories.put("cappuccino", new CappuccinoFactory());
Barista barista = new Barista(coffeeFactories);
Cashier cashier = new Cashier(barista);
System.out.println(cashier.orderCoffee("espresso")); // Output: Espresso is ready!
System.out.println(cashier.orderCoffee("latte")); // Output: Latte is ready!
System.out.println(cashier.orderCoffee("cappuccino")); // Output: Cappuccino is ready!
}
}
'OOP' 카테고리의 다른 글
[OOP] 객체지향의 원칙 SOLID - 인터페이스 분리 원칙 (ISP) (0) | 2024.06.23 |
---|---|
[OOP] 객체지향의 원칙 SOLID - 리스코프 치환 원칙 (LSP) (0) | 2024.06.23 |
[OOP] 객체지향의 원칙 SOLID - 개방 폐쇄 원칙 (OCP) (0) | 2024.06.21 |
[OOP] 객체지향의 원칙 SOLID - 단일 책임 원칙 (SRP) (0) | 2024.06.21 |