SOLID 원칙과 의존성 역전 원칙(DIP) | Today I Learned

hoya·2022년 11월 14일
1

Today I Learned

목록 보기
11/11
post-thumbnail

피드백은 언제나 환영합니다 :)

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

의존 관계를 맺을 때, 변화하기 쉬운것 보단 변화하기 어려운 것에 의존해야 한다.

DIP는 소프트웨어 업계에서 상당히 많이 사용하는 의존성 주입(Dependency Injection)과도 밀접한 관계를 맺고 있는 원칙이기 때문에 확실히 알아둘 필요가 있다. 예제를 보며 어떤 원칙인지 이해해보자.

fun main() {
    val 회사 = 회사()
    회사.개발시키기()
}

class 회사 {
    val 개발자A = 개발자A("자바")
    val 개발자B = 개발자B("코틀린")

    fun 개발시키기() {
        개발자A.개발()
        개발자B.개발()
    }
}

class 개발자A(private val 언어: String) {
    fun 개발() = println("$언어 (으)로 개발합니다.")
}

class 개발자B(private val 언어: String) {
    fun 개발() = println("$언어 (으)로 개발합니다.")
}

회사 클래스에서 개발자A, 개발자B 클래스를 인스턴스화 하고 내부의 메소드를 사용하고 있다.

이 상태를 짧게 아래와 같이 요약할 수 있다.

  • 회사 클래스는 개발자A, 개발자B 클래스에 의존하고 있다.
  • 회사 클래스는 개발자A, 개발자B 클래스보다 더 많은 정보를 가지고 있으므로 상위 계층이며, 개발자 클래스하위 계층이다.

그림으로 표현하면 아래와 같이 표현할 수 있다.

이 상황에서 개발자A 클래스에 변화가 생기면 어떻게 될까?

class 회사 {
    val 개발자A = 개발자A("자바") // Compile Error
   	val 개발자A_변경 = 개발자A("자바", "키크론 키보드")
}

class 개발자A(private val 언어: String, private val 장비: String) {
    fun 개발() = println("$장비 와 함께 $언어 (으)로 개발합니다.")
}

개발자A 클래스의 생성자가 변경되자 상위 계층인 회사 클래스도 함께 변경되는 모습을 볼 수 있다.

당연히 이런 코드 전개는 유지보수에 있어 굉장한 불편함을 초래할 것임을 자연스럽게 파악할 수 있을 것이다.

이러한 현상을 방지하기 위해서 회사 클래스 입장에서 변하기 쉬운 개발자A 클래스보다 변하지 않는 추상적인 것에 의존하는 방식으로 바꿔야 한다.

fun main() {
    val 개발자A = 개발자A("코틀린", "키크론 키보드")
    val 개발자B = 개발자B("코틀린")

    val 회사 = 회사(개발자A)
    val 회사2 = 회사(개발자B)

    회사.개발시키기()
    회사2.개발시키기()
}

class 회사(private val 개발자: 개발자) {
    fun 개발시키기() = 개발자.개발()
}

interface 개발자 {
    fun 개발()
}

class 개발자A(private val 언어: String, private val 장비: String) : 개발자 {
    override fun 개발() = println("$장비 와 함께 $언어 (으)로 개발합니다.")
}

class 개발자B(private val 언어: String) : 개발자 {
    override fun 개발() = println("$언어 (으)로 개발합니다.")
}

추상적인 것, 즉 interface 에 의존하는 방식으로 코드를 변경했다.

이제 개발자A 클래스에 변동사항이 생기더라도 상위 계층인 회사 클래스에서는 전혀 영향을 받지 않게 되었다.

그림으로 나타내면 아래와 같다.

기존에는 상위 계층인 회사 에서 하위 계층의 구현체들인 개발자A, 개발자B 클래스에 의존했지만, 이제는 반대로 하위 계층의 개발자A 클래스가 상위 계층의 개발자 에 의존하는 것을 확인할 수 있다.

다만 주의할 점으로 소스 상에서 회사 클래스는 개발자 인터페이스에 의존하고 있지만, 실제 런타임 환경에서는 하위 계층인 개발자A 혹은 개발자B 클래스를 의존하게 된다. 짧게 이야기해 런타임 환경에서 의존이 역전되는 것이 아니라, 단순히 소스 상에서 의존을 역전시키는 것이다.


🤔 OCP VS DIP

이전의 개방 폐쇄 원칙 포스팅을 봤다면, OCP와 DIP 사이에 어떤 차이가 있는지 의문이 생길 수 있다.

아래의 코드를 보면, 한 번에 이해할 수 있을 것이다.

class 회사(private val 개발자: 개발자) {
    fun 개발시키기() {
        when(개발자) {
            is 개발자A -> { . . . }
            is 개발자B -> { . . . }
        }
    }
}

interface 개발자 {
    fun 개발()
}

class 개발자A(private val 언어: String) : 개발자 {
    override fun 개발() { }
}

class 개발자B(private val 언어: String) : 개발자 {
    override fun 개발() { }
}

위의 코드는 추상적인 인터페이스에 의존했기 때문에 DIP를 준수했다고 말할 수 있지만, OCP는 준수했다고 이야기할 수 없다.

만약 개발자 인터페이스를 구현하는 개발자C 클래스가 추가된다면, when 문에 개발자C 클래스를 추가하게 되며 구현 자체가 변경될 것이고, 이는 변경에 닫혀있어야 할 것을 강조하는 OCP를 준수하지 못한 것이 된다.


✍️ TL;DR

  • 상위 계층은 하위 계층의 구현체에 의존해서는 안 된다. 상위 계층이 하위 계층에서 정의한 추상화에 의존해야 한다.
    • 상위 계층은 하위 계층에 의존해선 안된다.
    • 상위 계층, 하위 계층 모두 추상화에 의존해야 한다.
  • 추상화는 구현체에 의존해선 안되고, 구현체가 추상화에 의존해야 한다.
  • OCP와 DIP는 밀접한 관계를 지니고 있지만, 차이는 분명히 존재한다.

참고 및 출처

위키피디아 - DIP
SOLID 원칙 5 - DIP: 의존성 역전 원칙 (Dependency Inversion)

profile
즐겁게 하자 🤭

0개의 댓글