base

의존 역전 원칙(DIP: Dependency Inversion Principle)이란?

개발을 하다 보면 처음에는 빠르고 쉽게 만들었던 코드가 점점 더 복잡해지고 관리가 어려워지는 상황을 자주 마주하게 됩니다. 특히 특정 모듈이나 클래스가 변경될 때마다 연관된 다른 모듈들도 모두 수정해야 한다면, 유지보수가 굉장히 어려워집니다. 이런 경험이 있다면, 지금부터 다룰 의존 역전 원칙(Dependency Inversion Principle, DIP) 이 큰 도움이 될 수 있습니다.


흔히 "역전"이라는 표현은 조금 낯설고 어색하게 들릴 수도 있습니다. 하지만 이 원칙에서 말하는 '역전'은 오히려 여러분의 코드를 더 유연하고, 더 확장 가능하며, 더 유지보수하기 쉽게 만들어주는 놀라운 힘을 가지고 있습니다. 그 이유를 지금부터 천천히 살펴보겠습니다.


의존 역전 원칙(DIP)의 개념과 필요성

의존 역전 원칙(DIP)은 로버트 C. 마틴(Robert C. Martin, Uncle Bob)이 제안한 SOLID 원칙의 마지막 다섯 번째 원칙으로, 아래와 같은 내용을 담고 있습니다.

  • 고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 두 모듈 모두 추상(Interface 또는 Abstract Class)에 의존해야 한다.
  • 추상(인터페이스)은 세부 구현에 의존하지 말고, 세부 구현이 추상에 의존해야 한다.

한마디로 정리하면, 구체적인 구현 클래스에 의존하지 말고 추상화된 인터페이스나 추상 클래스를 통해 의존 관계를 설정하라는 뜻입니다. 이 원칙을 적용하면 모듈 간의 결합도가 낮아지고, 코드의 유연성과 확장성이 크게 증가합니다.


DIP를 위반한 예시 코드

실무에서 흔히 볼 수 있는 DIP 위반 사례를 코드로 살펴보겠습니다.

dip
// MySQL에 직접 의존하는 클래스
class MySQLDatabase {
  connect() {
    console.log('Connected to MySQL')
  }
}
 
// UserService가 직접 MySQLDatabase에 의존
class UserService {
  private db: MySQLDatabase
 
  constructor() {
    this.db = new MySQLDatabase()
  }
 
  getUser() {
    this.db.connect()
    // 유저 정보 조회 로직
  }
}

위 코드에서 UserService 클래스는 MySQLDatabase라는 특정 데이터베이스 구현체에 강하게 결합되어 있습니다. 만약 데이터베이스를 MySQL에서 MongoDB 등 다른 DB로 변경하려면 UserService의 코드도 반드시 함께 변경해야 합니다. 이는 재사용성을 떨어뜨리고 유지보수를 어렵게 만드는 원인이 됩니다.


DIP를 적용한 개선된 코드 예시

이제 DIP를 적용하여 인터페이스를 통해 의존 관계를 개선한 예시를 보겠습니다.

dip
// 추상 인터페이스 정의
interface Database {
  connect(): void
}
 
// MySQLDatabase는 Database 인터페이스를 구현
class MySQLDatabase implements Database {
  connect() {
    console.log('Connected to MySQL')
  }
}
 
// UserService는 이제 Database 인터페이스에만 의존
class UserService {
  constructor(private db: Database) {}
 
  getUser() {
    this.db.connect()
    // 유저 정보 조회 로직
  }
}
 
// 사용 예시 (의존성 주입, Dependency Injection)
const db = new MySQLDatabase()
const userService = new UserService(db)

이제 UserService 클래스는 더 이상 특정 데이터베이스의 구현에 의존하지 않고, 인터페이스를 통해 추상화된 개념에만 의존합니다. 따라서 데이터베이스가 변경되어도 인터페이스를 구현한 새로운 클래스만 추가하면 되고, 기존 로직의 변경은 최소화할 수 있습니다.


DIP의 핵심은 '의존성의 역전'입니다.

DIP의 핵심은 기존 방식과 달리 의존성의 방향을 '역전'시킨다는 데 있습니다. 보통은 고수준 모듈(비즈니스 로직 등)이 저수준 모듈(데이터베이스, 네트워크 등)에 직접 의존하는 구조를 가지고 있지만, DIP는 이를 인터페이스와 같은 추상화를 통해 저수준 모듈이 고수준 모듈의 추상화에 의존하도록 구조를 뒤집는 것을 의미합니다.

이렇게 하면 모듈 간의 강한 결합을 느슨한 결합(Loose Coupling)으로 바꿔서 변경 사항이 서로 최소한의 영향을 미치도록 만들어줍니다.


프레임워크에서 사용하기

DIP는 의존성 주입(DI, Dependency Injection) 기법과 함께 자주 사용됩니다. 객체 생성의 책임을 외부로 넘기고, 필요한 객체를 주입받는 방식으로 코드의 확장성과 테스트 가능성을 높입니다. NestJS, Spring 등 유명한 프레임워크들은 DIP 기반으로 설계되어 있기 때문에, 이를 활용하면 손쉽게 DIP를 적용할 수 있습니다.

// NestJS 예시
@Injectable()
class UserService {
  constructor(@Inject('Database') private db: Database) {}
 
  getUser() {
    this.db.connect()
    // ...
  }
}

주의

DIP를 적용할 때, 너무 많은 인터페이스를 과도하게 생성하지 않도록 주의해야 합니다. 모든 세부 구현마다 인터페이스를 생성하면 오히려 코드가 복잡해지고, 관리가 어려워질 수 있습니다. 중요한 것은 적절한 수준의 추상화를 유지하여 실질적으로 변경될 가능성이 높은 부분만 인터페이스로 분리하는 것이 좋습니다.


정리

  1. DIP는 고수준 모듈과 저수준 모듈이 서로의 구체적인 구현이 아닌, 추상화(Interface 또는 Abstract Class)에 의존하도록 구조를 재설계하는 원칙입니다.
  2. 이를 통해 모듈 간의 느슨한 결합(Loose Coupling)을 달성하고, 코드의 유지보수성과 확장성을 높일 수 있습니다.

참고