저번 포스팅에 이어 디자인 패턴 중 행동 패턴에 대해 계속해서 알아보도록 하자!
- 종류
책임 연쇄 패턴(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)
그럼 인터프리터 패턴부터 시작하겠다!
인터프리터 패턴(Interpreter Pattern)
: 인터프리터 패턴은 문법이 정의된 언어의 문장을 해석하고 평가하는 데 사용되는 패턴이다.
이 패턴은 간단한 문법 구조를 정의하고, 해당 구조에 따라 문장을 해석할 수 있는 인터프리터 클래스를 제공하여 언어나 표현식의 처리를 단순화한다.
인터프리터 패턴 구조
AbstractExpression
: 인터프리터의 기본 인터페이스를 정의. 공통된interpret()
메서드를 선언하여 구현 클래스가 문장을 해석할 수 있게 한다.TerminalExpression
: 문법의 가장 작은 단위를 처리하는 표현식 클래스. 실제로 문장을 해석할 수 있는 기본 로직을 포함한다.NonTerminalExpression
: 다른 표현식(복합 표현식)을 포함하여 문장을 해석하는 표현식 클래스. 여기에는 하나 이상의 표현식을 포함하고, 문장의 구조를 기반으로 해석을 수행.Context
: 문장을 해석할 때 필요한 전반적인 정보를 제공한다.Client
: 문장을 해석하기 위해 표현식을 구성하고interpret()
메서드를 호출하는 역할을 합니다.
사용 시기
- 특정 문법에 따라 해석해야 하는 언어가 정의된 경우, 예를 들어 간단한 계산기나 명령어 처리기 구현 시
- 자주 변경되지는 않지만 고정된 문법을 반복적으로 해석하고 평가해야 하는 경우
- 정규 표현식이나 수식의 평가와 같이 표현식의 해석이 필요한 경우
장점
- 특정 언어의 문법을 클래스로 캡슐화하여 직관적인 해석을 가능하게 한다
- 각 표현식을 클래스로 분리하여 유지보수 및 확장이 용이하다
- 문법에 새로운 규칙을 추가하기 쉬워지며, 해석을 체계화할 수 있다
단점
- 문법이 복잡할수록 클래스의 수가 증가하여 코드가 복잡해질 수 있다
- 런타임에 평가가 수행되므로 성능 저하를 초래할 수 있다
구현
변수를 포함하고 있는 간단한 산술 표현식을 해석하는 예제
Context
// Context: 해석에 필요한 변수 및 상태를 유지하는 클래스
class Context {
private Map<String, Integer> variables = new HashMap<>();
public void setVariable(String name, int value) {
variables.put(name, value);
}
public int getVariable(String name) {
return variables.getOrDefault(name, 0);
}
}
AbstractExpression
// AbstractExpression: 공통 인터페이스
interface Expression {
int interpret(Context context);
}
TerminalExpression
// TerminalExpression: 숫자 표현
class NumberExpression implements Expression {
private int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret(Context context) {
return this.number;
}
}
NonTerminalExpression
// TerminalExpression: 변수 표현
class VariableExpression implements Expression {
private String name;
public VariableExpression(String name) {
this.name = name;
}
@Override
public int interpret(Context context) {
return context.getVariable(name);
}
}
// NonTerminalExpression: 덧셈 표현
class AddExpression implements Expression {
private Expression leftExpression;
private Expression rightExpression;
public AddExpression(Expression leftExpression, Expression rightExpression) {
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}
@Override
public int interpret(Context context) {
return leftExpression.interpret(context) + rightExpression.interpret(context);
}
}
// NonTerminalExpression: 뺄셈 표현
class SubtractExpression implements Expression {
private Expression leftExpression;
private Expression rightExpression;
public SubtractExpression(Expression leftExpression, Expression rightExpression) {
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}
@Override
public int interpret(Context context) {
return leftExpression.interpret(context) - rightExpression.interpret(context);
}
}
Client
// Client 코드
public class Client {
public static void main(String[] args) {
Context context = new Context();
// 변수 'x'와 'y'의 값을 설정
context.setVariable("x", 10);
context.setVariable("y", 5);
// (x + 2) - (y + 3) 표현식
Expression expression = new SubtractExpression(
new AddExpression(new VariableExpression("x"), new NumberExpression(2)),
new AddExpression(new VariableExpression("y"), new NumberExpression(3))
);
int result = expression.interpret(context);
System.out.println("Result: " + result); // 출력: Result: 4
}
}
반복자 패턴(Iterator Pattern)
: 일련의 데이터 집합(컬렉션)에 대하여 순차적인 접근(순회)을 지원하는 패턴
특정 컬렉션마다 순회 방법이 다양하고 복잡하다. 이렇게 복잡하게 얽혀있는 자료 컬렉션들을 순회하는 알고리즘을 정의하는 것이다. 컬렉션 객체 안에 들어있는 모든 원소들에 대한 접근 방식이 공통화 되어 있다면 어떤 종류의 컬렉션에서도 이터레이터만 뽑아내면 여러 전략으로 순회가 가능하도록 하자!
자바의 컬렉션 프레임워크(JCF)에서 각종 컬렉션을 무리없이 순회할수 있는 것도 내부에 미리 이터레이터 패턴이 적용되어 있기 때문이다.
반복자 패턴 구조
Aggregate
:ConcreateIterator
객체를 반환하는 인터페이스iterator()
:ConcreateIterator
객체를 만드는 팩토리 메서드
ConcreateAggregate
: 여러 요소들이 이루어져 있는 데이터 집합체- 데이터 집합(컬렉션)을 가지고 있음
Iterator
: 집합체 내의 요소들을 순서대로 검색하기 위한 인터페이스hasNext()
: 순회할 다음 요소가 있는지 확인next()
: 요소를 반환하고 다음 요소를 반환할 준비를 하기 위해 커서를 이동시킴
ConcreateIterator
: 반복자 객체ConcreateAggregate
가 구현한 메서드로부터 생성되며,ConcreateAggregate
의 컬렉션을 참조하여 순회한다.- 어떤 전략으로 순회할지에 대한 로직을 구체화 한다.
Aggregate
// 집합체 객체 (컬렉션)
interface Aggregate {
Iterator iterator();
}
class ConcreteAggregate implements Aggregate {
Object[] arr; // 데이터 집합 (컬렉션)
int index = 0;
public ConcreteAggregate(int size) {
this.arr = new Object[size];
}
public void add(Object o) {
if(index < arr.length) {
arr[index] = o;
index++;
}
}
// 내부 컬렉션을 인자로 넣어 이터레이터 구현체를 클라이언트에 반환
@Override
public Iterator iterator() {
return new ConcreteIterator(arr);
}
}
Iterator
// 반복체 객체
interface Iterator {
boolean hasNext();
Object next();
}
class ConcreteIterator implements Iterator {
Object[] arr;
private int nextIndex = 0; // 커서 (for문의 i 변수 역할)
// 생성자로 순회할 컬렉션을 받아 필드에 참조 시킴
public ConcreteIterator(Object[] arr) {
this.arr = arr;
}
// 순회할 다음 요소가 있는지 true / false
@Override
public boolean hasNext() {
return nextIndex < arr.length;
}
// 다음 요소를 반환하고 커서를 증가시켜 다음 요소를 바라보도록 한다.
@Override
public Object next() {
return arr[nextIndex++];
}
}
Client
class Client {
public static void main(String[] args) {
// 1. 집합체 생성
ConcreteAggregate aggregate = new ConcreteAggregate(5);
aggregate.add(1);
aggregate.add(2);
aggregate.add(3);
aggregate.add(4);
aggregate.add(5);
// 2. 집합체에서 이터레이터 객체 반환
Iterator iter = aggregate.iterator();
// 3. 이터레이터 내부 커서를 통해 순회
while(iter.hasNext()) {
System.out.printf("%s → ", iter.next());
}
}
}
사용 시기
- 컬렉션에 상관없이 객체 접근 순회 방식을 통일하고자 할 때
- 컬렉션을 순회하는 다양한 방법을 지원하고 싶을 때
- 컬렉션의 복잡한 내부 구조를 클라이언트로 부터 숨기고 싶은 경우 (편의 + 보안)
- 데이터 저장 컬렉션 종류가 변경 가능성이 있을 때
장점
- 일관된 이터레이터 인터페이스를 사용해 여러 형태의 컬렉션에 대해 동일한 순회 방법을 제공
- 컬렉션의 내부 구조 및 순회 방식을 알지 않아도 된다.
- 집합체의 구현과 접근하는 처리 부분을 반복자 객체로 분리해 결합도를 줄 일 수 있다.
- Client에서 iterator로 접근하기 때문에 ConcreteAggregate 내에 수정 사항이 생겨도 iterator에 문제가 없다면 문제가 발생하지 않는다.
- 순회 알고리즘을 별도의 반복자 객체에 추출하여 각 클래스의 책임을 분리하여 단일 책임 원칙(SRP)를 준수한다.
- 데이터 저장 컬렉션 종류가 변경되어도 클라이언트 구현 코드는 손상되지 않아 수정에는 닫혀 있어 개방 폐쇄 원칙(OCP)를 준수한다.
단점
- 클래스가 늘어나고 복잡도가 증가한다.
- 만일 앱이 간단한 컬렉션에서만 작동하는 경우 패턴을 적용하는 것은 복잡도만 증가할 수 있다.
- 이터레이터 객체를 만드는 것이 유용한 상황인지 판단할 필요가 있다.
- 구현 방법에 따라 캡슐화를 위배할 수 있다.
구현
자바에선 이미 이터레이터 인터페이스를 지원한다. 자바의 내부 이터레이터를 재활용해서 메서드 위임을 통해 코드를 간단하게 구현할 수도 있다.
Iterator
// 저장 순서 이터레이터
class ListPostIterator implements Iterator<Post> {
private Iterator<Post> itr;
public ListPostIterator(List<Post> posts) {
this.itr = posts.iterator();
}
@Override
public boolean hasNext() {
return this.itr.hasNext(); // 자바 내부 이터레이터에 위임해 버림
}
@Override
public Post next() {
return this.itr.next(); // 자바 내부 이터레이터에 위임해 버림
}
}
// 날짜 순서 이터레이터
class DatePostIterator implements Iterator<Post> {
private Iterator<Post> itr;
public DatePostIterator(List<Post> posts) {
// 최신 글 목록이 먼저 오도록 정렬
Collections.sort(posts, (p1, p2) -> p1.date.compareTo(p2.date));
this.itr = posts.iterator();
}
@Override
public boolean hasNext() {
return this.itr.hasNext(); // 자바 내부 이터레이터에 위임해 버림
}
@Override
public Post next() {
return this.itr.next(); // 자바 내부 이터레이터에 위임해 버림
}
}
Aggregate
// 게시판
class Board {
// 게시글을 리스트 집합 객체로 저장 관리
List<Post> posts = new ArrayList<>();
public void addPost(String title, LocalDate date) {
this.posts.add(new Post(title, date));
}
public List<Post> getPosts() {
return posts;
}
// ListPostIterator 이터레이터 객체 반환
public Iterator<Post> getListPostIterator() {
return new ListPostIterator(posts);
}
// DatePostIterator 이터레이터 객체 반환
public Iterator<Post> getDatePostIterator() {
return new DatePostIterator(posts);
}
}
Client
public static void main(String[] args) {
// 1. 게시판 생성
Board board = new Board();
// 2. 게시판에 게시글을 포스팅
board.addPost("디자인 패턴 강의 리뷰", LocalDate.of(2020, 8, 30));
board.addPost("게임 하실분", LocalDate.of(2020, 2, 6));
board.addPost("이거 어떻게 하나요?", LocalDate.of(2020, 6, 1));
board.addPost("이거 어떻게 하나요?", LocalDate.of(2021, 12, 22));
// 게시글 발행 순서대로 조회하기
print(board.getListPostIterator());
// 게시글 날짜별로 조회하기
print(board.getDatePostIterator());
}
public static void print(Iterator<Post> iterator) {
Iterator<Post> itr = iterator;
while(itr.hasNext()) {
Post post = itr.next();
System.out.println(post.title + " / " + post.date);
}
}
옵저버 패턴(Observer Pattern)
: 옵저버(관찰자)들이 관찰하고 있는 대상자의 상태가 변화가 있을 때마다 대상자는 직접 목록의 각 관찰자들에게 통지하고, 관찰자들은 알림을 받아 조치를 취하는 행동 패턴
- 다른 디자인 패턴들과 다르게 일대다(one-to-many) 의존성을 가져 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다.
- Pub/Sub(발행/구독) 모델로도 알려져 있다.
옵저버 패턴 구조
Subject
: 관찰 대상자를 정의하는 인터페이스ConcreteSubject
: 관찰 대상자 / 발행자 / 게시자Observer
들을 리스트로 모아 합성(composition)하여 가지고 있음Subject
의 역할은 관찰자인Observer
들을 내부 리스트에 등록/삭제 하는 인프라를 갖고 있음 (register
,remove
)Subject
가 상태를 변경하거나 어떤 동작을 실행할 때,Observer
들에게 이벤트 알림(notify
)을 발행한다.
Observer
: 구독자들을 묶는 인터페이스ConcreteObserver
: 관찰자 / 구독자 / 알림 수신자Observer
들은Subject
가 발행한 알림에 대해 현재 상태를 취득한다.Subject
의 업데이트에 대해 전후 정보를 처리한다.
=> 핵심은 Subject
가 합성한 객체를 리스트로 관리하고 리스트에 있는 관찰자 객체들에게 모두 메서드 위임을 통한 전파 행위를 한다는 점
Subject
// 관찰 대상자 / 발행자
interface Subject {
void registerObserver(IObserver o);
void removeObserver(IObserver o);
void notifyObserver();
}
class ConcreteSubject implements Subject {
// 관찰자들을 등록하여 담는 리스트
List<IObserver> observers = new ArrayList<>();
// 관찰자를 리스트에 등록
@Override
public void registerObserver(IObserver o) {
observers.add(o);
System.out.println(o + " 구독 완료");
}
// 관찰자를 리스트에 제거
@Override
public void removeObserver(IObserver o) {
observers.remove(o);
System.out.println(o + " 구독 취소");
}
// 관찰자에게 이벤트 송신
@Override
public void notifyObserver() {
for(IObserver o : observers) { // 관찰자 리스트를 순회하며
o.update(); // 위임
}
}
}
Observer
// 관찰자 / 구독자
interface IObserver {
void update();
}
class ObserverA implements IObserver {
public void update() {
System.out.println("ObserverA 한테 이벤트 알림이 왔습니다.");
}
public String toString() { return "ObserverA"; }
}
class ObserverB implements IObserver {
public void update() {
System.out.println("ObserverB 한테 이벤트 알림이 왔습니다.");
}
public String toString() { return "ObserverB"; }
}
Client
public class Client {
public static void main(String[] args) {
// 발행자 등록
ISubject publisher = new ConcreteSubject();
// 발행자를 구독할 관찰자들 리스트로 등록
IObserver o1 = new ObserverA();
IObserver o2 = new ObserverB();
publisher.registerObserver(o1);
publisher.registerObserver(o2);
// 관찰자에게 이벤트 전파
publisher.notifyObserver();
// ObserverB가 구독 취소
publisher.removeObserver(o2);
// ObserverA 한테만 이벤트 전파
publisher.notifyObserver();
}
}
사용 시기
- 앱이 한정된 시간, 특정한 케이스에만 다른 객체를 관찰해야 하는 경우
- 대상 객체의 상태가 변경될 때마다 다른 객체의 동작을 트리거해야 할때
- 한 객체의 상태가 변경되면 다른 객체도 변경해야 할때. 그런데 어떤 객체들이 변경되어야 하는지 몰라도 될 때
- MVC 패턴에서도 사용됨 (
Model
,View
,Controller
)- MVC의
Model
과View
의 관계는 Observer 패턴의Subject
역할과Observer
역할의 관계에 대응된다. - 하나의
Model
에 여러개의View
가 대응한다.
- MVC의
장점
- Subject의 상태 변경을 주기적으로 조회하지 않고 자동으로 감지할 수 있다.
- 발행자의 코드를 변경하지 않고도 새 구독자 클래스를 도입할 수 있어 개방 폐쇄 원칙(OCP) 준수한다
- 런타임 시점에서에 발행자와 구독 알림 관계를 맺을 수 있다.
- 상태를 변경하는 객체(Subject)와 변경을 감지하는 객체(Observer)의 관계를 느슨하게 유지할 수 있다. (느슨한 결합)
단점
- 구독자는 알림 순서를 제어할수 없고, 무작위 순서로 알림을 받음
- 하드 코딩으로 구현할수는 있겠지만, 복잡성과 결합성만 높아지기 때문에 추천되지는 않는 방법이다.
- 옵저버 패턴을 자주 구성하면 구조와 동작을 알아보기 힘들어져 코드 복잡도가 증가한다.
- 다수의 옵저버 객체를 등록 이후 해지하지 않는다면 메모리 누수가 발생할 수도 있다.
구현
Subject
// Subject 인터페이스
interface Subject {
void attach(Observer observer); // 옵저버 추가
void detach(Observer observer); // 옵저버 제거
void notifyObservers(); // 옵저버에게 알림
}
// ConcreteSubject: 상태를 가지고 있고, 상태가 변경될 때 옵저버들에게 알림
class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
notifyObservers(); // 상태가 변경되면 모든 옵저버에게 알림
}
@Override
public void attach(Observer observer) {
observers.add(observer);
}
@Override
public void detach(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}
Observer
// Observer 인터페이스
interface Observer {
void update();
}
// ConcreteObserver: 주제(Subject)의 상태 변화를 알림받고 처리하는 구체적인 옵저버
class ConcreteObserver implements Observer {
private String name;
private ConcreteSubject subject;
public ConcreteObserver(String name, ConcreteSubject subject) {
this.name = name;
this.subject = subject;
this.subject.attach(this); // 옵저버 등록
}
@Override
public void update() {
System.out.println(name + " received update: Subject state is " + subject.getState());
}
}
Client
// Client 코드
public class ObserverPatternExample {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
// 두 개의 옵저버 생성
ConcreteObserver observer1 = new ConcreteObserver("Observer1", subject);
ConcreteObserver observer2 = new ConcreteObserver("Observer2", subject);
// 주제의 상태 변경 (옵저버에게 알림 전송)
subject.setState(10);
subject.setState(20);
// 옵저버 제거 후 상태 변경
subject.detach(observer1);
subject.setState(30);
}
}
옵저버 패턴을 직접 구현할수도 있지만 자바에서 java.util.Observable(인터페이스), java.util.Observer(클래스) 로 내장 옵저버 객체를 지원한다. 이 내장 객체를 이용하면 옵저버 패턴을 직접 구현할 필요 없이 간단히 해당 클래스를 상속하기만 하면 옵저버 구조를 이용할 수가 있다.
java.util.Observer
Observer
인터페이스를 구현한 클래스는 옵저버 클래스가 되게 된다.- 옵저버 클래스들은
update()
추상 메서드를 구현함으로써,Observable
클래스로 부터 데이터를 수신받게 된다.
java.util.Observable
- Observable 클래스는 등록된 옵저버들을 관리하며, 새로운 데이터가 들어오면 옵저버에게 데이터를 전달하는 발행자 역할을 한다. 즉, 우리가 구현한
Subject
와 동일 Vector
컬렉션을 통해 옵저버들을 관리- 동기화(Synchronized)가 걸려있어 Thread-Safe 하다
Observable
클래스를 상속(extends)한 클래스는 발행자 역할로서 수행
- Observable 클래스는 등록된 옵저버들을 관리하며, 새로운 데이터가 들어오면 옵저버에게 데이터를 전달하는 발행자 역할을 한다. 즉, 우리가 구현한
이러한 자바 내장 옵저버 객체는 매우 편리하여 우리가 구현해야 할 코드의 양을 크게 줄여주지만, 치명적인 한계점이 존재한다. 이는 Observable
이 클래스이기에 이를 상속해야 한다는 점인데, 자바에서는 단일 상속만 지원하기 때문에, 만일 발행자 역할을 해야 하는 클래스가 다른 클래스를 상속하고 있는 상태라면 java.util.Observable
클래스의 하위 클래스로 할 수 없게 되기 때문이다. 때문에, 결국 대부분의 상황에서는 옵저버 패턴을 직접 구현해서 사용한다.
방문자 패턴(Visitor Pattern)
: 객체의 구조와 작업을 분리하여, 구조를 변경하지 않고도 새로운 작업(기능)을 추가할 수 있게 하는 행위 패턴. 즉, 객체의 데이터 구조를 변경하지 않고도 데이터 구조에 새로운 기능을 추가할 수 있도록 한다.
방문자 패턴 구조
Visitor
: 객체 구조의 요소에 대해 새로운 작업을 수행하는visit
메서드를 선언하는 인터페이스ConcreteVisitor
:Visitor
인터페이스를 구현하며, 실제로 객체 요소들에 대해 수행할 구체적인 작업을 정의Element
: 방문자 객체를 수락(accept)하는accept
메서드를 정의하는 인터페이스.accept
메서드는 자신을 방문할Visitor
를 인자로 받아들인다.
ConcreteElement
:Element
를 구현하여,accept
메서드에서 구체적인Visitor
객체의visit
메서드를 호출
사용 시기
- 객체 구조는 변경되지 않으면서, 다양한 종류의 작업을 추가해야 하는 경우
- 서로 다른 종류의 객체들에 대해 다양한 작업을 수행해야 하고, 이 작업들을 객체 클래스에 추가하지 않고 별도로 관리하고자 하는 경우
- 객체 구조와 기능을 분리하여 구조는 변경하지 않으면서 기능을 유연하게 확장하고자 하는 경우
장점
- 객체 구조를 변경하지 않고도 새로운 작업을 추가할 수 있어, 개방-폐쇄 원칙(OCP) 준수
- 기능이 분리되어 구조와 동작을 각각 독립적으로 관리할 수 있다
단점
- 객체 구조가 자주 변경되는 경우 방문자 패턴을 적용하기 어렵다. 구조가 변경되면
Visitor
의 코드 역시 수정해야 한다. - 복잡한 구조에서는
Visitor
와Element
간의 의존성이 높아져 유지보수가 어려워진다.
구현
Shape라는 요소들(Circle과 Rectangle)에 대해 서로 다른 작업을 수행하는 예제
Visitor
// Visitor 인터페이스
// 각 도형 요소(Circle과 Rectangle)에 대해 visit 메서드를 선언하여 구체적인 작업을 수행할 수 있도록 함
interface Visitor {
void visit(Circle circle);
void visit(Rectangle rectangle);
}
// ConcreteVisitor: 도형의 면적을 계산하는 방문자
// 각 도형 요소를 방문할 때 수행할 작업을 정의
class AreaCalculator implements Visitor {
@Override
public void visit(Circle circle) {
double area = Math.PI * circle.getRadius() * circle.getRadius();
System.out.println("Circle area: " + area);
}
@Override
public void visit(Rectangle rectangle) {
double area = rectangle.getWidth() * rectangle.getHeight();
System.out.println("Rectangle area: " + area);
}
}
// ConcreteVisitor: 도형의 둘레를 계산하는 방문자
// 각 도형 요소를 방문할 때 수행할 작업을 정의
class PerimeterCalculator implements Visitor {
@Override
public void visit(Circle circle) {
double perimeter = 2 * Math.PI * circle.getRadius();
System.out.println("Circle perimeter: " + perimeter);
}
@Override
public void visit(Rectangle rectangle) {
double perimeter = 2 * (rectangle.getWidth() + rectangle.getHeight());
System.out.println("Rectangle perimeter: " + perimeter);
}
}
Element
// Element 인터페이스
interface Shape {
void accept(Visitor visitor);
}
// ConcreteElement: 원 도형
// accept 메서드를 구현하여 방문자를 받아 visit 메서드를 호출
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getRadius() {
return radius;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
// ConcreteElement: 사각형 도형
// accept 메서드를 구현하여 방문자를 받아 visit 메서드를 호출
class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
Client
// Client 코드
public class VisitorPatternExample {
public static void main(String[] args) {
Shape circle = new Circle(5);
Shape rectangle = new Rectangle(4, 6);
Visitor areaCalculator = new AreaCalculator();
Visitor perimeterCalculator = new PerimeterCalculator();
// 도형에 대해 면적 계산
System.out.println("Calculating area:");
circle.accept(areaCalculator);
rectangle.accept(areaCalculator);
// 도형에 대해 둘레 계산
System.out.println("\nCalculating perimeter:");
circle.accept(perimeterCalculator);
rectangle.accept(perimeterCalculator);
}
}
이처럼 방문자 패턴을 사용하면 Shape 구조에 새로운 작업(면적 계산, 둘레 계산 등)을 추가하면서도 기존의 Shape 구조를 수정할 필요가 없다. 단순히 Visitor의 구현체만 새로 추가해주면 된다.
중재자 패턴(Mediator Pattern)
: 객체 간의 복잡한 통신을 캡슐화하여 객체들이 서로 직접 소통하지 않고, 대신 중재자 객체를 통해 소통하도록 하는 행동 패턴. 이를 통해 객체 간 결합도를 줄이고, 객체가 중재자와만 상호작용하게 하여 상호작용의 복잡성을 관리한다.
중재자 패턴 구조
Mediator
: 객체 간의 소통을 관리하는 메서드를 정의하는 중재자 인터페이스.ConcreteMediator
:Mediator
를 구현하며, 관련 객체 간의 구체적인 통신 로직을 처리- 합성을 통해 Colleague를 관리하고 있다
Colleague
: 중재자와 소통하는 객체로,Mediator
와의 소통 방법을 정의하는 인터페이스 또는 추상 클래스ConcreteColleague
:Colleague
를 구현한 구체적인 클래스이며, 서로 직접 통신하는 대신Mediator
를 통해 소통- Mediator를 알고 있다.
사용 시기
- 여러 객체 간의 상호작용이 많고 복잡해져서 결합도가 높아지는 경우
- 객체들이 서로 직접 통신하지 않으면서 독립성을 유지하고자 하는 경우
- 여러 객체의 상호작용을 중앙에서 관리하여 효율적이고 단순하게 조정하고자 할 때
장점
- 객체 간의 결합도가 낮아져 독립성이 높아진다
- 상호작용이 중앙에서 관리되므로 코드가 직관적이고 유지보수가 용이해진다
단점
- 중재자 클래스가 복잡해지면 관리가 어려워질 수 있다
- 모든 소통이
Mediator
를 거치므로, 중앙 집중화가 심해질 수 있다
구현
간단한 채팅 프로그램
Mediator
// Mediator 인터페이스
interface ChatRoomMediator {
void showMessage(User user, String message);
}
// ConcreteMediator: 메시지를 전달하는 중재자 역할
class ChatRoom implements ChatRoomMediator {
private List<User> users = new ArrayList<>();
public void addUser(User user) {
users.add(user);
}
@Override
public void showMessage(User user, String message) {
System.out.println(user.getName() + ": " + message);
}
}
Colleague
// Colleague: 사용자 인터페이스
abstract class User {
protected ChatRoomMediator mediator;
protected String name;
public User(ChatRoomMediator mediator, String name) {
this.mediator = mediator;
this.name = name;
}
public abstract void send(String message);
public abstract void receive(String message);
public String getName() {
return name;
}
}
// ConcreteColleague: 특정 사용자가 메시지를 보내고 받는 역할
class ChatUser extends User {
public ChatUser(ChatRoomMediator mediator, String name) {
super(mediator, name);
}
@Override
public void send(String message) {
System.out.println(this.name + " sends: " + message);
mediator.showMessage(this, message); // 메시지를 중재자를 통해 전달
}
@Override
public void receive(String message) {
System.out.println(this.name + " receives: " + message);
}
}
Client
// Client 코드
public class MediatorPatternExample {
public static void main(String[] args) {
ChatRoom chatRoom = new ChatRoom();
User user1 = new ChatUser(chatRoom, "Alice");
User user2 = new ChatUser(chatRoom, "Bob");
User user3 = new ChatUser(chatRoom, "Charlie");
// ChatRoom에 사용자 추가
chatRoom.addUser(user1);
chatRoom.addUser(user2);
chatRoom.addUser(user3);
// Alice가 메시지 전송
user1.send("Hello everyone!");
// Bob이 메시지 전송
user2.send("Hi Alice!");
}
}
이를 통해 각 User는 다른 User들에 대해 알 필요가 없고, ChatRoom이 모든 통신을 관리하게 된다.
기념품 패턴(Memento Pattern)
: 객체의 상태를 저장하고 복원할 수 있도록 하여, 객체의 상태가 변경되더라도 특정 시점으로 되돌릴 수 있게 하는 행동 패턴. 이 패턴은 객체의 내부 상태를 외부에서 직접 접근하지 않고도 이전 상태로 복원할 수 있게 해준다.
기념품 패턴 구조
Memento
: 객체의 상태를 저장하는 역할로, 외부에서는 이 상태에 직접 접근할 수 없다.Originator
: 객체의 상태를 관리하는 클래스createMementor()
메서드를 통해 상태를 저장하고,Memnto
를 생성한다.restore(Memento)
메서드를 통해 상태를 복원한다.
CareTaker
:Memento
를 관리하는 역할을 하며,Originator
의 상태를 저장하거나 복원할 때Memento
를 사용
사용 시기
- 객체의 상태를 캡슐화하면서도, 이전 상태로 되돌리는 기능이 필요할 때
- 상태를 저장하고 되돌리는 기능이 필요하지만, 객체의 내부 상태를 외부에 노출시키고 싶지 않은 경우
- 객체의 복잡한 상태를 일정 시점으로 쉽게 복원할 필요가 있을 때
장점
- 객체의 상태를 캡슐화하면서도, 이전 상태로 되돌릴 수 있다.
- 객체의 내부 구현을 숨기면서도 특정 시점으로의 상태 복원이 가능
단점
- 저장할 상태가 많거나 객체의 상태가 크다면, 많은 메모리를 사용할 수 있다.
- 모든 상태 변경을 저장해야 하는 경우, Memento가 많이 쌓일 수 있다.
구현
간단한 텍스트 편집기의 상태를 저장하고 복원하는 예제
Memento
// Memento: Originator의 상태를 저장
class TextMemento {
private final String text;
public TextMemento(String text) {
this.text = text;
}
public String getText() {
return text;
}
}
Originator
// Originator: 상태를 관리하고 Memento를 통해 상태를 저장/복원
class TextEditor {
private String text = "";
public void addText(String newText) {
text += newText;
}
public String getText() {
return text;
}
// 현재 상태를 저장하는 Memento 생성
public TextMemento save() {
return new TextMemento(text);
}
// Memento로부터 상태를 복원
public void restore(TextMemento memento) {
text = memento.getText();
}
}
CareTaker
// Caretaker: 상태를 저장하고 복원하는 역할
class TextEditorHistory {
private Stack<TextMemento> history = new Stack<>();
public void save(TextEditor editor) {
history.push(editor.save());
}
public void undo(TextEditor editor) {
if (!history.isEmpty()) {
editor.restore(history.pop());
}
}
}
Client
// Client 코드
public class MementoPatternExample {
public static void main(String[] args) {
TextEditor editor = new TextEditor(); // Originator
TextEditorHistory history = new TextEditorHistory(); // CareTaker
editor.addText("Hello");
history.save(editor); // 상태 저장
System.out.println("Text after adding 'Hello': " + editor.getText());
editor.addText(" World");
history.save(editor); // 상태 저장
System.out.println("Text after adding ' World': " + editor.getText());
// Undo (마지막 저장된 상태로 복원)
history.undo(editor);
System.out.println("Text after undo: " + editor.getText());
// Undo (처음 저장된 상태로 복원)
history.undo(editor);
System.out.println("Text after second undo: " + editor.getText());
}
}
실행 결과
Text after adding 'Hello': Hello
Text after adding ' World': Hello World
Text after undo: Hello
Text after second undo:
'ETC' 카테고리의 다른 글
[오브젝트 - 기초편] 영화 예매 도메인 예제 (3) | 2024.12.30 |
---|---|
[Design Pattern] Structural Pattern (0) | 2024.11.04 |
[Design Pattern] Behavioral Pattern(행동 패턴) - 1 (1) | 2024.11.02 |
[Design Pattern] Creational Pattern (1) | 2024.11.01 |
[CS] 문자 인코딩(Character Encoding) (1) | 2024.10.09 |