목차
고수준이 저수준에 직접 매달린다
주문을 처리하고 완료 알림을 보내는 서비스가 있다.
public class OrderService {
private final EmailSender sender = new EmailSender(); // 구체 클래스를 직접 만든다
public void placeOrder(Order order) {
// ... 주문 처리 (핵심 정책)
sender.sendEmail(order.buyerEmail(), "주문이 완료됐습니다");
}
}OrderService가 하는 진짜 일은 주문 처리다. 알림은 곁다리인데, new EmailSender()로 이메일이라는 구체적인 수단에 딱 붙어 버렸다.
이제 알림을 SMS나 푸시로 바꾸려면? 주문 로직이 든 OrderService를 열어 고쳐야 한다. 곁다리 수단을 바꾸는데 핵심 정책이 든 코드를 헤집는 것이다.
DIP = 둘 다 추상화에 의존하라
의존성 역전 원칙(DIP)이 이 매달림을 겨눈다.
고수준 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다.
풀어 쓰면, OrderService(고수준)가 EmailSender(저수준)에 직접 매달리지 말고, 둘 다 그 사이의 추상화(인터페이스)에 매달리라는 것이다.
무엇이 고수준이고 저수준인가
말부터 정리하자.
- 고수준 - 무엇을 할지 정하는 정책. 여기선 “주문을 처리하고 알린다”는 흐름. 앱의 본질에 가깝다.
- 저수준 - 그걸 실제로 해내는 구체적 수단. 이메일 전송, DB 접근 같은 기술 세부.
자연스러운 그림은 “정책이 수단을 부른다”라 고수준이 저수준을 향한다. DIP는 이 방향이 문제라고 말한다. 본질(정책)이 세부(수단)에 끌려다니게 되기 때문이다.
왜 고수준이 저수준에 묶이면 안 되나
정책이 세부에 묶이면, 바뀔 이유가 다른 둘이 한 몸이 된다.
이메일에서 SMS로 바꾸는 건 저수준의 사정이다. 그런데 OrderService가 EmailSender를 직접 들고 있으면, 그 저수준 변경이 고수준 코드를 건드리게 만든다. 주문 정책은 그대로인데 알림 수단이 바뀌었다고 주문 코드를 여는 것이다.
핵심 정책은 오래 살아야 하는데, 자주 바뀌는 기술 세부에 매달리면 같이 흔들린다. 덜 바뀌어야 할 것이 더 바뀌는 것에 끌려간다.
사이에 추상화를 둔다
고침은 둘 사이에 인터페이스를 세우는 것이다.
public interface Notifier {
void notify(Order order, String message);
}
public class EmailNotifier implements Notifier {
public void notify(Order order, String message) { /* 이메일 */ }
}이제 OrderService는 구체 수단이 아니라 Notifier라는 추상화에만 매달린다.
구현은 밖에서 받는다
추상화를 세우는 것만으로는 반이다. OrderService가 여전히 안에서 new EmailNotifier()를 부른다면, 이름만 인터페이스일 뿐 결국 구체를 손에 쥐고 있다. 나머지 반은 구현을 밖에서 받는 것이다.
public class OrderService {
private final Notifier notifier; // 추상화에만 의존한다
public OrderService(Notifier notifier) { // 구현은 밖에서 받는다
this.notifier = notifier;
}
public void placeOrder(Order order) {
// ... 주문 처리
notifier.notify(order, "주문이 완료됐습니다");
}
}OrderService는 자기가 쓸 Notifier가 무엇인지 스스로 만들지 않는다. 생성자로 받을 뿐이다. 그래서 SMS로 바꾸고 싶으면 SmsNotifier를 하나 만들어 넣으면 된다. OrderService는 한 줄도 안 바뀐다. 무엇을 쓸지는 바깥이 정한다.
화살표가 뒤집힌다
여기서 “역전”이라는 이름이 나온다. 의존의 방향이 뒤집혔다.
전에는 고수준이 저수준을 향했다(고→저). 후에는 고수준도, 저수준도 가운데 추상화를 향한다. 특히 저수준(EmailNotifier)의 화살표가 위로 올라가 추상화를 가리킨다. 저수준이 고수준이 정한 약속을 따르는 모양으로 뒤집힌 것이다.
추상화는 누구 것인가
한 걸음 더. 이 뒤집힘의 핵심은 인터페이스가 누구를 위해 생겼느냐다.
Notifier의 모양(notify(order, message))은 OrderService가 필요로 하는 대로 정해졌다. 이메일 라이브러리가 제공하는 API 모양이 아니다. 즉 추상화는 고수준의 필요를 따라 만들어지고, 저수준은 거기에 자기를 맞춘다.
원래는 고수준이 저수준의 API에 맞춰야 했다(저수준이 갑). 이제는 고수준이 원하는 인터페이스를 정하고 저수준이 그걸 구현한다(고수준이 갑). 이게 “역전”의 진짜 뜻이다 - 주도권이 정책 쪽으로 넘어온다.
추상화는 고수준 쪽에 둔다
그래서 이 인터페이스는 고수준의 것이다. 개념적으로만이 아니라 자리도 그렇다. Notifier는 OrderService가 사는 쪽에 두고, EmailNotifier는 거기에 딸려 그 약속을 구현한다.
이게 왜 중요한가. 만약 Notifier를 이메일 쪽 모듈에 두면, 고수준이 그 모듈을 향해 손을 뻗게 되어 방향이 도로 원래대로 돌아간다. 추상화를 정책 쪽이 쥐고 있어야, 저수준이 정책을 향해 올라오는 그림이 유지된다. 추상화의 소유권이 정책에 있어야 역전이 완성된다 - 인터페이스를 세우는 것으로 끝이 아니라, 그걸 어느 쪽에 두느냐까지가 의존성 역전이다.
실무: 고수준 안의 new가 신호다
실무에서 DIP 위반은 대개 핵심 로직 안에서 구체 클래스를 직접 new 하는 것으로 나타난다.
private final EmailSender sender = new EmailSender(); // 이 new가 결합이다정책 클래스 안에 특정 기술(이메일 라이브러리, 특정 DB 드라이버)의 new가 박혀 있으면, 그 정책은 이미 그 기술에 묶인 것이다. 고칠 방향은 늘 같다. 구체를 직접 만들지 말고, 인터페이스를 세워 그 구현을 밖에서 받는다. 그러면 정책은 추상화만 알고, 어떤 구현이 오는지는 바깥이 정한다.
정리
| DIP란 | 고수준은 저수준 구현에 의존 말고, 둘 다 추상화에 의존한다 |
| 고수준/저수준 | 고수준=정책(본질), 저수준=구체 수단(기술 세부) |
| 문제 | 정책이 수단에 묶이면, 수단이 바뀔 때 정책 코드가 흔들린다 |
| 고침 | 사이에 인터페이스를 두고, 구현은 밖에서 받는다 |
| 역전 | 저수준이 고수준이 정한 추상화를 따른다 - 주도권이 정책 쪽으로 |
| 신호 | 핵심 로직 안에 구체 클래스의 new가 박혀 있다 |
본질이 세부에 끌려다니지 않게, 사이에 추상화를 세워 방향을 뒤집는 것. 그게 의존성 역전이 지키는 것이고, 다섯 원칙이 함께 향하던 곳이기도 하다 - 바뀌는 것과 안 바뀌는 것을 갈라, 변화가 번지지 않게 하는 것.