1. 이터레이터 패턴이란

1.1 정의

 반복자를 사용하여 컨테이너를 가로지르며 컨테이너의 요소들에 접근하는 디자인 패턴


 이터레이터 패턴을 사용하면 집합체 내에서 어떤 식으로 일이 처리되는지 몰라도 그 안에 들어있는 항목들에 대해서 반복작업을 수행 할 수 있다.

 예를 들어, 집합체가 C++의 vector이든, list이든 상관 없이 같은 코드로 반복 작업을 수행할 수 있게 하는 것이다.



1.2 구조




 여기서 Client는 Aggregate라는 집합체 클래스에 대해 어떤 반복 작업을 수행하려고 한다.

 반복 작업은 Aggregate의 createIterator() 메소드로 Iterator를 생성해서수행한다.


 Iterator의 메소드들은 조금 다르게 구성되는 경우도 있다. 이 다이어그램에서 Iterator는 java.util.Iterator이고 C++의 Iterator의 경우 ++, -- 연산자를 사용할 수 있고, begin(), end() 등의 메소드를 포함한다.

 중요한 것은 어떤 구조이든지간에 집합체 내부 구조와 상관없이 반복 작업을 수행할 수 있다는 것이다.



2. 장점

- 집합체 클래스의 응집도를 높여준다.

- 집합체 내에서 어떤 식으로 일이 처리되는지 알 필요 없이, 집합체 안에 들어있는 모든 항목에 접근 할 수 있게 해준다.


 응집도는 클래스나 모듈이 특정 목적이나 역할을 얼마나 일관되게 지원하는지를 나타내는 척도다.

 이터레이터 패턴을 사용하면 원래 클래스의 역할(집합체 관리)에서 반복 작업이라는 다른 역할을 분리시켜 응집도를 높일 수 있고, 그 덕에 컬랙션 변경에 의한 클래스의 변경, 반복자 기능 변경에 의한 클래스 변경을 방지할 수 있다.


 이터페이터 패턴을 사용하면 집합체 내에서 어떤 식으로 일이 처리되는지 알 필요 없이 반복작업을 수행할 수 있다.

 집합체가 ArrayList로 이루어져있든, List로 이루어져있든 상관없이 똑같은 코드로 반복 작업을 수행 할 수 있는 것이다.


3. 레퍼런스

- Head First Design Pattern(O'REILLY media)

https://ko.wikipedia.org/wiki/%EB%B0%98%EB%B3%B5%EC%9E%90_%ED%8C%A8%ED%84%B4

https://www.geeksforgeeks.org/iterators-c-stl/ : C++ iterator example






블로그 이미지

서기리보이

,

1. 퍼사드 패턴이란

1.1 퍼사드 패턴의 정의

어떤 서브시스템의 일련의 인터페이스에 대해 통합된 인터페이스를 제공하는 패턴.

고수준의 인터페이스를 정의하여 서브시스템을 더 쉽게 사용가능하게 함.


퍼사드 패턴은 인터페이스를 단순화시키기 위해서 사용된다.


어떤 작업을 하기 위해서 다수의 인터페이스로 구성된 서브시스템을 사용해야 할 때, 그 서브시스템을 사용하기 쉽게 포괄적인 인터페이스를 정의하는 패턴이다.


여기서 말하는 서브시스템은 실제 구현에서는 표현되지 않았을 수 있고, 전체 시스템 중의 일부분을 묶어서 서브시스템이라고 표현하는 것이다.


UML만으로 설명하면 더 헷갈릴 수 있을 것 같아서 바로 예제로 넘어가도록 하겠다.


2. 예제


Head First Desgin Pattern에 나오는 예제를 수정한 것이다.


한참동안 준비하여 DVD 플레이어, 프로젝터, 자동 스크린, 앰프 오디오, 팝콘기계, 라디오 등을 집에 배치했다.

이를 클래스 다이어그램을 표현하면 다음과 같다.



이제 이 장비들을 이용해 영화를 보고자 한다.

영화를 보려면 몇 가지 일들을 해야한다.


  1. 팝콘 기계를 켠다.

  2. 팝콘를 튀긴다

  3. 프로젝터를 켠다.

  4. 프로젝터에 DVD 신호가 입력되게 연결한다.

  5. 프로젝터를 와이드 스크린 모드로 전환한다.

  6. 앰프 오디오를 켠다.

  7. 앰프를 서라운드 음향 모드로 전환한다.

  8. 앰프 볼륨을 중간(5)로 설정한다.

  9. DVD 플레이어를 켠다.

  10. DVD를 재생한다.


이 작업을 하기 위해서 필요한 코드를 보자.


popcornPopper->on(); popcornPopper->pop(); projector->on(); projector->wideScreenMode(); amplifier->on(); amplifier->setDvd(this->dvdPlayer); amplifier->setSurroundSound(); amplifier->setVolume(5); dvdPlayer->on();


굉장히 복잡하다.

여기서 끝이 아니라 영화가 끝나고 나면 모든 장비들을 특정 순서대로 꺼야하고, 라디오를 들을 때도 복잡한 과정을 거쳐야한다.


퍼사드 패턴을 적용하면 클라이언트 측에서는 이런 복잡한 과정들을 거치지 않아도 된다.

퍼사드 패턴을 적용하면 어떻게 되는지 보자.



HomeTheaterFacade 클래스가 추가되었다.

이제 영화를 보고, 영화 보기를 끝내는 일련의 작업은 HomeTheaterFacade 클래스에서 처리하고, 클라이언트들은 이 클래스 객체에 대한 레퍼런스를 얻어 watchMoive(), endMovie() 메소드를 호출하면 된다.


watchMoive() 메소드와 endMovie() 메소드는 다음과 같이 구현된다.


//HomeTheaterFacade.cpp


void HomeTheaterFacade::watchMovie() { cout << "영화보기 모드 실행" << endl; this->popcornPopper->on(); this->popcornPopper->pop(); this->projector->on(); this->projector->wideScreenMode(); this->amplifier->on(); this->amplifier->setDvd(this->dvdPlayer); this->amplifier->setSurroundSound(); this->amplifier->setVolume(5); this->dvdPlayer->on(); this->dvdPlayer->off(); } void HomeTheaterFacade::endMovie() { cout << "영화 끄기" << endl; this->popcornPopper->off(); this->screen->up(); this->projector->off(); this->amplifier->off(); this->dvdPlayer->stop(); this->dvdPlayer->eject(); this->dvdPlayer->off(); }



구현된 프로젝트는 다음의 깃허브 페이지에서 확인 할 수 있다.

(https://github.com/InvincibleTyphoon/FacadePatternExample/tree/master)


3. 퍼사드 패턴의 장점


- 서브시스템의 복잡한 인터페이스를 단순화된 하나의 인터페이스로 묶을 수 있음

- 클라이언트 구현과 서브시스템을 분리 할 수 있음

- 서브시스템을 캡슐화하지 않기 때문에 서브시스템의 클래스에 직접 접근하여 저수준 기능도 그대로 이용 할 수 있음


퍼사드 패턴을 이용하면 복잡한 서브시스템을 하나의 인터페이스로 묶을 수 있다.


클라이언트의 구현과 서브시스템이 분리되기 때문에 서브시스템의 구현에 변화가 생겨도 클라이언트 코드는 수정하지 않아도 된다.


퍼사드 클래스로 간단하게 원하는 작업을 수행 할 수 있고, 퍼사드 패턴을 무시하고 서브시스템을 구성하는 클래스들을 그대로 사용 할 수도 있다.

퍼사드 클래스를 적용해도 원래의 시스템에서 아무런 제약이 추가되지 않는 것이다.


4. 레퍼런스

- Head First Design Pattern(O'REILLY media)

- GoF의 디자인 패턴(Pearson Addison Wesley)


블로그 이미지

서기리보이

,

1. 어댑터 패턴이란

1.1 어댑터 패턴의 정의

특정 클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환하는 패턴


어댑터 패턴은 클라이언트가 요구하는 인터페이스와 호환되지 않는 인터페이스를 가져서 함께 동작 할 수 없는 클래스를 함께 동작 할 수 있게 해준다.

그리고 어댑터패턴은 오브젝트 어댑터(Object Adapter)와 클래스 어댑터(Class Adapter)로 구현방법이 나뉘어지는데,  클래스 어댑터가 가지는 몇 가지 단점 때문에 이 글에서는 오브젝트 어댑터를 주로 다루려고 한다.



1.2 어댑터 패턴에 대한 대략적 이해


A라는 클래스를 B라는 클래스와 함께 동작하게 하고 싶지만,  B 클래스와 함께 동작하려면 C 인터페이스 형태를 따르도록 구현되어야 한다.

하지만 기존 코드를 수정하는 것(A 클래스가 C 인터페이스를 상속하도록 수정하거나 B 클래스가 A 클래스와 함께 동작할 수 있도록 수정하는 것)은 비효율적이고 오류를 일으키는 원인이 되는 일이므로 A 클래스를 C 인터페이스에 맞게 변환해주는 Adapter를 중간에 추가하여 문제를 해결한다.



그러면 A 클래스가 B 클래스와 함께 동작 할 수 있다.


1.3 오브젝트 어댑터(Object Adapter)

오브젝트 어댑터는 구성(Composition)을 사용하여 어댑터 패턴을 구현한다. UML과 같이 보도록 하자.



Client - Target 인터페이스를 요구하는 요소를 지닌 클래스

Target - 어떤 인터페이스

Adaptee - Client의 Target 인터페이스를 요구하는 요소에 집어넣고 싶은 클래스. Target 인터페이스에 호환되지 않음.

Adapter - Adaptee 클래스를 Target 인터페이스에 맞춰주는 클래스


여기서 Adapter는 Target 인터페이스의 request() 메소드를 구현 하기 위해 Adaptee 클래스의 method1() 메소드를 사용 할 것이다.

그러면 Client는 Adapter 클래스를 통해 Adaptee를 Target 인터페이스의 구상 클래스처럼 사용할 수 있다.


1.4 클래스 어댑터(Class Adapter)

클래스 어댑터는 상속(Inheritance)를 사용하여 어댑터 패턴을 구현한다. 이것도 UML과 같이 보도록 하자.



Client - Target 인터페이스를 요구하는 요소를 지닌 클래스

Target - 어떤 인터페이스

Adaptee - Client의 Target 인터페이스를 요구하는 요소에 집어넣고 싶은 클래스. Target 인터페이스에 호환되지 않음.

Adapter - Adaptee 클래스를 Target 인터페이스에 맞춰주는 클래스


클래스 어댑터는 다중 상속을 사용한다.

클래스 어댑터 패턴에서는 Adapter가 Target 클래스와 Adaptee 클래스를 둘 다 상속하여 Target 클래스가 필요한 곳에서도 사용 될 수 있고 Adaptee 클래스가 필요한 곳에서도 사용 될 수  있게 한다.


2. 예제

게임을 개발했다고 생각 해 보자.

개발하려는 게임은 정말 간단해서, 몬스터와 액티브 아이템, 그리고 UI로만 구성된다.


몬스터와 액티브아이템, UI는 각각 다른 인터페이스로 구현된다.

개발 초기에 클래스 다이어그램은 다음과 같을 것이다.




유저 인터페이스 클래스에서는 setActiveItem() 메소드로 사용할 액티브아이템을 결정하고, useItem() 메소드를 실행하면 ActiveItem 인터페이스의 useItem() 메소드를 이용하여 액티브아이템의 효과를 사용한다.


몬스터는 grawl() 메소드를 사용하면 울부짖는 소리를 낸다. 울부짖을때는 grawlSound 어트리뷰트에 저장된 소리로 울부짖는다.


그런데 게임 기획자가 새로운 아이디어를 떠올렸다. 유저가 몬스터를 포획하여 액티브아이템처럼 몬스터를 사용할 수 있게 하자는 것이다.

몬스터를 포획하면 몬스터가 액티브아이템으로 등록되고, 이 아이템을 사용하면 유저에게 몬스터가 울부짖는 소리를 들려준다.


이를 구현하고자 할 때 문제는 유저 몬스터 클래스는 유저 인터페이스의 setActiveItem() 메소드에 호환되지 않는다는 것이다.


위에서 언급했듯이, UserInterface에 setItem(Monster) 메소드를 추가하거나, Monster 인터페이스가 ActiveItem 인터페이스를 상속하게 하는 등 기존 코드를 수정하는 것은 좋은 선택이 아니다.


각각의 몬스터별로 새로운 아이템 클래스를 만드는 것도 좋은 방법이지만 그러면 몬스터의 숫자만큼 클래스의 수가 늘어나게 되기 때문에 더 좋은 방법이 있었으면 한다.


더 좋은 방법으로서 어댑터 패턴을 적용해보자.

몬스터를 액티브아이템처럼 사용될 수 있도록 적절한 어댑터를 만들어 클래스 구조를 다음과 같이 만든다.



MonsterToActiveItemAdapter가 바로 Monster 인터페이스를 ActiveItem 인터페이스처럼 사용 될 수 있게 해주는 클래스다.


MonsterToActiveItemAdapter 클래스의 useItem() 메소드는 Monster 인터페이스의 grawl() 메소드를 호출하도록 구현된다.

그래서 붙잡은 몬스터를 액티브아이템 처럼 사용 할 수 있다.


구현된 프로젝트는 다음의 깃허브 페이지에서 확인 할 수 있다.

(https://github.com/InvincibleTyphoon/AdapterPatternExample/tree/master)


3. 어댑터 패턴의 장점

- 인터페이스 호환성 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있음

- 상속이 아닌 구성(Composition)을 이용하기 때문에 Adaptee의 모든 서브클래스에 대해 어댑터를 사용가능(Object Adapter에만 해당)


인터페이스가 호환되지 않는 문제가 발생해도 어댑터 패턴을 적용하면 기존의 코드를 수정하지 않고도 문제를 해결 할 수 있다.


Class Adapter에는 적용되지 않는 장점이지만, 상속이 아닌 구성을 사용하기 때문에 Adaptee의 모든 서브클래스에 대해 어댑터를 적용 할 수 있고, 실행중에 동적으로 Adaptee를 바꿀수도 있다.



4. 오브젝트 어댑터 VS 클래스 어댑터

위의 예제에서는 오브젝트 어댑터를 적용했다.

오브젝트 어댑터와 클래스 어댑터를 비교했을 때, 서로다른 장단점이 있는데 우선 그 차이점을 비교해보자.

 

 Ojbect Adapter

Class Adapter 

장점 

상속이 아닌 구성(Composition)을 사용하기 때문에 더 유연하다 

Adapter가 Adaptee의 서브클래스이기 때문에 Adaptee의 행동을 오버라이드 할 수 있다.


Adaptee 객체를 만들지 않아도 된다. 

단점

 Adaptee 객체를 만들어야 사용가능하다.

상속을 이용하기 때문에 한 Adapter 클래스가 특정 Adatee 클래스에만 적용가능하다.


다중상속이 지원되는 언어에서만 사용가능하다. 



Object Adapter는 상속이 아닌 구성을 사용하기 때문에 더 유연하다. 하지만 Adaptee 객체를 만들어야하기 때문에 Class Adapter에 비해 관리해야할 객체가 하나 늘어난다는 단점이 있다.


Class Adapter는 Adapter가 Adaptee의 서브클래스이기 때문에 Adaptee의 행동을 오버라이드하여 사용 할 수 있고. Adaptee 객체를 따로 만들지 않고 Adapter 객체를 만들어서 사용가능하다는 장점이 있다. 관리해야 할 객체가 하나 적다는 것이다.

하지만 Class Adapter는 상속을 사용하기 때문에 한 Adapter 클래스가 특정 Adaptee 클래스 하나에 대해서만 적용된다. 그 Adaptee클래스의 서브클래스에 대해서 적용 불가능하고, 실행 중간에 동적으로 변경도 불가능한 것이다.

그리고 이것은 치명적인 단점이라고 할 수 있는데, 자바처럼 다중상속이 지원되지 않는 언어에서는 사용이 불가능하다.


개인적인 견해로 말하자면 유연성 측면에서 Object Adapter의 장점이 Class Adapter의 장점보다 낫고, Object Adapter의 단점은 그다지 치명적이거나 하지 않다고 본다.

상황에 따라서 어떤 방법을 쓸 지 선택애햐겠지만, 이 두가지 방법 중 한가지를 선택해야하는 상황이라면 나는 Object Adapter를 사용할 것 같다.


Head First Design pattern에서 클래스 어댑터는 객체어댑터에 비해 Adaptee 전체를 다시 구현하지 않아도 된다는 장점을 가진다고 하는데, 이에 대해 객체 어댑터도 Adaptee에게 필요한 일을 시키는 코드만 구현하면 된다고 답하는 내용이 있는 걸로 봐서, 클래스 어댑터 패턴이 Adaptee에게 일을 시키는 코드를 구현하지 않다도 되는게 장점이라는 의미로 그렇게 언급하는 것 같다.





5. 레퍼런스

- Head First Design Pattern(O'REILLY media)

- 위키피디아 Adapter Pattern(https://en.wikipedia.org/wiki/Adapter_pattern)

블로그 이미지

서기리보이

,

1. 커맨드 패턴이란

1.1 커맨드 패턴의 정의

요청을 객체로 캡슐화하여 클라이언트가 보낸 요청을 나중에 이용 할 수 있도록 메서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소 할 수 있게 하는 패턴.


이 정의에서 핵심은 요청을 캡슐화한다는 점이다.

요청이 캡슐화되기 떄문에 로그를 출력하거나 실행을 취소할 때 유용하게 사용 될 수 있다.


1.2 UML





Command - 추상화된 커맨드 인터페이스. execute() 메소드로 실행하고 undo() 메소드로 실행을 취소한다.

ConcreteCommand - Command 인터페이스를 구현한 구상 클래스

Client - ConcreteCommand를 생성하고 Receiver와 커맨드를 연결시키는 클래스

Invoker - 생성된 커맨드를 가지는 클래스.

Receiver - 요구사항을 수행하는 클래스


위 다이어그램에서 클라이언트가 리시버와 커맨드 객체를 생산하고, 커맨드를 보낼 때는 인보커를 통해서 보내게 된다.




2. 적용 예제


Head First Desgin Pattern에 나오는 예제를 수정한 것이다.


주식회사 홈 오토메이션에서는 가정집의 전등, 차고문, 오디오, 선풍기 등을 조종할 수 있는 만능 리모컨을 개발하려고 한다.

만능 리모컨은 전자기기와 연결 할 수 있는 7개의 슬롯과 그 각각을 켜고 끄는 버튼, 그리고 하나의 취소버튼을 가진다.

그런데 문제는 전자기기들의 작동 방식이 서로 다르다는 것이다.

각 전자기기들을 클래스화한 클래스 다이어그램을 보면서 이야기 하도록 하겠다.




켜기/끄기 버튼만을 가진 리모컨으로 조종하기엔 작동 방식이 너무 제각각이다. 전등(Light)의 경우는 켜고 끄는게 다지만, 오디오(Stereo)는 작동시키기 위해 setCD() 메소드로 CD명을 입력하고, setVolume() 메소드로 볼륨도 조절해줘야하고, 선풍기는 high(), medium(), low() 메소드로 속도를 조절해야한다.


리모컨의 몇번 슬롯에 어느 전자기기가 연결될 지 알 수 없기 때문에 버튼이 눌렸을 때 if-else 문으로 슬롯과 연결된 클래스의 타입을 검사하여 실행하도록 구현되어야 할 것이다.


하지만 그렇게 구현하면 새로운 클래스가 추가될 때마다 리모컨에 있는 코드를 고쳐야 되고, 이는 버그 발생률을 높이는 원인이 된다.


이런 문제를 해결하기 위해 커맨드 패턴을 적용해보도록 하자.


2.1 커멘드 인터페이스 및 구상 클래스 정의


커맨드 패턴 적용을 위해 Command 인터페이스를 만들고 모든 커맨드 클래스들이 이 인터페이스를 구현하도록 한다.

Command 인터페이스는 다음과 같이 리시버에세 요청을 실행하라고 전달하는 execute() 메소드와 실행을 취소하라고 전달하는 undo() 메소드를 가진다.



이제 리모컨 클래스는 이 Command 인터페이스를 이용하여 execute(), undo()메소드가 어떻게 구현되었는지는 신경쓰지 않도록 구현한다.


LightOnCommand

//LightOnCommand.h

#pragma once #include "Command.h" #include "../HomeObejcets/Light.h" //불 켜기 커맨드 //light의 name 어트리뷰트를 통해 어느 곳에 있는 전등인지 구별함 class LightOnCommand : public Command { public: LightOnCommand(Light* light); void execute(); void undo(); private: Light * light = NULL; };


//LightOnCommand.cpp

#include "LightOnCommand.h" LightOnCommand::LightOnCommand(Light* light) { this->light = light; } void LightOnCommand::execute() { light->lightOn(); } void LightOnCommand::undo() { light->lightOff(); }



StereoOnCommand

//StereoOnCommand.h

#pragma once #include "Command.h" #include "../HomeObejcets/Stereo.h" //노래 켜기 커맨드 class StereoOnCommand : public Command { public: StereoOnCommand(Stereo * stereo); void execute(); void undo(); private: Stereo * stereo = NULL; };


//StereoOnCommand.cpp

#include "StereoOnCommand.h" StereoOnCommand::StereoOnCommand(Stereo * stereo) { this->stereo = stereo; } void StereoOnCommand::execute() { this->stereo->setCD(*new string("Sonyun Jump")); this->stereo->setVolume(13); this->stereo->stereoOn(); } void StereoOnCommand::undo() { this->stereo->stereoOff(); }


참고로 StereoOnCommand의 execute() 메소드에서 설정하는 CD와 볼륨 값은 임의로 넣은 값이다. 사용자 편의를 고려해서 커스텀하면 된다.


2.3.1 매크로 커맨드 구현


커맨드들 중 간혹 다수의 커맨드를 연속으로 수행해야하는 경우가 있다.

이런 커맨드를 매크로 커맨드라고 부르는데, 매크로 커맨드는 Command 인터페이스에 대한 레퍼런스를 리스트로 가지고, 그 각각의 excute() 메소드를 호출하는 것으로 execute() 메소드가 구현된다.


//MacroCommand.h

#pragma once #include <vector> #include <algorithm> #include "Command.h" using namespace std; //다수의 커맨드를 실행하는 커맨드 class MacroCommand : public Command { public: MacroCommand(vector<Command*>& commands); void execute(); void undo(); private: vector<Command*> commands; };


//MacroCommand.cpp

#include "MacroCommand.h" MacroCommand::MacroCommand(vector<Command*>& commands) { this->commands = commands; //copy(commands.begin(), commands.end(), this->commands); } void MacroCommand::execute() { int size = this->commands.size(); for (int i = 0; i < size; i++) commands[i]->execute(); } void MacroCommand::undo() { int size = this->commands.size(); for (int i = 0; i < size; i++) commands[i]->undo(); }


우리 프로젝트에서는 PartyMode이라는 커맨드를 지원한다. PartyMode는 파티를 위해서 모든 문을 열고, 모든 전등을 켜고, 오디오를 켜는 커맨드로, 매크로 커맨드 클래스로 정의된다.


//main.cpp

vector<Command*> partyOn = { kitchenLightOnCommand,bedroomLightOnCommand,garageDoorOpenCommand,stereoOnCommand }; vector<Command*> partyOff = { kitchenLightOffCommand,bedroomLightOffCommand,garageDoorCloseCommand,stereoOffCommand}; Command * partyOnCommand = new MacroCommand(partyOn); Command * partyOffCommand = new MacroCommand(partyOff);





2.2 Invoker 역할의 RemoteController 정의


이제 위의 Command 인터페이스를 구현한 LightOnCommand, StereoOnCommand 클래스 코드를 예시로 어떻게 구현되는지 보도록 하자.




onCommands, offCommands는 각각 슬롯에 연결된 전자기기를 켜고 끄는 커맨드 클래스들을 저장한다.


각 슬롯에 커맨드를 할당하기 위해서 setCommand() 메소드로 i번 슬롯에 onCommand와 offCommand를 동시에 입력한다.


각 슬롯에 할당된 on/off 버튼을 누르기 위해서는 on/offButtonWasPushed() 메소드를 이용한다.


그리고 실행 취소를 위해서 unDoButtonWasPushed() 메소드를 사용한다.


다음은 구현된 RemoteController 클래스의 구현 코드다.


//RemoteController.h

#pragma once #include <vector> #include <stack> #include <iostream> #include "Commands/Command.h" #include "Commands/NoCommand.h" using namespace std; //여러 커맨드를 묶어서 리모컨처럼 사용하는 클래스 class RemoteController { public: RemoteController(); void setCommand(int slot, Command* onCommand, Command* offCommand); void onButtonClicked(int slot); void offButtonClicked(int slot); void undoButtonClicked(); private: //켜기 커맨드를 모은 벡터 vector<Command*> onCommands; //끄기 커맨드를 모은 벡터 vector<Command*> offCommands; //커맨드 실행 기록 stack<Command*> commandStack; };

//RemoteController.cpp

#include "RemoteController.h" RemoteController::RemoteController() { this->offCommands.resize(7); this->onCommands.resize(7); Command * noCommand = new NoCommand(); for (int i = 0; i < 7; i++) { this->offCommands[i] = noCommand; this->onCommands[i] = noCommand; } } void RemoteController::setCommand(int slot, Command* onCommand, Command* offCommand) { this->onCommands[slot] = onCommand; this->offCommands[slot] = offCommand; } void RemoteController::onButtonClicked(int slot) { this->onCommands[slot]->execute(); this->commandStack.push(onCommands[slot]); } void RemoteController::offButtonClicked(int slot) { this->offCommands[slot]->execute(); this->commandStack.push(offCommands[slot]); } void RemoteController::undoButtonClicked() { if (commandStack.empty()) { cout << "Remote Controller Undo Caution : no command history" << endl; return; } cout << "undo : "; commandStack.top()->undo(); commandStack.pop(); cout << endl; }


생성자에서 NoCommand 라는 클래스를 이용하는데, 이는 Command 인터페이스를 구현한, 아무런 기능을 하지 않는 클래스다.

슬롯이 비어있을 경우 if문을 이용하여 NULL인지 체크하는 것보다 일종의 NULL 객체인 NoCommand를 넣어 아무런 기능도 하지 않도록 구현했다.


이제 필요한 클래스들은 모두 구현되었다.

구현된 구조를 1.2절의 UML과 연관지어보면 다음과 같다.




이 시스템에서 클라이언트는 따로 클래스화 할 필요가 없어보여서 main() 함수가 클라이언트의 역할을 하도록 했다.


LightOnCommand, LightOffCommand는 Command 인터페이스를 구현한 구상 클래스다. 이 두 클래스를 이용해 전등을 켜고 끈다.


RemoteController는 Invoker의 역할이다. 7개의 슬롯에 각각의 커맨드를 할당하고 슬롯 번호로 사용한다.


이제 리모컨의 켜기/끄기 기능은 구현이 완료되었으니 undo 버튼 구현을 위한 내용들을 살펴보도록 하자.


2.3 UnDo 구현


UnDo 버튼은 직전에 실행했던 기능을 취소하는 기능이다.


우선 각 커맨드 구상 클래스에서 Undo 기능이 어떻게 구현되는지 살펴보겠다.


2.3.1 LightOn/OffCommand 클래스의 UnDo 구현


첫 번째는 가장 간단한 LightOnCommand 클래스다.


//LightOnCommand.cpp


void LightOnCommand::undo() { light->lightOff(); }

LightOnCommand는 불을 켜는 커맨드이기 때문에 불을 꺼주는 것으로 Undo 기능이 구현된다.


LightOffCommand는 반대로 불을 켜주는 것으로 Undo 기능을 구현하면 된다.


//LightOffCommand.cpp


void LightOffCommand::undo() { light->lightOn(); }


2.3.2 State와 Stack 이용한 FanSpeedChangeCommand, FanOffCommand 클래스의 Undo 구현


작업취소 기능을 구현할 때는 상태(State)를 저장해야하는 경우가 종종 있다.

선풍기의 경우도 그런데, Fan 클래스의 코드를 보면서 이야기해보도록 하겠다.


//Fan.h

#pragma once #include <string> #include <stack> #include <iostream> using namespace std; //선풍기 클래스 //꺼짐, 느린속도, 중간속도, 빠른속도의 상태를 가짐 class Fan { public: enum FanStatus { Off, LowSpeed, MediumSpeed, HighSpeed }; public: Fan(); /*********선풍기 속도 조절 메소드********/ void high(); void medium(); void low(); void off(); //////////////////////////////////////// //선풍기의 속도를 받아옴 FanStatus getSpeed(); string toString(); private: //현재 선풍기의 상태 FanStatus fanStatus; };

Fan은 Off(꺼짐), LowSpeed(느리게), MediumSpeed(중간속도), HighSpeed(빠르게) 의 상태를 갖는다.


이제 커맨드 클래스에서 작업 취소 기능을 구현하는 방법을 살펴보자. 작업 취소 기능을 구현하려면 커맨드가 실행되기 이전 상태를 알아야 한다.


그러기 위해 커맨드 클래스에 Fan::FanStatus prevStatus; 필드를 추가하면 될 것 같지만, 이렇게 하면 작업 취소를 여러 번 수행 할 수 없다. 

Undo버튼을 여러번 눌렀을 때 처리 할 수가 없다는 것이다.


이 문제를 해결하기 위해 스택을 활용한다.


//FanSpeedChangeCommand.h

#pragma once #include <stack> #include "Command.h" #include "../HomeObejcets/Fan.h" //선풍기 속도 조절 커맨드 //execute 시 꺼짐->느림->보통->빠름->느림->보통... 순으로 변경됨 class FanSpeedChangeCommand : public Command { public: FanSpeedChangeCommand(Fan * fan); void execute(); void undo(); private: Fan * fan = NULL; stack<Fan::FanStatus> fanStatusHistory; };

스택을 활용하면 커맨드가 실행되기 이전 상태가 쌓이고, 작업 취소를 수행 할 때 이 스택에서 상태를 pop 하여 작업 취소에 이용하면 된다.


이제 이 스택을 활용해서 작업 취소가 어떻게 구현되는지 살펴보자.


//FanSpeedChangeCommand.cpp

void FanSpeedChangeCommand::undo() { if (fanStatusHistory.empty()) { cout << "FanSpeedChangeCommand undo caution : no previous Ssatus exists" << endl; return; } Fan::FanStatus prev = fanStatusHistory.top(); fanStatusHistory.pop(); cout << "fan undo : (" << fan->getSpeed() << ") -> (" << prev << ")" << endl; switch (prev) { case Fan::Off: fan->off(); break; case Fan::LowSpeed: fan->low(); break; case Fan::MediumSpeed: fan->medium(); break; case Fan::HighSpeed: fan->high(); break; default: break; } }


FanOffCommand 클래스의 Undo 기능도 거의 유사하게 구현된다.

는지 살펴보자.


//FanOffCommand.cpp

void FanOffCommand::undo() { if (fanStatusHistory.empty()) { cout << "FanOffCommand undo caution : no previous Ssatus exists" << endl; return; } Fan::FanStatus prev = fanStatusHistory.top(); fanStatusHistory.pop(); cout << "fan undo : (" << fan->getSpeed() << ") -> (" << prev << ")" << endl; switch (prev) { case Fan::Off: fan->off(); break; case Fan::LowSpeed: fan->low(); break; case Fan::MediumSpeed: fan->medium(); break; case Fan::HighSpeed: fan->high(); break; default: break; } }


2.3.3 Stack을 이용한 RemoteController의 Undo버튼 기능 구현


지금까지 각 커맨드 클래스에서 작업 취소 기능을 구현하는 방법을 살펴보았다.

이제는 RemoteController 클래스에서 실제로 Undo 버튼을 클릭했을 때 어떻게 처리되는지 살펴볼 차례다.


RemoteController에서는 Undo 버튼을 여러번 클릭했을 때 작업 취소가 연속적으로 실행되어야 한다.

이는 앞에서 살펴본 선풍기 관련 커맨드의 실행취소 기능의 구현과 유사해서 다시 스택을 이용해서 구현된다.


RemoteController 클래스의 정의는 위에서 기술되었으니 undoButtonClicked() 메소드만 살펴보자.


//RemoteController.cpp

void RemoteController::undoButtonClicked() { if (commandStack.empty()) { cout << "Remote Controller Undo Caution : no command history" << endl; return; } cout << "undo : "; commandStack.top()->undo(); commandStack.pop(); cout << endl; }

commandStack은 stack<Command*>로 선언되었다.


구현된 프로젝트는 다음의 깃허브 페이지에서 확인할 수 있다.

(https://github.com/InvincibleTyphoon/CommandPatternExample/tree/master)



3. 커맨드 패턴의 장점


- 요구사항을 객체로 캡슐화 할 수 있다.

- 작업 취소 기능을 지원한다.

- 요청 내역을 큐에 저장할 수 있다.

- 요청 내역을 로그로 기록할 수 있다.


요구사항이 캡슐화되므로 요청을 보내는 내용의 코드 중복을 방지 할 수 있다.


작업 취소 기능을 지원하는 것은 앞서 살펴 본 바 있으니 넘어간다.


요청 내역을 큐에 저장하면 스레드 작업에 유용하다.

처리되는데 오래 걸리는 요청의 경우 스레드를 만들어 백그라운드에서 처리하는 것이 반응성을 높이는데 유용하다.

작업 큐에 다수의 커맨드 객체를 저장해두고 스레드를 여러개 만들면 각 스레드는 작업 큐에서 커맨드를 가져와 처리하면 된다. 그러면 어느 스레드가 먼저 작업을 마치든지 신경쓰지 않아도 된다.


어떤 어플리케이션에서는 모든 행동을 기록해놨다가 그 어플리케이션이 다운되었을 경우, 나중에 그 행동들을 다시 호출해서 복구 할 수 있도록 해야한다.

커맨드패턴에서는 로그를 디스크에 저장하는 store() 메소드와 로그를 읽어오는 load() 메소드를 추가하여 복구 기능을 구현 할 수 있다.



4. 레퍼런스

- Head First Design Pattern(O'REILLY media)

- GoF의 디자인 패턴(Pearson Addison Wesley)

블로그 이미지

서기리보이

,

1. Singleton 패턴이란

1.1 정의

특정 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근 할 수 있게 하기 위한 패턴.


싱글톤 패턴은 일반적으로 클래스의 생성자를 private으로 선언하고, 클래스 안에 static으로 자기 자신에 대한 레퍼런스를 두어 그 레퍼런스를 참조하게 하는 식으로 구현된다.


1.2 UML





Singleton 클래스는 instance라는 이름으로 자기 자신에 대한 레퍼런스를 static으로 가지고 접근 제한자는 private이다. 그리고 Singleton 클래스의 외부에서 이 클래스에 접근할 때는 getInstance()라는 static메소드를 통해 이 레퍼런스에 접근한다.


1.3 구현예시

UML 보다는 구현 예시로 보는 것이 이해가 빠를 듯 하다.

class Singleton { public: //싱글톤 객체를 받는다. static Singleton* getInstance()

{

if(instance == NULL)

instance = new Singleton();

return instance;

}

static Singleton* instance;

};

*C++에서 static Singleton* instance; 라고 선언만 해두면 instance가 정의되지 않았기 때문에 컴파일 오류가 난다. 원래라면 .cpp 파일에 Singleton::Singleton* instance; 라고 한 줄 적어주어야 오류가 안 나는데, 여기서는 이해를 위해서 해당 내용은 생략하였다.


getInstance() 메소드는 내부에 static으로 선언된 instance 변수가 NULL인지 확인하고, NULL이라면 새로운 Singleton 객체를 생성한 후 그 주소를 리턴하고, NULL이 아니라면 instance 변수에 있는 객체에 대한 주소를 리턴한다.

이런식으로 하면, 외부의 어떤 클래스에서든지 Singleton::getInstance()->method() 이런 식으로 Singleton 객체에 접근 할 수 있다.


1.4 싱글톤 패턴 적용시 고려사항


개인적인 경험으로, 싱글톤 패턴을 한번 사용해 보면 구현하기가 굉장히 편하고, 또 여기저기에 적용하기가 쉬워서 싱글톤 패턴을 남용하게 될 수도 있다.

싱글톤 패턴도 당연히 단점을 가지고 있기 때문에 싱글톤 패턴은 남용하지 않도록 다음의 원칙을 준수해야 한다.


- 클래스의 인스턴스가 유일함을 보장해야한다.

- 다른 모든 클래스에서 접근 가능해야한다.


싱글톤 패턴은 위의 두 가지가 모두 만족되는 경우에만 사용해야한다.


싱글톤 패턴을 적용한 클래스는 2개 이상의 인스턴스가 존재 할 수 있도록 설계를 변경하기 어렵다. 그렇기 때문에 싱글톤 패턴을 적용 할 때는 해당 클래스의 인스턴스가 하나여야함을 보장 할 수 있을 때에만 적용해야한다.


다른 모든 클래스들에서 접근 되는 것이 아니라면, 굳이 싱글톤 패턴을 적용할 필요가 없다.


2. 적용 예제

이 예제는 Head First Design Pattern의 예제를 가져온 것이다.


요즘은 거의 모든 초콜릿 공장에서 초콜릿을 끓이는 초콜릿 보일러를 컴퓨터로 제어한다. 초콜릿 보일러는 초콬릿을 채워 넣고, 끓이고, 보일러 안의 초콜릿을 비워내는 작업을 한다.


초콜릿 보일러는 한개만으로도 충분한 양의 초콜릿을 끓일 수 있고, 비싸기 떄문에 공장마다 단 하나의 초콜릿 보일러만을 가진다고 한다.


그리고 초콜릿 보일러는 다른 모든 클래스에서 접근 가능하게 구현되어야 한다. 초콜릿 보일러는 뜨거운 초콜릿을 관리하기 때문에 사고가 나게 하지 않기 위해 여러 안전장치들이 관리하고, 추후에 시스템에 이런 안전장치들을 클래스화하여 추가할 것이다.


그래서 초콜릿 보일러에 싱글톤 패턴을 적용하여 다음과 같이 구현한다.

<ChocolateBoiler.h>

#pragma once #include <iostream> using namespace std; //초콜릿을 끓이는 초콜릿 보일러 클래스 //싱글톤 패턴을 적용하여 구현됨 class ChocolateBoiler { public: //싱글톤 객체를 받는다. static ChocolateBoiler* getInstance(); //보일러에 초콜릿을 채워 넣는다. void fill(); //초콜릿을 끓인다. void boil(); //보일러 안에 담긴 초콜릿을 비워낸다. void drain(); bool isEmpty(); bool isBoiled(); private: bool empty; bool boiled; ChocolateBoiler(); };


<ChocolateBoiler.cpp>

#include "ChocolateBoiler.h" ChocolateBoiler::ChocolateBoiler() { empty = true; boiled = false; } ChocolateBoiler* ChocolateBoiler::getInstance() { static ChocolateBoiler* instance; if (instance == NULL) { cout << "ChocolateBoiler 객체 생성!" << endl; instance = new ChocolateBoiler(); } return instance; } void ChocolateBoiler::fill() { if (!isEmpty()) return; empty = false; boiled = false; cout << "Chocolate Boiler 채워짐" << endl; } void ChocolateBoiler::boil() { if (isEmpty() || isBoiled()) return; boiled = true; cout << "Chocolate Boiler 끓여짐" << endl; } void ChocolateBoiler::drain() { if (isEmpty() || !isBoiled()) return; empty = true; cout << "Chocolate Boiler 비워짐" << endl; } bool ChocolateBoiler::isEmpty() { return empty; } bool ChocolateBoiler::isBoiled() { return boiled; }

이제 ChocoloateBoiler 객체에 접근할 때는 다음과 같이 사용하면 된다.

ChocolateBoiler::getInstance()->fill(); ChocolateBoiler::getInstance()->boil(); ChocolateBoiler::getInstance()->drain();


3. Singleton 패턴의 장점

- 다른 모든 클래스에서 접근 할 수 있다.

- 인스턴스가 하나만 생성됨이 보장된다.

- Lazy initialization(게으르게 생성) 하여 구현 될 수 있다.

- 인스턴스의 개수를 변경하기가 자유롭다.


위의 2가지 장점이 Singleton 패턴을 사용하는 주된 이유이자 Singleton 패턴 적용시 꼭 지켜야 할 점이다.


게으르게 생성할 수 있다는 것은, 프로그램 실행 중에 인스턴스를 생성 할 수 있다는 것이다.

이는 Singleton 패턴을 적용하지 않고 전역변수를 사용 했을때에 비해서 장점인데, 전역변수를 사용하면 프로그램 실행시에 인스턴스를 바로 생성해야한다.

이렇게 할 때 문제점은, 생성된 인스턴스가 프로그램이 실행 되는 동안 한번도 사용되지 않을 경우 자원이 낭비된다.

그렇기 때문에 게으르게 생성되는 것이 Singleton의 장점이라고 할 수 있다.


인스턴스의 개수를 변경하기가 자유롭다는 말이 참 혼란스럽다. 싱글톤 패턴은 클래스의 인스턴스가 유일함을 보장 할 때만 사용해야 하는데, GoF의 디자인 패턴 책에서는 인스턴스의 갯수 변경이 자유롭다는 것을 장점이라고 설명한다.

물론 싱글톤 인스턴스에 대한 레퍼런스를 배열이나 링크드 리스트 등을 사용해서 구현하면 여러개의 인스턴스를 가지는 싱글톤 클래스를 구현 할 수는 있다.

싱글톤 패턴은, 여러개의 인스턴스를 가지는 싱글톤 클래스를 구현 할 수 있긴 하지만, 기본적으로는 한개의 인스턴스만이 존재해야 할 때 적용해야한다고 생각하면 될 듯하다.


4. Singleton 패턴의 단점

- 단일 책임의 원칙을 어긴다.

- Singleton 클래스에 대한 의존도가 높아진다.

- 싱글톤 클래스에 대한 서브클래스를 만들기 어려워진다.

- 멀티 스레드 적용 시 동기화 문제가 생길 수 있다.


단일 책임의 원칙은 모든 클래스가 하나의 책임만을 가져야 한다는 원칙이다.

싱글톤 클래스는 클래스의 작업을 처리하는 역할 뿐 아니라 자신의 인스턴스에 대한 접근을 관리하는 역할에 대해서도 책임을 져야한다.

이렇게 하는게 전체적인 설계를 간단하게 만들 수 있어서 장점이기도 하지만, 단일 책임의 원칙을 어긴다는 점도 고려해 봐야한다.


싱글톤 패턴을 적용하면 여러 클래스나 컴포넌트가 싱글톤 클래스에 의존하게 된다. 이는 시스템의 Coupling(결합도)를 높이는 결과를 가져오기 때문에 주의해야한다.


싱글톤 클래스는 생성자가 private으로 지정되기 때문에 상속이 불가능 한데, 이를 protected나 public으로 고치면 다른 클래스에서 인스턴스를 만들 수 있게 되서 더이상 싱글톤이라고 할 수 없게 된다.

생성자 문제 뿐만 아니라, 싱글톤 클래스는 static 변수를 바탕으로 하기 때문에 모든 서브클래스들이 그 변수를 공유하게 된다.

이를 해결하기 위해서는 '레지스트리'를 구현해두어야 한다.


멀티 스레드를 적용하면 여러 스레드에서 한개의 싱글톤 클래스에 접근하게 된다. 이는 동기화 문제를 발생시킬 수 있으니 volatile 키워드를 통해 Double Checking Locking을 적용하거나 getInstance() 메소드에 synchronized 키워드를 적용하여 동기화 해야한다.

그런데 synchronized 키워드를 적용하면 스레드에서 getInstance() 할 떄마다 속도가 느려지는 문제가 발생한다.



5. 레퍼런스

- Head First Design Pattern(O'REILLY media)

- GoF의 디자인 패턴(Pearson Addison Wesley)

- 위키피디아 Lazy Initialization

https://en.wikipedia.org/wiki/Lazy_initialization


- Code Project Singleton Pattern – Positive and Negative Aspects

https://www.codeproject.com/Articles/307233/Singleton-Pattern-Positive-and-Negative-Aspects-2


- 위키피디아 단일 책임 원칙 

https://ko.wikipedia.org/wiki/%EB%8B%A8%EC%9D%BC_%EC%B1%85%EC%9E%84_%EC%9B%90%EC%B9%99


블로그 이미지

서기리보이

,

1. Abstract Factory 패턴이란

1.1 Abstract Factory 패턴의 정의

인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있게 하는 패턴


정의만읽어서는 이게 무슨 내용인지 알기가 어렵다. UML과 함께 보도록 하자.


1.2 UML




AbstractFactoty - 각각의 Product 객체를 생산하는 메소드를 지닌 추상 팩토리 인터페이스

ConcreteFactory - AbstractFactory 인터페이스를 구현한 구상 팩토리 클래스

Client - 팩토리에게 객체의 생성을 요청하는 클래스

AbstractProductA,B - 팩토리에 의해 생성되는 클래스들에 대한 인터페이스

ProductA_1,A_2,B_1,B_2 - 실제 생성될 Product 구상 클래스





2. 적용 예제


Head First Desgin Pattern에 나오는 예제를 수정한 것이다.

앞부분 내용은 Factory Method 패턴 때 썼던 글과 똑같으니 대충 읽어봐도 괜찮을 듯 하다.


객체마을 피자가게에서는 클래스를 이용하여 판매하는 피자 종류를 관리하고, 피자를 주문받는다.

피자가게 관리를 위해서 다음의 클래스 구조를 이용하고 있었다.





우선 피자 인터페이와 구상 클래스 중 하나인 CheesePizza 클래스는 다음과 같이 정의된다.

<Pizza.h>

#pragma once #include <string> using namespace std; class Pizza { public: virtual void prepare() = 0; //피자의 재료를 준비한다. virtual void bake() = 0; //피자를 굽는다 virtual void cut() = 0; //피자를 자른다. virtual void box() = 0; //피자를 박스에 담는다. virtual string toString() = 0; //피자의 종류를 스트링으로 리턴한다. };

<CheesePizza.h>

#include <iostream> #include "Pizza.h" using namespace std; class CheesePizza : public Pizza { public: CheesePizza(); void prepare() override; void bake() override; void cut() override; void box() override; string toString() override; };


객체마을 피자가게를 나타내는 PizzaStore 클래스에서 피자를 주문하는 orderPizza 메소드는다음과 같이 구현된다.


Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

if(type.compare("cheese") == 0)

pizza = new CheesePizza();

else if(type.compare("pepperoni") == 0)

pizza = new PepperoniPizza();

else if(type.compare("clam") == 0)

pizza = new ClamPizza();

else if(type.compare("veggi") == 0)

pizza = new VeggiPizza();



//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}


그런데 객체마을 피자나라의 가게 관리 프로그램이 유명해지면서, 여러 도시의 피자가게들이 이 프로그램을 이용 할 수 있게 해달라고 요청했다.


우리는 이 프로그램의 클래스 구조를 수정하여 여러 도시의 피자가게들이 이 프로그램을 이용 할 수있게 새로 만들어야한다.


문제점은, 각 지역마다 피자를 만드는 방식에 차이가 있다는 것이다.


어떤 도시에서는 피자를 만들때 빵은 얇고, 치즈는 조금 적게 넣는 편이고, 또 다른 도시에서는 빵을 두껍게하고 소스를 듬뿍 뿌려 맛을 낸다.


이를 반영하기 위해 피자 클래스는 다음과 같이 지역별로 나뉘어야 한다.


그런데 변경된 피자 클래스 구조를 반영하여, orderPizza() 메소드를 수정하면 다음과 같이 구현 될 것이다.


Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

if(type.compare("ChicagoCheese") == 0)

pizza = new ChicagoCheesePizza();

else if(type.compare("ChicagoPepperoni") == 0)

pizza = new ChicagoPepperoniPizza();

else if(type.compare("ChicagoClam") == 0)

pizza = new ChicagoClamPizza();

else if(type.compare("ChicagoVeggi") == 0)

pizza = new ChicagoVeggiPizza();

else if(type.compare("NYCheese")==0)

pizza = new NYCheesePizz();

else if(type.compare("NYVeggi")==0)

pizza = new NYVeggiPizza();

...



//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}



이렇게 되면 이 프로그램을 이용하는 피자가게가 늘어 날 때마다 if-else 문이 점점 늘어나게 된다.


이런 문제를 해결하기 위해서 추상 팩토리 패턴을 적용하여 피자 객체 생성의 역할을 팩토리에게 넘기도록 다음의 구조로 수정한다.

러기 위해서 이제 팩토리 메소드 패턴을 적용하여, 피자가게와 피자 클래스를 다음과 같은 구조로 만든다.


이 다이어그램과 1.2의 UML과 비교하면, 각 요소는 다음과 같이 맵핑된다.


PizzaStore - Client

PizzaFactory - AbstractFactory

NYPizzaFactory, ChicagoFactory - ConcreteFactory1,2

Pizza 인터페이스 - Product 인터페이스

NYCheesePizza,NYVegiPizza,...ChacigoClamPizza,ChacigoPepperoniPizza - ConcreteProduct


그리고 PizzaFactory, NYPizzaFactory는 다음과 같이 정의/구현된다.

<PizzaFactory.h>

#pragma once #include <string> #include "../Pizza/Pizza.h" class PizzaFactory { public: //type을 기준으로 피자를 리턴 //만약 없는 타입이라면 NULL 리턴 virtual Pizza* createPizza(const string& type) = 0; };


<NYPizzaFactory.h>

class NYPizzaFactory : public PizzaFactory { public: NYPizzaFactory(); Pizza* createPizza(const string& type) override; };

<NYPizzaFactory.cpp>

NYPizzaFactory::NYPizzaFactory() { } Pizza* NYPizzaFactory::NYPizzaFactory::createPizza(const string& type) { Pizza * pizza = NULL; if (type.compare("cheese") == 0) pizza = new NYCheesePizza(); else if (type.compare("pepperoni") == 0) pizza = new NYPepperoniPizza(); else if (type.compare("clam") == 0) pizza = new NYClamPizza(); else if (type.compare("veggi") == 0) pizza = new NYVeggiPizza(); return pizza; }


이렇게 구현하고 나면, 사용 할 때는 PizzaStore를 생성 할 때는 다음과 같이 각 분점에 맞는 PizzaFactory를 넘겨주면 된다.


//뉴욕 피자 팩토리를 넘겨줘서 뉴욕식 피자를 만드는 PizzaStore 생성

PizzaStore nyPizzaStore(&nyPizzaFactory);

//시카고 피자 팩토리를 넘겨줘서 시카고 피자를 만드는 PizzaStore 생성 PizzaStore chicagoPizzaStore(&chicagoPizzaFactory);


구현된 프로젝트는 다음의 깃허브에서 확인 할 수 있다.

(https://github.com/InvincibleTyphoon/AbstractFactoryPattern/tree/master)

책에서는 Java를 사용하지만, 개인적으로 C++을 더 선호하는 관계로, 구현은 C++로 했다.


3. Abstract Factory 패턴의 장점

- 객체 생성을 캡슐화 할 수 있다.

- 구상 클래스가 아닌 추상 클래스/인터페이스에 맞춰 구현 할 수 있다.

- 관련 있는 클래스들을 묶어서 하나의 제품군으로 만들 수 있다.

- 싱글톤 패턴을 적용하기 적합하다.


객체를 생성하는 부분이 팩토리에 캡슐화되므로 코드 중복을 방지 할 수 있다.


구상 클래스가 아닌 추상 클래스/인터페이스에 맞춰 구현 할 수 있기 때문에 특정 구현에 덜 의존적이고, 프로그램을 더 유연하게 만들어 준다.


구상 팩토리가 생산해내는 제품별로 클래스를 분류 할 수 있다. 이 덕분에 제품군을 대체하기 쉽고, 서로 연관된 여러 제품들을 만들어야 할 때 유용하다.


한 제품군을 생산하는 팩토리는 프로그램 내에서 한개로 충분하다. 그래서 Abstract Factory 패턴을 적용할 때는 싱글톤 패턴으로 구현하는 것도 좋은 방법이 될 수 있다.


4. Abstract Factory 사용시 유의사항(단점)

- 새로운 종류의 제품을 제공하기 어렵다.


Abstract Factory 인터페이스가 생성 할 수 없는 새로운 종류의 제품이 생긴다면 그 제품을 반영하기 위해서는 Abstract Factory 와 모든 구상 팩토리들을 수정해야한다.


가령 위의 피자가게 예제에서 피클을 생산하는 기능을 추가하려면, PizzaFactory 인터페이스에 createPickle() 메소드가 추가되어야하고, NYPizzaFactory, ChicagoPizzaFactory 클래스에서 createPickle() 메소드를 구현해야한다.


5. 레퍼런스

- Head First Design Pattern(O'REILLY media)

- GoF의 디자인 패턴(Pearson Addison Wesley)

블로그 이미지

서기리보이

,

1. Factory Method 패턴이란

1.1 Factory Method 패턴의 정의

객체를 생성하기 위한 인터페이스를 정의하고, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 하는 패턴


정의만읽어서는 이게 무슨 내용인지 알기가 어렵다. UML과 함께 보도록 하자.


1.2 UML



Product - 생성될 객체의 인터페이스

ConcreteProduct - Product 인터페이스를 구현한 구상 클래스.

Creator - 객체를 생성하는 팩토리 메소드를 가진 인터페이스

ConcreteCreator - Creator 인터페이스를 구현한 구상 클래스.


여기서 ConcreteProduct와 ConcreteCreator 여러개의 구상 클래스들을 하나로 묶은것으로 봐도 된다.

즉, ConcreteProduct1,2,3... 이런식으로 여러 종류의 클래스가 Product 인터페이스를 구현 할 수 있고, ConcreteCreatort에 대해서도 마찬가지다.


Creator 인터페이스는 factoryMethod()를 통해 Product 인터페이스 객체를 생성한다. 하지만 Creator클래스는 Product 인터페이스의 구상 클래스들 중 어떤 클래스를 생성할 지 알 수 없다.

어떤 구상 클래스를 생성할지는 ConcreteCreator 클래스에서 factoryMethod()를 오버라이드하여 결정한다.


* Product 인터페이스 객체를 생성한다는 말이 조금 헷갈릴 수도 있을 것 같은데, 추상 클래스나 인터페이스는 원래 객체화가 불가능하다. 

객체화를 할 때는 당연히 Product 인터페이스를 구현한 구상 클래스를 객체화해야하고, 이 구상 클래스를 객체화 한다는 의미로 Product 인터페이스 객체를 생성한다는 말을 사용한 것이다.



2. 적용 예제


Head First Desgin Pattern에 나오는 예제이다.

이전에 Simple Factory 글을 쓸 때 나왔던 예제의 확장판이다.


객체마을 피자가게에서는 클래스를 이용하여 판매하는 피자 종류를 관리하고, 피자를 주문받는다.

피자가게 관리를 위해서 다음의 클래스 구조를 이용하고 있었다.





우선 피자 인터페이와 구상 클래스 중 하나인 CheesePizza 클래스는 다음과 같이 정의된다.

<Pizza.h>

#pragma once #include <string> using namespace std; class Pizza { public: virtual void prepare() = 0; //피자의 재료를 준비한다. virtual void bake() = 0; //피자를 굽는다 virtual void cut() = 0; //피자를 자른다. virtual void box() = 0; //피자를 박스에 담는다. virtual string toString() = 0; //피자의 종류를 스트링으로 리턴한다. };

<CheesePizza.h>

#include <iostream> #include "Pizza.h" using namespace std; class CheesePizza : public Pizza { public: CheesePizza(); void prepare() override; void bake() override; void cut() override; void box() override; string toString() override; };


객체마을 피자가게를 나타내는 PizzaStore 클래스에서 피자를 주문하는 orderPizza 메소드는다음과 같이 구현된다.


Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

if(type.compare("cheese") == 0)

pizza = new CheesePizza();

else if(type.compare("pepperoni") == 0)

pizza = new PepperoniPizza();

else if(type.compare("clam") == 0)

pizza = new ClamPizza();

else if(type.compare("veggi") == 0)

pizza = new VeggiPizza();



//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}


그런데 객체마을 피자나라의 가게 관리 프로그램이 유명해지면서, 여러 도시의 피자가게들이 이 프로그램을 이용 할 수 있게 해달라고 요청했다.


우리는 이 프로그램의 클래스 구조를 수정하여 여러 도시의 피자가게들이 이 프로그램을 이용 할 수있게 새로 만들어야한다.


문제점은, 각 지역마다 피자를 만드는 방식에 차이가 있다는 것이다.


어떤 도시에서는 피자를 만들때 빵은 얇고, 치즈는 조금 적게 넣는 편이고, 또 다른 도시에서는 빵을 두껍게하고 소스를 듬뿍 뿌려 맛을 낸다.


이를 반영하기 위해 피자 클래스는 다음과 같이 지역별로 나뉘어야 한다.


그런데 변경된 피자 클래스 구조를 반영하여, orderPizza() 메소드를 수정하면 다음과 같이 구현 될 것이다.


Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

if(type.compare("ChicagoCheese") == 0)

pizza = new ChicagoCheesePizza();

else if(type.compare("ChicagoPepperoni") == 0)

pizza = new ChicagoPepperoniPizza();

else if(type.compare("ChicagoClam") == 0)

pizza = new ChicagoClamPizza();

else if(type.compare("ChicagoVeggi") == 0)

pizza = new ChicagoVeggiPizza();

else if(type.compare("NYCheese")==0)

pizza = new NYCheesePizz();

else if(type.compare("NYVeggi")==0)

pizza = new NYVeggiPizza();

...



//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}



이렇게 되면 이 프로그램을 이용하는 피자가게가 늘어 날 때마다 if-else 문이 점점 늘어나게 된다.


이런 문제를 해결하기 위해서는 PizzaStore 클래스가 각 지역의 가게들 별로 나누어질 필요가 있다.

그러기 위해서 이제 팩토리 메소드 패턴을 적용하여, 피자가게와 피자 클래스를 다음과 같은 구조로 만든다.



이 다이어그램과 1.2의 UML과 비교하면, 각 요소는 다음과 같이 맵핑된다.


PizzaStore 인터페이스 - Creator 인터페이스

NYPizzaStore, ChicagoPizzaStore - ConcreteCreator

Pizza 인터페이스 - Product 인터페이스

NYCheesePizza,NYVegiPizza,...ChacigoClamPizza,ChacigoPepperoniPizza - ConcreteProduct



그리고 PizzaStore, NYPizzaStore, ChicagoPizzaStore는 각각 다음과 같이 정의/구현된다.

<PizzaStore.h>

#pragma once #include <string> #include "../Pizza/Pizza.h" using namespace std; //피자 가게 추상 클래스 class PizzaStore { public:

    //피자를 주문. Pizza* orderPizza(const string& type);  protected:

// 피자를 생성. 없는 타입이면 NULL 리턴. virtual Pizza* createPizza(const string& type) = 0; };

<PizzaStore.cpp>

#include "PizzaStore.h" PizzaStore::PizzaStore() { } Pizza* PizzaStore::orderPizza(const string& type) { Pizza * pizza; pizza = createPizza(type); pizza->prepare(); pizza->bake(); pizza->cut(); pizza->box(); return pizza; }

<NYPizzaStore.cpp>

#include "NYPizzaStore.h" NYPizzaStore::NYPizzaStore() { } Pizza * NYPizzaStore::createPizza(const string& type) { if (type.compare("cheese")) return new NYCheesePizza(); else if (type.compare("veggi")) return new NYVeggiPizza(); else if (type.compare("clam")) return new NYClamPizza(); else if (type.compare("pepperoni")) return new NYPepperoniPizza(); return NULL; }

<ChicagoPizzaStore.cpp>

#include "ChicagoPizzaStore.h" ChicagoPizzaStore::ChicagoPizzaStore() { } Pizza * ChicagoPizzaStore::createPizza(const string& type) { if (type.compare("cheese")) return new ChicagoCheesePizza(); else if (type.compare("veggi")) return new ChicagoVeggiPizza(); else if (type.compare("clam")) return new ChicagoClamPizza(); else if (type.compare("pepperoni")) return new ChicagoPepperoniPizza(); return NULL; }

구현된 프로젝트는 다음의 깃허브에서 확인 할 수 있다.

(https://github.com/InvincibleTyphoon/FactoryMethodPattern)

책에서는 Java를 사용하지만, 개인적으로 C++을 더 선호하는 관계로, 구현은 C++로 했다.


3. Factory Method 패턴의 장점

- 의존성 뒤집기 원칙이 지켜진다.

- 객체의 생성을 서브클래스가 담당한다

- 구상 클래스가 아닌 추상 클래스/인터페이스에 맞춰 구현 할 수 있음


Factory Method 패턴에서는 객체생성시 저수준 구성요소인 ConcreteProduct 대신 고수준 구성요소인 Product에 의존 하게된다.

피자가게 예제에서, PizzaStore 인터페이스는 피자 객체를 생성할 때, 저수준 구성요소인 NYCheesePizza, ChicagoClamPizza 등에 의존하지 않고, 고수준 구성요소인 Pizza에 의존한다.


객체의 생성을 서브클래스가 담당하기 때문에 생성할 객체 타입을 예측 할 수 없을 때 유용하고, 변화에 더 유연하게 대처 할 수 있다.

가령 위의 피자가게 예제에서, 캘리포니아(Califonia)의 피자가게에서도 이 프로그램을 이용 할 수 있게 해달라고 한다면 CalifoniaCheesePizza 등의 피자 클래스와 CalifoniaPizzaStore 클래스를 추가 해주면 되고, 뉴욕 피자가게에서 더이상 ClamPizza를 판매하지 않는다고 하면 NYClamPizza 클래스를 삭제하고 NYPizzaStore 클래스의 createPizza() 메소드에서 ClamPizza를 주문하는 부분을 삭제하면 된다.


그리고 구상 클래스가 아닌 추상 클래스/인터페이스에 맞춰 구현 할 수 있다는 점도 프로그램의 유연성을 늘려준다.





4. Factory Method 패턴 이용 시 유의 할 점(단점)

- 상속을 이용하기 때문에 서브클래싱이 안되는 경우를 고려해야한다.


C#를 예로 들면 C#에서는 인터페이스를 제외한 추상 클래스, 일반 클래스를 다중 상속 할 수 없다. 그래서 Creator가 모든 메소드를 가상 메소드로 선언한 인터페이스이거나, ConcreteCreator의 유일한 부모 클래스가 되어야 한다.

그리고 그 외의 대부분의 언어에서도 다중 상속을 사용하면 모호성이 발생할 수 있기 때문에 다중상속은 이용하지 않는 것을 권장한다.



5. 레퍼런스

- Head First Design Pattern(O'REILLY media)

- GoF의 디자인 패턴(Pearson Addison Wesley)

- 위키피디아 : Factory Method Pattern

(https://en.wikipedia.org/wiki/Factory_method_pattern)

- 윤성우저 열혈C++프로그래밍(Orange 

블로그 이미지

서기리보이

,

Simple Factory는 사실 디자인 패턴에 속하지 않는다. 하지만 프로그래밍을 하는데 있어서는 자주 사용되기도 하고, Factory Method 패턴이나 Abstract Factory 패턴과 많이 유사하기 때문에, '팩토리'의 대략적인 개념을 잡기위해 디자인 패턴 카테고리에 포함시켰다.


1. Simple Factory이란


Simple Factory는 객체를 생성하는 역할을 팩토리 클래스가 전담하게 하는 방법이다.


UML과 함께 보도록 하자.





Client - 객체의 생성을 요청하는 클래스

SimpleFactory - 객체의 생성을 담당하는 클래스

Product - 생성될 객체에 대한 인터페이스

ConcreteProduct1,2,3 - Product 인터페이스를 구현한 구상인터페이스


여기서 Client는 createProduct() 메소드를 통해 SimpleFactory에게 객체 생성을 전담하고 있다.

이렇게 하면 ConcreteProduct 객체들을 필요로하는 클래스가 여러개 있을 때, SimpleFactory 클래스를 통해 객체를 생성하기 때문에 ConcreteProduct 객체를 생성하는 부분에 대한 코드 중복을 방지 할 수 있다.



2. 적용 예제


Head First Desgin Pattern에 나오는 예제이다.


객체마을 피자가게에서는 클래스를 이용하여 판매하는 피자 종류를 관리하고, 피자를 주문받는다.

피자가게 관리를 위해서 다음의 클래스 구조를 이용하고 있었다.





우선 피자 인터페이와 구상 클래스 중 하나인 CheesePizza 클래스는 다음과 같이 정의된다.

<Pizza.h>

#pragma once #include <string> using namespace std; class Pizza { public: virtual void prepare() = 0; //피자의 재료를 준비한다. virtual void bake() = 0; //피자를 굽는다 virtual void cut() = 0; //피자를 자른다. virtual void box() = 0; //피자를 박스에 담는다. virtual string toString() = 0; //피자의 종류를 스트링으로 리턴한다. };

<CheesePizza.h>

#include <iostream> #include "Pizza.h" using namespace std; class CheesePizza : public Pizza { public: CheesePizza(); void prepare() override; void bake() override; void cut() override; void box() override; string toString() override; };


객체마을 피자가게를 나타내는 PizzaStore 클래스에서 피자를 주문하는 orderPizza 메소드는다음과 같이 구현된다.

Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

if(type.compare("cheese") == 0)

pizza = new CheesePizza();

else if(type.compare("greek") == 0)

pizza = new GreekPizza();

else if(type.compare("pepperoni") == 0)

pizza = new PepperoniPizza();


//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}


여기서 조개피자(Clam Pizza)와 야채피자(Veggi Pizza)가 

추가되고, 잘 팔리지 않는 그리스식 피자(Greek Pizza)가 제외된다고 하자 그러면 코드는 다음과 같이 수정된다.


Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

//바뀌는 부분

if(type.compare("cheese") == 0)

pizza = new CheesePizza();

else if(type.compare("greek") == 0)

pizza = new GreekPizza();

else if(type.compare("pepperoni") == 0)

pizza = new PepperoniPizza();

else if(type.compare("clam") == 0)

pizza = new ClamPizza();

else if(type.compare("veggi") == 0)

pizza = new VeggiPizza();


//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

//바뀌지 않는 부분

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}


orderPizza() 메소드에서 가장 문제가 되는 점은 판매하는 피자의 종류가 바뀔 때 마다 코드를 수정해야 한다는 점이다.

즉, 피자 객체를 생성하는 부분은 바뀌는 부분이다.


이제 바뀌는 부분을 알아냈으니 이 부분을 캡슐화 하면 된다.


바뀌는 부분인 객체를 생성하는 부분을 캡슐화 하기 위해서 팩토리 패턴을 적용하여 다음과 같은 구조로 구현한다.




여기서 SimplePizzaFactory가 피자의 생성을 담당한다.


SimplePizzaFactory의 createPizza() 메소드는 다음과 같이 구현된다.

<SimplePizzaFactory.cpp>


Pizza* SimplePizzaFactory::createPizza(const string& type) { Pizza * pizza = NULL; if (type.compare("cheese") == 0) pizza = new CheesePizza(); else if (type.compare("pepperoni") == 0) pizza = new PepperoniPizza(); else if (type.compare("clam") == 0) pizza = new ClamPizza(); else if (type.compare("veggi") == 0) pizza = new VeggiPizza(); return pizza; }


이제 객체를 생성하는 역할을 SimplePizzaFactory에게 전담시켰으니 orderPizza() 메소드를 다음과 같이 수정한다.

<PizzaStore.cpp>


Pizza* PizzaStore::orderPizza(const string& type) {

//팩토리를 통해 피자 객체 생성

SimplePizzaFactory factory; Pizza* pizza = factory.createPizza(type);

//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비. pizza->prepare(); pizza->bake(); pizza->cut(); pizza->box(); return pizza; }

이제 피자 객체의 생성을 SimplePizzaFactory 클래스가 전담하기 때문에, 또 다른 클래스가 피자 객체를 생성해야 하는 상황이 생긴다면 이 SimplePizzaFactory를 이용하면 된다.


구현된 프로젝트는 다음의 깃허브에서 확인 할 수 있다.

(https://github.com/InvincibleTyphoon/SimpleFactoryPattern)

책에서는 Java를 사용하지만, 개인적으로 C++을 더 선호하는 관계로, 구현은 C++로 했다.


3. Simple Factory의 장점

- 객체를 생성하는 부분을 한 클래스에 캡슐화함


Simple Factory의 장점은 이 한마디로 정리 될 수 있다.

객체를 생성하는 부분을 한 클래스에 캡슐화하면, 여러 클래스에서 어떤 클래스의 객체를 생성할 때, 그 클래스를 생성하는 코드 중복을 방지 할 수 있다.

가령 위의 예제에서 PizzaStore 클래스 뿐만 아니라, 피자에 대한 설명을 찾아서 활용하는 PizzaShopMenu 클래스, 피자를 준비 할 때 피자를 박스에 포장(box() 메소드 호출)한 후에 또 봉지에 싸서 포장해야하는 HomeDelivery 클래스가 추가되고, 피자의 메뉴에 변경이 발생하면 PizzaStore,PizzaShopMenu,HomeDelivery 세 클래스에서 피자 객체를 생성하는 부분을 수정해야하는 어려움이 있다.

하지만 객체 생성을 SimplePizzaFactory 클래스가 전담하면 SimplePizzaFactory 클래스만 수정하면 된다.


4. 레퍼런스

- Head First Design Pattern(O'REILLY media)

블로그 이미지

서기리보이

,