GoF 디자인 패턴: 행위 패턴(4) - 이터레이터, 상태 | 타입스크립트 예시
요약
GoF 디자인 패턴 중 행위 패턴의 네 번째 파트입니다. 컬렉션의 내부 구조를 노출하지 않고 요소에 순차적으로 접근하는 이터레이터(Iterator) 패턴과, 객체의 내부 상태에 따라 행동을 바꾸는 상태(State) 패턴의 개념과 TypeScript 예시를 알아봅니다.
지난 행위 패턴 (3)편에서는 책임 연쇄(Chain of Responsibility) 패턴과 메멘토(Memento) 패턴을 통해 요청을 여러 객체가 처리하거나 객체의 상태를 안전하게 저장하는 방법을 알아보았습니다.
이번 포스트에서는 컬렉션 데이터를 다루는 우아한 방법과 객체의 상태에 따라 행동을 바꾸는 세련된 방법을 배워보겠습니다.
- 🚶♂️ 이터레이터 (Iterator) 패턴: 컬렉션의 내부 구현을 노출하지 않고, 그 안의 모든 요소에 순차적으로 접근하는 방법을 제공합니다.
- 🚦 상태 (State) 패턴: 객체가 자신의 내부 상태가 바뀜에 따라 스스로 행동을 변경하도록 허락합니다. 마치 객체의 클래스가 바뀌는 것처럼 보입니다.
이 패턴들이 어떻게 코드의 복잡도를 낮추고 유지보수성을 높이는지 함께 살펴보시죠!
🚶♂️ 7. 이터레이터 (Iterator) 패턴
이터레이터 패턴은 배열, 리스트, 트리 등과 같은 컬렉션(Collection) 객체의 내부 표현(구조)을 외부에 노출시키지 않고, 그 요소들을 순차적으로 접근할 수 있는 방법을 제공하는 패턴입니다.
가장 쉬운 비유는 'TV 리모컨의 채널 버튼' 입니다. 우리는 '다음' 버튼(▶)을 누르기만 하면 다음 채널로 넘어갑니다. 우리는 방송국 주파수 목록이 어떻게 저장되어 있는지, 어떤 자료구조로 관리되는지 전혀 알 필요가 없습니다. 이터레이터 패턴은 이처럼 컬렉션의 복잡한 내부를 숨기고, '다음 것(next)', '끝났는가(done)'와 같은 단순한 인터페이스만으로 데이터에 접근하게 해주는 역할을 합니다.
사실 현대 프로그래밍 언어에서는 이 패턴이 이미 깊숙이 내장되어 있습니다. 자바스크립트의 for...of
루프, 배열의 map
, filter
메서드, 자바의 Iterator
인터페이스 등이 모두 이터레이터 패턴의 구현체입니다.
🏗️ 기본 구조
- 📜 Iterator: 이터레이터의 공통 인터페이스입니다. 보통 다음 요소를 가져오는
next()
메서드와 순회가 끝났는지 확인하는hasNext()
(또는next()
의 반환값에done
속성을 포함) 메서드를 정의합니다. - ConcreteIterator:
Iterator
인터페이스를 구현한 구체적인 클래스입니다. 특정 컬렉션에 맞춰 순회 로직을 구현하며, 현재 순회 중인 위치를 추적합니다. - 📦 Aggregate: 이터레이터를 생성하는
createIterator()
메서드를 가지는 컬렉션의 공통 인터페이스입니다. - ConcreteAggregate:
Aggregate
인터페이스를 구현한 구체적인 컬렉션 클래스입니다.createIterator()
메서드 내에서 자신에게 맞는ConcreteIterator
의 인스턴스를 생성하여 반환합니다.
💻 예시: 자바스크립트 이터러블 프로토콜 구현하기
자바스크립트/타입스크립트에서는 for...of
구문을 사용하기 위해 이터레이터 패턴을 직접 구현할 수 있습니다. [Symbol.iterator]
라는 특별한 메서드를 가진 객체를 '이터러블(iterable)'이라고 부릅니다.
알파벳 순서대로 단어를 순회하는 간단한 단어장(WordCollection
) 클래스를 만들어 보겠습니다.
// Aggregate 인터페이스 (참고용, TS에서는 구조적 타이핑으로 인해 명시적 구현 불필요)
// 제네릭 <T> :클래스나 인터페이스를 선언하는 시점에는 타입을 확정하지 않고, 실제로 사용하는 시점에 타입을 지정할 수 있게 해주는 문법
// 예 : Iterator<string>, Iterator<number> 등
interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
}
// Iterator 인터페이스 (참고용)
interface Iterator<T> {
next(): { value: T; done: boolean };
}
// ConcreteAggregate: 단어장 컬렉션
class WordCollection {
private words: string[] = [];
add(word: string): void {
this.words.push(word);
}
// 이터레이터 생성 메서드
[Symbol.iterator](): Iterator<string> {
let index = 0;
const sortedWords = [...this.words].sort(); // 알파벳 순으로 정렬
// ConcreteIterator 로직 (익명 객체로 구현)
return {
next: (): { value: string; done: boolean } => {
if (index < sortedWords.length) {
return {
value: sortedWords[index++],
done: false,
};
} else {
return {
value: undefined,
done: true,
};
}
},
};
}
}
// 클라이언트 코드
const collection = new WordCollection();
collection.add("TypeScript");
collection.add("GoF");
collection.add("Design Patterns");
collection.add("Iterator");
console.log("단어장을 순회합니다:");
// WordCollection은 이터러블 프로토콜을 따르므로 for...of 사용 가능
for (const word of collection) {
console.log(word);
}
// 출력:
// Design Patterns
// GoF
// Iterator
// TypeScript
클라이언트는 WordCollection
내부에서 단어들이 어떻게 words
배열에 저장되는지, 정렬이 어떻게 일어나는지 전혀 알 필요가 없습니다. 그저 for...of
라는 표준적인 방법으로 컬렉션의 요소에 하나씩 접근할 수 있습니다. 이것이 바로 이터레이터 패턴이 제공하는 '접근'과 '구현'의 분리입니다.
이터레이터 패턴 중요 키워드 🔑
- 🚶♂️ 컬렉션의 내부 구조를 노출하지 않고 요소에 순차적으로 접근합니다.
- 🤝 접근 방식과 컬렉션 구현을 분리하여 결합도를 낮춥니다.
- 🔄 하나의 컬렉션에 대해 다양한 순회 방식을 제공할 수 있습니다. (예: 정방향, 역방향 이터레이터)
- 💡
for...of
,map
,filter
등 현대 언어의 많은 기능이 이 패턴을 기반으로 합니다.
🚦 8. 상태 (State) 패턴
상태 패턴은 객체의 내부 상태가 변경될 때, 객체의 행동 방식도 함께 변경되도록 하는 패턴입니다. 이 패턴을 사용하면, 객체는 마치 자신의 클래스가 바뀌는 것처럼 보입니다. if/else
나 switch
문으로 가득 찬 상태 관리 코드를, 상태를 나타내는 각각의 클래스로 분리하여 훨씬 깔끔하게 만들 수 있습니다.
'신호등' 🚦 을 생각하면 쉽습니다. 신호등은 '초록불', '주황불', '빨간불'이라는 상태를 가집니다. 각 상태일 때 신호등의 행동(다음 상태로 변경되는 것)은 완전히 다릅니다. 초록불일 때는 주황불로, 주황불일 때는 빨간불로, 빨간불일 때는 초록불로 바뀝니다. 상태 패턴은 이처럼 상태에 따른 행동들을 별도의 클래스로 캡슐화하고, 상태가 변할 때 행동을 책임질 클래스 자체를 교체해버리는 방식입니다.
🏗️ 기본 구조
- 🎭 Context: 상태를 가지는 주체입니다. 현재 상태를 나타내는
State
객체에 대한 참조를 가집니다. 클라이언트의 요청을 받으면, 현재State
객체에게 행동을 위임합니다. 또한, 상태 객체가Context
의 상태를 변경할 수 있도록setState()
와 같은 메서드를 제공합니다. - 📜 State: 모든 구체적인 상태 클래스들이 구현해야 할 공통 인터페이스입니다.
Context
가 위임할 행동들을 메서드로 정의합니다. (예:handle()
) - ConcreteState:
State
인터페이스를 구현하는 구체적인 클래스입니다. 특정 상태일 때의 행동을 구현하며, 필요에 따라Context
의 다음 상태를 결정하고 변경합니다.
💻 예시: 온라인 문서의 게시 워크플로우
'초안(Draft)' -> '검토 중(Moderation)' -> '게시됨(Published)' 상태를 가지는 온라인 문서 객체를 만들어 보겠습니다.
먼저, 모든 상태가 따라야 할 DocumentState
인터페이스를 정의합니다.
// State 인터페이스
interface DocumentState {
publish(): void;
}
다음으로, 상태를 가질 Context
객체인 Document
클래스를 만듭니다.
// Context: 상태를 가지는 문서 객체
class Document {
private state: DocumentState;
constructor() {
// 초기 상태는 '초안'
this.state = new DraftState(this);
}
// 상태 변경을 위한 메서드
changeState(state: DocumentState): void {
this.state = state;
}
// 현재 상태에 행동을 위임
publish(): void {
this.state.publish();
}
}
이제 각 상태에 대한 행동을 정의하는 ConcreteState
클래스들을 만듭니다.
// ConcreteState 1: 초안 상태
class DraftState implements DocumentState {
constructor(private document: Document) {}
publish(): void {
console.log("[초안] -> [검토 중]으로 상태를 변경합니다.");
this.document.changeState(new ModerationState(this.document));
}
}
// ConcreteState 2: 검토 중 상태
class ModerationState implements DocumentState {
constructor(private document: Document) {}
publish(): void {
console.log(
"[검토 중] -> [게시됨]으로 상태를 변경합니다. 이제 글이 공개됩니다!"
);
this.document.changeState(new PublishedState(this.document));
}
}
// ConcreteState 3: 게시됨 상태
class PublishedState implements DocumentState {
constructor(private document: Document) {}
publish(): void {
console.log("[게시됨] 이미 게시된 상태입니다. 아무것도 하지 않습니다.");
// 상태 변경 없음
}
}
클라이언트 코드는 Document
객체의 publish()
메서드만 호출하면 됩니다.
// 클라이언트 코드
const myDocument = new Document();
console.log("--- 1. 초안 상태에서 게시 시도 ---");
myDocument.publish(); // 초안 -> 검토 중
console.log("\n--- 2. 검토 중 상태에서 게시 시도 ---");
myDocument.publish(); // 검토 중 -> 게시됨
console.log("\n--- 3. 게시된 상태에서 게시 시도 ---");
myDocument.publish(); // 아무 일도 일어나지 않음
Document
클래스 안에는 if (state === 'draft') { ... } else if (state === 'moderation') { ... }
와 같은 조건문이 전혀 없습니다. 모든 상태 관련 로직은 각각의 State
클래스에 위임되었습니다. 만약 '반려됨(Rejected)'이라는 새로운 상태가 필요하다면, RejectedState
클래스를 만들어 추가하기만 하면 되므로 '개방-폐쇄 원칙(OCP)'을 잘 따릅니다.
상태 패턴 중요 키워드 🔑
- 🚦 상태에 따른 행동을 별도의 클래스로 캡슐화합니다.
- 🎭 객체가 내부 상태에 따라 행동을 바꾸도록 합니다. (마치 클래스가 바뀌는 것처럼)
- 🧹 복잡한
if/else
또는switch
문으로 된 상태 머신 코드를 정리하는 데 매우 효과적입니다.- 💡 상태 전환의 책임이
Context
가 아닌 개별State
클래스에 있어 규칙이 명확해집니다.