base
개발을 하다 보면 처음에는 빠르고 쉽게 만들었던 코드가 점점 더 복잡해지고 관리가 어려워지는 상황을 자주 마주하게 됩니다. 특히 특정 모듈이나 클래스가 변경될 때마다 연관된 다른 모듈들도 모두 수정해야 한다면, 유지보수가 굉장히 어려워집니다. 이런 경험이 있다면, 지금부터 다룰 의존 역전 원칙(Dependency Inversion Principle, DIP) 이 큰 도움이 될 수 있습니다.
흔히 "역전"이라는 표현은 조금 낯설고 어색하게 들릴 수도 있습니다. 하지만 이 원칙에서 말하는 '역전'은 오히려 여러분의 코드를 더 유연하고, 더 확장 가능하며, 더 유지보수하기 쉽게 만들어주는 놀라운 힘을 가지고 있습니다. 그 이유를 지금부터 천천히 살펴보겠습니다.
의존 역전 원칙(DIP)은 로버트 C. 마틴(Robert C. Martin, Uncle Bob)이 제안한 SOLID 원칙의 마지막 다섯 번째 원칙으로, 아래와 같은 내용을 담고 있습니다.
한마디로 정리하면, 구체적인 구현 클래스에 의존하지 말고 추상화된 인터페이스나 추상 클래스를 통해 의존 관계를 설정하라는 뜻입니다. 이 원칙을 적용하면 모듈 간의 결합도가 낮아지고, 코드의 유연성과 확장성이 크게 증가합니다.
실무에서 흔히 볼 수 있는 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()
// 유저 정보 조회 로직
}
}
위 코드에서
이제 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)
이제
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를 적용할 때, 너무 많은 인터페이스를 과도하게 생성하지 않도록 주의해야 합니다. 모든 세부 구현마다 인터페이스를 생성하면 오히려 코드가 복잡해지고, 관리가 어려워질 수 있습니다. 중요한 것은 적절한 수준의 추상화를 유지하여 실질적으로 변경될 가능성이 높은 부분만 인터페이스로 분리하는 것이 좋습니다.