본문 바로가기

5편 객체지향 설계 5원칙 [SOLID] 의존성 역전 원칙

by Recstasy 2020. 4. 3.

SOLID원칙의 마지막은 의존성 역전 원칙이다. 


의존성 역전은 말 그대로 의존성이 역전되면 안 된다는 의미인데, 심각하게 번역체 느낌이 드는 용어해설이다. 개인적으로 '의존성 역전'보다는 '대리인 사용'과 같은 용어가 좀더 와닿지 않을까한다. 


대리인? 다소 생소하게 느껴질 수 있는데, 왜 '대리인 사용'이 의존성 역전 원칙인지 지금부터 하나씩 뜯어보자. 일단, 의존성이라는 의미에 앞서, 우리 주변의 대표적인 의존관계를 생각해보자. 


* 승객 - 운전사

* 학생 - 교사

* 아이 - 부모


'교사도 학생에게 배운다'라는 다소 철학적인 관점은 접어두고, 순수하게 논리적인 시각으로 관계를 설정했다. 위의 사례 외에도 우리 사회는 상당히 많은 의존관계들로 연결되어 움직인다. 그 중에서 "승객-운전기사"의 관계를 살펴보자. 


의존관계

승객-운전기사


일반적인 상황이라면, 승객과 운전기사 중에서 의존하고 있는 쪽은 생각할 것도 없이 승객이다. 기차, 비행기, 버스건 일단 승객의 상태는 운전기사에게 달려있다. 대표적으로 시내버스를 생각해보자. 이를 정리하면 아래와 같다.


승객 : 의존하기

기사 : 의존받기


시내버스에서 승객은 운전기사에게 의존하고 있지만 아이가 부모에게 떼를 쓰듯, 개인적인 요청을 할 수 없다. 만일 승객이 떼를 써서 자신의 집앞까지 태워달라고 운전기사에게 요구하면 어떻게 될까? 혹은 갑자기 여기서 세워달라거나 좀더 빨리 가자고 재촉한다거나 한다면 버스노선은 엉망이 되고, 버스정보 시스템은 무너질 것이다. 만일 시내버스의 노선체계가 무너지면, 심지어 한 도시의 교통시스템까지 멈춰버릴 수 있다. 


즉, 의존하는 객체(승객)가 직접적으로 자신의 요구사항을 중앙 시스템(운전기사)에 요청할 경우, 시스템 전체가 무너질 수 있다. 그래서 시내버스에는 다음과 같은 훌륭한 장치가 있다.



바로 '하차벨'이다. 


승객과 운전기사의 의존관계가 뒤바껴서 운전기사가 승객의 의견에 따라서는 안 된다. 승객은 어떻게보면 인터페이스다. 객체지향 프로그램은 어떠한 경우에도 인터페이스에서 중앙시스템에 직접적으로 접근해서는 안 된다. 인터페이스는 대리인을 통해 자신의 의사를 전달할 뿐이다. 만일 유저 인터페이스가 스스로 특정 기능을 조작할 수 있다면, 위의 버스사례처럼 도시 전체의 교통시스템이 무너져버릴 수 있다.


그래서 '대리인'이 필요하다. 대리인이 필요한 이유는 의존관계가 뒤바뀌지 않기 위해서다.



의존관계

대리인

세상에는 많은 대리인 직업이 있다. 

* 세금관리 : 세무사

* 법적문제 : 변호사

* 교육 : 교육전문가

* 경영 : 전문경영인

* 부동산 : 공인중개사

* 자동차 : 딜러

* 각종 중개인 


대리인을 두는 이유는 효율성을 높이기 위해서다. 일단 특정 분야의 일을 전문적으로 빨리 처리할 수 있고, 책임을 분산할 수 있다. 가령, 부동산 거래를 직접 할 수도 있지만 공인중개사에게 일임했을 때는 뭔가 문제가 발생했을 때 공인중개사가 일정부분 책임을 진다. 


그래서 각 분야를 전문 대리인에게 맡기고 본인은 자신의 분야에 집중하면 시간을 절약할 수 있고, 혼자서 모든 것을 처리할 때보다 결과도 좋다. 만일 대리인을 사용하지 않고 법, 회계, 경영, 부동산, 자녀교육 등...을 자신이 처리하다가 어느 한 부분에서 일이 터지면 나머지 일처리도 늦어지면서 모든 시스템이 무너질 수 있다. 


비용이 문제일 뿐, 대리인을 두면, 안전하며 또 일의 효율성이 높아진다.


객체지향 설계도 대리인이 필요하다. 고객과 접하는 인터페이스 클래스들은 각자 대리인을 두고 있으며, 대리인을 통해 결과를 얻는다. 그 과정은 모르며, 어쨌든 대리인 클래스들이 알아서 결과를 가져온다. 물론, 대리인 클래스를 사용하지 않고, 자신(인터페이스 클래스)이 직접 중앙 시스템에 접근할 수도 있지만 실수를 했을 때 시스템 전체가 타격받는 상황을 피할 수 없다. 만일 대리인 클래스가 실수를 했다면 해당 대리인 클래스를 해고하고 다른 대리인으로 교체해버리면 그만이지만 자신이 일을 저질러버리면 수습할 수 있는 방법이 없다.


따라서 객체지향 설계는 반드시 인터페이스 클래스와 중앙시스템 사이에 대리인 클래스가 존재해야 한다. 이것이 바로 의존성 역전 원칙이다.



의존성 역전

의존성 역전 위반하기

의존성 역전관계를 일부로 위반해보자. 

let Relationship = Object.freeze({

   parent : 0,

   child : 1,

   sibling : 2

});


class Parent{

    constructor(name){

       this.name = name;

    }

}


class Relationships{

      constructor(){

           this.data = [];

      }


      addParentAndChild(parent, child){

           this.data.push({

               from : parent,

               type : Relationship.parent,

               to : child

           });

           this.data.push({

               from : child,

               type : Relationship.child,

               to : parent

           });

      }

}


Relationship객체는 타입을 지정하고 있다. "부모:0, 자식:1, 형제:2"로 지정했으며, Parent클래스는 사용자의 이름을 저장하는 기능을 갖는다. 그리고 Relationships클래스는 parent와 child클래스를 받아서 data배열에 저장한다. addParentAndChild 메서드는 this.data라는 메인 DB에 직접적으로 접근할 수 있다. 그러므로 인터페이스에서 addParentAndChild메서드를 컨트롤한다면 의존관계가 역전된다


Research클래스(인터페이스)를 생성하여 의존관계가 역전되는 상황을 만들어보자. 


class Research{

        constructor(relationships){

           

            let relations = relationships.data;


           for(let rel of relations.filter(r=>{

               r.from.name === '철수' && r.type === Relationship.parent

            })){

              console.log(`철수의 자녀 이름은 ${rel.to.name}`);

           }

        }

}


let parent = new Person('철수');

let child1 = new Person('영희');

let child2 = new Person('민수');


let rels = new Relationships();

rels.addParentAndChild(parent,child1);

rels.addParentAndChild(parent,child2);


new Research(rels);


Research클래스는 철수가 부모이며, 자식1(영희),2(민수)로 등록되어 있는 Relationships클래스를 인자로 받는다. 그리고 for구문을 통해 부모이면서 동시에 이름이 철수라면 Relationships의 data값에서 배열로 뽑아낸다.(filter내장메서드)


위의 코드를 실행하면, 결과는 아무 이상없이 나온다. 


문제는 인터페이스 클래스인 Research가 메인 시스템의 데이터값을 요리하고 있다는 데에 있다. Reseach 인터페이스 클래스는 중간에 대리인을 내세우지 않고 자신이 직접 Relationships 데이터를 편집하고 있는데, 이렇게되면 메인 시스템의 데이터가 외부로 새어나갈 위험이 있고, 실수로 데이터값을 잘못 컨트롤 했을 때, Relationships클래스의 data가 전부 잘못될 수 있다. 따라서 대리인 클래스가 필요하다.


의존성 역전

대리인 내세우기

Relationships클래스의 대리인을 생성해보자. 


class RelationshipBrowser{

     constructor(){

         if(this.constructor.name === 'RelationshipBrowser'){

               throw new Error('RelationshipBrowser is abstract');

         }

          findAllchildrenOf(name){ }

     }

}

 

Research클래스가 직접적으로 Relationships클래스의 데이터를 휘젓는 상황을 방지하기 위해서 RelationshipBrowser클래스 생성한다. Relationship클래스는 데이터를 저장하는 원래의 기능만 갖는데 반해 RelationshipBrowser클래스는 대리인답게 findAllchildrenOf()라는 전문 기능을 갖고 있다. 따라서 Relationship클래스는 해당 기능을 차용하기 위해 아래와 같이 상속받는다.


class Relationships extends RelationshipBrowser{

      constructor(){

           super();

           this.data = [];

      }


      addParentAndChild(parent, child){

           this.data.push({

               from : parent,

               type : Relationship.parent,

               to : child

           });

           this.data.push({

               from : child,

               type : Relationship.child,

               to : parent

           });

      }


     findAllChildrenOf(name){

          return this.data.filter( r => 

             r.from.name === name &&

             r.type === Relationship.parent 

       ).map(r => r.to) 

     }

}


이제 Research클래스에서 직접 Relationships클래스의 데이터에 접근하지 않고, RelationshipBrowser의 기능을 상속받은 Relationships의 기능을 다음과 같이 사용할 수 있다


class Research{

      constructor(browser){

          for(let p of browser.findAllChildrenOf('철수')){

              console.log(`철수의 자녀 이름은 ${p.name}`)

          }

      }

}


Research클래스는 Relationships클래스를 인자로 받지만 findAllChildrenOf()메서드를 통해 RelationshipBrowser클래스에서 상속받은 메서드를 통해 Relationships클래스에 접근하고 있다. 따라서 Research클래스는 Relationships클래스의 값을 가져와서 조작하는 방식을 하지 않고 있으며, 이는 간접적인 접근법이다.


(...중략)

class Research{

      constructor(browser){

          for(let p of browser.findAllChildrenOf('철수')){

              console.log(`철수의 자녀 이름은 ${p.name}`)

          }

      }

}


let parent = new Person('철수');

let child1 = new Person('영희');

let child2 = new Person('민수');


let rels = new Relationships();

rels.addParentAndChild(parent,child1);

rels.addParentAndChild(parent,child2);


new Research(rels);



결론

의존성 역전 원칙은 인터페이스 분리 원칙과 떨어져있는 개념이 아니다. 인터페이스를 분리만 했을 뿐, 메인 시스템과 강하게 연결되어 있으면 시스템이 붕괴되는 건 시간문제다. 특히, 의존성 역전 원칙을 위배하기 시작하면 버전 업데이트가 진행될 때마다 코드가 꼬여간다. 좋은 설계는 유연하고, 느슨하다. 특정 클래스끼리 강한 결합(연관)이 이뤄졌다면 즉시 대리인 클래스를 생성하여, 유연한 설계로 돌아가자. 



댓글

최신글 전체

이미지
제목
글쓴이
등록일