Architecture

의존성 역전 원칙(DIP): 유연하고 확장 가능한 코드 설계의 핵심

백엔드 유성 2024. 2. 5. 21:07

의존성 역전의 원칙(Dependency Inversion Principle, DIP)은 객체지향 설계의 5대 원칙 중 하나로, 상위 수준 모듈이 하위 수준 모듈에 의존하지 않도록 하여 시스템의 결합도를 줄이고 유연성을 높이는 데 중점을 둡니다. 이 원칙은 애플리케이션이 변화에 쉽게 대응할 수 있도록 구조적으로 설계하는 데 중요한 역할을 합니다.

 

의존성 역전의 원칙(DIP)의 핵심 개념

 

DIP는 두 가지 주요 원칙으로 설명됩니다.

1. 상위 수준 모듈은 하위 수준 모듈에 의존해서는 안 된다.

상위 수준 모듈은 애플리케이션의 비즈니스 로직을 담당하고, 하위 수준 모듈은 데이터베이스나 네트워크와 같은 구체적인 기능을 처리합니다. 상위 모듈이 하위 모듈에 직접 의존하면, 하위 모듈이 변경될 때 상위 모듈도 영향을 받을 수 있습니다. DIP는 이를 방지하기 위해 상위 모듈이 하위 모듈의 구체적인 구현에 의존하는 대신, 추상화된 인터페이스에 의존하게 만듭니다.

 

2. 추상화는 구체화에 의존하지 않아야 한다.

구체적인 구현이 추상화된 인터페이스에 의존해야 하고, 그 반대는 성립하지 않아야 합니다. 즉, 인터페이스나 추상 클래스는 구체적인 구현 세부 사항에 의존하지 않고, 구체적인 하위 클래스들이 인터페이스를 구현해야 합니다. 이를 통해 하위 모듈의 구체적인 구현이 변경되어도 상위 모듈은 그대로 유지될 수 있습니다.

 

DIP 적용 예시

다음은 DIP를 적용한 간단한 예제입니다:

 

// 추상화된 인터페이스
interface MessageSender {
    fun sendMessage(message: String)
}

// 하위 수준 모듈 - 구체적인 구현
class EmailSender : MessageSender {
    override fun sendMessage(message: String) {
        println("Sending email: $message")
    }
}

// 또 다른 하위 수준 모듈
class SmsSender : MessageSender {
    override fun sendMessage(message: String) {
        println("Sending SMS: $message")
    }
}

// 상위 수준 모듈
class NotificationService(private val sender: MessageSender) {
    fun notifyUser(message: String) {
        sender.sendMessage(message)
    }
}

 

이 예제에서는 NotificationService라는 상위 수준 모듈이 MessageSender라는 인터페이스에 의존하고 있습니다. EmailSenderSmsSender 같은 하위 수준 모듈의 구체적인 구현에 직접적으로 의존하지 않으며, 이들은 MessageSender 인터페이스를 구현함으로써 상위 모듈과 연결됩니다. 따라서 NotificationService는 다양한 MessageSender 구현체로 대체 가능하여 유연한 구조를 유지할 수 있습니다.

 

DIP의 중요성

1. 변경에 유연한 구조

DIP를 적용하면 하위 모듈의 구체적인 구현을 쉽게 교체할 수 있습니다. 예를 들어, EmailSender 대신 SmsSender를 사용하거나, 새로운 메신저 서비스를 도입하는 경우에도 NotificationService에는 별다른 수정이 필요하지 않습니다. 상위 모듈이 구체적인 하위 모듈에 의존하지 않기 때문에 코드의 변경에 쉽게 대응할 수 있습니다.

 

2. 결합도 감소

상위 모듈과 하위 모듈의 결합도를 낮춤으로써, 각 모듈이 독립적으로 동작할 수 있게 됩니다. 이는 시스템 유지보수성을 높이고, 특정 모듈에서 발생하는 변경이 다른 모듈에 미치는 영향을 최소화합니다.

 

3. 테스트 용이성

DIP를 적용하면 모듈 간의 의존성이 낮아져서, 상위 모듈을 독립적으로 테스트하기가 훨씬 쉬워집니다. 실제로는 다양한 하위 모듈을 사용하는 상위 모듈의 기능을 테스트할 때, 하위 모듈을 Mocking하여 상위 모듈만 독립적으로 검증할 수 있습니다.

 

DIP를 적용하지 않았을 때의 문제점

DIP를 적용하지 않으면, 상위 모듈이 하위 모듈에 강하게 결합되어 코드의 유연성과 재사용성이 낮아집니다. 하위 모듈을 변경할 때마다 상위 모듈도 변경이 필요할 수 있으며, 이는 시스템의 유지보수 비용을 크게 증가시킵니다.

 

// DIP를 적용하지 않은 예시
class NotificationService {
    private val emailSender = EmailSender()

    fun notifyUser(message: String) {
        emailSender.sendMessage(message)
    }
}

 

위 코드에서는 NotificationServiceEmailSender의 구체적인 구현에 직접 의존하고 있어, EmailSender를 다른 메시지 전송 방식으로 교체하려면 NotificationService 코드도 수정해야 합니다. 이러한 방식은 유연하지 않고, 모듈 간 강한 결합을 초래하게 됩니다.

 

DIP의 실전 적용

DIP는 실제 프로젝트에서도 매우 중요합니다. 예를 들어, 대규모 애플리케이션에서 상위 모듈은 주로 비즈니스 로직을 포함하고, 하위 모듈은 데이터베이스, 네트워크, 파일 시스템 등 다양한 외부 시스템과 통신합니다. 상위 모듈이 하위 모듈에 직접 의존하면, 하위 모듈의 변경이 상위 모듈에 연쇄적으로 영향을 미치게 됩니다. DIP를 적용하면, 상위 모듈은 하위 모듈이 어떻게 동작하는지 신경 쓰지 않고, 하위 모듈이 제공하는 추상 인터페이스만 사용하면 됩니다.

 

예시: 데이터베이스 변경

예를 들어, 데이터베이스 모듈을 변경해야 하는 상황이 발생했다고 가정해봅시다. DIP를 적용한 설계에서는 데이터베이스의 구체적인 구현을 바꿔도 비즈니스 로직을 담당하는 상위 모듈은 영향을 받지 않습니다.

 

// 상위 모듈은 데이터베이스와 상관없이 추상화된 저장소 인터페이스를 사용
interface UserRepository {
    fun saveUser(user: User)
}

// 구체적인 MySQL 구현체
class MySQLUserRepository : UserRepository {
    override fun saveUser(user: User) {
        println("Saving user in MySQL")
    }
}

// 상위 모듈
class UserService(private val repository: UserRepository) {
    fun registerUser(user: User) {
        repository.saveUser(user)
    }
}

 

이와 같이 DIP를 적용하면, 상위 모듈(UserService)은 UserRepository라는 추상화된 인터페이스에 의존하게 되어, 데이터베이스의 구체적인 구현(MySQLUserRepository)이 변경되더라도 상위 모듈의 코드를 수정할 필요가 없습니다.

 

결론

 

의존성 역전의 원칙(DIP)은 코드의 결합도를 낮추고 변경에 유연하게 대응할 수 있는 구조를 설계하는 데 중요한 역할을 합니다. 상위 모듈은 구체적인 하위 모듈에 의존하지 않고 추상화된 인터페이스에 의존함으로써, 하위 모듈의 변경에 상위 모듈이 영향을 받지 않도록 만들어줍니다. 이를 통해 시스템은 확장 가능하고, 유지보수가 쉬운 구조로 발전할 수 있습니다.

 

DIP는 변경에 강한 소프트웨어 시스템을 구축하기 위한 중요한 원칙으로, 특히 다양한 구현체를 사용해야 하는 프로젝트에서 필수적인 설계 원칙입니다.