DIP, 의존성 역전 원칙
Last updated
Last updated
이 글은 베어코드 님의 Swift Object Oriented Progoramming 을 보고 정리한 글 입니다.
코드에서 통상적으로 발견되는 특별한 문제를 발견
상위 수준의 모듈이 하위 수준의 모듈에 의존성을 가지는 경향
정책이 구체적인 것에 의존하는 경향
DIP의 목적
모듈간의 의존관계를 끊는 방법 제시
변경이 다른 코드에 영향을 최소화 시킬 방법 제시
코드는 어떻게든 의존관계를 가지고 작성된다.
의존관계 자체를 없앨 수는 없다.
의존관계를 잘 정리하지 않으면 코드는 경직성을 띄게 된다. (기능의 추가/수정이 까다로워짐)
의존관계가 존재할 수 밖에 없다는 전제하에 무엇이 무엇에 의존하느냐에 따라 코드가 좋아질 수 있다.
생각보다 자주 볼 수 있는 형태
의존관계는 단방향으로 흘러가는 것이 좋음
이렇게 서로 의존관계를 가지는 모듈이면 weak을 사용해야 한다고 이미 알고 있을 것이다.
A는 B에 의존하고 있고, B는 C에 의존하고 있다. 이런 경우 C는 A,B에 의존하지 않기 때문에 독자적으로 사용할 수 있는 클래스이다. B도 C에 의존하고있지만 A에는 의존하지 않는다.
모든 클래스들이 의존관계가 없다면 최선이지만 현실적으로 어렵다.
만일 C가 A혹은 B에 의존해야 하는 상황이 있다면? 그니까 직접적으로 A, B를 참조해서 쓰면 안된다는 것이다.
의존관계는 어떤 방향으로 흘러가야 할까?
구체적인 부분 -> 추상적인 방향으로 의존관계를 가져야함.
추상적인 것이 구체적인 것에 의존하지 않아야 한다.
정책이 구체적인 것에 의존하는 경향 ( 정책은 추상적인 것 )
상위수준은 하위수준에 의존하면 안된다.
상위수준 : 추상적인 부분
하위수준 : 구체적인 부분
UITableView : 추상적
UITableView를 이용해서 구현된 코드 및 Cell : 구체적
무슨말이냐면, Swift에 구현되어있는 아래의 UITableView class를 보면 UITableView는 개발자가 커스텀한 Cell에 직접적으로 의존하지 않는다.(의존없음) 리턴값으로 커스텀셀 대신 Cell의 추상레벨인 UITableViewCell을 사용한다.
동작도 마찬가지다. 동작에 관한 부분은 UITableViewDatasource, UITableViewDelegate에 위임을 하게 된다. UITableView 입장에서는 2개의 프로토콜(datasource, delegateP)과 UITableViewCell 만으로 동작을 하게 되는 것이다.
즉, 직접적으로 의존하지 않고 인터페이스에 의해 의존함.
UIViewController와 내가 만든 클래스인 MyViewController가 있다.
UIViewController는 view life cycle에 대한 정책을 결정한다. (뷰디드로드, 뷰윌어피어...)
그리고 MyViewController는 내가 커스텀으로 구현한 구체적인 코드가 있다.
즉, MyViewController는 UIViewController에 의존하지만 반대로 UIViewController는 MyViewController에 의존하고 있지 않다. MyViewController가 존재하는지도 모른다.
소프트웨어 모듈들을 분리하는 원칙. 상위계층(정책결정)이 하위계층(세부사항)에 의존하는 전통적인 의존관계를 반전(역전)시켜 상위계층이 하위계층의 구현으로부터 독립되게 한다.
뭔말이냐면, 만약에 UITableView, UITableViewCell 클래스가 없었다고 치고 같은 기능을 구현한다고 했으면 그 테이블뷰를 커스텀셀에 의존되게 만들 가능성이 크다는 말이다. 그 말은 재사용이 불가능하게 설계를 했을 거라는 말이다. 위에서 UITableView가 리턴값으로 커스텀셀이아니라 UITableViewCell을 사용하는 것과 같은 맥락인 것 같다. 그렇게 함으로써 재사용이 가능해지고 의존하지 않게 분리하고 있으니까.
상위모듈은 하위모듈에 의존해서는 안된다. 상위모듈과 하위모듈 모두 추상화에 의존해야 한다.
추상화는 세부사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.
꼭 protocol을 사용하는 것만이 DIP를 사용하는 것이라고 정의되어 있지 않음. 그래서 이렇게도 생각해볼 수 있다.
superclass를 상속받아 subclass에서 사용할때 superclass의 기능을 확장하기 위해서 많이 쓰는데 사실 그것은 좋은 방법이 아니다. 오히려 subclass를 superclass의 구체화라는 개념으로 사용하는 것이 더 좋다.
이 말은 위에서 계속 설명한것처럼 superclass는 추상적인 부분, subclass는 구체적인 부분이해라고 이해할 수 있다.
superclass의 추상적인 로직은 subclass에 의존하지 않고 작성되어야 한다.
세부사항(구체화)에 의존되지 않음.
LSP와 개념적으로 유사한 부분이 있음.
구체화를 위해서 subclass는 superclass의 메소드를 override함. (뷰라이프싸이클 같은것처럼)
계속 같은 말을 하고 있다. 테이블뷰처럼. 커스텀셀이 뭐하든 말든 테이블뷰는 몰라야 된다. 마이뷰컨에서 뷰디드로드에 뭐하든 말든 UIViewContoller가 알게 뭐겠는가? 안그런가? 가끔 superclass가 subclass가 뭐하는지 알고있는 경우?에 코드가 꼬인경우인데(쌍방의존관계라고나할까?)그건 잘못된 설계이다.
"영화에 출연하고 싶으면 연락처를 주고 가세요 (먼저 연락하지 마세요), 필요하면 저희가 연락드리겠습니다."
배우(구체적인쪽)은 영화사(추상적인쪽)에게 연락하지말고 연락처만 주고가라는거다. 영화사는 그저 필요하면 부르면 된다.
UITableView (프로토콜)
구체화된쪽(내가 구현한쪽, VC)에서 delegate, datasource를 = self로 테이블뷰에다가 등록해놓으면(연락처를 남긴다) 추상화 된쪽(테이블뷰)이 필요할 때 콜백해서 지금 테이블뷰 그려라~!하면 이제 내가 구현해놓은 커스텀 delegate, datasource가 실행되면서 셀을 그리는거다.
결국 의존관계를 역전시켜서 통상 추상적인 부분이 구체적인 부분에 참조했는데 그걸 반대로 구체적인 부분이 추상적인 부분을 참조하도록 두 모듈의 의존관계를 단방향
으로 만들어서 만들어놓은 UITableView
를 재활용할 수 있게 만든 것이다.
MyViewController(상속)
프로토콜사용이 아니라 상속하는 경우에도 마찬가지다. 추상적인 쪽(UIViewContoller)는 구체적인 쪽(MyViewController)가 뭐하는지 모르고 알 것도 없다. 대신에 이제 view life-cycle을 관리하는 UIViewContoller가 필요할 때 MyViewController한테 알려주면, MyViewController는 viewDidLoad같은 함수를 override해서 자기가 원하는대로 코드를 구체화 해주는 것이다.
즉 MyViewController는 UIViewContoller를 참조하지만, UIViewContoller는 MyViewController를 참조하지 않는다.
그렇게 함으로써 UIViewContoller는 다른곳에서도 재활용이 가능하다.
두 모듈간의 의존관계를 단방향으로 만들어준다.
추상화된 부분의 코드는 재활용성을 증가시킬 수 있다.
지금까지는 테이블뷰나 뷰컨트롤러를 통해서 이미 정의되어있는 코드를 이야기했기 때문에 DIP를 몰라도 DIP를 사용하고 있었다. 근데 이제는 우리가 만들어야 되니까 더 이야기해본다.
무엇이 추상적인 개념이고 구체적인 개념인지를 정의해야 한다.
추상적인 부분(정책)
프로젝트에서 자주 변경되지 않을 부분을 추상적인 부분으로 정의한다.
해당 로직의 뼈대라고 볼 수 있다.
구체적인 부분(세부)
자주 변경되는 부분을 구체적인 부분으로 지정한다.
자꾸 추가되는 부분도 구체적인 부분으로 지정한다.
의존성의 이행이라 함은 그런것이다. A를 쓰려고했는데 B가딸려오고, B에있는 C까지 줄줄이 딸려오는 상황.
의존성의 이행은 스파게티 코드를 만든다.
적합한 위치에서 DIP를 사용하면 의존성 이행을 막을 수 있다.
이런 경우에는 인터페이스를 제공하는 쪽은 상위레이어(추상화, 정책, 클라이언트)이다.
상위레이어는 인터페이스만으로 코딩을 하며, 하위레이어에서는 그 인터페이스로 구현한 클래스를 구체화 하는식으로 만든다.
그러니 상위레이어는 변화가 없어야 한다. 또 상위레이어는 구체화에 의존되어 있지 않기 때문에 재활용이 쉬워진다. 상위레이어의 내용의 변경 역시 국지적(일정에 지역에 한정된것, 여기서는 코드가 변경되더라도 상위레이어 1개에서만 코드가 변경된다는 의미로 통하는듯)으로 이루어지니 테스트하기가 쉬워진다.
하위레이어(구체화된레이어)는 수시로 변경된다. 기능이 변하면 또 변경되고 그런다. 다만 이 변경이 다른 부분에 영향을 최대한 주지않게 설계되었기 때문에 기존에 작성된 (DIP적용안하는) 방식보다 더 변경에 유리하다.
인터페이스(Swift에서는 Protocol)는 누가 소유(제공)하는가 ?
클래스 A와 B가 있다. 그리고 클래스 A가 B를 가지고 있다면 ?
클래스 A가 인터페이스를 제공하고 클래스 B가 사용하는게 맞을까?
클래스 B가 인터페이스를 제공하고 클래스 A가 사용하는게 맞을까?
정답은 2번이다. 한 클래스(A)가 다른 클래스(B)를 직접적으로 참조한다면(갖고있다면) 반대쪽 클래스(B)가 인터페이스를 제공해서 의존관계를 역전시킨다.
우리는 통상적으로 유틸리티 라이브러리(코코아팟?)이 인터페이스를 처리하는 것에 익숙하다.
DIP를 적용하면 클라이언트(사용하는 쪽)가 추상인터페이스를 사용하고 서버(사용되는 쪽)가 그것에서 파생되어 나온다. (이건 예전에 공부할때 그.. 프로토콜정의해갖고 라이브러리로 제공되는 애들의 추상인터페이스를 갖고와갖고.. 나누게하는 뭐그런건가?애매쓰..)
그렇다면 UITableViewController와 UITableView의 관계에서 누가 클라이언트고 누가 서버일까?
헷갈린다.
정답은 UITableViewController가 서버고 UITableView가 클라이언트이다.
쉽게생각하면 UITableView가 테이블뷰의 기능을 제공하는 서버고 그것을 사용하는 UITableViewController가 클라이언트처럼 보인다. 여기서는 기능의 제공이 문제가 아니라 구체화된 내용을 누가 제공하냐에 따라 달라진다.
구체화된 내용을 제공하는 쪽이 서버이고, 그리고 그것을 사용하는 쪽이 클라이언트이다.
이 부분은 눈에 보이는 게 다가 아니라 이해를 해야 알 수 있는 부분들이다.
인터페이스의 변경은 클라이언트의 요구에 의해서만 발생하기 때문이다.
변경을 유발한 쪽에서 인터페이스를 가지고 있는 것이 유리하다.
어떤 변수로 구체 클래스에 대한 참조를 가지지 말라.
어떤 클래스도 구체 클래스에서 파생하지 말라.
어떤 메소드도 기반 클래스에서 구현된 메소드를 오버라이드 하지 말라.
쉽지않다.. 모든 경우에 이룰을 적용하는것은 어렵다.
그래서 다행히 비휘발적인 클래스의 경우 강하게 DIP를 적용할 필요가 없다.
비휘발적인 클래스에 의존하는 것은 큰 해가 되지 않기 때문이다.
여기서 비휘발성인 클래스란 ?
인터페이스와 로직이 변경이 거의 없는 클래스를 말한다.
SDK에서 제공되는 것들 String, Data같은 자료구조나 클래스들을 말한다. (String의 인터페이스가 바뀔 것을 우려해서 그것을 래핑해서 사용하진 않는다.)
추상화된 클래스는 비휘발적인 클래스에 가까움. 잘변하지 않기 때문에 추상화된 클래스에 의존하는 것이 더 낫다는 거다.
버튼이 램프를 제어
버튼이 램프를 가지고 있음
이 버튼은 램프에만 사용할 수 있음
버튼 클래스가 램프 클래스를 갖고있는 상황인거지. 그래서 버튼을 ON 해주면 버튼 안에있는 램프가 켜지는 상황이다.
버튼이 인터페이스(프로토콜)를 제공하고 그것을 이용해서 램프를 제어
DIP 적용
버튼은 이제 램프가 아니어도 버튼이 있는 다양한 다른 것들에 사용가능
뭔소린지 아직 모르겠음.
버튼, 램프 모두 소유권이 없는 인터페이스 (ex: SwitchableDevice) 사용
버튼과 램프는 인터페이스에만 의존성을 갖게 됨.
뭔소린지 아직 모르겠음.
어느 클래스도 독립적으로 사용될 수 없다. 최악임.
되도록이면 한방향으로 의존관계가 흘러가는 것이 큰 도움이 됨.
이럴때는 DIP가 해결책이 될 수 있다.