1. DIP (Dependency-Inversion Principle)

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

의존을 역전한다는 의미는

아버지가 돈을 벌어와서 집안이 꾸려나가다가

자식이 돈을 점점 성장해서 가장의 역할을 하게 되면

자식이 벌어온 돈으로 집안이 꾸려나가질때

자식이 아버지에게 의존을 하다가

아버지가 자식에게 의존을 하게 되면

의존하는 관계가 역전이 되었다고 할 수 있다.

예를 들다보니 좀 서글퍼지는데;; 훌쩍;;

이처럼 A가 B에게 의존하던 관계를 A가 주도권을 가지고 B가 A를 의존하는 구조로 바꾸는것을

"의존관계를 역전시킨다" 라고 한다.

이는 IOC 라고도 하는데 IOC 는 Inversion Of Control 의 약자이다.

즉, 주도권의 역전이라고 해석할 수 있다.

DIP를 적용하여 주도권을 역전시킴으로써

한곳에 몰려있던 관계를 각각 클래스에게 전가함으로써

재사용성을 높이고 결합력을 약하게 하는 이른바

High Cohesion, Loose Coupling (높은 응집도, 느슨한 결합도) 의 원칙을 지키게 되는것이다.

결합력이라는 것은

클래스와 클래스간의 연결고리가 많을 수록

두 클래스를 서로 떨어뜨리기 힘들어진다.

결과적으로 재사용성이 낮아지고

여러개의 클래스가 서로 결합이 복잡할 수록

하나를 수정했을때 여러군데 오류가 날 수 있음을 의미한다.

즉 수정을 다 했다고 하더라도 어딘가 예상치 못한 오류가 남아있을 수 있음을 의미한다.

이런 현상을 Shotgun Surgery (산탄총 수술) 이라고 한다.

마치 산탄총을 쏜 것처럼

한번의 수정으로 여러군데를 수정해야한다는 것을 의미한다.

이럴때 Coupling(결합력) 을 낮추기 위해서

B가 A에게 의존하던 부분을 B가 스스로 처리하게 함으로써

A가 보다 가벼워지고 둘 사이의 결합도는 낮아지게 된다.



자 우리가 만들어볼 예제는

1초 단위로 특정 동작을 하기 위해서

Counter 라는 클래스를 만들어 볼것이다.

자 아래와 그 예를 한번 살펴보자.

아래 IEventDispathcer 를 구현한 부분은 넘어가도 된다.

package counter1
{
     import flash.events.Event;
     import flash.events.IEventDispatcher;
     import flash.events.TimerEvent;
     import flash.utils.Timer;
    
     public class Counter implements IEventDispatcher
     {
          public function get count(): int
          {
               return this._count;
          }
         
          public function Counter()
          {
              
          }
         
          private var timer: Timer;
          public function start(): void
          {
               if( this.timer != null )
                    this.timer.stop();
              
               this.timer = new Timer( 1000 );
               this.timer.addEventListener( TimerEvent.TIMER, onTimer );
               this.timer.start();
          }
         
          private var _count: int;
          private function onTimer( e: TimerEvent ): void
          {
               this._count = this.timer.currentCount;
              
               this.dispatchEvent( new Event( "count" ) );
          }
         
          public function stop(): void
          {
               if( this.timer == null)
                    return;
              
               this.timer.stop();
               this._count = 0;
          }
         
          /**
          *
          * Implements IEventDispatcher
          *
          * */

         
          import flash.events.EventDispatcher;
          import flash.events.Event;
         
          private var dispatcher:EventDispatcher = new EventDispatcher();
         
          public function hasEventListener(type:String):Boolean
          {
          return this.dispatcher.hasEventListener.apply(null, arguments);
          }
         
          public function willTrigger(type:String):Boolean
          {
          return this.dispatcher.willTrigger.apply(null, arguments);
          }
         
          public function addEventListener(type:String, listener:Function, useCapture:Boolean=false, priority:int=0.0, useWeakReference:Boolean=false):void
          {
          this.dispatcher.addEventListener.apply(null, arguments);
          }
         
          public function removeEventListener(type:String, listener:Function, useCapture:Boolean=false):void
          {
          this.dispatcher.removeEventListener.apply(null, arguments);
          }
         
          public function dispatchEvent(event:Event):Boolean
          {
          return this.dispatcher.dispatchEvent.apply(null, arguments);
          }
     }
}

자 이 클래스의 인터페이스는 다음과 같다.

 - start() : 카운터를 시작할 메소드.
 - stop() : 카운터를 중지시키는 메소드.
 - count : 현재 카운트를 참조할 수 있는 메소드.

자 사용하는 방법은 다음과 같다.

package counter1
{
     import flash.events.Event;
    
     public class Main
     {
          private var counter: Counter;
         
          public function Main()
          {
               this.counter = new Counter();
               this.counter.addEventListener( "count", onCounter );
               this.counter.start();
          }
         
          private function onCounter( e: Event ): void
          {
               this.doAction( this.counter.count );
          }
         
          private function doAction( count: int ): void
          {
               // do action.
          }
     }
}

카운터를 만들고 "count" 라는 이벤트가 발생할 때마다

Counter.count 라는 속성을 참조해서 원하는 카운트에 원하는 동작을 처리하면 된다.

이 두 클래스의 흐름을 시퀀스 다이어그램을 이용해서 표현하면 아래와 같다.

사용자 삽입 이미지

보다시피 Main 클래스가 Counter 에 이벤트가 일어날때마다 count 속성을 참조한다.

Counter 는 매 이벤트가 일어날때마다

현재 카운트를 제공만하고 실제 필요한지 안필요한지는

Main 에게 의존해야한다.

카운팅을 담당하는 클래스는 Counter 임에도 불구하고 필요한 카운트만 주는게 아닌

매 카운트마다 Main 에게 그 역할을 담당하게 하는 구조로 되어 있는것이다.

즉 Counter 는 자신이 카운팅을 담당하는 클래스임에도 불구하고

Main 에게 의존을 너무 많이한 나머지

자신은 정작 숫자를 세는 일밖에 하지 않게 된것이다.

이런 구조를 어떻게 변경해서

Main 에게 쏠려있는 의존도를 Counter 에 위임할 수 있을까?

해답은 우리가 이벤트를 주고 받을때

이벤트를 등록시켜놓고 그 이벤트가 발생했을때 자동으로 리스너가 작동하는 방식으로 하면 좋을거 같다.

그럼 내가 필요한 카운트에 특정 메소드를 작동시킬 수 있도록

Counter.addListener( func: Listener, dispatchCount: int ): void 라는 메소드를 추가해서

해당 카운트가 발생했을때 Counter 가 스스로 동작하게 하면 멋질거 같다.

자 그럼 만들어보자.

package counter2
{
     import flash.events.TimerEvent;
     import flash.utils.Timer;
    
     public class Counter
     {
          private var timer: Timer;
         
          public function Counter()
          {
               this.arrListeners = [];
          }
         
          private var arrListeners: Array;
          public function addListener( listener: Function, dispatchCount: int ): void
          {
               // Object 형식으로 arrListener 에 추가한다.
               this.arrListeners.push( { "count": dispatchCount, "listener": listener } );
          }
         
          public function start(): void
          {
               // 이미 timer 가 있다면 중지.
               if( this.timer != null)
                    this.timer.stop();
              
               this.timer = new Timer( 1000 );
               this.timer.addEventListener( TimerEvent.TIMER, onTimer );
               this.timer.start();
          }
         
          public function stop(): void
          {
               this.timer.stop();
          }
         
          private function onTimer( e: TimerEvent ): void
          {
               // count 로 listener 를 찾는 메소드에 위임하여 메소드를 받아냄.
               var func: Function = this.findListener( this.timer.currentCount );
              
               // listener 가 존재할때만 실행.
               if( func != null )
                    func();
          }
         
          /**
          * count 로 listener 를 찾는 메소드.
          */

          private function findListener( count: int ): Function
          {
               var func: Function;
              
               var length: int = this.arrListeners.length;
               while( length-- )
               {
                    // "count" 값이 count 와 같을때 그 메소드를 func 에 저장하고 while 문 빠져나오기.
                    if( this.arrListeners[ length ][ "count" ] == count )
                    {
                         func = this.arrListeners[ length ][ "listener" ];
                         break;
                    }
               }
              
               return func;
          }
     }
}

자 클래스가 좀 복잡해졌는데

인터페이스를 살펴보자.

 - addListener( Function, int ) : 해당 카운트와 그때 실행할 리스너를 등록해준다.
 - start() : 카운터를 작동시킨다.
 - stop() : 카운터를 중지시킨다.

기존의 count 가 없어지고 addListener 로 필요한 카운트를 판단하는 역할이

Main 이 아니라 Counter 가 직접 하도록 위임되었다.

사용하는 방법은 아래와 같다.

package counter2
{
     public class Main
     {
          private var counter: Counter;
         
          public function Main()
          {
               this.counter = new Counter();
               this.counter.addListener( this.listener5, 5 );
               this.counter.addListener( this.listener10, 10 );
               this.counter.start();
          }
         
          private function listener5(): void
          {
               trace( "count 5" );
          }
         
          private function listener10():void
          {
               trace( "count 10" );
          }
     }
}

해당 카운트때 실행될 메소드들을 만들어놓고

동작할때 Counter 가 알아서 실행해준다.

위 코드를 실행해보면 5초 후에 "count 5" 라는 메세지가

10초 후에 "count 10" 이라는 메세지가 찍힌다.

얼핏봐도 Main 에서 하는일이 크게 줄어보인다.

그럼 시퀀스 다이어그램을 보고 얼마나 줄어들었는지 확인해보자.

사용자 삽입 이미지

한눈에 봐도 Process 부분이 모두 Counter 에게 위임된것을 볼 수 있다.

addListener 는 생성단계와 같은 수준으로 봐도 무방하므로

실제 동작을 하는 부분은 모두 Counter 에게로 위임되었다.

반드시 필요한 생성단계에 listener 만 등록받고

그외에는 전혀 Counter 가 Main 에게 의존할 필요가 없게 되었다.

이렇게 Counter 가 본연의 역할에 충실하도록 Main 에게 쏠려있던 일들을 가져옴으로써

클래스의 본분에 충실하게 된것이다.

다른 말로 하면 DIP 원칙에 따라 SRP 를 지키게 되었다고 표현할 수 있다.

DIP 를 지키기 위한 노력은 SRP 를 위한 노력이라고도 볼 수 있다.

이처럼 A 와 B 간의 의존관계가 역전됨으로써 OOP의 기본 구조인

재사용성은 극대화되는것이다.

외부에서 필요하지 않았던 count 속성도 없이지고 내부에서만 판단하게 되므로

변화에 Closed 되어 있기 때문에 OCP 도 잘 적용되어 있고

자신의 역할에 충실하기 때문에 SRP 도 잘 적용되어 있다고 볼 수 있다.

이는 모두 처음의 구조로부터 DIP 원칙을 적용함으로써 클래스의 의존관계를 역전시켰기 때문에 가능하게 된 일이다.


DIP 를 설명함에 있어서 다른 언어에서는 헐리우드의 원칙이라고 하는

"Don't call me, I'll call you." (날 부르지마, 내가 알아서 부를께.)

라는 문구를 인용한다고 한다. GoF 에서 인용한 글인데

시간나면 GoF 책에서 인용한 글을 참고하기 바란다.



2. LSP (Liskov Substitution Principle)

리스코프의 치환 법칙.

1988년 Babara Liskov 는 자신의 논문에서 "자식 클래스들은 부모 클래스의 인터페이스를 통해서 사용 가능해야 하고 사용자는 그 차이를 몰라야 한다" 라고 주장했다.

이는 2000년에 재해석되긴 했지만

그 의미는 간단하게 말하자면 상위 클래스가 사용되는 곳에는 하위 클래스가 사용될 수 있어야 한다. 라는 의미이다.

간단한 한두개의 클래스로는 설명하기 어렵지만

Flash 의 내장 클래스에 잘 적용된 예가 있다.

바로 addChild 의 파라미터가 Sprite 나 MovieClip 이 아니라

DisplayObject 인 점.

바로 이부분이 LSP 를 잘 보여주고 있다.

화면에 보여질 수 있는 최상위 클래스가 바로 DisplayObject 이기 때문에

DisplayObject 가 파라미터로 쓰이는 addChild 메소드에는

DisplayObject 의 하위클래스는 모두 사용될 수 있게 되어 있다.

Sprite, Bitmap, Video, TextField 모두 addChild 할 수 있다는 의미다.

바꿔 이야기 하자면

자신의 하위 클래스들이 자기가 사용되는곳에 문제없이 사용될 수 있도록

클래스의 역할을 잘 정의해야된다는 이야기다.

예를 들어 A.com 쇼핑몰에서 구매액의 일정 비율을 캐쉬백해주는 서비스가 있어서

아래와 같은 구조로 UserAccount 클래스를 만든다고 해보자.

package login1
{
     public class UserAccount
     {
          private var id: String;
          private var name: String;
         
          public function UserAccount()
          {
              
          }
         
          protected const CASH_BACK_RATE: Number = 0.02;
          protected var _point: Number;
          public function addCashBack( amount: int ): void
          {
               // 적립포인트 += 사용한 돈 * 캐쉬백퍼센트.
               this._point += amount*CASH_BACK_RATE;
          }
         
          public function get point(): Number
          {
               return this._point;
          }
     }
}

유저가 구매를 하면

구매한 금액*포인트비율(0.02) 를 적용해서

포인트를 쌓는다.

자 그러면 이제 모든 결제 프로세스에는

UserAccount 클래스를 기준으로 제작이 된다.

이후에 만들어지는 회원정보는 UserAccount 클래스를 상속하여 제작하면 된다.

자 이번엔 우수 고객들을 VIP 고객으로 대우해서

VIP Point 를 추가적으로 서비스하게 되었다.

그래서 아래와 같은 VIPAccount 클래스가 개발되었다.

package login1
{
     public class VIPAccount extends UserAccount
     {
          public function VIPAccount()
          {
              
          }
         
          protected const VIP_CASH_BACK_RATE: Number = 0.01;
          public function addVIPCashBack( amount: int ): void
          {
               this._point += amount*VIP_CASH_BACK_RATE;
          }
     }
}

VIP 회원을 관리할때는 앞으로 추가로 관리를 해줘야 할것이다.

package login1
{
     public class AccountManager
     {
          private var account: UserAccount;
         
          public function payment( amount: int ): void
          {
               this.account.addCashBack( amount );
              
               if( this.account is VIPAccount )
                    VIPAccount( this.account ).addVIPCashBack( amount );
          }
     }
}

위처럼 account 가 VIPAccount 일 경우 추가적인 보너스를 주는 형식이다.

자 그런데 서비스를 실시하려고 테스트를 하자

엄청난 오류들이 발생했다.

이유는 기존의 모든 결제 시스템이 UserAccount 로 구현되어 있기 때문에

VIP 서비스를 도입하려면 결제시스템을 통채로 수정해야될 판이 된것이다.

이문제를 어떻게 하면 될것인가?

이는 LSP 원칙이 깨지면서 발생된 오류이다.

상위 클래스가 사용되는곳에 그 확장클래스인 하위클래스가

사용되지 못하게 된것이다.

그렇다면 LSP 의 원칙에 따르려면 어떻게 수정되어야 할까?

캐쉬백을 해주는 부분을 확장성 있도록

기본 포인트를 지급한후 추가적인 포인트 지급이 있다는 가정하에 메소드 하나를 지원해주면 될것 같다.

package login2
{
     public class UserAccount
     {
          private var id: String;
          private var name: String;
         
          public function UserAccount()
          {
              
          }
         
          protected const CASH_BACK_RATE: Number = 0.02;
          protected var _point: Number;
         
          public function addCashBack( amount: int ): void
          {
               this._point += amount*CASH_BACK_RATE;
              
               this.addEtcCashBack( amount );
          }
         
          protected function addEtcCashBack( amount: int ): void
          {
               // not any more.
          }
     }
}

위 처럼 포인트를 적립한 후 추가적인 적립을 예상하여 추가적인 메소드를

protected 접근자로 외부에는 숨긴채 지원하도록 하였다.

그럼 VIPAccount 는 어떻게 변하게 될까?

package login2
{
     public class VIPAccount extends UserAccount
     {
          public function VIPAccount()
          {
              
          }
         
          protected const VIP_CASH_BACK_RATE: Number = 0.01;
          override protected function addEtcCashBack( amount: int ): void
          {
               this._point += amount*VIP_CASH_BACK_RATE;
          }
     }
}

위 처럼 addEtcCashBack 메소드를 override 하여

추가적인 적립을 구현하면 된다.

몇개가 늘어나건 얼마든지 확장할 수 있는 구조가 된것이다.

이렇게 함으로써 UserAccount 를 사용하던 기존의 결제 프로세스에도

전혀 영향을 미치지 않게 되었다.

비로소 LSP 의 원칙을 유지하게 된것이다.

이처럼 LSP 의 원칙은 어플리케이션의 규모가 커지면 커질수록

어떤 부분을 상위클래스에 포함시키고

어떤 부분을 따로 클래스를 빼야 하는지 어려워지게 된다.

이때 기준이 될만한 사항이 SRP 에서 미리 언급했던

4가지 사항이 마찬가지로 도움이 된다.

 - 두 클래스가 같은 일을 한다면 하나의 클래스로 표현하고 구분하는 속성을 추가하여 판단한다.
 - 똑같은 메소드를 제공해야하지만 알고리즘이 다르다면 공통 인터페이스를 두고 이를 구현한다.
 - 공통된 메소드가 없다면 서로 다른 클래스로 만든다.
 - 하나의 클래스가 다른 클래스의 기능에 추가적인 기능을 제공한다면 상속으로 구현한다.

여기서 방금 UserAccount 와 VIPAccount 에 LSP 를 적용하면서 사용된 예는

마지막 항목이 될것이다.

LSP 는 위의 예제처럼 매우 간단한 문제일수도 있지만

실제 개발환경에서는 안타깝게도 가장 난해하고

높은 추상화를 요구하는 원칙이기 때문에

위와 같이 깔끔하게 떨어지는 LSP 원칙은 비교적 드물다.

하지만 LSP 의 원칙은 가장 단순하면서도 의아할정도로 간단한 원리지만

실제 개발 현장에서는 가장 지키기 어려운 원칙이기도 하다.



3. AOP

Flash 에는 해당하지 않지만

요즘 (요즘이라기에는 10년이나 된) 에는 이런 OOP의 많은 방법론과

연구에도 불구하고 현실에서는 많은 개발자들이 이런 OOP의 장점을 훌륭히 살리지 못하고 있다는 점을

자각하고 있다.

규모가 커지면서 점점 클래스는 커져만 가고

리팩토링을 할 수록 예상하지 못한 요구사항이 추가되면서 재사용성과 상속을 이용한 확장은

현실적으로 점점 지키기 어렵다는 것을 현장에서나 이론적으로나 나타나고 있는 실정이다.

이런 문제점을 보완하기 위해서

AOP 라는 방법론이 화두가 되고 있다.

컴파일러는 따로 쓴다는 단점 때문에

많이 퍼지진 않았지만

분명 기존의 OOP의 문제점을 보완하고 더 나은 개발방법을 가능케 한다는점은 분명하다.

AOP 란 Aspect-Oriented Programming 이라고 해서

관점 지향 프로그래밍이라고 한다.

즉, 개발을 객체를 기준으로 하기 보다는

관점 즉, 서비스의 입장에서 그 기준을 나눈다는 이야기이다.

AOP 를 설명할때 주로 쓰이는 부분이 인터넷 뱅킹 시스템인데

사용자 삽입 이미지
위 그림 같이

보통 OOP 구조로는 계좌이체 클래스, 입출금 클래스, 이자계산 클래스

이렇게 나누지만

AOP 에서는 횡단관심으로 구조를 바라보기 때문에

로깅, 보안, 트랜잭션, 그리고 각 역할로 구성되도록 한다.

만약 계좌이체 클래스를 만든다고 하면

class 계좌이체
{
private 보안;
private 로깅;
private 트랜잭션;

public function 계좌이체()
{
보안.보안체크( this );
로깅.로깅체크( this );
트랜잭션.트랜슬레이트( this.amount );

보안.보안끝();
}
}

위처럼 될것이다.

그런데 AOP 관점에서 만든다고 하면 (단지 예일뿐이다.)

class 계좌이체
{
[시작(보안,로깅)]

private 트랜잭션;

public function 계좌이체()
{
트랜잭션.트랜슬레이트( this.amount );
}

[끝남]
}

처럼 된다.

위에 [] 로 묶여있는 부분이 바로 기존과 다른 컴파일러를 써야되는 부분이다.

컴파일러에서 해당 클래스에서 어떤어떤 모듈이 적용될지 판단한다.

물론 저처럼 간단하지도 않고 건성건성이지도 않지만 비슷하다.

이 같은 AOP 의 장점은

예를 들어 A 은행에서 인터넷뱅킹을 훌륭히 개발을 완료했다.

그래서 B 은행에서 새로운 프로젝트를 맡게 되었는데

A 은행에서 사용된 보안 정책만 비슷하기 때문에

보안 모듈만 사용하고 싶은데

기존의 OOP 방식으로는 보안 모듈만 따로 띌수가 없다.

위에서는 보안.보안체크() 라고 하지만

보안에 들어가는 프로세스는 실제로는 굉장히 복잡하다.

그래서 따로 띌수가 없다.

하지만 AOP 에서는 횡적으로 바라보기 때문에

보안 부분만 따로 띄어낼수가 있다.

이같은 장점은 어플리케이션이 커지면 커질수록 위력을 발휘하며

실제 작동되는 소스코드의 양이 대폭으로 줄어들기 때문에

가독성이 올라감은 물론 보다 높은 추상화를 추구할 수 있다.

보다 높은 추상화가 가능하다는 것은 보다 더 체계적인 아키텍쳐가 가능하다는 것이고

보다 변화에 대응할 수 있다는 많은 장점들을 가져다주며

궁극적으로 기존의 OOP의 한계를 극복할 수 있다는 점이다.

OOP 를 대체하는 방법론이 아니라 AOP 는 OOP의 한계를 극복하도록 도와주는

방법론이라는 점이 중요하다.

때문에 앞으로 Flash 에서도 많은 수준높은 아키텍쳐들이 구현될테고

이러한 방법론이 있다는 것을 기억하고 있으면

언젠가 AOP 가 Flash 에도 적용될 수 있을때 우리도 변화에 대처할 수 있을것이다.

분명한것은 AspectJ 를 시작으로 Java, Ruby, PHP 에서 AOP 는 대단히 환영받는 방법론이며

Flash 가 이제 언어로써의 자격을 갖음으로써

우리에게 전혀 동떨어진 이야기는 아니라는 뜻이다.



가만히 있기 위해서는 힘껏 달려야한다.

'Architecture > OOP 5대 원칙' 카테고리의 다른 글

[AS3.0 강좌] OOP의 5대 원칙 Part.2  (4) 2007.12.11
[AS3.0 강좌] OOP의 5대 원칙 Part.1  (27) 2007.12.10

+ Recent posts