본문 바로가기

기타/객체지향의 탄생(2013)

객체지향의 탄생-Commander Pattern

 

  1. 커맨드 패턴(Command)

1. 어렸을때 나의 머리는 온통 삼국지의 광활한 무대였다. 제갈량의 전략을 음미하고 조자룡의 무예를 상상하며 유비가 말년의 고집으로 이릉에서 육손에게 대패하여 죽음을 맞게 된 안타까움과 헌제와 정적에게 지독하게 잔인했던 조조에 대한 분노 그리고 당대의 영웅호걸들을 녹인 초선 같은 미녀들에 대한 상상까지 집에 누우면 삼국지에 대한 온갖 상상의 무대를 천장에 그려보았고, 책을 읽으면 읽었던 삼국지를 또 읽었고, 컴퓨터를 키면 삼국지 게임에 매달리곤 했다.

 

지금에 와서 삼국지를 생각하면 미처 생각하지 않았던 존재이지만 나름 중요한 역할을 수행하는 사람들이 있었는데 바로 '사자'라고 부른 사람들이다.

 

사자는 주군의 전령을 받고 동맹군이나 적군에게 달려가 전령을 전달하는 역할을 했던 사람들이다. 그래서 전장의 흐름을 이어주는 중요한 역할을 수행했던 사람들이다. 이들은 목숨을 걸고 임무를 수행했다. 적군의 심기를 건드리는 전령을 보여줬을 경우 목이 베일 각오도 해야 했던 사람들이다.

 

나는 오늘 배우려는 디자인패턴의 영감을 얻기 위해 우리 주군이 사자에게 전령을 준 다음 적군의 주군에게 전령을 전달하여 원하는 응답을 받을때까지를 정리해 보았다.

 

주군은 사자를 알고 있고 어떤 전령을 보내야 할지 알고 있고 응답받길 원하는 적장에 대해서도 알고 있다. 주군이 적장에 대해 파악한다. 주군이 적장으로부터 원하는 응답을 받기 위해 전령을 작성한다. 전령을 사자에게 주어 적장에게 갈것을 명령한다. 사자는 전령을 갖고 적장에게 가서 전령을 전달한다. 적장으로부터 응답을 받는다.

 

사실은 이 과정이 전형적인 커맨드 패턴의 흐름이다.

 

2. 삼국지의 아군과 타군과의 교류를 프로그램으로 짜보자. 보통 생각없이 내맘대로 프로그래밍을 한다면, 아군과 타군과의 교류에 복잡스러워 보이기만 한 커맨드 패턴 쓸 필요 없이 그냥 손이 가는대로 개발할 것이다. 그렇다면 아군과 타군과의 교류과정에 '사자'와 '전령'도 없어진다.

 

먼저 사자가 없으므로 주군은 적장을 직접 상대해야 한다. 주군이 적장을 직접상대한다는 것은 식량확보, 군비증진, 백성긍휼등의 훨씬 중요한 업무를 놔두고 주군 자신이 직접 먼길 적장을 만나러 가야 하며 잘못하면 자신의 목숨이 위태로워질 각오도 해야 한다.

 

문서화된 전령도 없다. 만약 같은 얘기를 여러 상대편 주군에게 전해야 한다면 문제는 더 심각해 진다. 주군은 같은 얘기라도 여러 상대편 주군을 직접 만나 계속 반복하여 처음부터 차곡차곡 얘기해야 한다. 사자와 전령이 없는 주군은 마치 독수리앞의 앵무새와 같다.

 

또는 사자는 있는데 전령이 없는 경우라면, 사자는 주군이 전하는 내용을 알고 가야 한다. 사자가 주군이 전하는 내용을 알고가는 문제는 심각하다. 만약 제갈량의 사자가 동맹군 손권에게 '몇날 몇시에 조조의 형주성을 공략합시다.' 라는 내용을 전하러 가던중 조조군에게 붙잡힌다면 중대한 비밀이 고스란히 조조군에게 넘어간다. 만약 전령이 있다면 문서화된 전령이 암호화 되어 있다고 가정하면 해독하는데 애를 먹었을 것이다.

 

또한 똑같은 내용을 여러 사자 통해 여러 주군에게 보내야 한다면, 주군은 같은 명령을 앵무새처럼 반복하여 사자들에게 주입해야 한다. 만약 문서화된 전령이 있다면 단지 전령 몇개를 복사하여 사자들에게 나눠주면 될 것이다.

 

앞에 객체지향의 여러 요소를 배울때 클래스는 하나의 일만 잘해야 하며 중복된 코드가 없어야 된다고 알고 있다. 만약 주군 객체가 적장 객체에게 보낼 '요청'을 직접 코딩한다면 클래스는 하나의 일만 잘해야 한다. 라는 SRP 디자인 원칙과 어긋날 수 있다. 또한 변하는 것을 캡슐화하라는 디자인 원칙을 대입하면 적장 객체에게 보낼 '요청'은 매 요청마다 변할수 있는 내용들이므로 따로 관리하는것이 좋다.

 

무엇보다 주군 객체의 '요청'은 변할수 있는 내용이면서 여러 적장 객체에게 중복하여 보낼수 있는 내용들이다. 만약 중복되는 요청도 여러 적장에게 보낼때 중복된 코드로 작성 되어진다면 공통되는 부분을 추출하여 추상화하고 한곳에 두어 중복을 피한다. 라는 DRY 원칙과도 어긋난다.

 

또는 명령 전달자 객체는 있고 명령 객체는 없다면 명령 전달자는 클라이언트가 전하려는 명령을 하나하나 알고 코딩해야 되기 때문에 의존성이 높아지고 결합도가 높아진다. 명령 전달자의 코딩이 지저분해지면서 클래스는 하나의 일만 잘해야 한다.는 SRP 디자인 원칙과도 어긋나게 된다. 역시 중복되어 코딩되어질수 있기 때문에 DRY원칙과도 어긋난다.

 

3. 만약 아군과 타군과의 교류에 '사자'와 '전령'요소가 다시 들어간다면, 사자와 전령의 고마움에 주군은 감사해 할 것이다.

 

먼저 주군은 사자를 통해 전령을 보내므로 군비확장, 백성긍휼등의 자신의 중요한 본업에 더욱 더 집중할 수 있다. SRP 원칙 준수

 

만약 여러 상대편 주군에게 전령을 보낸다면 문서화된 전령을 여러 사자에게 뿌리기만 하면 되기 때문에 업무가 간편해진다. DRY 원칙 준수

 

사자는 문서화된 전령을 몸에 품고 상대편 주군에게 전달만 하면 되기 때문에 아무것도 모른체 홀가분하게 임무를 수행한다. 낮은 의존성 확보, 낮은 결합도 높은 응집성 준수

 

이렇게 명령을 따로 캡슐화하면 여러가지 바람직한 객체지향 효과를 얻게 된다. 비록 삼국지의 사자와 전령은 너무도 당연한 과거 역사의 교류과정이지만 실제 프로그래밍에서는 요청을 별도로 캡슐화하는 과정이 낯설어 명령을 전달하는 클라이언트 객체가 직접 명령을 코딩하는 경우가 대부분일 것이다. 이제 명령을 직접 코딩 할 경우의 문제점과 커맨드 패턴을 알고 명령하는 부분을 별도의 객체로 캡슐화하여 누릴수 있는 여러가지 유익함을 알았기 때문에 커맨드 패턴을 잘 배워서 써먹게 될 것이다.

 

4. 삼국지의 주군 혼자 알아서 다하는 프로그램은 다음과 같이 구성된다.

 

[주군은 직접 전령작성과 전달도 수행하며 여러 나라에 의존하는 문제를 안고 있다.]

 

주군은 백성긍휼, 군비확장의 자신이 가장 잘해야 하는 일을 해야 되지만, 명령작성 및 수행 업무 때문에 잘 못하고 있다. 명령 수행이라도 대리인을 만들어 보자. (명령작성의 비전문가인 대리인이 명령을 알아야 하고 작성해야하는 문제가 있다. 또한 사자가 여러나라를 다 알아야 한다.)

    

주군 = client, 사자=invoker, 전령=command, 적군=receiver

[중간에 사자가 전령을 대신 전달한다.]

 

이제 주군은 명령 수행을 사자에게 위임했지만, 주군이 직접 명령을 작성하거나 사자가 명령 작성을 대신해야 하기 때문에 주군이나 사자나 명령에 의존적이고 명령이 중복되는 위험을 안고 있다.

 

[명령 템플릿을 두어 명령의 양식이 통일되었고, 명령 작성이 쉬워졌으며, 이제 각 역할별로 명확히 분담이 되었다.]

 

public class King {

 

    public static void main(String[] args) {

        Saja saja = new Saja();

 

        Ohnara ohnara = new Ohnara();

 

        OhnaraCommand oCommand = new OhnaraCommand(ohnara);

 

        saja.setCommand(oCommand);

        saja.runOrderTravel("오나라는 항복하라.");

        saja.backTravel();

    }

} 

[주군 클래스, 사자를 임명(생성)하고, 나라는 오나라를 지정한다. 오나라에 보낼 명령을 생성한다. 사자에게 오나라에 보낼 명령서를 전달한다.]

 

public class Saja {

    CommandTemplate command;

 

    public Saja() {

 

    }

 

    public void setCommand(CommandTemplate inCommand) {

        this.command = inCommand;

    }

 

    public void runOrderTravel(String order) {

        command.execute(order);

    }

 

    public void backTravel() {

        command.undo();

    }

} 

[사자 클래스, 사자 클래스는 정해진 규격의 정식 명령서를 받을 준비가 되어 있다.]

 

public interface CommandTemplate {

 

    public void execute(String comment);

    public void undo();

} 

[명령 템플릿 클래스, 실행과 취소로 구성된다.]

 

public class OhnaraCommand implements CommandTemplate {

    Ohnara oh;

    String memoryComment;

 

    public OhnaraCommand(Ohnara inOh) {

        this.oh = inOh;

    }

 

    @Override

    public void execute(String comment) {

        this.memoryComment = comment;

        oh.receiveJunryung(comment);

    }

 

    @Override

    public void undo() {

        oh.undoJunryung(memoryComment+"취소 하라고 합니다.");

    }

 

} 

[오나라 커맨드 클래스, 오나라에 보낼 전용 커맨드 클래스이다.]

 

public class Ohnara {

    public void receiveJunryung(String comment) {

        System.out.println("전령을 받았소. ["+comment+"] 내용이군.");

    }

 

    public void undoJunryung(String undoComment) {

        System.out.println("["+undoComment+"] 방금 전령이 무효라니 알았소.");

    }

}

[오나라 클래스]

 

이제 전형적인 커맨드 패턴이다. 주군이 전령을 선택하여 사자에게 넘겨주면 사자는 어떤 전령인지는 몰라도 된 상태에서 상대편 주군에게 전령을 넘겨줄 수 있다.

 

실행된 커맨드를 취소할수도 있다. 예를들어 조조가 손권에게 가족중 한명을 인질로 바치라는 전령을 보냈는데 뒤늦게 정보를 입수해보니 손권이 군사력이 더 강해졌다는 것을 알고 먼저 보낸 전령을 취소하려 한다.

 

public class OhnaraCommand implements CommandTemplate {

    Ohnara oh;

    String memoryComment;

 

    public OhnaraCommand(Ohnara inOh) {

        this.oh = inOh;

    }

 

    @Override

    public void execute(String comment) {

        this.memoryComment = comment;

        oh.receiveJunryung(comment);

    }

 

    @Override

    public void undo() {

        oh.undoJunryung(memoryComment+"취소 하라고 합니다.");

    }

 

} 

[오나라 커맨드 클래스]

 

public class Ohnara {

    public void receiveJunryung(String comment) {

        System.out.println("전령을 받았소. ["+comment+"] 내용이");

    }

 

    public void undoJunryung(String undoComment) {

        System.out.println("["+undoComment+"] 방금 전령이 무효라니 알았소.");

    }

} 

[오나라 클래스]

 

이렇게 하면 마지막에 실행되어진 명령을 쉽게 취소할 수 있다.

 

Null 처리는 귀찮지만 꼭 체크해야 한다. Null 체크를 안해서 프로그램이 다운될 수 있다. Command 객체에 아무일도 하지 않는 NoCommand를 작성하면 Null 체크를 하지 않아도 된다.

 

Command noCommand = new NoCommand();

onCommand = noCommand

 

// 객체를 사용하면 아래if 문을 생략하고 사용할 있음.

if (onCommand != null) {

onCommand.execute();

}

[널 체크를 하지 않아도 된다.]

 

자주보내는 전령을 좀더 세분화하고 그 전령들을 모아서 한꺼번에 보내려고 한다. 예를 들어 '공량미를 보내시오.', '특산물을 보내시오.', '선박을 보내시오.', '지키지 않을 경우 각오하시오' 이런 전령들을 한꺼번에 모아서 보내는 것이다. 이 방법을 매크로 커맨드라고 한다.

 

public class MacroCommand implements Command {

Command[] command;

 

public MacroCommand(Command[] command) {

this.command = command;

}

 

public void execute() {

for (int i = 0; i < command.length; i++) {

command[i].execute();

}

}

 

public void undo() {

for (int i = 0; i < command.length; i++) {

command[i].undo();

}

}

} 

[배열과 for문을 이용하여 한번에 여러 Command를 실행한다.]

 

이렇게 하여 자주 쓰는 전령들을 세분화하고 추상화하여 한꺼번에 묶어서 보낼수도 있다.

 

5. 내가 처음 커맨드 패턴을 배울때는 많이 낯설었다. 예를들어 자동차, 휴대폰등의 사물을 객체화한것에 비교할때 커맨드 패턴은 '명령'을 객체화 시키고 캡슐화 시킨것이라 뜬구름 잡는 것 같았다.

 

그러나 어느날 삼국지의 사자와 전령을 떠올리면서 오히려 현실세계, 그것도 고대에서 커맨드 패턴과 비슷한 흐름이 이루어졌다는 것에 놀랐다.

 

커맨드 패턴을 통해 요청 자체를 객체로 캡슐화하여 요청을 하는 객체와 요청을 수행하는 객체를 분리한다. 그래서 클라이언트에서 요청을 자유롭게 변경 및 확장 하고 요청에 관한 중복 코드와 의존성을 줄일 수 있는 디자인 패턴이다.

[커맨드 패턴 원본]

 

덧 ) 이 객체지향의 탄생 원고는 제가 책으로 내려다가 일단 잘 안되었는데요. 이유는 비문이 많다. 단락내 주제가 중복된다. 어떤 상황 설명을 과장한다.등 입니다. 그래도 원고를 일단 블로그에 몽땅 풀어보고 언젠가 제대로 교정해서 다시 도전할 생각입니다. 이점을 감안해서 읽고 객체지향을 이해하는데 도움이 되셨으면 좋겠습니다. 의견도 주셨으면 좋겠습니다. 원고 조금만 교정하면 괜찮을것 같은 출판사 관계자분의 피드백도 환영합니다. 특별한 일 없으면 매주 월수 발행 예정입니다.