0. 서두, Prologue
Flash 는 특성상 개발 기간이 짧고 디자인과 언어로써의 역할을 모두 해왔다.
게다가 Flash가 부흥하게 된 계기 역시 Tween 을 이용한 애니메이션이 지대한 공을 하였기 때문이고
거기다가 그당시 지원되던 메소드들은
말그대로 몇몇 "기능"일 뿐이었다.
솔직히 말해서 디자인툴도 아닌것이;;
개발언어도 아닌것이;;
참 희한했던 툴로 보였을 것이다.
언어로써의 모습도 아니었거니와 돌이켜 생각해보면 참 많은 내용을 하나의 메소드로 제공했었다는 것을 알 수 있다.
그래서 Flash 의 역사는 적지 않게 흘렀지만
이제서야 ActionScript 3.0이 나타났고 비로소 객체지향이라는 언어로써의 자격이 생겼다.
물론 아직도 미흡한 부분이 많고 앞으로 나갈길이 많지만
OOP를 지향하기에는 부족함이 없다.
OOP를 향한다는 이야기는 핵심적인 기술, 특정한 알고리즘하나, 단순 노가다가 아니라
구조로써 변화에 대응할 수 있고
보다 효율적인 관계를 구현전에 미리 구상할 수 있기 때문에 비용이 줄어든다는 이야기며
프레임웍과 개발 방법론을 익힘으로써 큰 규모의 어플리케이션으로써의 면모를 갖출 수 있다는 이야기다.
그렇다면 이미 많은 시간이 지난 이 시점에서 우리가 가져야할 자세는 무엇일까?
다른 진영에서 수십년간 쌓아온 우리 선배들의 꿀물지식들을
낼름 가져와서 우리것으로 익혀 미래에 대응해야 할것이다.
1. 5원칙, 5 Principle
우리가 만드는 클래스들, 클래스들간의 관계들, 구조들...
과연 어떤게 좋은 구조라고 할 수 있을까?
어떻게하면 보다 효율적으로 관리하고, 나중에 손이 덜 가도록 만들 수 있을까?
이에 대한 질문은 다른 영역에서 수 없이 논의되었었다.
오랜 시간 논의된 결과 아래 5가지 원칙을 기준으로 하게 되었다.
- OCP : Open-Closed Principle (개방-폐쇄의 원칙)
- SRP : Single-Responsibility Principle (단일 책임의 원칙)
- DIP : Dependency Inversion Principle (의존 역전의 원칙)
- ISP : Interface Segregation Principle (인터페이스 분리의 원칙)
- LSP : Liskov Substitution Principle (리스코프의 대체 원칙)
딱 보기에는 약자도 거기서 거기고 영어로 보자니 먼말인지 모르겠고
한글로 읽어도 글자만 한글이지 당췌 이해가 잘 가지 않을 것이다.
어디서 본거 같긴 한데 정확하게는 무슨 말인지 잘 모르겠고
무슨 내용인지 궁금해서 살살 입질이 오리라 믿는다.
위 5가지 원칙을 기준으로 앞으로 설명을 해 나갈텐데
저 5가지 원칙은 서로 다른 뜻이나 관계가 아니라
서로 연관이 되어 있고 반대되는 원리 같은데 같이 쓰이기도 하는
서로 밀접한 관계가 있는 원칙들이기 때문에
세미나를 진행하면서 순서 없이 하나씩 등장할것이다.
2. OCP ( Open-Closed Principle ) : 개방-폐쇄의 원칙
사전적인 뜻풀이로는
라고 되어 있다.
무슨 뜻인고 하니 우선 예를 들기전에 간단하게 개념부터 이야기 하고 가자면
상속을 자유롭게 하기 위해서 특정 하위 클래스가 할일을 미리 상위 클래스에서
구현해버리면 그 기능을 하지 않아야할 클래스까지도 그 기능을 강제로 물려받는다는 이야기다.
변경에 닫혀야한다는 이야기는
상위클래스는 모든 하위클래스들이 공통적으로 쓰는 기능들을 가지고 있기 때문에
특정 하위 클래스라고 해서 상위 클래스의 기본 기능을 바꿀수 있거나 간섭할 수 있어서는 안된다라는 이야기다.
결국은 OOP의 기본 개념인 자유로운 상속을 통한 확장과 재사용성을 추구하기위한
제 1법칙인 것이다.
자 그러면 널린 Java 예제 말고 ActionScript 로 그 예를 한번 살펴보자.
|
위와 같은 Shape 클래스가 있다.
이 클래스는 원, 사각형, 타원, 삼각형을 그릴 수 있는 모든 메소드를 가지고 있다.
그중에 하나로 Circle 클래스를 살펴보자.
|
Circle 클래스는 자신의 타입을 "circle" 로 지정하고 Shape 클래스를 상속하였다.
그럼 사용할때는 어떻게 사용하는지 보자.
|
Painter 클래스에서는 사용될 shape 클래스중 하나를 인스턴스화해서
draw 라는 메소드에서 사용할때
type 이라는 변수를 가져다가 체크하는 수 밖에 없다.
클래스 다이어그램으로 그려보면 다음과 같다.
그러면 직사각형, 별모양, 오각형, 마름모, ................
늘어날때마다 타입은 하나씩 늘어나고
메소드도 하나씩 계속 늘어나야된다.
이런 문제를 어떻게 해결 할 수 있을까?
다른 언어에서는 Abstract 메소드를 이용해서 구현하지만 Flash 에서는 지원되지 않는 만큼
인터페이스를 통하여 추상화하는 방법을 사용한다.
또 슬슬 용어가 어려워지는데
아래 결과를 먼저 보자.
|
어떤 쉐입이 오던지 IShape 을 구현한 클래스이면
draw() 메소드만 실행해주면 된다.
아래는 IShape 인터페이스의 모습이다.
|
draw() 메소드 밖에 없다.
즉, IShape 을 구현한 클래스는 모두 draw 라는 공통적인 메소드를 가지지만
어떤 동작을 할지는 구현된 클래스가 알아서 처리하는것이다.
interface 는 OCP 를 위해서 태어난게 아닐까 싶다.
즉 Oval은 public 으로 draw 메소드를 구현해놓고
그 안에서 원을 그릴지 어떨지 판단하면 된다.
Oval클래스는 아래와 같다.
|
훨씬 간결해졌으면서도 훨씬 사용하기 편해졌다.
이와 같이 상위 클래스가 하위 클래스의 역할을 벗어던짐으로써
상위 클래스 대신 인터페이스가 그자리를 차지하면서 생긴 효과는
재사용성을 극대화 시키고 IShape 을 사용하는 곳이면
그 후에 어떤 도형을 추가하더라도 사용되는 곳의 소스는 수정할 필요없이
얼마든지 추가할 수 있게 되었다.
위 구조를 클래스 다이어그램으로 나타내면 아래와 같다.
이처럼 확장이 열려있도록 하기 위해서는
인터페이스를 통하여 어떤 동작을 할것이라는 것만 정의를 해주고
실제 동작하는 것은 하위클래스가 담당하도록 확장을 자유롭게 해줄 수 있다.
이런 구조가 바로 확장에 있어서 열린 (Open) 된 구조다.
이렇게 됨으로써 상속을 하면서 상위 클래스를 변경하지 않아도 되게 되었다.
이것이 변경으로 부터 닫혀 (Closed) 된 구조다.
ActionScript 기본 클래스중에서
예를 볼 수 있는 부분은 바로 ByteArray 와 URLStream, Socket, FileStream 클래스들이다.
기본적으로 생각할 수 있기로는
어차피 바이트를 읽고 쓰는 클래스들인데
ByteArray 를 상위 클래스로 놓고 쓰지 않을까?
라는 생각이 들수도 있지만
바이트를 읽고 쓰는 동작이 ByteArray 에 의해서 고정되어 버렸다면
바이트 정보를 메모리가 아닌 로컬 파일을 쓰는 FileStream,
바이트 정보를 메모리나 로컬 파일이 아닌 패킷으로 보내는 Socket 등의 클래스들은
탄생하지 못했을 것이다.
탄생했더라도 ByteArray 와는 상관없는 상위 클래스를 가지고 있을것이기 때문에
바이너리 데이타를 소켓으로 보내거나
바이너리 데이타를 파일로 저장하는등의 방법은 아마 매우 어려워졌을 것이다.
하지만 IDataInput, IDataOutput 이라는 인터페이스 둘로 분리시킴으로써
바이트 정보를 읽고 쓰는 행위를 한다는
동작만 정의해두고
실제로 동작하는 부분은 이 인터페이스를 구현하는 클래스들이
따로따로 구현할 수 있게 된것이다.
OOP : Object-Oriented Programming (객체 지향 프로그래밍)
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로더를 한번 만들어 보자.
|
|
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가지 하위 클래스를 만들면 될것 같다.
여기서 짚고 넘어갈 부분은
로드를 담당하는 클래스는 자신이 불러올 수 있는 컨텐츠의 모든 종류를
하위클래스에서 구현하고 있기 때문에 단독으로 쓰일 경우가 없다는 것을 전제하에
설계를 해보겠다.
|
소스가 좀 길어보이는데
길어진 이유는 EventDispatcher 클래스를 상속하지 않고
IEventDispatcher 인터페이스를 구현했기 때문이다.
왜 굳이 소스 길어지게시리 인터페이스를 구현했냐하면
EventDispatcher 를 상속하면 내가 필요하지 않은 Activate, Deactivate 이벤트가 따라오기 때문이다.
필자가 만든 AbastractLoader 는 최상위 클래스다.
즉, 앞으로 만들 로드에 관련된 클래스는 AbstractLoader 를 상속하도록 설계를 했다는 뜻이다.
자 그럼 하나씩 뜯어보자.
다른 부분은 대충 어렵지 않은데 중간에
|
이부분이 특이하게 보일 것이다.
protected 로 선언하고 정작 내용은 에러를 내버리는 구문이기 때문이다.
이렇게 한 이유는 OCP 할때도 나왔지만
Flash 에서는 abstract 접근자를 지원하지 않기 때문이다.
abstract 접근자는 다른 언어를 경험해보지 않은 개발자에게는 낯선 접근자 일것이다.
abstract 는 메소드는 존재하지만 실제 단독으로는 작동되지 않는 추상접근자 이다.
즉, 만약 init 메소드를 abstract 으로 선언을 해놓는다면
하위클래스에서는 override 할 필요없이 인터페이스 구현하듯이 구현만 하면 된다.
존재만 하되 구현되지 않은 메소드이기 때문에
그 메소드를 구현하기만 하도록 하는 "추상화"접근자라는 이야기다.
하지만 우리는 사용할 수 없기 때문에
하위 클래스만 접근할 수 있는 protected 로 선언을 하고
AbstractLoader는 단독으로 쓰이지 않는다는 전제하에
그 내용은 Error 을 발생하도록 하여
반드시 상속해서 override 해서 사용하도록 강제하는 것이다.
이 기법은 앞으로도 효율적인 구조를 잡는데 유용하게 쓰일것이다.
즉, init 이라는 메소드는
AbstractLoader 클래스를 상속하는 하위클래스에서는 반드시 override 하여
컨텐츠를 가공하는 프로세스를 구현하도록 해놓은 것이다.
자 그럼 XMLLoader 를 구현하면 어떤 모습이 되는지 살펴보자.
|
크~ 간단하지 않은가?
로드를 담당하는 역할은 상위 클래스가 알아서 해주기 때문에
로드된 컨텐츠를 받아서 가공하는 것만 담당하면 된다.
자 그럼 아까 막혔던 VariablesLoader 를 구현해보자.
|
우와~ 매우 간단해졌다.
사용하는 방법 역시 동일하다. (에러메세지는 두개로 늘어났지만 이부분을 합치는 부분은 직접 구현해보길 바란다.^^ 내 블로그 어딘가에 CustomEvent 에 대한 글이 있으니 참고할 수 있다)
|
자 똑같다.
이처럼 하나의 클래스가 하나의 역할만 담당하도록 하는 원리를
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 이다.
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 를 구현한 부분은 넘어가도 된다.
|
자 이 클래스의 인터페이스는 다음과 같다.
- start() : 카운터를 시작할 메소드.
- stop() : 카운터를 중지시키는 메소드.
- count : 현재 카운트를 참조할 수 있는 메소드.
자 사용하는 방법은 다음과 같다.
|
카운터를 만들고 "count" 라는 이벤트가 발생할 때마다
Counter.count 라는 속성을 참조해서 원하는 카운트에 원하는 동작을 처리하면 된다.
이 두 클래스의 흐름을 시퀀스 다이어그램을 이용해서 표현하면 아래와 같다.
보다시피 Main 클래스가 Counter 에 이벤트가 일어날때마다 count 속성을 참조한다.
Counter 는 매 이벤트가 일어날때마다
현재 카운트를 제공만하고 실제 필요한지 안필요한지는
Main 에게 의존해야한다.
카운팅을 담당하는 클래스는 Counter 임에도 불구하고 필요한 카운트만 주는게 아닌
매 카운트마다 Main 에게 그 역할을 담당하게 하는 구조로 되어 있는것이다.
즉 Counter 는 자신이 카운팅을 담당하는 클래스임에도 불구하고
Main 에게 의존을 너무 많이한 나머지
자신은 정작 숫자를 세는 일밖에 하지 않게 된것이다.
이런 구조를 어떻게 변경해서
Main 에게 쏠려있는 의존도를 Counter 에 위임할 수 있을까?
해답은 우리가 이벤트를 주고 받을때
이벤트를 등록시켜놓고 그 이벤트가 발생했을때 자동으로 리스너가 작동하는 방식으로 하면 좋을거 같다.
그럼 내가 필요한 카운트에 특정 메소드를 작동시킬 수 있도록
Counter.addListener( func: Listener, dispatchCount: int ): void 라는 메소드를 추가해서
해당 카운트가 발생했을때 Counter 가 스스로 동작하게 하면 멋질거 같다.
자 그럼 만들어보자.
|
자 클래스가 좀 복잡해졌는데
인터페이스를 살펴보자.
- addListener( Function, int ) : 해당 카운트와 그때 실행할 리스너를 등록해준다.
- start() : 카운터를 작동시킨다.
- stop() : 카운터를 중지시킨다.
기존의 count 가 없어지고 addListener 로 필요한 카운트를 판단하는 역할이
Main 이 아니라 Counter 가 직접 하도록 위임되었다.
사용하는 방법은 아래와 같다.
|
해당 카운트때 실행될 메소드들을 만들어놓고
동작할때 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 클래스를 만든다고 해보자.
|
유저가 구매를 하면
구매한 금액*포인트비율(0.02) 를 적용해서
포인트를 쌓는다.
자 그러면 이제 모든 결제 프로세스에는
UserAccount 클래스를 기준으로 제작이 된다.
이후에 만들어지는 회원정보는 UserAccount 클래스를 상속하여 제작하면 된다.
자 이번엔 우수 고객들을 VIP 고객으로 대우해서
VIP Point 를 추가적으로 서비스하게 되었다.
그래서 아래와 같은 VIPAccount 클래스가 개발되었다.
|
VIP 회원을 관리할때는 앞으로 추가로 관리를 해줘야 할것이다.
|
위처럼 account 가 VIPAccount 일 경우 추가적인 보너스를 주는 형식이다.
자 그런데 서비스를 실시하려고 테스트를 하자
엄청난 오류들이 발생했다.
이유는 기존의 모든 결제 시스템이 UserAccount 로 구현되어 있기 때문에
VIP 서비스를 도입하려면 결제시스템을 통채로 수정해야될 판이 된것이다.
이문제를 어떻게 하면 될것인가?
이는 LSP 원칙이 깨지면서 발생된 오류이다.
상위 클래스가 사용되는곳에 그 확장클래스인 하위클래스가
사용되지 못하게 된것이다.
그렇다면 LSP 의 원칙에 따르려면 어떻게 수정되어야 할까?
캐쉬백을 해주는 부분을 확장성 있도록
기본 포인트를 지급한후 추가적인 포인트 지급이 있다는 가정하에 메소드 하나를 지원해주면 될것 같다.
|
위 처럼 포인트를 적립한 후 추가적인 적립을 예상하여 추가적인 메소드를
protected 접근자로 외부에는 숨긴채 지원하도록 하였다.
그럼 VIPAccount 는 어떻게 변하게 될까?
|
위 처럼 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 가 이제 언어로써의 자격을 갖음으로써
우리에게 전혀 동떨어진 이야기는 아니라는 뜻이다.
가만히 있기 위해서는 힘껏 달려야한다.
출처 : http://wooyaggo.tistory.com/
댓글