Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
iOS Developer/Swift

의존 역전 원칙? 의존성 주입이란? (Dependency Injection), (Dependency Inversion Principle)

by Toughie 2023. 3. 20.

의존성 주입 의존성 주입 주변에서 얘기만 많이 들었는데 갑자기 너무 궁금해져서 공부해 보았다.

우선 오늘은 러프하게라도 개념을 이해해 보고, 실제 코드에서 활용하게 되는 케이스가 있을 때 보충할 예정이다.

 

객체지향 설계원칙 SOLID에서 마지막 파트를 이해하기 위해 필요하다.

1. 단일 책임 원칙 (Single Responsiblity Principle)

모든 클래스는 각각 하나의 책임만 가져야 한다. 클래스는 그 책임을 완전히 캡슐화해야 함을 말한다.

 

2. 개방-폐쇄 원칙 (Open Closed Principle)

확장에는 열려있고 수정에는 닫혀있는. 기존의 코드를 변경하지 않으면서( Closed), 기능을 추가할 수 있도록(Open) 설계가 되어야 한다는 원칙을 말한다.

 

3. 리스코프 치환 원칙 (Liskov Substitution Principle)

자식 클래스는 언제나 자신의 부모 클래스를 대체할 수 있다는 원칙이다. 즉 부모 클래스가 들어갈 자리에 자식 클래스를 넣어도 계획대로 잘 작동해야 한다.

자식클래스는 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행하도록 해야 LSP를 만족한다.

 

4. 인터페이스 분리 원칙 (Interface Segregation Principle)

한 클래스는 자신이 사용하지않는 인터페이스는 구현하지 말아야 한다. 하나의 일반적인 인터페이스보다 여러개의 구체적인 인터페이스가 낫다.

 

5. 의존 역전 원칙 (Dependency Inversion Principle)

의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 것이다. 한마디로 구체적인 클래스보다 인터페이스나 추상 클래스와 관계를 맺으라는 것이다.

 

의존 역전 원칙을 간단한 예시를 통해 이해해 보자

동물원(High Level Module)에는 개(인스턴스)도 있고 고양이(인스턴스)도 있다고 치자.

여기서 개나 고양이 인스턴스는 Low Level Module이다.



동물원은 현재 개나 고양이 인스턴스에 의존하고 있는데, 각 동물의 인스턴스를 직접 넣는게 아니라

'동물'이라는 클래스를 만들고, 개와 고양이는 동물 클래스를 상속해서 만들어져 있다고 치자.



그러면 이제 동물원에는 동물 클래스를 상속한 녀석들이 들어가게 하는 것이다.



동물원 -> 개/고양이의 직접 의존관게에서



동물원 -> 동물 클래스 <- 개/고양이(동물 인스턴스)로 화살표의 방향이 역전된 것이다.

이걸 의존성 역전법칙으로 이해하면 좋을듯 하다.

이렇게 의존성 역전법칙이 적용되면 다른 동물들이 추가되어도(동물 클래스를 상속한)

동물원 코드 자체는 건드릴 필요가 없다!

 

의존성 주입 얘기를 하려면 프로토콜을 알아야 하는데.. 프로토콜은 자격증으로 이해하면 쉽다.

최소한의 요구사항이 나열되어 있고, 이 프로토콜을 채택해서 내부에서 요구사항에 대한 구현을 하면(준수하면)

특정한 능력이 생긴다고 비유할 수 있겠다. 

 

의존성이 무엇인지 아래의 예시코드를 통해 알아보자

import UIKit

// 아래 프로토콜을 채택하여 준수하면 말할 수 있는 능력이 생길 것이다.

protocol Talking {
	var saying: String { get set }
	func sayHi()
}

class TalkingImplementation: Talking {
	var saying: String = "말하기"
    
    func sayHi() {
    	print("말을 합니다.")
        }
}

class FirstTalker: Talking {
	var saying: String = "첫 번째에요"
    
	func sayHi() {
    print("저는 말을 잘해요.") 
    }
}

class SecondTalker: Talking {
	var saying: String = "두 번째에요"
    
	func sayHi() { 
    print("무슨 말을 해야 하죠..?") 
    }
}

class Friend {
	var talkProvider: Talking
    //Talking 프로토콜을 채택한 것들이 할당 될 수 있음.
    
    var saying: String {
    	get {
        	 talkProvider.saying
             }
        }
    
    init(_ talkProvider: Talking) {
    	self.talkProvider = talkProvider
    }
    
    func sayHi() {
    	talkProvider.sayHi()
    }
}

let firstFriend = Friend(FirstTalker())
firstFriend.sayHi()
//"저는 말을 잘해요"

firstFriend.saying
//"첫 번째에요"

let secondFriend = Friend(FirstTalker())
secondFriend.sayHi()
//"무슨 말을 해야 하죠..?"

secondFriend.saying
//"두 번째에요"

즉 위와같은 형태로 코드를 작성하면

프로토콜을 채택하는 녀석들은 프로토콜에 의존하는 것이다.

혹은 프로토콜을 채택하는 클래스를 만들어 상속할 수도 있고..

 

 

의존성은 하나의 코드가 다른 코드에 의존하는 상태를 말한다.

(클래스 A에서 클래스B의 인스턴스를 가지고 있다면, 클래스A는 클래스B에 의존함)

 

원래는 클래스A가 클래스B에 직접 (참조,생성)의존했는데

(클래스 안에서 다른 클래스 인스턴스를 찍어내는 것처럼)

중간에 매개체를 만들어서 필요할 때마다 가져오고 한다면?

여기서 IOC의 개념이 나온다.

IOC, Inversion of Control -> 제어권 역전

아래의 사진을 참고하자

 

직접 의존성을 제어하다가 중간 매개체를 통해 제어를 위임한다고 생각하면 좋다.

매개체 -> IoC Container (의존성 주입 및 메모리 해제의 역할까지 함_주로 프레임워크로 작업)

 

[장점]

클래스 안에서 직접 생성하면 너무 의존성이 강한데, IoC를 통하면

'의존성이 감소한다.'

즉 변화에 유연하고, 재사용성도 좋아지며, 유지보수도 용이해 진다.

의존하는 모델이 바뀌어도 상관없기 때문이다.

 

또한 코드의양 자체도 감소한다.

테스트 하기도 더 편하다. (원하는 객체에 필요한 것들만 의존성을 주입하면 되기 때문에)

 

 

즉 너무 구체적인 녀석들에 의존하면.. 뭐가 바뀔 때마다 코드를 다 바꿔줘야 하기 때문에,(or코드가 다르게 작동)

인스턴스와 같이 구체화된 녀석에 의존하는 것이 아니라!

좀 더 추상화된 녀석(클래스,프로토콜 등)을 통해 의존성을 주입하거나,

IoC를 통해서 의존성을 주입하면 유지보수 등 여러 면에서 장점들을 누릴 수 있다는 것이다!

 

 

 

참고자료

https://hckcksrl.medium.com/solid-%EC%9B%90%EC%B9%99-182f04d0d2b

https://youtu.be/1vdeIL2iCcM

https://youtu.be/Jau0a0IvveY

https://youtu.be/DYmtue0k1cc