← Posts

SOLID · DIP · OOP

Dependency Inversion Principle

고수준 정책이 저수준 구현에 매달리지 않게. 둘 사이에 추상화를 두어 의존의 방향을 뒤집는 마지막 SOLID 원칙.

목차
  1. 고수준이 저수준에 직접 매달린다
  2. DIP = 둘 다 추상화에 의존하라
  3. 무엇이 고수준이고 저수준인가
  4. 왜 고수준이 저수준에 묶이면 안 되나
  5. 사이에 추상화를 둔다
  6. 구현은 밖에서 받는다
  7. 화살표가 뒤집힌다
  8. 추상화는 누구 것인가
  9. 추상화는 고수준 쪽에 둔다
  10. 실무: 고수준 안의 new가 신호다
  11. 정리

고수준이 저수준에 직접 매달린다

주문을 처리하고 완료 알림을 보내는 서비스가 있다.

java
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로 바꾸는 건 저수준의 사정이다. 그런데 OrderServiceEmailSender를 직접 들고 있으면, 그 저수준 변경이 고수준 코드를 건드리게 만든다. 주문 정책은 그대로인데 알림 수단이 바뀌었다고 주문 코드를 여는 것이다.

핵심 정책은 오래 살아야 하는데, 자주 바뀌는 기술 세부에 매달리면 같이 흔들린다. 덜 바뀌어야 할 것이 더 바뀌는 것에 끌려간다.

사이에 추상화를 둔다

고침은 둘 사이에 인터페이스를 세우는 것이다.

java
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()를 부른다면, 이름만 인터페이스일 뿐 결국 구체를 손에 쥐고 있다. 나머지 반은 구현을 밖에서 받는 것이다.

java
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는 한 줄도 안 바뀐다. 무엇을 쓸지는 바깥이 정한다.

화살표가 뒤집힌다

여기서 “역전”이라는 이름이 나온다. 의존의 방향이 뒤집혔다.

diagramdiagram

전에는 고수준이 저수준을 향했다(고→저). 후에는 고수준도, 저수준도 가운데 추상화를 향한다. 특히 저수준(EmailNotifier)의 화살표가 위로 올라가 추상화를 가리킨다. 저수준이 고수준이 정한 약속을 따르는 모양으로 뒤집힌 것이다.

추상화는 누구 것인가

한 걸음 더. 이 뒤집힘의 핵심은 인터페이스가 누구를 위해 생겼느냐다.

Notifier의 모양(notify(order, message))은 OrderService가 필요로 하는 대로 정해졌다. 이메일 라이브러리가 제공하는 API 모양이 아니다. 즉 추상화는 고수준의 필요를 따라 만들어지고, 저수준은 거기에 자기를 맞춘다.

원래는 고수준이 저수준의 API에 맞춰야 했다(저수준이 갑). 이제는 고수준이 원하는 인터페이스를 정하고 저수준이 그걸 구현한다(고수준이 갑). 이게 “역전”의 진짜 뜻이다 - 주도권이 정책 쪽으로 넘어온다.

추상화는 고수준 쪽에 둔다

그래서 이 인터페이스는 고수준의 것이다. 개념적으로만이 아니라 자리도 그렇다. NotifierOrderService가 사는 쪽에 두고, EmailNotifier는 거기에 딸려 그 약속을 구현한다.

이게 왜 중요한가. 만약 Notifier를 이메일 쪽 모듈에 두면, 고수준이 그 모듈을 향해 손을 뻗게 되어 방향이 도로 원래대로 돌아간다. 추상화를 정책 쪽이 쥐고 있어야, 저수준이 정책을 향해 올라오는 그림이 유지된다. 추상화의 소유권이 정책에 있어야 역전이 완성된다 - 인터페이스를 세우는 것으로 끝이 아니라, 그걸 어느 쪽에 두느냐까지가 의존성 역전이다.

실무: 고수준 안의 new가 신호다

실무에서 DIP 위반은 대개 핵심 로직 안에서 구체 클래스를 직접 new 하는 것으로 나타난다.

java
private final EmailSender sender = new EmailSender();   // 이 new가 결합이다

정책 클래스 안에 특정 기술(이메일 라이브러리, 특정 DB 드라이버)의 new가 박혀 있으면, 그 정책은 이미 그 기술에 묶인 것이다. 고칠 방향은 늘 같다. 구체를 직접 만들지 말고, 인터페이스를 세워 그 구현을 밖에서 받는다. 그러면 정책은 추상화만 알고, 어떤 구현이 오는지는 바깥이 정한다.

정리

DIP란고수준은 저수준 구현에 의존 말고, 둘 다 추상화에 의존한다
고수준/저수준고수준=정책(본질), 저수준=구체 수단(기술 세부)
문제정책이 수단에 묶이면, 수단이 바뀔 때 정책 코드가 흔들린다
고침사이에 인터페이스를 두고, 구현은 밖에서 받는다
역전저수준이 고수준이 정한 추상화를 따른다 - 주도권이 정책 쪽으로
신호핵심 로직 안에 구체 클래스의 new가 박혀 있다

본질이 세부에 끌려다니지 않게, 사이에 추상화를 세워 방향을 뒤집는 것. 그게 의존성 역전이 지키는 것이고, 다섯 원칙이 함께 향하던 곳이기도 하다 - 바뀌는 것과 안 바뀌는 것을 갈라, 변화가 번지지 않게 하는 것.

관련 글