ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 의존관계역전원칙(DIP)과 의존성 주입(DI)
    프로그래밍 2024. 1. 4. 16:26
    728x90

    의존관계역전원칙이란?

    위키백과에 따르면 객체 지향 설계의 다섯 가지 기본 원칙(SOLID) 중 D가 가르키는 원칙입니다.

    위키백과를 참조하여 쉽게 말하면 소프트웨어 모듈들을 분리하는 특정 형식을 지칭합니다.

     

    의존관계역전원칙을 따랐을 경우에 상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존관계를 반전(역전) 시킴으로 써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있습니다.

     

    해당 원칙은 다음과 같은 내용을 가지고  있습니다

    첫째. 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과  하위 모듈 모두 추상화에 의존해야 한다.

    둘째. 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.

    이 원칙은 '상위와 하위 객체 모두가 동일한 추상화에 의존해야 한다'는 객체 지향적 설계의 대원칙을 제공합니다.

     

    쉽게 말하자면 변경 가능성이 있거나 쉬운것 들은 추상화 (인터페이스 혹은 추상 클래스)를 사용하여 의존성을 관리하는 것 입니다.

     

    예를 들어 붕어빵을 만든다고 하였을 때 속재료는 구매자의 취향에 따라서 팥 혹은 슈크림으로 정할 수 가 있습니다.

    편의를 위해 속재료를 제외한 모든 것들은 불변이라고 가정을 하겠습니다.

     

    DIP 를 따르지 않은 경우

    interface BasicMaterial {
        fun selectMaterial()
    }
    
    class CustardCream : BasicMaterial {
        override fun selectMaterial() {
            println("슈크림을 선택하였습니다.")
        }
    }
    
    class RedBean : BasicMaterial {
        override fun selectMaterial() {
            println("팥을 선택하였습니다.")
        }
    }
    
    class FishBread {
        private val material = CustardCream()
    
        fun selectMaterial() {
            material.selectMaterial()
        }
    }
    
    fun main() {
        val fishBread = FishBread()
        fishBread.selectMaterial()
    }
    
    // 출력 결과
    슈크림을 선택하였습니다

     

    출력결과를 보면 원하는 대로 슈크림을 선택하였다는 거 아닌가? 라는 생각이 들 수도 있습니다.

    하지만 상기 코드를 보면 FishBread()에서 하기와 같이 CustardCream() 에 직접 의존하고 있어 CustardCream()이 변경되면 FishBread()도 영향을 받게 됩니다. DIP를 위반하고 있다고 볼 수 있습니다.

    private val material = CustardCream()

     

    좀 더 쉽게 말하면 재료를 팥으로 변경하거나 다른 재료를 선택하고 싶을 경우에도 FishBread() 코드를 수정해야 합니다. 

    이는 OCP(개방-패쇄 원칙) 에도 위배가 됩니다.

     

    그렇다면 DIP 및 OCP 에 위배되지 않도록 상기 코드를 수정해보겠습니다.

     

    interface BasicMaterial {
        fun selectMaterial()
    }
    
    class CustardCream : BasicMaterial {
        override fun selectMaterial() {
            println("슈크림을 선택하였습니다.")
        }
    }
    
    class RedBean : BasicMaterial {
        override fun selectMaterial() {
            println("팥을 선택하였습니다.")
        }
    }
    
    class FishBread(private val material: BasicMaterial) {
        fun selectMaterial() {
            material.selectMaterial()
        }
    }
    
    fun main() {
        val fishBread = FishBread(CustardCream())
        fishBread.selectMaterial()
    }

     

    이전 코드와 큰 차이점으로는 FishBread() 는 CustardCream()이나 RedBean() 같은 구체적인 클래스에 의존하지 않으며,, 해당 클래스의 인스터스는 생성자를 통해 주입 받습니다. 따라서 FishBread()는 어떤 종류의 BasicMaterial을 사용하더라도 그에 따라 변경될 필요가 없고, 코드의 유연성을 높여줍니다.

    이러한 방식을 의존성 주입(DI) 방식이라고 합니다.

    의존성 주입 이란?

    생성자를 통하여 객체를 주입하여 DIP를 위배하지 않는 코드를 봤었습니다.

    의존성 주입이란 의존성 역전 원칙(DIP)를 구현하는 기술 중 하나이며, 객체간의 결합도를 낮추는데 사용되는 설계 패턴 중 하나이며 핵심 원리는 클래스가 사용할 '의존성'을 직접 생성하지 않고, 외부에서 주입받아 사용하는 것입니다.

     

    DI(의존성 주입) 방식의 장점으로는

    첫 번째. 코드 재사용성 향상 : 컴포넌트 간의 결합도가 낮아지면, 각 컴포넌트를 독립적을  재사용할 수 있게 됩니다.

     

    두 번째. 코드 유지 관리 용이성 : 의존성이 명확하게 분리되어 있으면, 코드를 이해하고 수정하기 쉬워집니다.

     

    세 번째. 테스트 용이성 : 테스트 시에는 실제 객체 대신 모의 객체를 주입할 수 있어, 테스트를 보다 쉽게 수행할 수 있습니다.

     

    의존성 주입 방식

    1. 생성자 주입 (흔히 가장 많이 사용하는 방식)

    생성자 주입은 객체가 생성될 때 필요한 의존성을 생성자의 매개변수로 전달하는 방식이며, 객체가 생성되는 시점에 모든 의존성을 갖추게 되므로, 객체의 상태를 안정적으로 유지할 수 있습니다.

     

    2. Setter (세터) 메서드를 통한 주입

    객체가 생성된 이후 필요한 의존성을 세터 메소드를 통해 주입하는 방식입니다. 이 방법은 객체 생성 후 필요에 따라 의존성을 변경할 수  있다는 장점이 있지만, 객체가 일시적으로 완전하지 않은 상태에 놓일 수 있다는 단점이 있습니다.

     

    3. 인터페이스를 통한 주입

    인터페이스를 통한 주이브은 특정 인터페이스를 구현한 클래스를 통해 의존성을 주입하는 방식입니다. 이 방법은 유연성이 높지만 구현이 복잡할 수 있습니다.

Designed by Tistory.