의존 역전 원칙 (DIP : Dependency Inversion Principle)
SOLID 원칙의 마지막 원칙이다.
이 원칙을 지키기 위해 해야할 행동은 꽤나 심플하기 때문에 그리 어렵지 않은 원칙이다.
우선, 사전적으로 이 원칙은 다음과 같은 내용을 가지고 있다.
1. 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
2. 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.
나는 사전적 의미가 싫다. 쉽게 직설적인 의미로 설명하지 않기 때문이다.
상위 모듈과 하위 모듈이 뭘까?
이 부분을 정확히 정의하기 위해 구글링을 엄청나게 했는데,
감이 잘 오지 않는다.
어디는 상위 모듈이 유저 인터페이스와 가까운 레이어에 있는 모듈이라고 하고,
다른데는 하위 모듈이 유저 인터페이스와 가까운 레이어라고 하고,
또 다른 곳은 의미 있는 단일 기능을 구현한게 상위 모듈이라 하고,
자바와 관련해서 설명하는 곳은 변화가 쉬운 것에 의존하지 않는게 DIP라고 하고,
사람 더 헷갈리게 만드는 설명들이 따로 없다.
누가 추상화를 장려하는 원칙 아니랄까봐 그런지 설명들도 참 추상적이다.
어떤 곳은 상위 모듈이 추상화, 인터페이스를 뜻하고, 하위 모듈은 구현 클래스를 뜻한다고 하는데,
이건 너무 결과만 놓고 본 설명이다,.
영문 사이트도 뒤져보고 이것저것 정보를 추합해서 이해한 결과.
정말 직설적으로 게임 개발쪽과 연관지어서 설명하자면,
상위 모듈: UI 같이 게임을 개발할 때 유저에게 노출되는, 최종 구현 클래스가 되는 부분
하위 모듈: Service, Manager 클래스 혹은 라이브러리 같이 게임 뒷단에 있는 코어가 되는 부분
이렇게 설명할 수 있다.
(왜 이렇게 확 와닿게 설명하는 곳이 없는건지 모르겠다.)
이 정의를 토대로 의존 역전 원칙의 성립 조건을 다시 설명하자면,
'최종 구현 클래스에서 매니저 클래스 자체를 참조하지 말고, 인터페이스를 통해 추상화한 타입으로 참조하라'라고 할 수 있다.
이 정의를 한번 더 가공하면 이렇게 설명할 수 있다.
'인터페이스 써라'
의존 역전을 위한 의존성 주입 (Dependency Injection)
의존 역전을 실천하기 위해서 의존성 주입이라는 패턴에 대해 알아야할 필요가 있다.
이름은 무시무시한데, 정말 이론 자체는 정말 간단하다.
'내부 구현 클래스에서 참조(또는 생성)하던 외부 클래스를 인터페이스를 통해 외부로 부터 주입되게 하는 것'이다.
예를 들어, 게임 내에 광고 기능을 추가한다고 생각해보자.
회의를 통해 우리는 A 회사에서 제공하는 광고 플러그인을 사용하기로 하고,
A사의 플러그인 API에 맞춰서 매니저 클래스와, 광고 시청 버튼을 만들었다.
만약, 유저가 광고 버튼을 누르면 광고 매니저 클래스를 통해, 광고를 재생시킬 것이다.
public class AdvertisementButton
{
public void OnClick()
{
AdvertisementManager.Instance.PlayAd();
}
}
public class AdvertisementManager
{
public static AdvertisementManager Instance;
public void PlayAd()
{
// A사의 광고 플러그인 API 호출
}
}
AdvertisementManager는 많은 개발자가 사랑하는 싱글턴 클래스로 되어 있다. (디테일한 구현은 넘어가자)
그리고 AdvertisementButton클래스는 그 매니저 클래스를 직접 접근해서 호출하고 있다.
이런 경우가 바로 의존 역전 관계를 지키지 않는 경우이다.
만약, 추가한 광고 플러그인이 맘에 안들거나 문제가 있어서, 다른 회사의 광고 플러그인을 넣는다면?
그리고 그 회사의 플러그인의 API가 기존에 구현한 인터페이스와 다르다면?
(그리고 또 결정이 바뀌어서 다시 기존의 광고 플러그인을 다시 사용하기로 한다면???)
우린 AdvertisementManager를 사용하는 모든 클래스를 수정해야할 것이다.
이 문제를 해결하기 위해서 사용되는 것이 바로 의존성 주입 패턴이다.
일단, 의존성 주입을 하기 위해선 기존 클래스들을 인터페이스로 추상화 시켜야한다.
public interface IAdvertisementManager
{
void PlayAd();
}
public class AdvertisementManager : IAdvertisementManager
{
public void PlayAd()
{
}
}
어떤 광고 플러그인이던 가장 중요한 광고를 재생하는 함수는 존재할 것이다,
그래서 광고 재생 함수를 인터페이스로 추상화 시켰다.
이제 이 인터페이스를 광고 버튼 클래스에 넣어주면 되는데,
그냥 사용하는게 아니고 외부로부터 인터페이스의 참조를 주입시키는 통로를 만들어줘 값을 대입시키게 만들어야한다.
public class AdvertisementButton
{
private IAdvertisementManager _advertisementManager;
public void Initialize(IAdvertisementManager advertisementManager)
{
_advertisementManager = advertisementManager;
}
public void OnClick()
{
_advertisementManager.PlayAd();
}
}
예시 코드에선 함수를 통해 참조값을 주입시켰다.
이렇게 하면, 어떤 광고 플러그인을 쓰던 버튼 클래스는 인터페이스를 통해 광고를 재생시키기 때문에,
전혀 코드를 수정할 필요가 없다. 그저 외부에서 버튼에게 실제 참조 클래스를 주입시키는 코드만 바꿔주면된다.
이게 바로 의존성 주입이다. 이름 그대로 '외부에서 의존성을 주입시켜준다'해서 붙여진 이름이다.
앞서 얘기한대로, 의존성 주입은 다양한 통로로 넣어줄 수 있다. 속성이던 함수이던 생성자이던 상관없다.
구현 클래스를 내부에서 직접 생성하거나 참조만 하지 않으면 된다.
이렇게 의존성 주입 패턴을 사용하면, 의존 역전 원칙을 지킬 수 있다.
외부 어디에서 의존성을 붙여주는가?
의존성 주입 패턴을 사용하면,
분명 어딘가에는 인터페이스의 참조값을 넣어주는 클래스가 존재해야한다.
그럼 이걸 어디에서 호출해줘야할까?
이건 각자 정의하기 나름이다.
보편적으로는 프로그램이 정상적으로 작동하기 위해서
프로그램 시작할 때 맨 처음 호출 되는 부분에 인터페이스들의 구현 클래스들을 생성하는 코드들 만들어주곤 한다.
public class SomeClass
{
public AdvertisementButton _adButton;
public void OnStartUp()
{
AdvertisementManager adManager = new AdvertisementManager();
_adButton.Initialize(adManager);
}
}
(어딘가에는 이런 코드가 존재한다.)
싱글톤과 서비스 로케이션을 버려야한다.
싱글톤과 서비스 로케이션 패턴은 편리하면서도 보편적으로 쓰이는 패턴이다.
하지만 의존 역전 관계에서 이들은 안티 패턴(지양해야할 패턴)들이다.
public class AdvertisementButton
{
private IAdvertisementManager _advertisementManager;
public void Initialize()
{
_advertisementManager = Locator.GetInstance<IAdvertisementManager>();
}
public void OnClick()
{
_advertisementManager.PlayAd();
}
}
서비스 로케이터의 전역 함수를 통해, 인터페이스 타입을 가져오지만,
AdvertisementButton은 다시 Locator에 의존성을 가지고 있다.
Locator은 모든 코드에 전반적으로 쓰이는 코드인데, 만약에 이 클래스의 함수들이 바뀐다면?
AdvertisementManager에서 발생한 문제와 똑같은 문제를 일으킬 것이다.
결국 뭔가 괜찮은 방법을 넣긴했으나, 의존성의 문제점은 여전히 가지고 있는 셈이다.
static 인스턴스 사용은 SOLID 원칙에서는 지양해야할 코드 습관 중 하나이다.
왜 '의존 역전 원칙'일까?
솔직히 쉽고 정확하게 딱 이해되진 않지만,
일반적으로는 기존(Button에서 Manager에 의존하는) 의존 관계의 개념이 뒤집어졌다는 뜻에서
역전이라는 의미를 뜻한다고 한다.
어떤 곳에서는
----
상위
↓
하위
----
에서
----
인터페이스 ← 상위
↑
하위
----
개념으로 의존 방향이 역전 되어서 역전이라고 의존 역전이라고 설명하는 곳도 있긴하다.
정확한 개념은 나도 모르겠다.
어차피 이 원칙에서 제일 중요한건 클래스들 간에 의존성을 가지지 않도록 해야한다는거다.
의존성 주입하는 방법은 다양하게 있다
여기서 말하는 방법은 메소드, 생성자 주입 같은 것을 말하는게 아니라
이전 OnStartUp() 예시처럼 인스턴스를 초기화 하고 의존성을 주입 것에 대한 방법을 말한다.
예시처럼 시작 단계에서 초기화해서 수동적으로 주입하는 방법도 있고,
IoC 프레임워크에서 자동으로 주입하는 방법도 있다.
제어의 역전(역행)(IoC : Inversion of Control)과 DI Framework
사전적인 의미부터 설명하자면,
일반적인 프로그래밍처럼, 프로그래머가 외부 라이브러리를 호출하는게 아닌,
외부 라이브러리가 프로그래머가 짠 코드를 호출하여, 제어의 흐름이 역전되었다는 뜻을 가진 개념(혹은 패턴)이다.
프레임워크가 바로 이런 개념을 가진 대표적인 예이다.
(프레임워크: 특정한 구조와 규약을 가져서, 개발자가 그 위에서 작업을 하면, 자동으로 코드를 관리하고 제어하도록 만들어진 구조.)
의존성 주입 개념에 프레임워크 개념을 이용하여, 프로그래머가 수동으로 의존성을 주입하는게 아닌,
인스턴스를 생성하고 관리하는 컨테이너라고 불리는 제어자를 통하여 자동으로 의존성이 주입되도록 하는 DI Framework가 존재한다.
이런 개념들에 대해 찾다보면, IoC, IoC Container, DI, DI Framework 용어들에 대한 시간적인 순서가 조금 헷갈린데,
찾아본바에 의하면, IoC -> DIP -> IoC Container -> DI, DI Framework 인거 같다.
추측하기로는
DIP 개념으로부터 인스턴스들의 의존성을 자동으로 관리해주는 프레임워크 개념인 IoC Container가 나왔고,
IoC Container의 IoC 단어는 우리가 아는 의존성 주입의 개념으로써 사용되고 있었다.
그런데 이 IoC의 개념이 일반적인 IoC의 개념과 충돌하여 혼동의 여지가 있기 때문에
IoC를 DI라고 명명하여 사용하기 시작했고 DI Container(Framework)란 단어가 생긴 것 같다.
의존성 주입을 위한 프레임워크 쓰자!
의존성 주입을 간편하게 사용하기 위한 다양한 프레임워크 존재한다.
비주얼 스튜디오에서도 존재하고 자바에선 스프링이란 프레임 워크도 존재하고 유니티에서도 존재한다.
필자는 유니티에서 Zenject라는 플러그인을 사용한적이 있다.
매우 강력한 프레임워크 플러그인으로 이걸 사용 하면,
각종 시스템 클래스들을 직관적이게 초기화 할 수 있고,
구현 클래스에서 싱글톤이나 로케이터 패턴보다 훨씬 편하고 깔끔하게 접근할 수 있다.
https://github.com/modesttree/Zenject
(에셋 스토어에도 있다.)
'Refactoring & Clean Code' 카테고리의 다른 글
SOLID : 인터페이스 분리 원칙 (0) | 2020.04.12 |
---|---|
SOLID : 리스코프 치환 원칙 (0) | 2020.04.12 |
SOLID : 개방 폐쇄 원칙 (0) | 2020.04.10 |
SOLID : 단일 책임 원칙 (0) | 2020.04.10 |
리팩토링에 대한 개인적인 생각 (0) | 2020.04.10 |