본문 바로가기
개발용어

SOLID 원칙 - 의존성 역전 원칙, DIP (Dependency Inversion Principle)

by devscb 2022. 10. 15.
반응형


SOLID 원칙 - 의존성 역전 원칙, DIP (Dependency Inversion Principle)


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


의존성 역전 원칙은 소프트웨어 모듈을 느슨하게 결합하기 위한 구체적인 방법론 입니다.
고수준 모듈에서 저수준 모듈로 설정된 종래의 종속 관계가 역전되어,
저수준의 모듈 구현 세부 사항과 무관하게 고수준 모듈을 구현할 수 있도록 하는 원칙입니다.

의존성 역전 원칙을 잘 지키기 위해서는
"저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존하도록 하며,
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다" 준수하면 됩니다.

좀 더 쉽게 말씀드리면 "자신보다 변하기 쉬운 것에 의존하지 않도록 한다"라고 이해하시면 되겠습니다.


* 고수준 모듈: 어떤 의미 있는 단일 기능을 제공하는 모듈
* 저수준 모듈: 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현


 예시로 이해하기 - 고수준과 저수준의 이해


DIP 에 대해 구체적인 예시를 들어보겠습니다.
다양한 코덱의 비디오 파일을(mp4, wmv, avi) 지원하는 비디오 플레이어를 만들려고 합니다.
비디오 플레이어는 코덱의 종류에 따라 파일을 디코딩할 수 있는 기능이 있어야 합니다.
그러면 아래와 같이 설계할 수 있습니다.



설계에 따라, 구체적인 코드 구현은 아래와 같이 개발할 수 있을것입니다.
(코덱에 따라 디코딩후, 렌더링)


class VideopPlayer{
 void play(byte[] data, string codec){
  byte[] decodedData = null;
  switch(codec){
   case "MP4":
    decodedData = (new CodecMP4()).decode(data);
    break;
   case "WMV":
    decodedData = (new CodecWMV()).decode(data);
    break;
   case "AVI":
    decodedData = (new CodecAVI()).decode(data);
    break;
   default:
    throw new UnsupportedCodecException();
  }
    
  render(data);
 }
 ...
}

class CodecMP4{
 byte[] encode(byte[] data){
  //세부 구현
 }
 byte[] decode(byte[] data){
  //세부 구현
 }
 ...
}

class CodecWMV{
byte[] encode(byte[] data){
  //세부 구현
 }
 byte[] decode(byte[] data){
  //세부 구현
 }
 ...
}

class CodecAVI{
 byte[] encode(byte[] data){
  //세부 구현
 }
 byte[] decode(byte[] data){
  //세부 구현
 }
 ...
}
 


이 상황에서 비디오를 재생한다라는 행위는 VideoPlayer는 고수준의 모듈이 되며,
Codec??? 클래스는 저수준 모듈이라고 볼 수 있습니다.

고수준의 모듈은 좀 더 추상화된 모듈이라고 볼 수 있고,
저수준은 좀 더 구체적인 모듈이라고 생각할 수 있습니다.

또 다른 예시를 들어보자면 아래와 같은 것이 있겠습니다.
- 고수준-컴퓨터, 저수준-스피커, 모니터
- 고수준-자동차, 저수준-현대에서 생산한 자동차, BMW에서 생산한 자동차
- 고수준-핸드폰, 저수준-GPS센서, 카메라



 예시로 이해하기 - 의존성의 이해


앞선 예제에서, VideoPlayer는 아래와 같은 코드로 표현할 수 있다고 보여드렸습니다.


class VideopPlayer{
 void play(byte[] data, string codec){
  byte[] decodedData = null;
  switch(codec){
   case "MP4":
    decodedData = (new CodecMP4()).decode(data);
    break;
   case "WMV":
    decodedData = (new CodecWMV()).decode(data);
    break;
   case "AVI":
    decodedData = (new CodecAVI()).decode(data);
    break;
   default:
    throw new UnsupportedCodecException();
  }
    
  render(decodedData);
 }
 ...
}



VideoPlayer를 구현하기 위해 CodecMP4, CodecWMV, CodecAVI클래스가 "필요"로 합니다.
이처럼 한 클래스가 다른 클래스를 필요로 할떄 의존성이 있다라고 표현하며,
VideoPlayer는 CodecMP4, CodecWMV, CodecAVI클래스에 "의존한다"라고 말할 수 있습니다.

이를 아래 UML에서 살펴보면,
화살표의 출발점에 해당하는 클래스가 도착점에 해당하는 클래스를 의존한다라고 UML을 이해할 수 있습니다.




 예시로 이해하기 - 저수준 모듈이 고수준 모듈에서 정의한 추상 타입


의존성 역전 원칙을 잘 지키기 위해서는
"저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존하도록 하며,
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다"를 준수하면 된다는 말씀을 드렸습니다.

뒷문장에서 "고수준 모듈에서 정의한" 을 빼면
"저수준 모듈이 추상 타입에 의존한다"로 하면 됩니다.

추상타입에 의존한다에서 추상타입은,
상위의 class, abstract class, interface등이 있습니다.
가능한 추상화된 것에 의존하도록 하는게 좋으므로 interface를 써보겠습니다.
또한, DIP 원칙을 적용시킬때에는 일반적으로 interface를 사용합니다.

우선, 저수준의 모듈은 각 코덱이 되겠고, 각 코덱들은 decode와 encode라는 일정한 로직이 있습니다.
따라서 interface를 아래와 같이 선언할 수 있습니다.


interface Codec{
 byte[] encode(byte[] data);
 byte[] decode(byte[] data);
}



추상타입은 이것으로 설계가 되었습니다.
"고수준 모듈에서 정의한"을 만족시키려면 어떻게 하면 될까요?
바로 아래와 같이 고수준 모듈과 같이 패키징하면 됩니다.




 예시로 이해하기 - 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다


고수준 모듈은 VideoPlayer이고, 저수준 모듈은 Codec???였습니다.
그렇다면 저수준 모듈에 의존하지 않으려면 어떻게 해야할까요?
바로 앞단계에서 사용했던 interface를 사용하도록 해주면 됩니다.
이를 위해 디자인패턴의 전략패턴을 적용하여 개선해보겠습니다.
그러면 아래와 같이 굉장히 코드가 간단해 졌습니다.


class VideopPlayer{
 void play(byte[] data, Codec codec){
  byte[] decodedData = codec.decode(data);
  render(decodedData);
 }
 ...
}


잠깐 말씀드리자면, switch와 if문을 없앨 수 있는 방법을 적용하면, 이처럼 가독성이 매우 좋아진답니다.
아무튼, 이렇게 하면 이제 더이상 저수준 모듈(Codec???)에 의존하지 않게 되도록 하였습니다.


 예시로 이해하기 - 의존성의 역전


앞서 정리했던 내용을 UML로 정리해보겠습니다.
그러면 아래와 같이 그려질 수 있습니다.

 


VideoPlayer는 Codec 인터페이스를 의존하고,
Codec???는 Codec 인터페이이스를 구현하여 의존하므로 Codec 인터페이스를 의존합니다.

처음 설계했던 UML을 다시 살펴보면, 아래와 같이 VideoPlayer가 Codec???를 의존했었습니다.



변환한 설계에서는 VideoPlayer와 동일한 패키지에 있는 interface를
Codec???가 의존합니다.
이렇듯, 자연스럽게 설계했을때에는 고수준이 저수준모듈을 의존했었는데,
인터페이스를 도입하여 저수준의 모듈이 고수준의 모듈쪽으로 의존하도록 하는것으로 "역전"된다고 하여
의존성 역전 법칙이라고 한답니다.


 예시로 이해하기 - 의존성의 역전이 필요한 이유?


의존성 역전이 왜 필요한지 살펴보겠습니다.
구체적인 시나리오로, 지원하려는 코덱이 추가되는 시나리오를 생각해봅시다.
mov라는 코덱을 지원해보겠습니다.
그렇다면 기존의 코드는 VideoPlayer클래스를 아래와 같이 수정해야합니다.


class VideopPlayer{
 void play(byte[] data, string codec){
  byte[] decodedData = null;
  switch(codec){
   case "MP4":
    decodedData = (new CodecMP4()).decode(data);
    break;
   case "WMV":
    decodedData = (new CodecWMV()).decode(data);
    break;
   case "AVI":
    decodedData = (new CodecAVI()).decode(data);
    break;
   //추가되는 부분
   case "MOV":
    decodedData = (new CodecMOV()).decode(data);
    break;
   default:
    throw new UnsupportedCodecException();
  }
    
  render(decodedData);
 }
 ...
}



개선된 코드에 비해 가독성이 떨어짐은 물론이고, VideoPlayer가 수정이 되어야만 합니다.
반면에 개선된 코드는? CodecMOV 클래스만 Codec 인터페이스를 구현하도록 개발하면 VideoPlayer가 수정될 일은 없습니다.


이처럼 변경에 대한 용이성을 확립하기 위해,
기능적 요구사항이 바뀌더라도 영향이 되는 모듈을 줄이기 위해 DIP를 도입하는게 좋다라고 말씀드릴 수 있습니다.


 총평


DIP는 추상화를 잘 할 수 있어야 하는 능력이 중요하다고 생각합니다.
추상화를 잘 하려면 각 객체들에 대해 어떤 공통점이 있는지 잘 파악할 수 있어야 하는 것 같습니다.
또한, 어느 단계까지 추상화를 할것인지도 잘 설계해야하고,
어느점이 변경이 일어날지, 어느부분은 변경을 안 일어나게 할지 등을 잘 상상할 수 있는 능력도 필요한것 같습니다.
혼자서 작은 코드만 다룬다면 왜이렇게 여러 파일을 만들어? 라고 생각할 수도 있겠습니다.
하지만, 프로그램은 보통 여러사람이 개발하기에,
외부요구사항이 변경되면 최대한 각 모듈의 변경점이 적도록 해야 개발 비용/시간이 줄어 들기 때문에 익숙해지는게 좋겠습니다.
한 사람이 개발하더라도 과거의 나와 현재의 나, 미래의 나를 생각하면 이것도 여러사람이라고 볼 수 있지 않을까 싶습니다.
막 개발하다보면 DIP를 지키기가 쉽지 않은데,
변경에 대한 시나리오를 잘 가정하고 상상해서 설계를 하면 DIP를 잘 고려할 수 있지 않을까 생각하며 연습을 많이 해봐야겠습니다.



#SOLID,#SOLID원칙,#의존,#의존성,#역전,#원칙,#SOLIDprinciple,#DIP,#의존성역전원칙,#interface,#segregation,#principle

 

https://devscb.com/post/131

 

SOLID Principle - Dependency Inversion Principle (DIP)

SOLID Principle - Dependency Inversion Principle (DIP) What is the dependency inversion principle? (DIP, Dependency Inversion Principle)The dependency inversion principle is a specific methodology for

devscb.com

 

728x90
반응형

댓글