원문: http://ryulib.tistory.com/111

"구현보다는 인터페이스에 맞춰서 코딩 하라


이것은 OOP의 핵심을 찌르는 문장 입니다.  따라서 한 번 자세히 살펴보도록 하겠습니다.

우선인터페이스가 무엇인지 한 번 생각해보기로 하겠습니다.  인터페이스는 서로 다른 객체들끼리 메시지를 전달하기 위한 통로를 의미합니다.  좀 더 쉽게 말해서, 객체의 public에 해당하는 메소드와 속성을 인터페이스라고 해도 무방 합니다.  객체의 내부를 알 필요 없이 인터페이스만 알고 있다면해당 객체에게 기능(메소드)이나 정보(속성)를 요청 할 수 있어야 한다는 뜻이기도 합니다.

인터페이스를 제공하고 이것을 통해서 객체에게 기능이나 속성을 요청한다는 것은 서로 의존성을 가졌다는 의미가 되기도 합니다.  이러한 의존 관계에는 크게 두 가지 종류가 있습니다.  어떤 요인(이벤트)으로 인해서 다른 객체의 메소드를 호출하거나다른 메소드를 호출한 결과를 통해서 처리 결과 또는 데이터를 가져오는 경우 입니다.

 

[그림 1] 두 가지의 의존 관계

시스템을 요구사항에 맞춰 동작하는 것에만 집중하면서 구현에 맞춰 코딩 하게 되면시스템을 구축하는 코드들이 서로 한 덩어리처럼 엉겨 붙게 되어 유연성 및 확장성이 심각하게 떨어지게 됩니다.  이것은 다른 코드의 인터페이스만을 이용하는 경우와 달리그 구현 내부에 깊숙하게 의존하여 시스템을 구축하게 되기 때문입니다.

이제 인터페이스에 맞춰서 코딩 하는 방법을 설명하기 위해서, [그림 2]와 같이, http://ryulib.tistory.com/109 에서 작성한 슬라이딩 퍼즐 게임의 최초 설계를 기준으로 의존성을 분석해보도록 하겠습니다.


[그림 2] 슬라이딩 퍼즐 게임 전반에 대한 프로세스 흐름

 

우리가 구축해야 할 시스템은 PuzzleBoard로써 퍼즐 조각의 움직임 처리를 담당하는 BlockControl과 퍼즐 조각들의 정보를 관리하는 Blocks 객체로 분업화 되어 있습니다.  

나리오는 최대한 단순하게 표현하였으며외부에서 게임 시작을 알리는 이벤트가 발생했을 때, PuzzleBoardshuffle() 메소드를 실행하는 것이 전부 입니다.  이후내부의 두 객체들이 서로 협력(의존)하여 요구사항을 수행하고 있습니다.  BlockControl shuffle()은 퍼즐 조각을 무작위로 섞어주는 기능을 담당하며내부의 slide() 메소드를 통해서Blocks moveBlock() 메소드를 호출 합니다.  BlockControl은 어떻게 움직여야 하는 가에 해당하는 논리적인 부분만 처리하고실제 데이터가 이동하고 화면에 표시되는 등의 역할은 Blocks가 담당합니다.  퍼즐 조각이 클릭되면, Clicked 이벤트가 발생하여 해당 퍼즐 조각을 이동하게 됩니다.

객체들 간의 의존성이 모두 분석되었으므로 이제 인터페이스가 결정된 것이나 마찬가지입니다.  자기 자신이 아닌 다른 객체로부터 호출된 적이 있는 메소드는 당연히 public에 정의되어야 하며이것들이 인터페이스 부분에 해당하게 됩니다.

 

[그림 3] 의존성 분석을 통해 밝혀진 인터페이스

 

이제 [그림 2]의 동적분석과 [그림 3]의 구조분석을 토대로 실제 구현하는 방법에 대해서 고민해보도록 하겠습니다.  다시 한 번 상기시켜드린다면의존성에 해당하는 인터페이스를 중심으로 코딩을 하되구현은 최대한 숨기는 것이 목표입니다.

 

 

Invisible path pattern

구현 방법의 첫 번째로는 제가 만든 디자인 패턴을 소개해보도록 하겠습니다.  이 방법은 [그림 4]와 같이 추상 클래스를 통해서 인터페이스와 동적 흐름을 담당하는 인터페이스 계층과 실제 구현을 담당하는 구현 계층으로 확연하게 나눠서 코딩 할 수 있다는 장점이 있습니다.


[그림 4] Invisible path pattern

 

[소스 1] PuzzleBoard.java

   1 : package app.main;

   2 :

   3 : public class PuzzleBoard {

   4 :

   5 : BlockControl _BlockControl = new BlockControlImp(this);

   6 : Blocks _Blocks = new BlocksImp(this);

   7 :

   8 : public void shuffle() {

   9 :        _BlockControl.shuffle();

  10 : }

  11 :

  12 : }

5-6: 실제 객체는 구현 계층 클래스를 통해서 생성합니다.

9: [그림 2]에서 분석한 대로 외부에서 shuffle()을 호출하면, BlockControl에게 위임합니다.


[소스 2] BlockControl.java

   1 : package app.main;

   2 :

   3 : public class BlockControl {

   4 :

   5 : public BlockControl(PuzzleBoard puzzleBoard) {

   6 :        super();

   7 :       

   8 :        _PuzzleBoard = puzzleBoard;

   9 : }

  10 :

  11 : private PuzzleBoard _PuzzleBoard = null;

  12 :

  13 : public void shuffle() {

  14 :        // 구현 계층에서 Override해서 사용

  15 : }

  16 :

  17 : protected final void onNeedMoveBlock() {

  18 :        _PuzzleBoard._Blocks.moveBlock();

  19 : }

  20 :

  21 : public void slide() {

  22 :        // 구현 계층에서 Override해서 사용

  23 : }

  24 :

  25 : }

13-15: 외부에서 호출 할 수 있는 인터페이스만 제공할 뿐실제 구현은 구현 계층 클래스에서 이루어집니다.  [그림 2]를 통해 분석된 것처럼구현 과정에서 필요에 따라 slide()를 호출하게 됩니다.  shuffle() 무작위로 퍼즐을 섞는 논리만을 담당하고실제 이동하는 방식은 slide()에서 담당합니다.

21-23:  외부에서 호출 할 수 있는 인터페이스만 제공할 뿐실제 구현은 구현 계층 클래스에서 이루어집니다.  구현 과정에서 onNeedMoveBlock()을 호출하여 실제 데이터를 변경합니다.

17-19: 구현 과정에서 호출하는 메소드이기 때문에 protected로 선언되어 있습니다.


[소스 3] Blocks.java

   1 : package app.main;

   2 :

   3 : public class Blocks {

   4 :

   5 : public Blocks(PuzzleBoard puzzleBoard) {

   6 :        super();

   7 :       

   8 :        _PuzzleBoard = puzzleBoard;

   9 : }

  10 :

  11 : private PuzzleBoard _PuzzleBoard = null;

  12 :

  13 : protected final void onClicked() {

  14 :        _PuzzleBoard._BlockControl.slide();

  15 : }

  16 :

  17 : public void moveBlock() {

  18 :        // 구현 계층에서 Override해서 사용

  19 : }

  20 :

  21 : }

17-19:  부에서 호출 할 수 있는 인터페이스만 제공할 뿐실제 구현은 구현 계층 클래스에서 이루어집니다.

13-14: 퍼즐 조각이 클릭되었을 때호출되는 메소드 입니다.  이후, BlockControl에게 이동하는 알고리즘을 위임합니다.

 

전반적인 흐름을 설명하고자 한 것이기 때문에구현 계층은 설명을 생략하도록 하겠습니다.

이것으로 얻을 수 있는 장점은 BlockControl Blocks의 구현 계층 객체가 자신이 override 해야 하는 메소드와protected로 선언된 메소드만 알고 있으면 다른 외부 객체나 정보를 전혀 알 필요가 없다는 것 입니다.  인터페이스 계층에서는 서로의 의존적 관계만을 구현하고구현 계층에서는 자기 자신에게만 신경쓰면 됩니다.

단점으로는 BlockControl Blocks PuzzleBoard에게 의존적이 때문에별개로 재사용 하기가 어렵다는 점 입니다.  또한이미 존재하는 클래스를 PuzzleBoard의 종속 객체(BlockControl, Blocks)로 사용하고자 할 때 문제가 발생 할 수도 있습니다.  추상 클래스는 인터페이스(Interface)에 비해서 유연하지 못하기 때문에 발생하는 부담이 있을 수 있습니다.


 

이벤트 리스너를 통한 방법

제가 가장 많이 선호하는 방식이며구성 객체들을 별개로 재사용 하기가 쉽다는 것이 제일 큰 장점입니다.

[소스 4] PuzzleBoard.java

   1 : package app.main;

   2 :

   3 : public class PuzzleBoard {

   4 :

   5 : public PuzzleBoard() {

   6 :        super();

   7 :       

   8 :        _BlockControl.setOnNeedMoveBlock(_OnNeedMoveBlock);

   9 :        _Blocks.setOnClicked(_OnBlockClicked);

  10 : }

  11 :

  12 : BlockControl _BlockControl = new BlockControl();

  13 : Blocks _Blocks = new Blocks();

  14 :

  15 : public void shuffle() {

  16 :        _BlockControl.shuffle();

  17 : }

  18 :

  19 : private OnNotifyEvent _OnNeedMoveBlock =

  20 :        new OnNotifyEvent() {

  21 :               @Override

  22 :               public void onNotify(Object sender) {

  23 :                     _Blocks.moveBlock();

  24 :               }

  25 :        };

  26 :

  27 : private OnNotifyEvent _OnBlockClicked =

  28 :        new OnNotifyEvent() {

  29 :               @Override

  30 :               public void onNotify(Object sender) {

  31 :                     _BlockControl.slide();

  32 :               }

  33 :        };

  34 : }

12-13: Invisible path pattern과 달리 인터페이스 계층과 구현 계층이 따로 없기 때문에 BlockControl Blocks는 하나의 클래스로 작성됩니다.  또한, PuzzleBoard의 레퍼런스를 넘겨 줄 필요가 없기 때문에 의존성도 더욱 낮아집니다.

8-9: 이벤트 리스너를 통해서 의존 관계를 설정하고 있습니다.

19-25: BlockControl 객체에서 OnNeedMoveBlock 이벤트가 발생하면 23: 라인을 통해서 Blocks moveBlock() 메소드를 호출 합니다.

27-33: Blocks 객체에서 OnBlockClicked 이벤트가 발생하면, 31: 라인을 통해서 BlockControl slide() 메소드를 호출 합니다.

 

[소스 5] BlockControl.java

   1 : package app.main;

   2 :

   3 : public class BlockControl {

   4 :

   5 : private OnNotifyEvent _OnNeedMoveBlock = null;

   6 :

   7 : public final void shuffle() {

   8 :        // TODO

   9 : }

  10 :

  11 : private void onNeedMoveBlock() {

  12 :        if (_OnNeedMoveBlock != null) {

  13 :               _OnNeedMoveBlock.onNotify(this);

  14 :        }

  15 : }

  16 :

  17 : public final void slide() {

  18 :        // TODO

  19 : }

  20 :

  21 : public final void setOnNeedMoveBlock(OnNotifyEvent value) {

  22 :        _OnNeedMoveBlock = value;

  23 : }

  24 :

  25 : public final OnNotifyEvent getOnNeedMoveBlock() {

  26 :        return _OnNeedMoveBlock;

  27 : }

  28 :

  29 : }

11-15: setOnNeedMoveBlock() 메소드를 통해서 이벤트 리스너가 지정되어 있다면, 13: 라인에서 해당 이벤트 리스너를 실행합니다.  slide() 메소드 구현 중에 호출하게 됩니다.

 

[소스 6] Blocks.java

   1 : package app.main;

   2 :

   3 : public class Blocks {

   4 :

   5 : private OnNotifyEvent _OnClicked = null;

   6 :

   7 : public final void moveBlock() {

   8 :        // TODO

   9 : }

  10 :

  11 : private final void onClicked() {

  12 :        if (_OnClicked != null) {

  13 :               _OnClicked.onNotify(this);

  14 :        }

  15 : }

  16 :

  17 : public final void setOnClicked(OnNotifyEvent value) {

  18 :        _OnClicked = value;

  19 : }

  20 :

  21 : public final OnNotifyEvent getOnClicked() {

  22 :        return _OnClicked;

  23 : }

  24 :

  25 : }

11-15: setOnClicked() 메소드를 통해서 이벤트 리스너가 지정되어 있다면, 13: 라인에서 해당 이벤트 리스너를 실행합니다.  


이벤트 리스너를 통하여 인터페이스를 중심으로 코딩 하는 경우에도, BlockControl Blocks를 인터페이스 계층과 구현 계층으로 나눠서 개발 할 수 있습니다.

이벤트 리스너를 통한 방법의 또 다른 장점으로는 PuzzleBoard가 교통 정리를 담당하게 되어이벤트의 흐름을 한 곳에서 파악 할 수가 있다는 것 입니다.

 

 

Interface를 이용한 방법

이 방식에 대한 예제는 http://ryulib.tistory.com/109 의 첨부 파일을 참고하시기 바랍니다.  Interface를 이용하는 방식의 장점이라면구현 클래스가 다른 클래스를 상속 받아야 하는 경우에도 자연스럽게 사용 할 수가 있으며추후 전혀 다른 클래스로 대체하고자 할 때도 유연하게 대체 할 수 있다는 점 입니다.

단점으로는 각 객체가 의존적인 호출을 내부에 감추게 되어이벤트 리스너를 통한 방식처럼 전체 흐름을 한 곳에서 파악 할 수가 없다는 점 입니다.  또한, Invisible path pattern처럼 BlockControl Blocks PuzzleBoard를 의존하게 되는 문제가 발생 합니다.

 

 

프레임워크를 통한 방법

다음으로는 프레임워크를 통한 방법이 있습니다.  프레임워크의 종류도 많고간단하게 설명 할 수 있는 것이 아니기에,설명은 생략합니다.

대부분 프레임워크는 웹 어플리케이션이나 데이터베이스 관련 기업용 솔루션에 적합하도록 만들어져 있습니다.  필자는 일반 응용 어플리케이션에서도 사용 할 수 있는 가벼운 프레임워크를 만들어서 여러 차례 발표한 적이 있으나바쁜 일정에 쫓기고 있어서그 완성도가 무척 떨어집니다.  언젠가 다시 발표 할 수 있는 날이 오기를 기대합니다 ^^;  현재는 델파이와 자바 버전 두 가지로 작성 중 입니다.

 

 

디자인 패턴

디자인 패턴은 객체지향적 프로그래밍의 전반적인 훈련을 하는데 상당히 좋은 교재입니다.  다양한 방법으로 유연한 코드를 생산하는 방법을 제시합니다.  여러 번 반복하여 공부하시기를 권장합니다.