의존성 역전(Dependency Inversion Principle, DIP)
- 정의
- 구체적인 구현(클래스)에 직접 의존하지 말고 인터페이스(추상화)에 의존하도록 설계하라는 원칙
- 의존성을 “역전”시킨다는 의미는 기존의 “고수준 → 저수준” 의존 방향을 “고수준 → 추상화 ← 저수준” 으로 바꾸는 것
- 고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.
- 추상화는 세부 사항에 의존해서는 안 된다. 세부 사항이 추상화에 의존해야 한다.
- 특징
- 목적: 코드가 깨지거나 손상되는 취약점으로부터 보호
- 방법: 구체 클래스 대신 인터페이스(추상화)에 의존
- 효과: 유연성↑, 확장성↑, 테스트 용이성↑, 결합도↓
- 구현: 추상 기본 클래스(ABC) + 의존성 주입(Dependency Injection)
강한 의존성을 가진 예

EventStreamer가Syslog에 직접 의존한다Syslog의 전송 방식이 바뀌면EventStreamer를 수정해야 한다.- 새로운 전송 대상(이메일, 메시지 큐 등)을 추가할 때마다
stream()메서드를 계속 수정해야 한다. - 고수준 모듈이 저수준 모듈의 변화에 취약해진다.
직접 구체 클래스에 의존하면 강한 결합이 발생한다. DIP는 이 결합을 인터페이스라는 “계약”으로 느슨하게 만든다.
의존성을 거꾸로

EventStreamer → DataTargetClient(인터페이스) ← Syslog
↑
(구현)
EventStreamer는 이제 인터페이스(DataTargetClient)에만 의존한다.Syslog는 그 인터페이스를 구현한다.- 새로운 전송 대상이 추가되어도
EventStreamer코드는 전혀 수정할 필요가 없다.- 이메일 같은 새로운 유형의 데이터 대상이 추가되어도
send()메서드 구현은 모두 새로운 클래스에서 담당
- 이메일 같은 새로운 유형의 데이터 대상이 추가되어도
- 실제 구현체가 바뀌어도 고수준 코드는 영향받지 않는다.
- 핵심: 세부 사항(구체 클래스)이 추상화에 의존해야 한다.
왜 굳이 추상 기본 클래스(인터페이스)를 정의하는 것일까?
- Python은 덕 타이핑이 가능하기 때문에 굳이
ABC를 쓰지 않아도send()메서드만 있으면 동작함 - 하지만 ABC를 사용하는 것이 좋은 습관(클린 코드)인 이유
- 가독성 – “이 객체는
DataTargetClient이다”라는is-a관계(상속)가 명확해진다. - 의도 표현 – 코드 자체가 문서가 된다.
- 실수 방지 – Python이 너무 유연해서 발생하는 실수를 줄여준다.
- 도구 지원 – mypy, IDE가 인터페이스 위반을 더 잘 잡아준다.
- 가독성 – “이 객체는
의존성을 역전시켰다면, 이제 어떻게 실제 객체를 연결할 것인가? → 의존성 주입(Dependency Injection)을 사용해 연결한다.
의존성 주입(Dependency injection)
- 개념: 객체가 협업할 때 필요한 의존성(객체)을 자신이 직접 생성하지 않고 외부로부터 전달(주입)받는 기법
ex1) 직접 생성(안 좋은 방법)
class EventStreamer:
def __init__(self):
self._target = Syslog() # 내부에서 직접 생성 → 강한 결합
def stream(self, events: list[Event]) -> None:
for event in events:
self._target.send(event.serialise())
문제점
- 유연성이 떨어진다.
- 테스트하기 어렵다. (
Syslog를 강제로 사용해야 함) - 다른 구현체로 교체하기 어렵다.
- 초기화 로직이 클래스 내부에 숨겨진다.
ex2) 생성자 주입
class EventStreamer:
def __init__(self, target: DataTargetClient):
self._target = target # 외부에서 주입받음
def stream(self, events: list[Event]) -> None:
for event in events:
self._target.send(event.serialise())
장점
- 어떤
DataTargetClient구현체든 주입 가능 - 단위 테스트 시 Mock/Test Double을 쉽게 넣을 수 있다.
- Mock:
- Test Double의 한 종류로, 행위 검증(Behavior Verification) 에 특화된 객체
- 단순히 값을 반환하는 것을 넘어, “어떤 메서드가 몇 번 호출되었는지”, “어떤 인자로 호출되었는지”를 기록하고 검증할 수 있음
- Python에서는 unittest.mock.Mock 또는 pytest-mock 등을 사용해 쉽게 만들 수 있음
- Test Double:
- 테스트할 때 실제 의존성(여기서는 Syslog 같은 진짜 객체) 대신 사용하는 대용 객체의 총칭
- “스턴트 더블(Stunt Double)”처럼 실제 객체를 대신해서 테스트 환경에서 역할을 수행하는 객체를 의미
- Dummy, Stub, Spy, Mock, Fake 등을 모두 포함하는 상위 개념
- Mock:
- 런타임에도 의존성을 동적으로 교체 가능
c.f.) Test Double, Mock 예시
from unittest.mock import Mock
def test_event_streamer():
# Test Double 중 Mock을 생성
mock_target = Mock(spec=DataTargetClient)
streamer = EventStreamer(target=mock_target)
streamer.stream([Event(...)])
# 행위 검증: send()가 호출되었는지 확인
mock_target.send.assert_called_once()
TIP
__init__에서 의존성을 직접 생성하지 말고, 항상 파라미터로 받도록 설계하자.- 만약 복잡한 초기화 과정을 가졌거나 초기화 인자가 많은 경우라면, 종속성 그래프를 만들고 관련 라이브러리가 생성을 담당하도록 하는 것이 좋은 방법. 즉, 객체를 연결하기 위한 글루 코드(glue code)에서 보일러플레이트(boilderplate) 코드를 제거할 수 있다.
왜 단순 생성자 주입만으로는 부족해지는가?
단순한 경우라면 이렇게 해도 충분
streamer = EventStreamer(target=Syslog())
하지만 실제 프로젝트에서는 다음과 같은 상황이 자주 발생함
EventStreamer가 필요로 하는 의존성이 5~10개 이상으로 늘어남- 각 의존성 자체도 또 다른 의존성을 가짐 (중첩된 의존성)
- 초기화 과정에서 설정 파일 읽기, 네트워크 연결, 인증 정보 로드 등 복잡한 작업이 필요함
- 테스트할 때는 Mock 객체를, 운영 환경에서는 실제 객체를 넣고 싶음
이런 상황에서는 객체를 생성하고 연결하는 코드가 점점 길어지고 지저분해지는데, 이 코드를 글루 코드(Glue Code) 라 부름
- 글루 코드: 프로그램의 핵심 비즈니스 로직과는 아무 관련이 없지만, 여러 객체들을 “접착제처럼” 연결해주기 위해 작성하는 코드
- 글루 코드가 많아지면 보일러플레이트(Boilerplate) 코드가 된다. (반복적이고 지루하며, 실수하기 쉬운 코드)
해결책: 종속성 그래프(Dependency Graph) + DI 컨테이너
복잡한 초기화 과정을 한 곳에서 선언적으로 관리하고, 실제 객체 생성과 연결은 라이브러리(DI 컨테이너)에게 맡기는 방식, 이 역할을 하는 것이 Object Graph
- Object Graph = “어떤 객체가 어떤 객체에 의존하는지”를 정의한 설계도
- DI 컨테이너가 이 설계도를 보고 실제로 객체를 생성하고 주입해 줌
의존성 주입 프레임워크 활용 (pinject 예시)
import pinject
class EventStreamer:
def __init__(self, target: DataTargetClient):
self.target = target
def stream(self, events: list[Event]) -> None:
for event in events:
self.target.send(event.serialise())
# BindingSpec
class _EventStreamerBindingSpec(pinject.BindingSpec):
def provide_target(self):
return Syslog()
# Object Graph 생성
object_graph = pinject.new_object_graph(
binding_specs=[_EventStreamerBindingSpec()]
)
# 실제 객체 생성 및 주입
event_streamer = object_graph.provide(EventStreamer)
BindingSpec은 “의존성을 어떻게 제공할 것인가”를 정의하는 명세서provide_target→target이라는 이름의 의존성을 제공하겠다는 의미pinject는 메서드 이름에서provide_를 떼고 나머지 이름을 의존성 이름으로 인식함- 즉,
EventStreamer.__init__의 파라미터 이름인target과provide_target메서드가 이름으로 매칭됨
new_object_graph는 바인딩 스펙들을 모아서 전체 의존성 그래프를 만듬- 이 시점에서는 아직 실제 객체가 생성되지 않음(설계도만 만들어진 상태)
- 실제 객체 생성 및 주입시 순서(
event_streamer = object_graph.provide(EventStreamer))- 1.
EventStreamer를 생성하려고 함 - 2.
__init__에target파라미터가 필요한 것을 발견 - 3. 바인딩 스펙에서
provide_target메서드를 찾음 - 4.
provide_target()을 호출하여Syslog()인스턴스를 얻음 - 5. 그 인스턴스를
EventStreamer(target=Syslog())에 주입하여 생성 - 6. 완성된
EventStreamer객체를 반환
- 1.
- 시스템이 커지면 객체 초기화 과정이 복잡해지거나 주입해야 할 인자가 많아질 수 있다.
- 글루 코드(Glue Code) 분리: 이런 경우 객체 간의 관계를 엮어주는 보일러플레이트 코드(글루 코드)를 수동으로 작성하는 대신, 별도의 라이브러리(예: pinject)가 생성을 담당하게 하는 것이 좋다.
- 바인딩 스펙 정의: 라이브러리를 사용하면 바인딩 스펙(Binding Specification) 객체를 정의하여, 특정 파라미터(예: target)에 어떤 구체 클래스(예: Syslog)를 주입할지 한 곳에서 명시적으로 선언하고 관리할 수 있다.
- 팩토리 역할: 이는 마치 팩토리 객체처럼 객체의 초기화와 관계 설정을 도구가 대신 수행하게 만드는 방식이다.
- 유연성 유지: 의존성 주입 라이브러리를 사용해 객체 그래프(Object Graph)를 구성하더라도, 기존 클래스는 여전히 인터페이스 규격만 맞추면 어떤 객체든 주입받아 사용할 수 있는 원래의 유연성을 잃지 않는다.
댓글