ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SOLID - DIP (의존성 역전 원칙)
    어플리케이션 2026. 8. 9. 21:31

     

    안녕하세요~ 오랜만이네요 오늘은 SOLID의 마지막 원칙인 의존성 역전 원칙(DIP)에 대해 알아보겠습니다!

     

    의존성 역전 원칙(Dependency Inversion Principle, DIP)

    고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다.

     

    다시 풀어서 설명하자면,

    안쪽에 있는 상위 계층이 바깥에 있는 하위 계층의 구체적인 구현에 직접 의존하지 않도록 하고, 인터페이스를 통해 의존해야 한다.

     

    라고 이해할 수 있습니다.

     

    그렇다면 왜 이렇게 해야 할까요?

    바로 변경에 유연하게 대응하고, 모듈 간 결합도를 낮추기 위해서입니다.

    상위 계층이 하위 계층의 구체적인 구현에 직접 의존하고 있다면, 하위 계층이 변경될 때 상위 계층까지 함께 수정해야 할 수 있습니다. 반대로 추상화에 의존하도록 만들면 하위 계층의 구현이 변경되더라도 상위 계층의 코드를 변경하지 않아도 됩니다.

     

    한번 구체적인 예시를 살펴보겠습니다🐦

     

    의존성 역전 원칙이 지켜지지 않은 상황

    class LightBulb {
        fun turnOn() { print("Light bulb on") }
    }
    
    class Switch(private val lightBulb: LightBulb) {
        fun operate() { lightBulb.turnOn() }
    }

     

    현재 Switch 클래스는 생성자를 통해 LightBulb를 직접 전달받고 있습니다.

    즉, Switch가 구체적인 클래스인 LightBulb에 직접 의존하고 있는 상황입니다.

    만약 나중에 전구가 아니라 다른 전자기기를 연결하고 싶다면 어떻게 될까요?

    예를 들어 Fan이라는 새로운 기기를 추가한다고 해보겠습니다.

    class LightBulb {
        fun turnOn() { print("Light bulb on") }
    }
    
    class Fan {
        fun turnOn() { print("Fan on") }
    }
    
    class Switch(private val lightBulb: LightBulb) {
        fun operate() { lightBulb.turnOn() }
    }

     

    현재 Switch는 LightBulb에 의존하고 있기 때문에 Fan을 연결하기 어렵습니다.

    이처럼 특정 구현에 직접 의존하게 되면 모듈 간 결합도가 높아지고, 새로운 구현을 추가하거나 기존 구현을 변경하기 어려워집니다.

     

    의존성 역전 원칙을 적용하면?

    이번에는 추상화 인터페이스를 만들어보겠습니다.

    interface Switchable {
        fun turnOn()
    }
    
    class LightBulb: Switchable {
        override fun turnOn() { print("Light bulb on") }
    }
    
    class Fan: Switchable {
        override fun turnOn() { print("Fan on") }
    }
    
    class Switch(private val device: Switchable) {
        fun operate() { device.turnOn() }
    }

     

    Switch가 LightBulb라는 구체적인 클래스가 아니라 Switchable이라는 인터페이스에 의존하고 있습니다.

    따라서 Switch는 실제로 어떤 기기가 연결되어 있는지 알 필요가 없습니다.

    void main() {
        val lightBulb = LightBulb()
        val fan = Fan()
        val switchWithLightBulb = Switch(lightBulb)
        switchWithLightBulb.operate() // "Light bulb on"
        val switchWithFan = Switch(fan)
        switchWithFan.operate() // "Fan on"
    }

     

    이처럼 Switch의 코드를 변경하지 않고도 새로운 기기를 연결할 수 있습니다. 즉, Switch는 특정 구현체가 아니라 추상화인 Switchable 인터페이스에 의존하게 되었습니다.

    여기서 인터페이스가 중요한 역할을 합니다.

    Switch는 "전구를 켜야 한다"가 아니라, "켜질 수 있는 장치라면 전부 된다" 라는 사실만 알고 있으면 됩니다.

    이처럼 구체적인 구현이 아닌 추상화에 의존하도록 만드는 것이 DIP의 핵심입니다.

     

    그렇다면 실제 안드로이드에서는 DIP 원칙을 어떻게 적용할 수 있을까요?

    간단한 UserRepository 예제를 통해 살펴보겠습니다.

     

    DIP가 적용되지 않은 상황

    먼저 다음과 같은 코드가 있다고 가정해보겠습니다.

    class UserRepository {
        private val localDataSource = LocalDataSource()
        private val remoteDataSource = RemoteDataSource()
        fun getUser(): User {
            return if(localDataSource.hasUser()) {
            	localDataSource.getUser()
            } else {
            	remoteDataSource.getUser()
            }
        }
    }
    
    class LocalDataSource {
        fun hasUer(): Boolean = true
        fun getUser(): User = User("Local User", "영희")
    }
    
    class RemoteDataSource {
        fun getUser(): User = User("Remote User", "철수")
    }

     

    현재 UserRepository가 LocalDataSource와 RemoteDataSource를 직접 생성하여 강하게 결합되어 있습니다. 이런 구조에서는 문제가 발생할 수 있습니다. 예를 들어, 테스트를 위해 실제 LocalDataSource 대신 가짜 객체를 사용하고 싶어도 UserRepository 내부에서 직접 LocalDataSource를 생성하고 있기 때문에 교체하기 어렵습니다. 

    또한, 데이터 저장 방식이나 네트워크 통신 방식이 변경되었을 때 UserRepository까지 영향을 받을 수 있습니다.

     

    인터페이스를 통해 의존성 역전하기

    의존성 역전을 위해 인터페이스라는 추상화를 추가해보겠습니다.

    이때 Local과 Remote는 역할이 다르기 때문에 각각 필요한 기능을 인터페이스로 분리합니다.

    interface LocalUserDataSource {
        fun hasUser(): Boolean
        fun getUser(): User
    }
    
    interface RemoteUserDataSource {
        fun getUser(): User
    }

     

    그리고 각각의 구현체가 인터페이스를 구현하도록 변경합니다.

    class LocalDataSource: LocalUserDataSource {
        override fun hasUser(): Boolean = true
        override fun getUser(): User = User("Local User", "영희")
    }
    
    class RemoteDataSource: RemoteUserDataSource {
        override fun getUser(): User = User("Remote User", "철수")
    }

     

    이제 UserRepository가 구체적인 LocalDataSource와 RemoteDataSource가 아닌 인터페이스에 의존하도록 변경합니다.

    class UserRepository(
        private val localDataSource: LocalUserDataSource
        private val remoteDataSource: RemoteUserDataSource
    ) {
        fun getUser(): User {
            return if(localDataSource.hasUser()) {
            	localDataSource.getUser()
            } else {
            	remoteDataSource.getUser()
            }
       }
    }

     

    의존성의 방향을 살펴보면 다음과 같습니다.

    추상화를 통한 의존성 제거

     

    UserRepository는 더 이상 LocalDataSoure나 RemoteDataSource라는 구체적인 구현체를 알 필요가 없습니다. 단지 추상체인 인터페이스가 제공하는 기능만 알면 됩니다. 따라서 구현체가 변경되더라도 UserRepository의 코드를 수정할 필요가 없어집니다.

     

    DIP와 클린 아키텍처의 관계 

    DIP 개념을 활용하면, 클린 아키텍처에서 안쪽 계층인 도메인 레이어가 바깥쪽 계층인 데이터 레이어의 존재를 알지 않아도 되는 이유를 자연스럽게 이해할 수 있습니다.

    도메인 레이어에서는 필요한 기능을 인터페이스로 정의하고, 데이터 레이어에서 그 인터페이스를 구현하도록 만듭니다.

    클린 아키텍처의 도메인, 데이터 레이어

     

    이렇게 하면 도메인 레이어는 데이터 레이어의 구현체가 아닌 인터페이스에만 의존하게 됩니다. 따라서 도메인 레이어에서는 인터페이스가 제공하는 기능만 사용하면 될 뿐, 실제로 데이터가 어떻게 처리되는지 알 필요가 없습니다.

    결과적으로 계층 간 결합도를 낮추고, 구현체를 쉽게 교체할 수 있으며, 테스트하기 더 쉬운 구조를 만들 수 있습니다.

     

    마무리

    DIP의 핵심은 단순히 "인터페이스를 사용하자"가 아닙니다.

    고수준 모듈이 인터페이스를 통해 저수준 모듈의 구체적인 구현에 의존하지 않도록 만들자는 것입니다.

    결국 SOLID가 지향하는 것은 변경에 유연하게 대응하고, 유지보수하기 쉽도록 하는 것이라고 생각하면 좋을 것 같습니다.

     

    지금까지 SOLID의 마지막 원칙인 의존성 역전 원칙(DIP)에 대해 알아보았습니다. 읽어주셔서 감사합니다!🐦

    '어플리케이션' 카테고리의 다른 글

    Rive Animation이란?  (0) 2026.09.06
    의존성 주입이란?  (0) 2026.08.23
    SOLID - ISP (인터페이스 분리 원칙)  (1) 2026.01.11
    SOLID - LSP (리스코프 치환 원칙)  (0) 2025.12.14
    SOLID - OCP (개방 폐쇄 원칙)  (0) 2025.11.30
Designed by Tistory.