Skip to main content

State Pattern

Overview

객체 내부 상태에 따라 객체의 행동이 달라지는 패턴
특정 상태를 조건으로 하는 행위를 분리할 수 있다.
새 행위를 추가해도 기존 행위에 영향을 미치지 않는다.

State pattern overview diagram

Why to use

로직이 특정 상태에 따라 조건부로 실행되면
코드를 위에서 아래로 읽는 방식으로 시나리오 파악이 어렵다.
이럴 때 상태 패턴을 활용한다.

When to use

  • Operation 을 상태에 따라 실행시키는 로직을 구조화 하고 싶을 때

Example

온라인 강의 - 학생이 존재하고 온라인 강의 상태는 DRAFT, PRIVATE, PUBLISHED 3가지를 갖는다. 온라인 강의 상태에 따라 다음과 같이 권한이 부여된다.

Index학생 등록리뷰 등록
DRAFT1명만 가능불가능
PRIVATE사전 등록된 학생만 가능가능
PUBLIC가능가능

상태별 행위를 온라인 강의 클래스 내에서 모두 관리하면 다음과 같이 복잡한 코드가 만들어진다.

OnlineCourse.java

@Getter
public class OnlineCourse {

public enum State {
// 총 3개의 상태를 갖고, 이 상태마다 행위가 달라진다.
DRAFT, PUBLISHED, PRIVATE
}

private State state = State.DRAFT;
private List<String> reviews = new ArrayList<>();
private List<Student> students = new ArrayList<>();

// 리뷰 등록 아모르파티
public void addReview(String review, Student student) {
if (this.state == State.PUBLISHED) {
this.reviews.add(review);
} else if (this.state == State.PRIVATE && this.students.contains(student)) {
this.reviews.add(review);
} else {
throw new UnsupportedOperationException("리뷰를 작성할 수 없습니다.");
}
}

// 학생 등록 아모르파티
public void addStudent(Student student) {
if (this.state == State.DRAFT || this.state == State.PUBLISHED) {
this.students.add(student);
} else if (this.state == State.PRIVATE && availableTo(student)) {
this.students.add(student);
} else {
throw new UnsupportedOperationException("학생을 해당 수업에 추가할 수 없습니다.");
}

if (this.students.size() > 1) {
this.state = State.PRIVATE;
}
}

public void changeState(State newState) {
this.state = newState;
}

private boolean availableTo(Student student) {
return student.isEnabledForPrivateClass(this);
}
}

How to use

info
  1. 상태에 따라 달라지는 행위를 State 인터페이스로 위임한다.
  2. 상태 개수만큼 State 인터페이스를 구현하는 Concrete Class 를 생성한다.
    이때, 각각의 상태는 Context 로 온라인 강의를 참조한다.

온라인 강의 상태에 따라 학생 등록과 리뷰 등록 행위가 달라지므로 두 메소드를 state 패턴의 State interface 로 위임할 필요가 있다.

// state 에 얽혀있는 메소드 => State interface 로 위임.
public void addStudent(Student student) {}
// state 에 얽혀있는 메소드 => State interface 로 위임.
public void addReview(String review, Student student) {}

Context

OnlineCourse.java

@Getter
public class OnlineCourse {
private State state = new DraftState(this);
private Set<Student> students = new HashSet<>();
private List<String> reviews = new ArrayList<>();

public void changeState(State newState) {
this.state = newState;
}

public boolean isRegisteredStudent(Student student) {
return this.students.contains(student);
}

// add -> 직접 online course 에 적용
public void addStudent(Student student) {
this.students.add(student);
}

public void addReview(String review) {
this.reviews.add(review);
}
// register -> state 에 위임.
public void registerStudent(Student student) {
if (isRegisteredStudent(student)) throw new IllegalArgumentException("이미 등록된 학생입니다.");
this.state.addStudent(student);
}

public void registerReview(Student student, String review) {
this.state.addReview(student, review);
}
}

State

public interface State {
void addStudent(Student student);
void addReview(Student student, String review);
}

Client (Test code)

ClientTest.java

class ClientTest {
private Student falcon;
private Student vladimir;
private OnlineCourse onlineCourse;

@BeforeEach
void beforeEach() {
falcon = new Student("falcon");
vladimir = new Student("vladimir");
onlineCourse = new OnlineCourse();
}

@DisplayName("드래프트 상태 리뷰 등록 실패")
@Test
void addReviewFailTest() {
assertThrows(UnsupportedOperationException.class, () -> onlineCourse.registerReview(falcon, "나 팰콘인데 이거 ㄹㅇ 개좋음"));
}

@DisplayName("프리이빗 상태 사전 미등록 학생 등록 실패")
@Test
void addStudentFailTest() {
this.onlineCourse.changeState(new PrivateState(onlineCourse));

falcon.registerCourse(onlineCourse);
onlineCourse.registerStudent(falcon); // 성공

// vladimir 는 미등록
assertThrows(UnsupportedOperationException.class, () -> onlineCourse.registerStudent(vladimir));
}

@DisplayName("공개 수업은 모든 학생 등록 및 리뷰 등록 성공")
@Test
void addStudentsAndReviewsTest() {
this.onlineCourse.changeState(new PublishedState(onlineCourse));

onlineCourse.registerStudent(falcon);
onlineCourse.registerReview(vladimir, "나 블라디인데 이거 듣지도 않았지만 리뷰 등록 된다해서 해봄 ㅋ");
}
}

Class Diagram

State pattern class diagram

Pros and Cons

장점

  • 상태에 따른 행동을 깔끔하게 분리할 수 있다.
    코드를 위에서 아래로 읽어 시나리오를 분석할 수 있게 된다.
  • 상태 - 행위별로 단위 테스트가 쉬워진다.
  • Client 기존 코드 변경 없이 새로운 상태 확장이 가능하다. (OCP)

단점

  • 복잡도 증가
  • 상태 개수가 증가한 만큼 클래스를 생성해야하고 관리해야함.
    상태 조건 - 트리거가 완벽하게 구성된 것 같지만 이렇게 복잡한 상태가 존재할 경우
    Node.js 같은 Event-Driven 프로그래밍을 제공하는 플랫폼을 사용하는 것이 더 나아보인다.

🔗 Reference

코딩으로 학습하는 GoF의 디자인 패턴 - 백기선