OOP : Object-Oriented Programming (객체 지향 프로그래밍)

우리는 Part.1 에서 OCP (Open-Closed Principle) 에 대해서 알아보았다.

OOP에 있어서 본질인 "확장"에 대한 중요한 부분이다.

확장을 자유롭게 하되 기본적인 본질에 대해서는 신뢰를 줄 수 있는 그런 아키텍쳐를 가능하게 한다.

그렇다면 또다른 OOP의 본질은 무엇일까?

바로 "재사용성"이다.

유용한 기능이나 구성을 한번쓰고 마는것이 아니라

한번 만들어놓은것을 다음번에도, 그 다음번에도 사용할 수 있도록 하는것이다.

그럴려면 한번 만들때 잘 만들어야 된다는건 굳이 말 안해도 알 것이다.

1회용 막코딩보다 개발시간은 약간 더 길어지겠지만

개발자로써는 당연히 지향해야하는 자세일것이다.



1. SRP (Single-Responsibility Principle)

재사용이라는 것은 이번에 사용하고 훗날 다른 프로젝트에서도 쓰인다는 뜻일것이다.

그렇다면 한번 만들때 제대로 만들어서 나중에 손댈필요가 없어야한다.

그렇게 하기 위해서는 어떤 원칙을 기준삼아서 생각할 수 있을까?

일단 상식적으로 생각해보면

 - 특정 프로젝트에 연관되는 기능이나 네이밍이 있으면 안된다.
 - 특정 프로젝트에서만 쓰이는 기능은 다른 프로젝트에서는 쓰이지 않기 때문에 가능하면 공통되는 기능으로 바꾸거나 분리를 해내는게 좋을거 같다.
 - 오랜 시간이 지난 후에 보더라도 이해할 수 있으면 좋을거 같다.

예를 들어 교육청 프로젝트를 진행할때 만들었던 배열의 평균을 구하는 클래스의 이름을 EduGovSum.averageArr( arr: Array ): Number 라고 만들어 놓는다면

다음번에 국방부 프로젝트를 한다고 했을때

왠 EduGov ??

어째 뭔가 아마추어틱하고 찜찜하기 이를데 없을것이다.

그런데 만약 ArrayUtil.average( arr: Array ): Number 라고 만들어 놓았다면

더 그럴듯해보이고 뭔가 체계적인거 같다.

이처럼 특정 프로젝트를 진행하다가 필요한 기능이 생겨서 만들게 될 시에는

즉흥적으로 만들기 보다는

지금 내가 원하는 기능이 앞으로도 계속 사용될 만한 가치가 있는것일까?

내가 원하는 이 기능을 잘 표현해줄 수 있는 단어는 뭘까?

라는 생각을 곰곰히 해본후에 만드는것이 좋다.



네이밍이 그렇다면 기능도 마찬가지지 않을까?

이번 프로젝트에만 쓰이는 기능을 넣어버리거나

당장 필요없다고 해서 하나의 기능에 다른 기능까지 넣어버린다거나 해버리면

다음에 재사용하기 불편해질 것이다.

자 다음과 같은 상황을 예로 들어보자.

우리는 서버측과 통신을 할때

php나 jsp, asp, xml 을 주로 사용한다.

이때 웹페이지가 뿌려주는 값을 Flash 가 받아와서

우리는 그것을 가공하여 쓰는데

이때 뿌려주는 값은 3가지로 구분된다.

 - Text : 단순 문자열.
 - Variables : 변수=값&변수=값
 - XML : XML Document

자 그럼 가장 많이 쓰이는 방식이 XML 이기 때문에

허구헌날 URLLoader 로 로드해서쓰지 말고

간단한 XML로더를 한번 만들어 보자.

package loader1
{
     import flash.events.Event;
     import flash.events.EventDispatcher;
     import flash.events.IOErrorEvent;
     import flash.net.URLLoader;
     import flash.net.URLRequest;
    
     [Event ( name="complete", type="flash.events.Event" ) ]
     [Event ( name="ioError", type="flash.events.IOErrorEvent" ) ]
    
     public class XMLLoader extends EventDispatcher
     {
          public function get url(): String
          {
               return this.loadURL;
          }
         
          public function get xml(): XML
          {
               return this._xml;
          }
         
          public function XMLLoader()
          {
              
          }
         
          private var loader: URLLoader;
          private var loadURL: String;
          public function load( url: String ): void
          {
               this.loadURL = url;
              
               this.loader = new URLLoader();
               this.loader.addEventListener( Event.COMPLETE, loadComplete );
               this.loader.addEventListener( IOErrorEvent.IO_ERROR, loadFail );
               this.loader.addEventListener( SecurityErrorEvent.SECURITY_ERROR, loadFail );
               this.loader.load( new URLRequest( this.loadURL ) );
          }
         
          private var _xml: XML;
          private function loadComplete( e: Event ): void
          {
               this._xml = XML( this.loader.data );
              
               this.dispatchEvent( new Event( Event.COMPLETE ) );
          }
         
          private function loadFail( e: Event ): void
          {
               trace( "Error occured by : " + e.toString() );
              
               this.dispatchEvent( new IOErrorEvent( IOErrorEvent.IO_ERROR ) );
          }
     }
}

위 클래스는 아래와 같이 사용할 수 있다.

package loader1
{
     import flash.display.Sprite;
     import flash.events.Event;

     public class Main extends Sprite
     {
          private var loader: XMLLoader;
         
          public function Main()
          {
               this.loader = new XMLLoader();
               this.loader.addEventListener( Event.COMPLETE, loadComplete );
               this.loader.addEventListener( IOErrorEvent.IO_ERROR, loadFail );
               this.loader.load( "sample.xml" );
          }
         
          private function loadComplete( e: Event ): void
          {
               trace( this.loader.xml.toXMLString() );
          }
         
          private function loadFail( e: Event ): void
          {
               trace( "load fail." );
          }
     }
}

load 메소드에 호출할 xml 경로를 주고

Complete 와 IO Error 이벤트를 기다려

로드가 완료되면 xml 을 가져다 쓸 수 있는것이다.

XMLLoader 의 인터페이스를 보면

url (getter) : 호출한 url 을 참조할 수 있는 속성.
xml (getter) : 로드된 xml 객체를 참조할 수 있는 속성.
load( String ) : 로드할 경로를 파라미터로 하는 로드 메소드.

이렇게 3가지로 되어 있다.

자 그럼 이 클래스를 가만히 보면

깔끔하기도하고 군더더기 없이 만들어진거 같다.



그런데 이번에 Variable 형식으로 되어 있는 웹페이지를 호출하여 쓰는 클래스를 만들고 싶은데

기존의 XMLLoader 와 거의 동일해서 어떻게 사용할 수 있을까 고민해보았다.

Variables 형식은 a=1&b=3 형식으로 되어 있는 형식이라서

로드하는 부분은 거의 XMLLoader 와 비슷하고

마지막에 받아온 값만 좀 다를 뿐이기 때문이다.



이 시점에서 XMLLoader 클래스는 두가지 역할을 하고 있다는것을 알 수 있다.

즉, 로드를 하는 역할과 로드된 컨텐츠를 XML 로 변환하는 두가지 역할을 하는걸 알 수 있다.

만약 기능이 로드하는 클래스도 있고

로드된 컨텐츠를 XML 로 변환하는 클래스가 나뉘어져 있다면

Variables 형식의 로더를 만들때는

로드하는 클래스만 가져다가 로드된 컨텐츠를 Object 형식으로 치환할 수 있을것이다.



이처럼 하나의 클래스는 하나의 역할만 담당하도록 하는 설계

SRP - Single-Responsibility Principle 이라고 한다.

즉, 하나의 책임만 지는 원칙이라는 뜻이다.

SRP 는 노련한 개발자일 수록 본능적으로 지키게 되는 원칙이다.

두세단계를 앞서서 판단할 줄 알고

현재 프로젝트에 얽매여서 몰두하기보다는

앞으로의 흐름을 파악하고 현재 내가 만들 이 기능이

앞으로 어떤 방향으로 사용될것인지,

이 기능을 이렇게 만듦으로써 앞으로 어떤 구조로 프레임웍이 짜여질지

미리 판단할 수 있는것이다.

어떻게 보면 이 SRP 는 우리가 언어를 공부하면서 가장 처음 배우는 원리일지도 모른다.

무슨 말이냐면 Class 라는 개념을 처음배울때

예를 드는 설명이 바로 이 SRP의 의미와 같기 때문이다.

Class 는 하나의 역할을 하는 오브젝트라고 배우기 때문이다.

그것이 바로 SRP 의 의미와 같지 않은가~



자 그럼 다시 예제로 돌아가서

위에서 만든 XMLLoader 를 어떻게 만들면 좋을지 고민해보자.

아마 로드하는 부분과 컨텐츠를 가공하는 부분을 서로 다른 두개의 클래스로 만들면

사용하는 입장에서 좀 불편할지도 모르겠다.

하나만 사용하는 이전께 오히려 더 사용하는 입장에서는 더 편하기 때문이다.

그렇다면 로드하는 클래스를 상위 클래스로 해서

각각 XMLLoader, VariablesLoader, TextLoader 3가지 하위 클래스를 만들면 될것 같다.

여기서 짚고 넘어갈 부분은

로드를 담당하는 클래스는 자신이 불러올 수 있는 컨텐츠의 모든 종류를

하위클래스에서 구현하고 있기 때문에 단독으로 쓰일 경우가 없다는 것을 전제하에

설계를 해보겠다.

package loader2
{
     import flash.events.Event;
     import flash.events.IEventDispatcher;
     import flash.events.IOErrorEvent;
     import flash.events.SecurityErrorEvent;
     import flash.net.URLLoader;
     import flash.net.URLRequest;
    
     [Event ( name="complete", type="flash.events.Event" ) ]
     [Event ( name="ioError", type="flash.events.IOErrorEvent" ) ]
     [Event ( name="securityError", type="flash.events.SecurityErrorEvent" ) ]
    
     public class AbstractLoader implements IEventDispatcher
     {
          public function get url(): String
          {
               return this.loadURL;
          }
         
          public function AbstractLoader()
          {
              
          }
         
          private var loadURL: String;
          private var loader: URLLoader
          public function load( url: String ): void
          {
               this.loadURL = url;
              
               this.loader = new URLLoader();
               this.loader.addEventListener( Event.COMPLETE, loadComplete );
               this.loader.addEventListener( IOErrorEvent.IO_ERROR, loadFailIOError );
               this.loader.addEventListener( SecurityErrorEvent.SECURITY_ERROR, loadFailSecurity );
               this.loader.load( new URLRequest( this.loadURL ) );
          }
         
          private function loadComplete( e: Event ): void
          {
               var content: String = this.loader.data as String;
              
               this.init( content );
              
               this.dispatchEvent( new Event( Event.COMPLETE ) );
          }
         
          private function loadFailIOError( e: IOErrorEvent ): void
          {
               this.dispatchEvent( new IOErrorEvent( IOErrorEvent.IO_ERROR ) );
          }
         
          private function loadFailSecurity( e: Event ): void
          {
               this.dispatchEvent( new SecurityErrorEvent( SecurityErrorEvent.SECURITY_ERROR ) );
          }
         
          protected function init( content: String ): void
          {
               throw new Error( "AbstractLoader.init must be overriden." );
          }
         
          /**
          *
          * 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);
          }
     }
}

소스가 좀 길어보이는데

길어진 이유는 EventDispatcher 클래스를 상속하지 않고

IEventDispatcher 인터페이스를 구현했기 때문이다.

왜 굳이 소스 길어지게시리 인터페이스를 구현했냐하면

EventDispatcher 를 상속하면 내가 필요하지 않은 Activate, Deactivate 이벤트가 따라오기 때문이다.

필자가 만든 AbastractLoader 는 최상위 클래스다.

즉, 앞으로 만들 로드에 관련된 클래스는 AbstractLoader 를 상속하도록 설계를 했다는 뜻이다.



자 그럼 하나씩 뜯어보자.

다른 부분은 대충 어렵지 않은데 중간에

          protected function init( content: String ): void
          {
               throw new Error( "AbstractLoader.init must be overriden." );
          }

이부분이 특이하게 보일 것이다.

protected 로 선언하고 정작 내용은 에러를 내버리는 구문이기 때문이다.

이렇게 한 이유는 OCP 할때도 나왔지만

Flash 에서는 abstract 접근자를 지원하지 않기 때문이다.

abstract 접근자는 다른 언어를 경험해보지 않은 개발자에게는 낯선 접근자 일것이다.

abstract 는 메소드는 존재하지만 실제 단독으로는 작동되지 않는 추상접근자 이다.

즉, 만약 init 메소드를 abstract 으로 선언을 해놓는다면

하위클래스에서는 override 할 필요없이 인터페이스 구현하듯이 구현만 하면 된다.

존재만 하되 구현되지 않은 메소드이기 때문에

그 메소드를 구현하기만 하도록 하는 "추상화"접근자라는 이야기다.

하지만 우리는 사용할 수 없기 때문에

하위 클래스만 접근할 수 있는 protected 로 선언을 하고

AbstractLoader는 단독으로 쓰이지 않는다는 전제하에

그 내용은 Error 을 발생하도록 하여

반드시 상속해서 override 해서 사용하도록 강제하는 것이다.

이 기법은 앞으로도 효율적인 구조를 잡는데 유용하게 쓰일것이다.

즉, init 이라는 메소드는

AbstractLoader 클래스를 상속하는 하위클래스에서는 반드시 override 하여

컨텐츠를 가공하는 프로세스를 구현하도록 해놓은 것이다.

자 그럼 XMLLoader 를 구현하면 어떤 모습이 되는지 살펴보자.

package loader2
{
     public class XMLLoader extends AbstractLoader
     {
          private var _xml: XML
          public function get xml(): XML
          {
               return this._xml;
          }
         
          public function XMLLoader()
          {
              
          }
         
          override protected function init( content: String ): void
          {
               this._xml = XML( content );
          }
     }
}

크~ 간단하지 않은가?

로드를 담당하는 역할은 상위 클래스가 알아서 해주기 때문에

로드된 컨텐츠를 받아서 가공하는 것만 담당하면 된다.

자 그럼 아까 막혔던 VariablesLoader 를 구현해보자.

package loader2
{
     import flash.net.URLVariables;
    
     public class VariablesLoader extends AbstractLoader
     {
          private var _param: URLVariables
          public function get variables(): URLVariables
          {
               return this._param;
          }
         
          public function VariablesLoader()
          {
              
          }
         
          override protected function init( content: String ): void
          {
               this._param = URLVariables( content );
          }
     }
}

우와~ 매우 간단해졌다.

사용하는 방법 역시 동일하다. (에러메세지는 두개로 늘어났지만 이부분을 합치는 부분은 직접 구현해보길 바란다.^^ 내 블로그 어딘가에 CustomEvent 에 대한 글이 있으니 참고할 수 있다)

package loader2
{
     import flash.display.Sprite;
     import flash.events.Event;
    
     public class Main extends Sprite
     {
          private var loader: XMLLoader
         
          public function Main()
          {
               this.loader = new XMLLoader();
               this.loader.addEventListener( Event.COMPLETE, loadComplete );
               this.loader.load( "sample.xml" );
          }
         
          private function loadComplete( e: Event ): void
          {
               trace( this.loader.xml.toXMLString() );
          }
     }
}

자 똑같다.

이처럼 하나의 클래스가 하나의 역할만 담당하도록 하는 원리를

SRP : Single-Responsibility Principle 이라고 한다.

다른 뜻으로는 여러 복합된 클래스를 단일 책임을 담당하는 단일 클래스들로 분리한다는 의미와 같다.

첫번째 만든 XMLLoader 가 가지던 로드와 가공의 두가지 역할을

로드클래스와 가공 클래스 두가지 역할로 분리해낸 작업도 SRP 과정을 거쳤다라고 할 수 있다.



OCP 와 SRP 가 OOP의 기본 개념을 충실하게 대변하는 두 원칙이니

앞으로 클래스 하나를 만들더라도

좀 더 고민하는 시간이 늘어날 것이다.

하지만 지금 5분 더 고민함으로 인해서 앞으로 이익으로 돌아오는 시간은

상상할 수 없을만큼 클 것임을 보장한다.



2. ISP (Interface-Segregation Principle)

ISP : Interface - Segregation Principle.

ISP 는 언뜻 보기에는 SRP 와 비슷하다.

SRP 는 하나의 클래스를 단일 책임을 담당하게 하기 위해서

클래스를 분리한다는 의미이다.

인터페이스는 본래 클래스보다 한단계 상위 수준이기 때문에

이해가 좀 난해한 부분이 없지 않다.



자 다음과 같은 Flash 내장클래스를 한번 살펴보자.

ByteArray 클래스는 IDataInput 와 IDataOutput인터페이스 두가지로 이뤄져있다.

왜 나눠놨을까?

뭐 언뜻보기에는 input, output 이 다르게보이기도 하기 때문이지만

그건 이미 나눠진것을 봤기 때문에 그럴 수도 있다.

하지만 뭔가 이유가 있지 않을까?

바로 ISP 의 원칙이 충실이 적용되어 있기 때문이다.

서로 다른 역할을 하는 기능들은 인터페이스를 분리하여 구현하는것이 바로

ISP 이다.

이렇게 함으로써 IDataInput인터페이스만 상속하여

"읽기전용"클래스를 만들 수 있다.

즉 LocalStream 객체는 IDataInput 인터페이스만 구현해서

"읽을"수는 있지만 "쓸수"는 없다.

즉 애초에 IData 인터페이스가 아니라 "읽는"기능과 "쓰는"기능을 두가지 기능으로 고려햇기 때문에

인터페이스가 둘로 나뉘어지게 된것이다.

이처럼 하나의 인터페이스를 구성할때 비슷하지 않다고 생각되는 기능들은 따로 빼내어서

별도의 인터페이스를 나눠서 구성하는것을 ISP 원칙에 따른다고 볼 수 있다.

이렇게 함으로써 얻는 이득은 구조가 커지면 커질수록 위력을 발휘한다.

즉, 데이타를 읽어서 사용만 하는 클래스는

read( data: IDataInput ) 으로 인터페이스를 타입으로 받아들일 수 있으며

출력만 하는 클래스는

write( data: IDataOutput ) 으로 해서 읽기전용 속성의 클래스는 원척적으로 봉쇄할 수 있다.

즉 이렇게 함으로써 클래스는 본인의 역할에 충실할 수 있게 되는것이다.



SRP 와 ISP 의 개념은 원리는 같지만 목표는 구분된다.

즉, SRP 는 클래스 분리를 통하여 변화에 대비할 수 있고

ISP 는 인터페이스 분리를 통하여 같은 목표를 추구할 수 있다.

즉 분리를 통하여 다형성을 추가하는게 SRP 라면

분리를 함으로써 같은 효과를 내기 위한것이 ISP 이다.

OOP가 그렇듯이 지금 당장은 잘 이해가 안가더라도

나중에 언젠가 되돌아보면 크게 와닿을 것이다.



3. 어떻게 분리할 것인가?

그렇다면 SRP 나 ISP 나 어느정도 살펴보았다.

그러면 가장 중요한것이 내가 어떻게 사용할것이냐인데

이 문제에 꽤 유용할만한 조언이 있다.

바로 IS-A 관계를 예로 드는 비유법인데

흔히 종업원과 매니저를 예로 든다.

매니저는 종업원이 될 수 있다. (매니저도 종업원이 바쁘거나 클레임걸면 종업원으로써 고객을 대하기 때문)

하지만 종업원은 매니저가 될 수 없다. (종업원은 매출관리를 할 수 없고 매장관리도 할 수 없다.)

이처럼 IS-A 관계가 적용되지만 그 역이 성립되지 않을때는

서로 다른 클래스가 되어야 한다.

즉 우리가 예로 든 XMLLoader 처럼

AbstractLoader 는 로드해서 XML 을 사용할 수 있지만

XMLLoader 는 로드만 하는 용도로 사용할 수 없다. (XML 로 변환해야하기 때문)

이처럼 AbstractLoader is a XMLLoader 는 성립하지만

XMLLoader is not a AbstractLoader 로 IS-A 관계가 성립되지 않는다.

자 그럼 클래스를 나누는 기준들을 살펴보자.

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

위 사항을 잘 기억해놓고 구조를 잡을때

한번씩 상기해서 설계하면 보다 OOP에 충실한 구조가 될것이다.

이것이 ISP 와 SRP 이다.

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

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

+ Recent posts