Table of contents
- DIP — 고수준이 저수준에 끌려다니면 안 된다
- 의존성이 뒤집힌다는 것
- DI와 DIP — 이름은 비슷하지만 다르다
- 계층형 아키텍처에서 DIP
- 실무에서의 DIP — 어디까지 적용할 것인가
- 핵심 정리
DIP — 고수준이 저수준에 끌려다니면 안 된다
DIP(Dependency Inversion Principle)는 Robert C. Martin이 제안한 원칙으로, 두 가지 규칙으로 구성된다.
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
번역하면 이렇다.
- 고수준 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다.
- 추상화는 세부사항에 의존하면 안 된다. 세부사항이 추상화에 의존해야 한다.
고수준이니 저수준이니 하는 말이 추상적으로 느껴진다. 구체적으로 풀어보자.
- 고수준 모듈: 비즈니스 정책, 핵심 로직.
주문을 생성하고 결제를 처리한다같은 규칙 - 저수준 모듈: 구현 세부사항.
MySQL에 저장한다,SMTP로 메일을 보낸다같은 기술적 메커니즘
고수준 모듈이 저수준 모듈에 직접 의존하면, 저수준의 변경이 고수준을 흔든다. 핵심 비즈니스 로직이 DB 종류나 메일 라이브러리에 끌려다니는 것이다. DIP는 이 의존 방향을 뒤집으라고 말한다.
의존성이 뒤집힌다는 것
뒤집기 전 — 자연스러운 의존 방향
코드를 처음 작성할 때 가장 자연스러운 구조는 이렇다.
public class OrderService {
private final MySqlOrderRepository repository = new MySqlOrderRepository();
public void createOrder(Order order) {
// 비즈니스 로직
order.validate();
order.calculateTotal();
// 저장
repository.save(order);
}
}
public class MySqlOrderRepository {
public void save(Order order) {
// MySQL에 INSERT
String sql = "INSERT INTO orders ...";
// JDBC 연결, 실행
}
}
의존 방향을 그려보면 고수준(OrderService)이 저수준(MySqlOrderRepository)을 직접 참조한다.
flowchart TB
subgraph before["의존성 역전 전"]
direction TB
OS["OrderService<br/>고수준 — 비즈니스 로직"] -->|"직접 의존"| MySQL["MySqlOrderRepository<br/>저수준 — MySQL 구현"]
end
style MySQL fill:#ff6b6b,color:#fff
이 구조의 문제는 명확하다. MySQL을 PostgreSQL로 바꾸려면 OrderService를 수정해야 한다. 테스트할 때 실제 MySQL이 돌아가야 한다. 비즈니스 로직이 인프라 기술에 종속되어 있다.
뒤집은 후 — 추상화에 의존
인터페이스를 하나 도입한다. 그리고 고수준 모듈이 이 인터페이스에 의존하게 만든다.
public interface OrderRepository {
void save(Order order);
Order findById(Long id);
}
고수준 모듈은 인터페이스에만 의존한다.
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public void createOrder(Order order) {
order.validate();
order.calculateTotal();
repository.save(order);
}
}
저수준 모듈이 인터페이스를 구현한다.
public class MySqlOrderRepository implements OrderRepository {
@Override
public void save(Order order) {
// MySQL에 INSERT
}
@Override
public Order findById(Long id) {
// MySQL에서 SELECT
return null;
}
}
의존 방향을 다시 그려보면 화살표가 달라졌다.
flowchart TB
subgraph after["의존성 역전 후"]
direction TB
OS2["OrderService<br/>고수준 — 비즈니스 로직"] -->|"추상화에 의존"| ORI["OrderRepository<br/>인터페이스 — 추상화"]
MySQLImpl["MySqlOrderRepository<br/>저수준 — MySQL 구현"] -->|"추상화를 구현"| ORI
end
style ORI fill:#339af0,color:#fff
style OS2 fill:#51cf66,color:#fff
style MySQLImpl fill:#ffd43b,color:#000
OrderService는 OrderRepository 인터페이스에 의존한다. MySqlOrderRepository도 OrderRepository 인터페이스에 의존한다(구현이라는 형태로). 둘 다 추상화에 의존하게 되었다. 저수준 모듈이 고수준 모듈을 향해 의존하는 방향으로 바뀌었기 때문에 역전(Inversion)이라 부른다.
역전의 핵심 — 인터페이스는 누가 소유하는가
여기서 중요한 포인트가 있다. OrderRepository 인터페이스는 어디에 위치해야 하는가?
직관적으로는 “구현체 옆에 두면 되지 않나?”라고 생각할 수 있다. MySQL 관련 코드와 같은 패키지에 두는 것이다. 하지만 DIP에서 인터페이스의 소유자는 사용하는 쪽, 즉 고수준 모듈이다.
com.example.order/
├── OrderService.java ← 고수준 모듈
├── OrderRepository.java ← 인터페이스 (고수준 패키지에 위치)
└── ...
com.example.order.infrastructure/
├── MySqlOrderRepository.java ← 저수준 모듈 (인터페이스를 구현)
└── ...
인터페이스가 고수준 패키지에 있으면, 저수준 패키지가 고수준 패키지를 의존한다. 패키지 수준에서도 의존성이 역전된다. 이 구조가 DIP의 완전한 적용이다.
flowchart LR
subgraph domain["com.example.order — 고수준"]
OS3["OrderService"]
ORI2["OrderRepository<br/>인터페이스"]
OS3 --> ORI2
end
subgraph infra["com.example.order.infrastructure — 저수준"]
MySQLImpl2["MySqlOrderRepository"]
end
MySQLImpl2 -->|"구현"| ORI2
DI와 DIP — 이름은 비슷하지만 다르다
Spring을 사용해 본 개발자라면 DI(Dependency Injection, 의존성 주입)가 떠오를 것이다. DI와 DIP는 이름이 비슷해서 자주 혼동되지만, 서로 다른 개념이다.
- DIP는 설계 원칙이다. 의존의 방향이 추상화를 향해야 한다는 방향성을 제시한다
- DI는 기법이다. 객체가 필요한 의존성을 직접 생성하지 않고, 외부에서 주입받는 방식이다
DI는 DIP를 구현하는 데 자주 쓰이는 도구이지만, DIP 없이도 DI를 사용할 수 있고, DI 없이도 DIP를 적용할 수 있다.
DI 없이 DIP 적용
팩토리 패턴으로 DIP를 적용하는 예다. 프레임워크 없이도 가능하다.
public class OrderRepositoryFactory {
public static OrderRepository create() {
String dbType = System.getProperty("db.type");
if ("mysql".equals(dbType)) {
return new MySqlOrderRepository();
}
return new PostgresOrderRepository();
}
}
// 사용하는 쪽
OrderRepository repository = OrderRepositoryFactory.create();
OrderService service = new OrderService(repository);
OrderService는 여전히 OrderRepository 인터페이스에만 의존한다. DIP는 지켜지고 있다. DI 컨테이너가 없을 뿐이다.
DIP 없이 DI 적용
반대로 DI를 사용하면서도 DIP를 위반할 수 있다.
public class OrderService {
private final MySqlOrderRepository repository;
// 생성자 주입 — DI는 적용됨
public OrderService(MySqlOrderRepository repository) {
this.repository = repository;
}
}
MySqlOrderRepository를 외부에서 주입받으니 DI는 적용된 셈이다. 하지만 인터페이스가 아닌 구체 클래스에 의존하고 있으므로 DIP는 위반이다. 주입의 형태가 아니라 의존의 방향이 DIP의 관건이다.
Spring에서의 DIP + DI
Spring Boot에서는 DIP와 DI가 자연스럽게 결합된다.
인터페이스를 선언한다.
interface OrderRepository {
fun save(order: Order)
fun findById(id: Long): Order?
}
구현체에 @Repository를 붙인다.
@Repository
class JpaOrderRepository(
private val jpaRepository: OrderJpaRepository
) : OrderRepository {
override fun save(order: Order) {
jpaRepository.save(order.toEntity())
}
override fun findById(id: Long): Order? {
return jpaRepository.findByIdOrNull(id)?.toDomain()
}
}
서비스는 인터페이스에 의존한다. Spring이 구현체를 찾아서 주입해 준다.
@Service
class OrderService(
private val orderRepository: OrderRepository // 인터페이스에 의존
) {
fun createOrder(order: Order) {
order.validate()
order.calculateTotal()
orderRepository.save(order)
}
}
DIP(인터페이스에 의존)와 DI(Spring이 구현체를 주입)가 함께 동작한다. Spring의 IoC 컨테이너(Inversion of Control, 제어의 역전)는 DIP를 편리하게 적용할 수 있게 해주는 인프라인 셈이다.
계층형 아키텍처에서 DIP
전통적인 3계층 아키텍처(Presentation → Business → Data Access)에서 의존 방향은 위에서 아래로 흐른다.
flowchart TB
subgraph traditional["전통적 계층 구조 — 의존이 아래로"]
Pres["Presentation Layer<br/>Controller"]
Biz["Business Layer<br/>Service"]
Data["Data Access Layer<br/>Repository"]
Pres --> Biz
Biz --> Data
end
이 구조에서는 비즈니스 계층이 데이터 접근 계층에 의존한다. DB 기술이 바뀌면 비즈니스 로직에 영향이 갈 수 있다. DIP를 적용하면 비즈니스 계층이 인터페이스를 소유하고, 데이터 접근 계층이 이를 구현하는 형태가 된다.
flowchart TB
subgraph dip_arch["DIP 적용 — 의존이 안쪽으로"]
Pres2["Presentation Layer"]
Biz2["Business Layer<br/>Service + Repository 인터페이스"]
Data2["Data Access Layer<br/>Repository 구현체"]
Pres2 --> Biz2
Data2 -->|"구현"| Biz2
end
style Biz2 fill:#339af0,color:#fff
데이터 접근 계층의 화살표 방향이 바뀌었다. 비즈니스 계층을 향해 의존한다. 이제 비즈니스 계층은 어떤 DB를 쓰는지 모르고, 알 필요도 없다. MySQL을 MongoDB로 바꿔도 비즈니스 계층의 코드는 한 줄도 바뀌지 않는다.
이 원리를 더 밀고 나간 것이 헥사고날 아키텍처(Hexagonal Architecture, 포트와 어댑터 아키텍처)나 클린 아키텍처(Clean Architecture)다. 이 아키텍처들에서 DIP는 핵심 원칙으로 자리 잡고 있다. 비즈니스 로직이 중심에 있고, 외부 기술(DB, API, UI)이 비즈니스 로직에 맞추어 플러그인처럼 연결되는 구조다.
실무에서의 DIP — 어디까지 적용할 것인가
DIP의 이론을 알면 “모든 곳에 인터페이스를 만들어야 하나?”라는 의문이 든다. 정답은 아니다. 인터페이스를 만드는 데도 비용이 있다. 파일이 늘어나고, 추적해야 할 코드가 늘어나며, 단순한 구조가 복잡해질 수 있다.
DIP를 적용하기 좋은 지점은 다음과 같다.
- 외부 시스템과의 경계 — DB, 메시지 큐, 외부 API, 파일 시스템. 인프라 기술은 바뀔 가능성이 높고, 테스트에서 목(mock)으로 교체해야 하는 경우가 많다
- 핵심 비즈니스 로직 — 도메인 로직이 프레임워크나 라이브러리에 종속되면 안 된다
- 변경 가능성이 높은 정책 — 할인 정책, 알림 방식 등 비즈니스 요구에 따라 바뀌는 부분
반면 다음 경우에는 직접 의존해도 무방하다.
- 안정적인 라이브러리 —
String,List같은 표준 라이브러리에 인터페이스를 씌우지 않는다 - 변경 가능성이 거의 없는 유틸리티 — 날짜 변환, 문자열 처리 등
- 구현체가 하나뿐인 경우 —
나중에 바뀔 수도 있으니까라는 막연한 이유로 인터페이스를 만드는 것은 과설계(over-engineering)다. 실제로 두 번째 구현체가 필요해질 때 추출해도 늦지 않는다
DIP 적용 판단 기준을 정리하면 이렇다
// 인터페이스가 필요한 경우 — 외부 시스템과의 경계
interface NotificationSender {
fun send(userId: Long, message: String)
}
class EmailNotificationSender : NotificationSender { /* ... */ }
class SlackNotificationSender : NotificationSender { /* ... */ }
// 인터페이스가 불필요한 경우 — 변경 가능성 없는 내부 유틸리티
class DateFormatter {
fun format(date: LocalDateTime): String =
date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
}
NotificationSender는 이메일에서 슬랙으로, 카카오톡으로 바뀔 수 있다. 인터페이스로 분리할 가치가 충분하다. DateFormatter는 날짜를 포맷하는 게 전부다. 구현체가 두 개 될 일이 없다면 직접 의존해도 된다.
핵심 정리
| 개념 | 설명 |
|---|---|
| DIP | 고수준 모듈과 저수준 모듈 모두 추상화에 의존해야 한다 |
| 의존성 역전 | 저수준이 고수준을 향해 의존하도록 방향을 뒤집는 것 |
| 인터페이스 소유 | 인터페이스는 사용하는 쪽(고수준)이 소유한다 |
| DI vs DIP | DI는 주입 기법, DIP는 설계 원칙. 서로 독립적이지만 함께 쓰면 강력하다 |
| 적용 기준 | 외부 시스템 경계, 핵심 도메인, 변경 가능성이 높은 정책 |
SOLID 다섯 원칙을 세 편에 걸쳐 살펴봤다. SRP는 변경의 이유를 하나로 제한하고, OCP는 확장에 열린 구조를 만들며, LSP는 다형성이 올바르게 동작하도록 보장하고, ISP는 인터페이스를 클라이언트 기준으로 분리하며, DIP는 의존의 방향을 추상화를 향해 뒤집는다.
이 원칙들은 개별적으로 적용하기보다 함께 어우러질 때 진가를 발휘한다. SRP로 책임을 나누고, OCP로 확장 포인트를 만들고, LSP로 다형성의 정확성을 지키고, ISP로 인터페이스를 정돈하고, DIP로 의존 방향을 잡는다. 결국 모두 같은 목표 — 변경에 강한 코드 — 를 향한다.
다음 편에서는 SOLID 바깥에서 객체 간 커뮤니케이션을 다루는 원칙들을 살펴본다. TDA(Tell, Don’t Ask), 디미터 법칙(Law of Demeter), CQS(Command-Query Separation)가 그 주인공이다.




Loading comments...