상태를 통해 행동을 관리하는, 상태패턴(State Pattern)

학습 동기

크루들과 블랙잭 미션 이야기하다가 오리한테 상태패턴에 대해서 들었다. 네오가 오늘 강의에서 상태패턴을 적용하는 과정을 보여줬는데, 너무 신기해서 한번 직접 구현해보려고 공부하게됐다. 지금 미션에서는 플레이어가 자신의 상태를 정하고 게임이 마치면 플레이어가 계산을 하도록 구현이 되어있다. 이를 상태 패턴으로 구현하게 되면 상태에 따라서 행동이 달라지도록 만들 수 있다.


상태 패턴(State Pattern)

정의:

특정한 상태에 따라 행동이 달라지는 객체들을 위한 패턴

<예시>
1. TV 리모컨은 TV의 상태에 따라 행동이 달라진다.

  • TV가 켜짐 : 볼륨 증가 버튼을 누르면 볼륨이 증가한다.
  • TV가 꺼짐 : 볼륨 증가 버튼을 누르면 아무일도 일어나지 않는다.
  1. 블로그 글의 상태에 따라서, 작성중일 때는 다른 사람이 볼 수 없어야 하고, 좋아요, 댓글도 남길 수 없어야한다.
  • 공개된 상태: 조회, 좋아요, 댓글 가능
  • 비공개된 상태: 작성자외에 사람은 조회, 좋아요, 댓글 불가능
  1. 블랙잭 미션에서는 게임의 진행 상태에 따라서 구현해볼 수 있다.
  • 게임 중인 상태: 카드를 받을 수 있다, 점수를 계산할 수 없다
  • 게임 종료 상태: 카드를 받을 수 없다, 점수를 계산할 수 있다

적용 예시

온라인 강의라는 클래스를 정의해보자. 상태는 작성중, 작성완료, 비밀글 총 3가지가 있다.
두가지 기능을 구현해야하는데

<기능 구현 목록>
1. 리뷰를 등록하는 기능
2. 수업에 수강생을 등록하는 기능

  1. 리뷰를 등록하는 기능
    작성완료 상태이거나 비밀글이지만 해당 학생은 접근 가능한 상태 이면 리뷰가 추가 가능하고 아니라면 리뷰를 작성할 수 없다고 해야합니다.

  2. 수업에 수강생을 등록하는 기능
    작성 완료 상태 이거나 작성중 상태 이거나 비밀 글이지만 접근가능한 상태 라면 수강생을 추가할 수 있지만 아니라면 수강생을 받을 수 없습니다.

상태 패턴을 적용하지 않고 코드를 작성한다면, 이렇게 작성할 수 있다.

<적용 전>

public class OnlineCourse {

    public enum State {
        DRAFT, PUBLISHED, PRIVATE
    }

    private State state = State.DRAFT;
    private final List<String> reviews = new ArrayList<>();
    private final 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;
        }
    }

딱 봐도 코드에 if문이 너무 많다, 조건을 일일히 만들어서 확인하고 같은 일을 반복해야 한다. 만약 여기에 새로운 상태가 추가된다면? 모든 코드를 손봐야한다, 유지보수에 부적절한 코드가 될 것이다. 그러면 이제 상태 패턴을 적용해보고 어떻게 구현할 수 있을지 보자.

<적용 후>

public class OnlineCourse {
    private State state = new Draft(this);

    private List<Student> students = new ArrayList<>();
    private List<String> reviews = new ArrayList<>();

    public void addStudent(Student student) {
        this.state.addStudent(student);
    }

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

위 코드와 마찬가지로 state를 필드로 가지고, 행동을 state에서 받아서 사용하게 된다. 학생을 추가할 때 OnlineCourse에서 조건을 확인하는게 아니라 해당 상태를 나타내는 구현체에서 기능을 구현할 수 있다.

public interface State {

    void addReview(String review, Student student);

    void addStudent(Student student);
}

public class Draft implements State{

    private OnlineCourse onlineCourse;

    public Draft(OnlineCourse onlineCourse) {
        this.onlineCourse = onlineCourse;
    }

    @Override
    public void addReview(String review, Student student) {
        throw new UnsupportedOperationException("드래프트 상태에서는 리뷰를 남길 수 없습니다.");
    }

    @Override
    public void addStudent(Student student) {
        this.onlineCourse.getStudents().add(student);
        if (onlineCourse.getStudents().size() > 1) {
            onlineCourse.changeState(new Private(this.onlineCourse));
        }
    }
}

상태 인터페이스에서 기능을 추상적으로 구현해놓고, 각 상태의 구현체에서 해당 조건에 따라서 기능을 구현하도록 만든다. 이로써 우리는 상태에 따라 행동이 달라지는 객체를 만들 수 있게 됐다.

장단점

장점

  1. 상태에 따른 행동을 개별 클래스에서 관리할 수 있다.
  2. 새로운 상태가 추가될 때, 기존 상태에 따른 동작을 변경하지 않아도 된다.

단점

단순한 상태에서 구현하게 되면 오버 엔지니어링할 가능성이 높다


정리

블랙잭 같은 경우 상태에 따라서 할 수 있는 기능들이 달라진다. 상태패턴을 이용하면 클래스는 많아지지만 if 문을 줄이고 유지보수에 유리한 코드가 나올 것 같다.

Reference

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

좋은 웹페이지 즐겨찾기