상태 패턴
State Pattern
· 마지막 업데이트: 2026-10-24 · 19 min read
상태 패턴(State Pattern)은 현재 상태에 따라 달라지는 행동을 상태별 구현에 모으고, 본체가 현재 상태 객체에 일을 맡기는 GoF 행위 패턴이다. 문서가 초안일 때는 내용을 고칠 수 있지만 검토 중에는 수정할 수 없고, 게시 뒤에는 제출이나 승인도 더 할 수 없는 경우라면, 같은 Edit()나 Approve() 요청을 받아도 현재 상태의 구현이 허용하거나 거부하도록 나눌 수 있다. 이처럼 본체가 요청을 받고 실제 처리는 다른 객체의 메서드로 넘기는 것을 위임이라고 한다.
로그인 실패 횟수, 변수에 저장한 값, 편집 중인 본문은 모두 프로그램의 상태 정보다. 일반 개념과 그 역사는 프로그래밍에서 상태(state)의 역사에서 다루고, 여기서는 같은 요청을 상태에 따라 다르게 처리할 때 그 행동을 어디에 모을지 설명한다.
상태 값과 상태 머신과 State 패턴
문서 검토 기능을 만들려면 현재 어느 단계인지 기록하고, 그 단계에서 어떤 요청을 허용하며 요청 뒤에 어디로 옮길지 정해야 한다. 이는 데이터와 동작 규칙을 정하는 일이다. 이 규칙을 조건문이나 전이표로 처리할지, 상태별 구현에 모을지는 그다음 코드 구성의 문제라서 상태 값·상태 머신·State 패턴을 나누어 설명한다.
구분 | 의미 | 예 |
|---|---|---|
상태 값 | 다음 동작에 영향을 주는 현재 정보 | 문서의 |
상태 머신 | 사건에 따라 상태와 행동이 어떻게 달라지는지 정한 모델 | 초안에서 제출하면 검토 중이 된다 |
State 패턴 | 상태별 행동을 별도 구현에 모으고 현재 상태에 위임한다 | 본체의 |
- 구분
상태 값
- 의미
다음 동작에 영향을 주는 현재 정보
- 예
문서의
Draft값, 편집 중인 본문
- 구분
상태 머신
- 의미
사건에 따라 상태와 행동이 어떻게 달라지는지 정한 모델
- 예
초안에서 제출하면 검토 중이 된다
- 구분
State 패턴
- 의미
상태별 행동을 별도 구현에 모으고 현재 상태에 위임한다
- 예
본체의
Submit()이 현재 상태 객체의Submit()을 호출한다
상태 머신은 enum + switch, 전이표, 순수한 전이 함수로도 구현할 수 있다. State 패턴은 이런 상태 머신을 상태별 객체로 구현하는 한 방법이다. Robert Nystrom의 Game Programming Patterns도 상태 머신과 enum + switch를 먼저 설명한 뒤, 상태별 구현에 위임하는 GoF 패턴을 별도로 다룬다.1
상태는 어떤 기준으로 나누는가
상태 클래스부터 만드는 대신, 같은 요청을 받아도 다르게 처리해야 하는 상황을 먼저 찾는다. 이 문서에서는 작성 중인 내용과 검토할 내용을 구분하고, 승인된 문서는 더 수정하지 않는다는 규칙을 정했다. 그 차이를 세 상태로 나타낸다.
상태 | 따로 구분하는 이유 |
|---|---|
초안 ( | 내용을 고치는 단계다. 아직 검토에 제출하지 않았으므로 수정과 제출을 허용한다. |
검토 중 ( | 제출한 본문을 고정하고 결정을 기다리는 단계다. 수정 대신 승인이나 반려를 받는다. |
게시됨 ( | 승인이 끝난 단계다. 이 예제에서는 게시된 내용을 다시 수정하거나 검토하지 않는다. |
- 상태
초안 (
Draft)- 따로 구분하는 이유
내용을 고치는 단계다. 아직 검토에 제출하지 않았으므로 수정과 제출을 허용한다.
- 상태
검토 중 (
InReview)- 따로 구분하는 이유
제출한 본문을 고정하고 결정을 기다리는 단계다. 수정 대신 승인이나 반려를 받는다.
- 상태
게시됨 (
Published)- 따로 구분하는 이유
승인이 끝난 단계다. 이 예제에서는 게시된 내용을 다시 수정하거나 검토하지 않는다.
본문이 똑같아도 초안에서는 수정할 수 있고 검토 중에는 수정할 수 없다. 본문만 보고는 이 차이를 알 수 없으므로 문서가 현재 어느 단계인지 따로 기억한다. 세 상태의 이름과 규칙은 GoF가 정해 준 목록이 아니라 이 문서 검토 기능에서 정한 것이다.
그렇다면 반려됨은 왜 별도 상태가 아닐까? 이 예제에서는 반려 후에도 초안처럼 수정하고 다시 제출하므로 Draft로 돌아가고, 반려 사유만 데이터로 보관한다. 반려된 문서에만 허용하는 별도 동작이 필요해지면 그때 상태를 더 나눌지 검토한다.
패턴을 만들기 전에 전이부터 정한다
문서 검토 기능의 규칙을 다음처럼 정했다고 하자. 게시 후 다시 편집하는 기능은 이 예제에 없으며, 반려된 초안은 수정한 뒤 다시 제출할 수 있다.
현재 상태 | 요청 | 결과 |
|---|---|---|
초안 | 수정 | 본문을 바꾸고 초안으로 남는다 |
초안 | 제출 | 검토 중으로 바뀐다 |
검토 중 | 승인 | 게시됨으로 바뀐다 |
검토 중 | 반려 | 사유를 가진 초안으로 돌아간다 |
검토 중 | 수정 또는 다시 제출 | 거부한다 |
게시됨 | 수정·제출·승인·반려 | 거부한다 |
초안 | 승인 또는 반려 | 거부한다 |
- 현재 상태
초안
- 요청
수정
- 결과
본문을 바꾸고 초안으로 남는다
- 현재 상태
초안
- 요청
제출
- 결과
검토 중으로 바뀐다
- 현재 상태
검토 중
- 요청
승인
- 결과
게시됨으로 바뀐다
- 현재 상태
검토 중
- 요청
반려
- 결과
사유를 가진 초안으로 돌아간다
- 현재 상태
검토 중
- 요청
수정 또는 다시 제출
- 결과
거부한다
- 현재 상태
게시됨
- 요청
수정·제출·승인·반려
- 결과
거부한다
- 현재 상태
초안
- 요청
승인 또는 반려
- 결과
거부한다
화살표에는 뒤의 객체 구현에서 사용할 메서드 이름을 붙였다. Edit()는 초안의 본문만 바꾸므로 같은 상태로 돌아오고, Reject(reason)는 반려 사유를 가진 초안으로 돌아간다. 그림에 없는 요청은 앞의 표처럼 거부하며, 게시 상태에서 되돌아가는 경로도 이 예제에는 없다.
전이만 계산하면 되는 요구라면 다음 함수로도 충분할 수 있다. 이것은 상태 머신의 전이를 코드로 옮긴 것이며, 아직 State 패턴은 아니다. 잘못된 전이와 정의되지 않은 enum 값은 예외로 거부한다.
using System;
public enum ReviewStage { Draft, InReview, Published }
public enum ReviewAction { Submit, Approve, Reject }
public static class ReviewTransitions
{
public static ReviewStage Next(ReviewStage stage, ReviewAction action) =>
(stage, action) switch
{
(ReviewStage.Draft, ReviewAction.Submit) => ReviewStage.InReview,
(ReviewStage.InReview, ReviewAction.Approve) => ReviewStage.Published,
(ReviewStage.InReview, ReviewAction.Reject) => ReviewStage.Draft,
_ => throw new InvalidOperationException("허용되지 않은 전이입니다.")
};
}이 함수는 전이의 허용 여부와 다음 상태만 계산하며, 본문 저장이나 반려 사유 처리는 맡지 않는다. 작은 작업 흐름에서는 이 정도로 시작할 수 있다. 클래스로 나누는 일이 필요한지는 상태에 따라 달라지는 행동을 더 살펴본 뒤 결정하면 된다.
언제 State 패턴이 도움이 되는가
수정, 제출, 승인, 반려 메서드마다 현재 상태를 검사하는 코드가 반복되고, 상태 하나의 규칙을 바꾸려면 여러 메서드를 찾아가야 한다면 상태별로 행동을 묶을 이유가 생긴다. 초안에만 필요한 반려 사유나 검토 중에만 필요한 임시 데이터가 본체에 계속 쌓이는 경우도 같은 신호다.
상태가 많아도 전이표 하나로 충분히 읽힐 수 있고, 상태가 적어도 각 상태의 행동과 데이터가 크면 나누는 편이 낫다. 전이 함수가 이미 짧고 명확한데 상태마다 파일을 만드는 것으로 시작하면 규칙을 확인하려고 더 많은 곳을 열어야 한다.
State 패턴을 적용하더라도 전이의 소유자는 정해야 한다. 상태 객체가 다음 상태를 정할 수도 있고 별도의 전이 조정 코드가 맡을 수도 있는데, 같은 규칙을 양쪽에 중복해서 두지는 않는다. 아래 구현에서는 상태 객체가 자신에게 허용된 요청을 처리하며 다음 상태를 정하고, 본체는 현재 상태를 보관한다.
C#으로 문서 검토 상태 분리하기
ReviewDocument가 본문과 현재 상태를 보관하고, 수정·제출·승인·반려 요청은 현재 상태 객체로 전달한다. DraftState, InReviewState, PublishedState는 문서 안에 감춰 두었으므로 외부 코드가 임의의 상태 객체를 주입하거나 상태를 바로 바꿀 수 없다. Stage는 조회용 값이고 행동을 분기하는 또 다른 저장 필드가 아니다.
앞의 ReviewTransitions와 아래 객체 구현은 같은 규칙을 서로 다른 형태로 보여준다. 비교와 테스트를 위해 함께 둔 독립된 구현이므로, 아래 코드는 앞의 전이 함수를 호출하지 않는다. 실제 적용에서는 전이 규칙을 어느 쪽에서 관리할지 정하고 같은 규칙을 두 곳에서 따로 갱신하지 않는다.
이번 그림은 전이가 아니라 타입 관계다. 실선의 빈 삼각형은 공통 부모인 DocumentState를 향하고, 각 상자에는 해당 상태가 재정의한 동작만 적었다. PublishedState는 동작을 재정의하지 않아 부모의 거부 동작을 그대로 사용하며, Stage 등 속성과 보조 코드는 그림에서 생략했다.
public sealed class ReviewDocument
{
private string content;
private DocumentState state = new DraftState(null);
public string Content => content;
public ReviewStage Stage => state.Stage;
public string? RejectionReason => state.RejectionReason;
public ReviewDocument(string content)
{
this.content = RequireText(content, nameof(content));
}
public void Edit(string content) =>
state.Edit(this, RequireText(content, nameof(content)));
public void Submit() => state.Submit(this);
public void Approve() => state.Approve(this);
public void Reject(string reason) =>
state.Reject(this, RequireText(reason, nameof(reason)));
private static string RequireText(string value, string parameterName)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("비어 있지 않은 내용이 필요합니다.", parameterName);
return value;
}
private abstract class DocumentState
{
public abstract ReviewStage Stage { get; }
public virtual string? RejectionReason => null;
// 상태별로 허용한 동작만 재정의한다. 나머지는 명시적으로 거부한다.
public virtual void Edit(ReviewDocument document, string content) =>
throw Denied(nameof(Edit));
public virtual void Submit(ReviewDocument document) =>
throw Denied(nameof(Submit));
public virtual void Approve(ReviewDocument document) =>
throw Denied(nameof(Approve));
public virtual void Reject(ReviewDocument document, string reason) =>
throw Denied(nameof(Reject));
private InvalidOperationException Denied(string operation) =>
new($"{Stage} 상태에서는 {operation} 요청을 처리할 수 없습니다.");
}
private sealed class DraftState : DocumentState
{
public override ReviewStage Stage => ReviewStage.Draft;
public override string? RejectionReason { get; }
public DraftState(string? rejectionReason)
{
RejectionReason = rejectionReason;
}
public override void Edit(ReviewDocument document, string content) =>
document.content = content;
public override void Submit(ReviewDocument document) =>
document.state = new InReviewState();
}
private sealed class InReviewState : DocumentState
{
public override ReviewStage Stage => ReviewStage.InReview;
public override void Approve(ReviewDocument document) =>
document.state = new PublishedState();
public override void Reject(ReviewDocument document, string reason) =>
document.state = new DraftState(reason);
}
private sealed class PublishedState : DocumentState
{
public override ReviewStage Stage => ReviewStage.Published;
}
}처음 상태와 다음 상태는 누가 정하는가
새 문서는 아직 제출되지 않았으므로 초안에서 시작한다. 코드의 private DocumentState state = new DraftState(null);가 문서마다 처음 사용할 상태 객체를 만든다. 여기서 null은 상태가 없다는 뜻이 아니라, 처음 초안에는 반려 사유가 없다는 뜻이다.
DocumentState는 상태들이 공통으로 받는 요청의 형태를 정의하고, state 필드는 지금 그 요청을 처리할 객체 하나를 가리킨다. 처음에는 실제 객체가 DraftState이므로 문서의 Submit()도 초안 상태의 구현으로 전달된다.
다음 상태는 메서드가 처리한 사건에 따라 정한다. 호출자가 Submit()을 요청하면 DraftState.Submit()이 document.state = new InReviewState();를 실행하고, 그다음 요청부터는 새 검토 상태가 처리한다. 승인할 때는 InReviewState.Approve()가 PublishedState로, 반려할 때는 InReviewState.Reject(reason)이 사유를 가진 DraftState로 교체한다. 시간이 지났다고 상태가 저절로 바뀌는 것은 아니다.
Stage 값도 따로 대입하지 않는다. Stage => state.Stage가 현재 객체의 값을 읽으므로 상태 객체를 교체하면 조회 결과도 함께 바뀐다. 외부에는 state를 임의로 바꾸는 setter를 열지 않고, 허용한 요청을 통해서만 전이하게 했다. 저장된 문서를 불러와 상태를 복원하는 기능은 이 예제에 포함하지 않는다.
ReviewDocument.Edit()는 if (Stage == Draft)로 직접 분기하지 않고 현재 상태의 Edit()을 호출하는데, 이 동작을 허용한 것은 초안 상태뿐이다. 검토 중에는 같은 수정 요청을 거부하고 승인 요청은 문서를 게시 상태로 바꾸므로, 검토 상태의 규칙을 확인하려면 그 상태 구현을 보면 된다.
반려 사유는 현재 초안에 딸린 데이터라서 수정하는 동안에는 유지되지만, 다시 제출하면 새 검토 상태로 바뀌면서 사라진다. 검토 이력은 이 객체에 보관하지 않는다. 누가 언제 무엇을 반려했는지 남겨야 한다면 별도의 이력 저장 계약이 필요하다.
잘못된 텍스트는 ArgumentException, 현재 상태에서 허용되지 않는 요청은 InvalidOperationException으로 구별한다. 이 예제의 입력 검사와 거부 경로에서는 본문이나 상태를 바꾸지 않으며, 호출자가 오류를 확인할 수 있도록 예외를 전달한다.
허용된 전이와 거부 경로 테스트
다음 테스트는 정상적인 검토 흐름뿐 아니라 각 상태에서 금지한 요청, 빈 본문·반려 사유, 재제출 후 반려 사유의 수명을 확인한다. 비교용 전이 함수와 객체 구현의 전이도 대조한다.
public static class Program
{
private static int checks;
public static void Main()
{
var document = new ReviewDocument("첫 초안");
Equal(ReviewStage.Draft, document.Stage);
Equal("첫 초안", document.Content);
Equal<string?>(null, document.RejectionReason);
document.Edit("수정한 초안");
Equal("수정한 초안", document.Content);
document.Submit();
Equal(ReviewStage.InReview, document.Stage);
document.Reject("근거를 보강해 주세요");
Equal(ReviewStage.Draft, document.Stage);
Equal("근거를 보강해 주세요", document.RejectionReason);
document.Edit("근거를 보강한 본문");
Equal("근거를 보강해 주세요", document.RejectionReason);
document.Submit();
Equal<string?>(null, document.RejectionReason);
document.Approve();
Equal(ReviewStage.Published, document.Stage);
Equal("근거를 보강한 본문", document.Content);
Expect<ArgumentException>(() => new ReviewDocument(" "));
Expect<ArgumentException>(() => new ReviewDocument(null!));
var draft = new ReviewDocument("본문");
Expect<ArgumentException>(() => draft.Edit(" "));
Equal("본문", draft.Content);
var inReview = At(ReviewStage.InReview);
Expect<ArgumentException>(() => inReview.Reject(" "));
Equal(ReviewStage.InReview, inReview.Stage);
// 모든 상태와 요청의 조합을 확인한다. 금지된 요청은 상태와 내용을 보존한다.
foreach (var stage in Enum.GetValues<ReviewStage>())
{
foreach (var action in Enum.GetValues<ReviewAction>())
{
var item = At(stage);
var before = (item.Stage, item.Content, item.RejectionReason);
bool allowed = (stage, action) is
(ReviewStage.Draft, ReviewAction.Submit) or
(ReviewStage.InReview, ReviewAction.Approve) or
(ReviewStage.InReview, ReviewAction.Reject);
if (allowed)
{
var expected = ReviewTransitions.Next(stage, action);
Execute(item, action);
Equal(expected, item.Stage);
Equal("본문", item.Content);
}
else
{
Expect<InvalidOperationException>(() => ReviewTransitions.Next(stage, action));
Expect<InvalidOperationException>(() => Execute(item, action));
Equal(before, (item.Stage, item.Content, item.RejectionReason));
}
}
}
foreach (var stage in new[] { ReviewStage.InReview, ReviewStage.Published })
{
var item = At(stage);
var before = (item.Stage, item.Content, item.RejectionReason);
Expect<InvalidOperationException>(() => item.Edit("새 본문"));
Equal(before, (item.Stage, item.Content, item.RejectionReason));
}
Expect<InvalidOperationException>(() => ReviewTransitions.Next((ReviewStage)999, ReviewAction.Submit));
Expect<InvalidOperationException>(() => ReviewTransitions.Next(ReviewStage.Draft, (ReviewAction)999));
Console.WriteLine($"State pattern checks: {checks}");
}
private static ReviewDocument At(ReviewStage stage)
{
var item = new ReviewDocument("본문");
if (stage is ReviewStage.InReview or ReviewStage.Published) item.Submit();
if (stage is ReviewStage.Published) item.Approve();
return item;
}
private static void Execute(ReviewDocument document, ReviewAction action)
{
switch (action)
{
case ReviewAction.Submit: document.Submit(); break;
case ReviewAction.Approve: document.Approve(); break;
case ReviewAction.Reject: document.Reject("검토 의견"); break;
default: throw new ArgumentOutOfRangeException(nameof(action));
}
}
private static void Equal<T>(T expected, T actual)
{
if (!System.Collections.Generic.EqualityComparer<T>.Default.Equals(expected, actual))
throw new Exception($"기댓값: {expected}, 실제값: {actual}");
checks++;
}
private static void Expect<TException>(Action action) where TException : Exception
{
try { action(); }
catch (TException) { checks++; return; }
throw new Exception($"{typeof(TException).Name} 예외가 필요합니다.");
}
}전략 패턴과 언제 구별하는가
전략 패턴은 같은 일을 수행할 여러 방법을 교체할 때 쓴다. 문서의 내보내기 형식을 PDF와 HTML 중에서 고르는 것은 전략으로 설명하기 쉽고, 문서가 초안인지 게시됐는지에 따라 수정·제출·승인 요청을 다르게 처리하는 것은 상태 모델의 문제다. 후자의 행동을 상태별 구현에 모아 위임하는 것이 State 패턴이다.
상태와 전략은 함께 쓸 수 있다. 게시된 문서를 내보낼 때 현재 상태가 내보내기 허용 여부를 결정하고, 선택된 포맷 전략이 변환을 수행하게 할 수 있다. 그렇다고 DraftPdfState, DraftHtmlState, PublishedPdfState, PublishedHtmlState처럼 상태와 출력 형식의 모든 조합을 클래스로 만들 필요는 없다. 두 변화가 독립적이라면 따로 보관하고 조립하는 편이 낫다.
"외부에서 바꾸면 전략이고 내부에서 바꾸면 상태"라고 외우지는 않는다. 외부의 승인 요청이 상태 전이를 일으킬 수도 있고 본체가 데이터 크기에 따라 전략을 고를 수도 있다. 현재 모드의 행동을 모으려는지, 같은 역할의 대안들을 교체하려는지가 구별 기준이다.
실무에 넣을 때 별도로 정할 것
상태는 권한과 같지 않다
검토 중이면 승인할 수 있다는 상태 규칙과, 지금 요청한 사용자가 승인 권한을 가졌다는 사실은 다르다. 이 코드는 사용자 인증이나 권한 검사를 구현하지 않으므로 실제 서비스에서는 신뢰할 수 있는 권한 확인 경계를 거친 뒤 요청을 수행해야 한다. Approve()라는 메서드 이름이나 상태 객체가 권한을 증명하지는 않는다.
객체 하나의 전이는 데이터베이스 트랜잭션이 아니다
이 예제는 한 실행 흐름에서 사용하는 메모리 객체다. 둘 이상의 스레드가 동시에 변경하거나 여러 서버가 같은 문서를 수정할 때의 동기화는 제공하지 않는다. 실제 저장소에서는 기대한 버전과 현재 상태를 확인하는 갱신, 영향받은 행 수 검사, 트랜잭션 등으로 충돌을 다뤄야 하며, 상태 필드를 객체로 바꾼 것만으로 경쟁 상태가 사라지는 것은 아니다.
또한 상태를 바꾸고 알림을 보내는 도중 한쪽만 성공할 수 있다. 저장소 갱신과 외부 부작용의 실패를 어떻게 구분하고 복구할지 정해야 하며, 상태 객체 안에서 네트워크 요청을 호출했다고 그 둘이 원자적으로 묶이지는 않는다.
상태 객체의 데이터와 수명
이 구현은 초안 상태마다 반려 사유를 가질 수 있다. 이런 상태 객체를 여러 문서가 공유하면 문서별 데이터가 섞일 수 있으므로 공유하지 않는다. 아무 요청별 데이터도 없는 상태 구현은 공유할 수 있는지 검토할 수 있지만, 상태라는 이름만 보고 전부 싱글턴으로 만들지는 않는다.
상태 진입·종료 때 자원을 열고 닫아야 한다면 실패 순서도 정해야 한다. 새 상태의 준비가 실패했는데 기존 상태를 이미 해제한 상황 등을 처리해야 하므로, 이 예제의 참조 대입만으로 자원 전이까지 구현됐다고 볼 수는 없다.
선택 기준
현재 코드의 모습 | 먼저 볼 방법 |
|---|---|
현재 상태를 화면에 표시할 뿐이다 | 상태 값과 표시 문구 대응표 |
허용된 상태 전이만 검사하면 된다 | 전이 함수나 전이표 |
상태별 행동이 여러 연산에 반복되고 함께 바뀐다 | State 패턴 |
대안 알고리즘 하나를 교체하려 한다 | Strategy |
일련의 단계를 순서대로 실행해야 한다 | 파이프라인이나 명시적 작업 흐름 |
- 현재 코드의 모습
현재 상태를 화면에 표시할 뿐이다
- 먼저 볼 방법
상태 값과 표시 문구 대응표
- 현재 코드의 모습
허용된 상태 전이만 검사하면 된다
- 먼저 볼 방법
전이 함수나 전이표
- 현재 코드의 모습
상태별 행동이 여러 연산에 반복되고 함께 바뀐다
- 먼저 볼 방법
State 패턴
- 현재 코드의 모습
대안 알고리즘 하나를 교체하려 한다
- 먼저 볼 방법
Strategy
- 현재 코드의 모습
일련의 단계를 순서대로 실행해야 한다
- 먼저 볼 방법
파이프라인이나 명시적 작업 흐름
작은 상태 머신을 일부러 객체 계층으로 키우기보다, 상태 하나의 규칙을 고칠 때 관련 행동과 데이터를 한곳에서 살펴볼 수 있는지를 기준으로 삼으면 된다. State 패턴은 그 행동과 데이터를 상태별로 묶는 방법이다.
역사와 참고문헌
GoF의 Design Patterns는 State를 행위 패턴으로 정리했다. 출판사 서지는 이 책의 출판일을 1994년 10월 31일로 적으며, 이것을 상태 개념이나 상태 머신이 처음 등장한 시점으로 읽어서는 안 된다.2
관련 문서: 프로그래밍 패턴
예제는 .NET 8 SDK(C# 12) 기준이며, 세 C# 코드 블록을 한 파일에 순서대로 넣어 실행한다.
각주
- Robert Nystrom. Game Programming Patterns, State 저자 원문. 상태 머신,
enum + switch, 상태 객체에 위임하는 State 패턴의 구별. ↩ - Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software, State. Addison-Wesley, 1994. 출판사 서지. ↩