행동 패턴(Behavioral Pattern)은 클래스와 객체들이 상호작용하는 방법과 역할을 분담하는 방법을 다루는 패턴이다.
- 종류
- 책임 연쇄 패턴(Chain of responsibility Pattern)
- 템플릿 메서드 패턴(Template Method Pattern)
- 전략 패턴(Strategy Pattern)
- 상태 패턴(State Pattern)
- 커맨드 패턴(Command Pattern)
- 인터프리터 패턴(Interpreter Pattern)
- 반복자 패턴(Iterator Pattern)
- 옵저버 패턴(Observer Pattern)
- 방문자 패턴(Visitor Pattern)
- 중재자 패턴(Mediator Pattern)
- 기념품 패턴(Memento Pattern)
이번 포스팅에서는 책임 연쇄 패턴 ~ 커맨드 패턴까지 다뤄보고, 다음 포스팅에서 인터프리터 패턴 ~ 기념품 패턴까지 다뤄보겠다!
책임 연쇄 패턴(Chain of Responsibility Pattern, COR)
: 클라이언트의 요청에 대한 세세한 처리를 여러 개의 처리 객체들을 두어 사슬(Chain)처럼 연결해 연쇄적으로 처리하는 행동 패턴
- 처리 객체들을 핸들러(handler)라고 부른다. 요청을 받으면 각 핸들러는 요청을 처리할 수 있는지 확인 후, 있다면 처리하고 없다면 체인의 다음 핸들러로 처리에 대한 책임을 전가한다
- 각 객체들을 독립시키고 결합도를 느슨하게 만들며, 상황에 따라 요청을 처리할 객체가 변하게 하여 유연한 처리가 가능하다
- if-else 문을 최적화하는데 유용하다
- 요청의 상태에 따라 처리를 다르게 하고 싶은 경우 if-else 대신 각 요청에 해당하는 처리 객체를 만들고 처리 가능하면 처리, 불가능하면 전가하는 방식!
- if-else 문의 경우 처리해야 하는 로직이 추가될 경우, 하나의 클래스 혹은 메서드 내에 분기문이 많아지며 코드가 더 복잡해질 것
책임 연쇄 패턴 구조
Handler
: 요청을 수신하고 처리 객체들의 집합을 정의하는 인터페이스ConcreteHandler
: 요청을 처리하는 실제 처리 객체- 핸들러에 대한 필드를 내부에 가지고 있으며, handleRequest()를 통해 요청을 처리한다.
- 처리할 수 없는 요구라면 자신과 연결된 다음 체인의 핸들러에게 요청을 떠넘긴다.
사용 시기
- 특정 요청을 2개 이상의 여러 객체에서 판별하고 처리해야 할때
- 특정 순서로 여러 핸들러를 실행해야 하는 경우
- 프로그램이 다양한 방식과 종류의 요청을 처리할 것으로 예상되지만 정확한 요청 유형과 순서를 미리 알 수 없는 경우
- 요청을 처리할 수 있는 객체 집합이 동적으로 정의되어야 할 때 (체인 연결을 런타임에서 동적으로 설정) = 처리 객체가 특정적이지 않은 경우
장점
- 요청의 발신자와 수신자를 분리를 통해 결합도를 낮춤 = 디커플링
- 요청 처리 방법이 변경되어도 발신자 쪽의 코드는 변경 필요 없음!
- 클라이언트는 체인 내부 구조를 알 필요가 없음
- 새로운 요청에 대한 처리객체 생성이 매우 편리함
- 집합 내의 처리 순서를 변경하거나 처리객체를 추가 또는 삭제할 수 있어 유연성이 향상
- 수많은 if-else 문을 최적화 시켜 가독성이 좋음
단점
- 실행 시에 코드의 흐름이 많아져서 과정을 살펴보거나 디버깅 및 테스트가 쉽지 않음
- 집합 내부에서 무한 사이클이 발생할 위험
- 책임을 전가하는 방식이다보니 요청의 수행 완료를 보장해주지 않음
- 책임 연쇄로 인한 처리 지연 문제가 발생할 수 있음 = 구조적 이점과 성능 사이의 Trade-Off..
구현
Handler
// 구체적인 핸들러를 묶는 인터페이스 (추상 클래스)
abstract class Handler {
// 다음 체인으로 연결될 핸들러
protected Handler nextHandler = null;
// 생성자를 통해 연결시킬 핸들러를 등록
public Handler setNext(Handler handler) {
this.nextHandler = handler;
return handler; // 메서드 체이닝 구성을 위해 인자를 그대로 반환함
}
// 자식 핸들러에서 구체화 하는 추상 메서드
protected abstract void process(String url);
// 핸들러가 요청에 대해 처리하는 메서드
public void run(String url) {
process(url);
// 만일 핸들러가 연결된게 있다면 다음 핸들러로 책임을 떠넘긴다
if (nextHandler != null)
nextHandler.run(url);
}
}
ConcreteHandler
class ProtocolHandler extends Handler {
@Override
protected void process(String url) {
int index = url.indexOf("://");
if (index != -1) {
System.out.println("PROTOCOL : " + url.substring(0, index));
} else {
System.out.println("NO PROTOCOL");
}
}
}
class DomianHandler extends Handler {
@Override
protected void process(String url) {
int startIndex = url.indexOf("://");
int lastIndex = url.lastIndexOf(":");
System.out.print("DOMAIN : ");
if (startIndex == -1) {
if (lastIndex == -1) {
System.out.println(url);
} else {
System.out.println(url.substring(0, lastIndex));
}
} else if (startIndex != lastIndex) {
System.out.println(url.substring(startIndex + 3, lastIndex));
} else {
System.out.println(url.substring(startIndex + 3));
}
}
}
class PortHandler extends Handler {
@Override
protected void process(String url) {
int index = url.lastIndexOf(":");
if (index != -1) {
String strPort = url.substring(index + 1);
try {
int port = Integer.parseInt((strPort));
System.out.println("PORT : " + port);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
}
}
Client
class Client {
public static void main(String[] args) {
// 1. 핸들러 생성
Handler handler1 = new ProtocolHandler();
Handler handler2 = new DomianHandler();
Handler handler3 = new PortHandler();
// 2. 핸들러 연결 설정 (handler1 → handler2 → handler3)
handler1.setNext(handler2).setNext(handler3);
// 3. 요청에 대한 처리 연쇄 실행
String url1 = "http://www.youtube.com:80";
System.out.println("INPUT: " + url1);
handler1.run(url1);
System.out.println();
String url2 = "https://www.inpa.tistory.com:443";
System.out.println("INPUT: " + url2);
handler1.run(url2);
System.out.println();
String url3 = "http://localhost:8080";
System.out.println("INPUT: " + url3);
handler1.run(url3);
}
}
이러한 책임-연쇄 패턴의 대표적인 예시로 nodejs의 middleware나 Spring Security의 FilterChain 등이 있다.
전략 패턴(Strategy Pattern)
: 런타임에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행동 패턴
- 여기서 '전략'은 특정 알고리즘이나 기능, 동작 등 특정한 목표를 수행하기 위한 행동 계획을 의미한다
- 어떤 일을 수행하는 알고리즘이 여러가지일때, 동작들을 미리 전략으로 정의함으로써 손쉽게 전략을 교체할 수 있는, 알고리즘 변형이 빈번하게 필요한 경우에 적합한 패턴
전략 패턴 구조
Strategy
: 모든 전략 구현체에 대한 공용 인터페이스ConcreteStrategy
: 알고리즘, 행위, 동작을 객체로 정의한 구현체Context
: 알고리즘을 실행해야 할때마다 해당 알고리즘과 연결된 전략 객체의 메서드를 호출 => 전략 등록 및 실행 담당
class Client {
public static void main(String[] args) {
// 1. 컨텍스트 생성
Context c = new Context();
// 2. 전략 설정
c.setStrategy(new ConcreteStrateyA());
// 3. 전략 실행
c.doSomething();
// 4. 다른 전략 설정
c.setStrategy(new ConcreteStrateyB());
// 5. 다른 전략 시행
c.doSomething();
}
}
SOLID 원칙을 만족하는 구조이다
사용 시기
- 전략 알고리즘의 여러 버전 또는 변형이 필요할 때 클래스화를 통해 관리
- 알고리즘 코드가 노출되어서는 안 되는 데이터에 액세스 하거나 데이터를 활용할 때 (캡슐화)
- 알고리즘의 동작이 런타임에 실시간으로 교체되어야 할때
장점
- 유연한 알고리즘 교체 -> 새로운 기능을 추가하거나 기존 알고리즘을 수정해도 클래스 간 결합도가 낮아져 코드 변경이 용이
- 메인(클라이언트) 클래스의 복잡성 감소 -> 가독성 증가 & 유지보수 용이
- OCP -> 기존 코드를 수정하지 않고도 새로운 전략을 추가할 수 있어 확장 용이
- SRP -> 각 전략 클래스는 하나의 책임(행동)만 수행하여 코드의 모듈화와 테스트 용이성 증가
- 런타임 시점에서의 전략 선택 가능
단점
- 알고리즘, 행위, 동작이 많아질 수록 객체의 수도 함께 증가한다
- 복잡도가 증가한다
- 애플리케이션 특성 상 알고리즘이 많지 않고 자주 변경되지 않는다면, 이러한 패턴의 도입은 성능 저하 및 복잡성 증가만 초래할 수 있다
구현
Context
// 컨텍스트(전략 등록/실행)
public class Robot {
MoveStrategy moveStrategy;
TranslateStrategy translateStrategy;
Robot(MoveStrategy moveStrategy, TranslateStrategy translateStrategy) {
this.moveStrategy = moveStrategy;
this.translateStrategy = translateStrategy;
}
void move() {
moveStrategy.move();
}
void translate() {
translateStrategy.translate();
}
void setMove(MoveStrategy moveStrategy) {
this.moveStrategy = moveStrategy;
}
void setTranslate(TranslateStrategy translateStrategy) {
this.translateStrategy = translateStrategy;
}
}
Strategy
// Run / Walk 전략(추상화된 알고리즘)
interface MoveStrategy {
void move();
}
class Walk implements MoveStrategy {
public void move() {
System.out.println("걸어서 배달합니다 삐-빅");
}
}
class Run implements MoveStrategy {
public void move() {
System.out.println("뛰러서 배달합니다 삐-빅");
}
}
// 한국어 / 일본어 번역 전략(추상화된 알고리즘)
interface TranslateStrategy {
void translate();
}
class Korean implements TranslateStrategy {
public void translate() {
System.out.println("한국어로 번역합니다 삐-비-빅");
}
}
class Japanese implements TranslateStrategy {
public void translate() {
System.out.println("일본어로 번역합니다 삐-비-빅");
}
}
Client
// 클라이언트(전략 교체/전략 실행한 결과를 얻음)
class User {
public static void main(String[] args) {
Robot robot = new Robot(new Walk(), new Korean());
robot.move(); // 걸어서 배달합니다 삐-빅
robot.translate(); // 한국어로 번역합니다 삐-비-빅
// 로봇의 전략(기능)을 run과 Japanese 번역으로 변경
robot.setMove(new Run());
robot.setTranslate(new Japanese());
robot.move(); // 뛰러서 배달합니다 삐-빅
robot.translate(); // 일본어로 번역합니다 삐-비-빅
}
}
위처럼 Context는 1개의 전략만 사용하지 않고, 여러 개의 전략을 동시에 다룰 수도 있다. KoreanWalkingRobot
, KoreanRunningRobot
, JapaneseWalkingRobot
, JapaneseRunningRobot
등의 구현체를 만드는 것이 아니라 Robot이라는 컨택스트 안에서 발생하는 행위를 전략 객체로 만들어 전략만 변경해주면 된다.
템플릿 메서드 패턴(Template Method Pattern)
: 여러 클래스에서 공통으로 사용하는 메서드를 템플릿화하여 상위 클래스에서 정의하고, 하위 클래스마다 세부 동작 사항을 다르게 구현하는 행동 패턴
즉, 공통인 변하지 않는 기능(템플릿)은 상위 클래스에 정의하고, 자주 변경되고 확장되는 기능은 하위 클래스에서 정의하여 상위의 메서드 실행 동작 순서는 고정하면서 세부 실행 내용은 다양화 될 수 있는 경우에 사용된다.
=> 상속이라는 기술을 극대화하여, 알고리즘의 뼈대를 맞추는 것에 초점
템플릿 메서드 패턴 구조
AbstractClass
(추상 클래스): 템플릿 메서드를 구현하고, 템플릿 메서드에서 돌아가는 추상 메서드를 선언한다. 이 추상 메서드는 하위 클래스인ConcreteClass
역할에 의해 구현된다ConcreteClass
(구현 클래스):AbstractClass
를 상속하고 추상 메서드를 구체적으로 구현한다. 여기서 구현한 메서드는AbstractClass
의 템플릿 메서드에서
AbstractClass
abstract class AbstractTemplate {
// 템플릿 메소드 : 메서드 앞에 final 키워드를 붙이면 자식 클래스에서 오버라이딩이 불가능함.
// 자식 클래스에서 상위 템플릿을 오버라이딩해서 자기마음대로 바꾸도록 하는 행위를 원천 봉쇄
public final void templateMethod() {
// 상속하여 구현되면 실행될 메소드들
step1();
step2();
if(hook()) { // 안의 로직을 실행하거나 실행하지 않음
// ...
}
step3();
}
boolean hook() {
return true;
}
// 상속하여 사용할 것이기 때문에 protected 접근제어자 설정
protected abstract void step1();
protected abstract void step2();
protected abstract void step3();
}
ConcreteClass
class ImplementationA extends AbstractTemplate {
@Override
protected void step1() {}
@Override
protected void step2() {}
@Override
protected void step3() {}
}
class ImplementationB extends AbstractTemplate {
@Override
protected void step1() {}
@Override
protected void step2() {}
@Override
protected void step3() {}
// hook 메소드를 오버라이드 해서 false로 하여 템플릿에서 마지막 로직이 실행되지 않도록 설정
@Override
protected boolean hook() {
return false;
}
}
Client
class Client {
public static void main(String[] args) {
// 1. 템플릿 메서드가 실행할 구현화한 하위 알고리즘 클래스 생성
AbstractTemplate templateA = new ImplementationA();
// 2. 템플릿 실행
templateA.templateMethod();
}
}
사용 시기
- 클라이언트가 알고리즘의 특정 단계만 확장하고, 전체 알고리즘이나 해당 구조는 확장하지 않도록 할때
- 동일한 기능은 상위 클래스에서 정의하면서 확장, 변화가 필요한 부분만 하위 클래스에서 구현할 때
장점
- 클라이언트가 대규모 알고리즘의 특정 부분만 재정의하도록 하여, 알고리즘의 다른 부분에 발생하는 변경 사항의 영향을 덜 받도록 한다
- 상위 추상클래스로 로직을 공통화 하여 코드의 중복을 줄일 수 있다
- 서브 클래스의 역할을 줄이고, 핵심 로직을 상위 클래스에서 관리하므로서 관리가 용이해진다
헐리우드 원칙 (Hollywood Principle)
: 고수준 구성요소에서 저수준을 다루는 원칙 (추상화에 의존)
단점
- 알고리즘의 제공된 골격에 의해 유연성이 제한될 수 있다
- 알고리즘 구조가 복잡할수록 템플릿 로직 형태를 유지하기 어려워진다
- 추상 메소드가 많아지면서 클래스의 생성, 관리가 어려워질 수 있다
- 상위 클래스에서 선언된 추상 메소드를 하위 클래스에서 구현할 때, 그 메소드가 어느 타이밍에서 호출되는지 클래스 로직을 이해해야 할 필요가 있다
- 로직에 변화가 생겨 상위 클래스를 수정할 때, 모든 서브 클래스의 수정이 필요 할수도 있다
- 하위 클래스를 통해 기본 단계 구현을 억제하여 리스코프 치환 법칙을 위반할 여지가 있다
구현
AbstractClass
abstract class CaffeineBeverage {
// 템플릿 메서드
final void prepareRecipe() {
boilWater(); // "물 끓이기"
brew();
pourInCup(); // "컵에 따르는 중"
if (customerWantsCondiments()) {
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
// hook 메서드
boolean customerWantsCondiments() {
return false;
}
public void boilWater() {
System.out.println("물 끓이기");
}
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
}
ConcreteClass
class Coffee extends CaffeineBeverage {
public void brew() {
System.out.println("필터를 통해 커피를 우려내는 중");
}
public void addCondiments() {
System.out.println("설탕과 우유를 추가하는 중");
}
boolean customerWantsCondiments() {
String answer = "";
System.out.print("커피에 우유와 설탕을 넣어 드릴까요? (y/n) : ");
try (BufferedReader in = new BufferedReader(new InputStreamReader(System.in))) {
answer = in.readLine();
} catch (IOException ioe) {
System.out.println("IO 오류");
}
if (answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
}
class Tea extends CaffeineBeverage {
public void brew() {
System.out.println("차를 우리는 중");
}
public void addCondiments() {}
}
Client
class client {
public static void main(String[] args) {
CaffeineBeverage coffee = new Coffee();
System.out.println("커피 만드는중...");
coffee.prepareRecipe();
System.out.println("\n--------------------------------\n");
CaffeineBeverage tea = new Tea();
System.out.println("홍차 만드는중...");
tea.prepareRecipe();
}
}
Strategy Pattern VS. Template Method Pattern
- 유사점
- 전략 패턴과 템플릿 메서드 패턴은 알고리즘을 때에 따라 적용한다는 컨셉이라는 공통점을 가지고 있음
- 전략 및 템플릿 메서드 패턴은 개방형 폐쇄 원칙Visit Website을 충족하고 코드를 변경하지 않고 소프트웨어 모듈을 쉽게 확장할 수 있도록 하는 데 사용 가능
- 차이점
- 전략 패턴은 합성(composition)을 통해 해결책을 강구하며, 템플릿 메서드 패턴은 상속(inheritance)을 통해 해결책을 제시
- 때문에, 전략 패턴은 클라이언트와 객체 간의 결합이 느슨한 반면, 템플릿 메서드 패턴에서는 두 모듈이 더 밀접하게 결합
- 전략 패턴에서는 대부분 인터페이스를 사용하지만, 템플릿 메서드 패턴서는 주로 추상 클래스나 구체적인 클래스를 사용
- 전략 패턴에서는 전체 전략 알고리즘을 변경할 수 있지만, 템플릿 메서드 패턴에서는 알고리즘의 일부만 변경되고 나머지는 변경되지 않은 상태로 유지 (템플릿에 종속됨)
- 단일 상속만이 가능한 자바에서 상속 제한이 있는 템플릿 메서드 패턴보다는, 다양하게 많은 전략을 implements 할 수 있는 전략 패턴이 협업에서 많이 사용되는 편
- 전략 패턴은 합성(composition)을 통해 해결책을 강구하며, 템플릿 메서드 패턴은 상속(inheritance)을 통해 해결책을 제시
상태 패턴(State Pattern)
: 객체가 특정 상태에 따라 행위를 달리하는 상황에서, 상태를 조건문으로 검사해서 행위를 달리하는 것이 아닌, 상태를 객체화 하여 상태가 행동을 할 수 있도록 위임하는 패턴
여기서 '상태'란, 객체가 가질 수 있는 어떤 조건이나 상황을 의미한다. 전략 패턴이 객체의 '행동(메서드)'을 클래스로 표현한 것이라면, 상태 패턴은 객체의 '상태'를 클래스로 표현한 패턴이다. 때문에, 구조 상 전략 패턴과 굉장히 유사하다.
상태 패턴 구조
State
: 상태를 추상화한 고수준 모듈을 의미하는 인터페이스ConcreteState
:State
를 구현하여 구체적인 각각의 상태를 클래스로 표현한 구현체. 다음 상태가 결정되면Context
에 상태 변경을 요청하는 역할도 함- 각 상태 클래스는 싱글톤으로 구현한다. 상태는 대부분의 상황에서 유일하게 존재해야 한다.
Context
:State
를 사용하는 시스템. 시스템 상태를 나타내는State
객체를 합성(composition)하여 가지고 있음. 클라이언트로부터 요청을 받으면State
객체에 행위 실행을 위임
State & ConcreteState
interface State {
void requestHandle(Context cxt);
}
class ConcreteStateA implements State {
@Override
public void requestHandle(Context cxt) {}
}
class ConcreteStateB implements State {
@Override
public void requestHandle(Context cxt) {
// 상태에서 동작을 실행한 후 바로 다른 상태로 바꾸기도 함
// 예를 들어 전원 on 상태에서 끄기 동작을 실행한후 객체 상태를 전원 off로 변경 하듯이
cxt.setState(ConcreteStateC.getInstance());
}
}
class ConcreteStateC implements State {
@Override
public void requestHandle(Context cxt) {}
}
Context
class Context {
State state; // composition
void setState(State state) {
this.state = state;
}
// 상태에 의존한 처리 메소드로서 state 객체에 처리를 위임함
void request() {
state.requestHandle(this);
}
}
Client
class Client {
public static void main(String[] args) {
Context context = new Context();
// 1. StateA 상태 설정
context.setState(new ConcreteStateA());
// 2. 현재 StateA 상태에 맞는 메소드 실행
context.request();
// 3. StateB 상태 설정
context.setState(new ConcreteStateB());
// 4. StateB 상태에서 또다른 StateC 상태로 변경
context.request();
// 5. StateC 상태에 맞는 메소드 실행
context.request();
}
}
사용 시기
- 객체의 메서드가 상태(state)에 따라 각기 다른 행동을 할때
- 상태 및 전환에 걸쳐 대규모 조건 분기 코드와 중복 코드가 많을 경우
- 조건문의 각 분기를 별도의 클래스에 넣는것이 상태 패턴의 핵심
- 런타임단에서 객체의 상태를 유동적으로 변경해야 할때
장점
- 상태(State)에 따른 동작을 개별 클래스로 옮겨서 관리
- 상태(State)와 관련된 모든 동작을 각각의 상태 클래스에 분산시킴으로써, 코드 복잡도 감소
- SRP -> 특정 상태와 관련된 코드를 별도의 클래스로 구성
- OCP -> 기존 State 클래스나 컨텍스트를 변경하지 않고 새 State를 도입
- 하나의 상태 객체만 사용하여 상태 변경을 하므로 일관성 없는 상태 주입을 방지하는데 도움이 된다
단점
- 상태 별로 클래스를 생성하므로, 관리해야할 클래스 수 증가
- 상태 클래스 갯수가 많고 상태 규칙이 자주 변경된다면, Context의 상태 변경 코드가 복잡해지게 될 수 있다
- 객체에 적용할 상태가 몇가지 밖에 없거나 거의 상태 변경이 이루어지지 않는 경우 패턴을 적용하는 것이 과도할 수 있다
구현
State & ConcreteState
// State 인터페이스 정의
interface State {
void insertCoin();
void ejectCoin();
void pressButton();
void dispense();
}
// Concrete State: 동전이 투입되지 않은 상태
class NoCoinState implements State {
VendingMachine vendingMachine;
public NoCoinState(VendingMachine vendingMachine) {
this.vendingMachine = vendingMachine;
}
@Override
public void insertCoin() {
System.out.println("동전이 투입되었습니다.");
vendingMachine.setState(vendingMachine.getHasCoinState());
}
@Override
public void ejectCoin() {
System.out.println("동전이 없습니다. 동전을 투입하세요.");
}
@Override
public void pressButton() {
System.out.println("동전이 없습니다. 동전을 투입하세요.");
}
@Override
public void dispense() {
System.out.println("동전이 투입되지 않아 상품을 제공할 수 없습니다.");
}
}
// Concrete State: 동전이 투입된 상태
class HasCoinState implements State {
VendingMachine vendingMachine;
public HasCoinState(VendingMachine vendingMachine) {
this.vendingMachine = vendingMachine;
}
@Override
public void insertCoin() {
System.out.println("이미 동전이 투입되었습니다.");
}
@Override
public void ejectCoin() {
System.out.println("동전이 반환되었습니다.");
vendingMachine.setState(vendingMachine.getNoCoinState());
}
@Override
public void pressButton() {
System.out.println("버튼이 눌렸습니다.");
vendingMachine.setState(vendingMachine.getSoldState());
}
@Override
public void dispense() {
System.out.println("버튼을 눌러야 상품이 제공됩니다.");
}
}
// Concrete State: 상품 제공 상태
class SoldState implements State {
VendingMachine vendingMachine;
public SoldState(VendingMachine vendingMachine) {
this.vendingMachine = vendingMachine;
}
@Override
public void insertCoin() {
System.out.println("잠시 기다려 주세요. 상품이 제공되고 있습니다.");
}
@Override
public void ejectCoin() {
System.out.println("이미 상품이 제공되었기 때문에 동전을 반환할 수 없습니다.");
}
@Override
public void pressButton() {
System.out.println("이미 버튼을 눌렀습니다.");
}
@Override
public void dispense() {
vendingMachine.releaseProduct();
if (vendingMachine.getCount() > 0) {
vendingMachine.setState(vendingMachine.getNoCoinState());
} else {
System.out.println("상품이 모두 소진되었습니다.");
vendingMachine.setState(vendingMachine.getSoldOutState());
}
}
}
// Concrete State: 매진 상태
class SoldOutState implements State {
VendingMachine vendingMachine;
public SoldOutState(VendingMachine vendingMachine) {
this.vendingMachine = vendingMachine;
}
@Override
public void insertCoin() {
System.out.println("상품이 매진되었습니다. 동전을 투입할 수 없습니다.");
}
@Override
public void ejectCoin() {
System.out.println("동전을 투입하지 않았습니다.");
}
@Override
public void pressButton() {
System.out.println("상품이 매진되었습니다.");
}
@Override
public void dispense() {
System.out.println("상품이 매진되었습니다.");
}
}
Context
// Context: VendingMachine 클래스
class VendingMachine {
private State noCoinState;
private State hasCoinState;
private State soldState;
private State soldOutState;
private State currentState;
private int count = 0;
public VendingMachine(int numberOfProducts) {
noCoinState = new NoCoinState(this);
hasCoinState = new HasCoinState(this);
soldState = new SoldState(this);
soldOutState = new SoldOutState(this);
this.count = numberOfProducts;
if (numberOfProducts > 0) {
currentState = noCoinState;
} else {
currentState = soldOutState;
}
}
public void insertCoin() {
currentState.insertCoin();
}
public void ejectCoin() {
currentState.ejectCoin();
}
public void pressButton() {
currentState.pressButton();
currentState.dispense();
}
void setState(State state) {
this.currentState = state;
}
void releaseProduct() {
System.out.println("상품이 제공되었습니다.");
if (count > 0) {
count--;
}
}
public State getNoCoinState() {
return noCoinState;
}
public State getHasCoinState() {
return hasCoinState;
}
public State getSoldState() {
return soldState;
}
public State getSoldOutState() {
return soldOutState;
}
public int getCount() {
return count;
}
}
Client
public class Client {
public static void main(String[] args) {
VendingMachine vendingMachine = new VendingMachine(3);
vendingMachine.insertCoin();
vendingMachine.pressButton();
vendingMachine.insertCoin();
vendingMachine.ejectCoin();
vendingMachine.insertCoin();
vendingMachine.pressButton();
vendingMachine.insertCoin();
vendingMachine.pressButton();
vendingMachine.insertCoin();
}
}
앞서 설명한 것처럼 모든 State 구현체는 싱글톤으로 구현하는 것이 좋다. 이 부분은 따로 진행하지 않겠다.
커맨드 패턴(Command Pattern)
: 행동(명령)을 객체로 캡슐화하여 호출자(Invoker)와 수행자(Receiver)를 분리하는 패턴
각 명령(커맨드)은 요청을 수행할 구체적인 메서드나 동작에 대한 정보를 포함하며, 실행을 지연하거나 취소할 수도 있게 한다. 이를 통해 명령을 파라미터화하거나, 요청을 큐에 저장하고 나중에 실행하거나, 명령 실행을 기록 및 되돌릴 수 있다.
구조
Command
: 모든 커맨드의 공통 인터페이스로,execute()
메서드를 정의하여 특정 행동을 실행ConcreteCommand
:Command
인터페이스의 구현체. 요청을 처리할 구체적인 동작을 정의하며, 실제 작업을 수행할Receiver
객체를 참조Receiver
: 명령을 실제로 수행하는 객체입니다. ConcreteCommand에서 호출하여 원하는 동작을 수행Invoker
:Command
객체를 호출하는 역할을 하며, 이 객체가execute()
메서드를 호출하여 명령을 수행Client
:ConcreteComman
d와Receiver
를 생성하고,Invoker
에게Command
를 전달하여 명령 실행을 준비
사용 시기
- 단일 실행 메서드로 여러 요청을 다뤄야 할 때: 실행될 명령을 캡슐화하여 여러 커맨드 객체를 반복적으로 사용할 수 있음
- 호출자와 실행 로직의 분리가 필요할 때: Invoker는 Command 인터페이스를 통해 호출만 하고, 실제 작업은 Receiver에서 처리
- 요청을 큐에 저장하거나, 실행을 지연하거나, 취소해야 할 때: 커맨드 객체를 저장하여 요청을 나중에 실행하거나, 필요한 경우 순서를 바꿔가며 큐에 추가
- 작업의 로깅 및 복구가 필요할 때: 요청을 커맨드 객체로 만들어 기록하고, 나중에 실행 취소 기능을 구현 가능
장점
- 작업의 캡슐화: 작업을 객체로 만들어 실행 로직을 분리하여 코드의 결합도를 낮춤 (디커플링)
- 확장성: 새로운 명령이 필요할 때마다
ConcreteCommand
클래스를 추가하면 되므로, 기존 코드를 변경하지 않고도 확장이 가능 - 명령 실행 관리: 요청을 저장하고 나중에 실행하거나 취소할 수 있으며, 작업 기록 및 로깅에 유리
- 실행 취소 및 되돌리기 구현 가능:
ConcreteCommand
에서 이전 상태를 저장하고 이를 통해 되돌리기 기능을 추가 가능
단점
- 클래스 수 증가: 각 명령마다 개별
ConcreteCommand
클래스를 만들어야 하기 때문에, 명령의 종류에 따라 클래스 수도 증가 -> 이로 인해 코드가 복잡해지고 유지보수 비용이 증가 - 간단한 요청에 대한 오버헤드: 단순한 작업을 수행하는 데도 Command 객체와 Receiver, Invoker 등을 생성해야 하므로 불필요한 객체가 늘어나고, 시스템의 복잡성이 증가. 단순한 작업에서는 커맨드 패턴이 과도한 설계가 될 수 있음
- 코드 가독성 저하: 요청을 캡슐화하고 분리하는 방식이긴 하지만, 코드의 흐름이 직관적이지 않아 가독성이 떨어질 수 있음. 특히, 요청과 실행이 분리되어 있어 명령을 추적하거나 디버깅하기가 어려울 수 있음
- 실행 취소(Undo) 기능 구현의 어려움: 커맨드 패턴에서 실행 취소 기능을 구현하려면, 명령마다 현재 상태를 저장해야 하기 때문에 상태 관리가 복잡해질 수 있음. 명령이 복잡해질수록 실행 취소를 위해 필요한 추가 구현이 늘어나며, 이로 인해 시스템 자원 소모도 증가할 수 있음
구현
리모컨에서 버튼을 눌러서 가전제품을 켜고 끄는 시나리오
Command & ConcreteCommand
// Command 인터페이스
interface Command {
void execute();
void undo();
}
// ConcreteCommand 클래스
class LightOnCommand implements Command {
private Light light; // Receiver
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
@Override
public void undo() {
light.turnOff();
}
}
class LightOffCommand implements Command {
private Light light; // Receiver
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
@Override
public void undo() {
light.turnOn();
}
}
Receiver
// Receiver 클래스
class Light {
void turnOn() {
System.out.println("Light is ON");
}
void turnOff() {
System.out.println("Light is OFF");
}
}
Invoker
// Invoker 클래스
class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
public void pressUndo() {
command.undo();
}
}
Client
// Client
public class Client {
public static void main(String[] args) {
// Receiver
Light livingRoomLight = new Light();
// ConcreteCommand
Command lightOnCommand = new LightOnCommand(livingRoomLight);
Command lightOffCommand = new LightOffCommand(livingRoomLight);
// Invoker
RemoteControl remoteControl = new RemoteControl();
// Turn light on
remoteControl.setCommand(lightOnCommand);
remoteControl.pressButton();
// Undo turning light on
remoteControl.pressUndo();
// Turn light off
remoteControl.setCommand(lightOffCommand);
remoteControl.pressButton();
// Undo turning light off
remoteControl.pressUndo();
}
}
참고
[한빛출판네트워크 - 많이 쓰는 14가지 핵심 GoF 디자인 패턴의 종류]
https://www.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823
[Inpa Dev - GOF] ]https://inpa.tistory.com/category/%EB%94%94%EC%9E%90%EC%9D%B8%20%ED%8C%A8%ED%84%B4/GOF?page=2
[Design pattern] 많이 쓰는 14가지 핵심 GoF 디자인 패턴의 종류
디자인 패턴을 활용하면 단지 코드만 ‘재사용’하는 것이 아니라, 더 큰 그림을 그리기 위한 디자인도 재사용할 수 있습니다. 우리가 일상적으로 접하는 문제 중 상당수는 다른 많은 이들이 접
www.hanbit.co.kr
'디자인 패턴/GOF' 카테고리의 글 목록 (2 Page)
성장 욕구가 가파른 초보 개발자로서 공부한 내용을 쉽게 풀어쓴 기술 개발자 블로그를 운영하고 있습니다.
inpa.tistory.com
'ETC' 카테고리의 다른 글
[오브젝트 - 기초편] 영화 예매 도메인 예제 (3) | 2024.12.30 |
---|---|
[Design Pattern] Structural Pattern (0) | 2024.11.04 |
[Design Pattern] Behavioral Pattern(행동 패턴) - 2 (3) | 2024.11.03 |
[Design Pattern] Creational Pattern (1) | 2024.11.01 |
[CS] 문자 인코딩(Character Encoding) (1) | 2024.10.09 |