ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 상태 패턴 (State Pattern)
    디자인 패턴 2022. 8. 13. 13:18

    상태 패턴


    객체 내부 상태 변경에 따라 객체의 행동이 달라지는 패턴

    • 상태에 특화된 행동들을 분리해 낼 수 있으며, 새로운 행동을 추가하더라도 다른 행동에 영향을 주지 않는다.

     

    예로 티비가 켜져있다면 음량버튼을 누르면 음량이 증가하거나 감소한다. 하지만 티비의 전원버튼을 끈다면 음량버튼을 아무리 눌러도 티비의 음량은 바뀌지 않는다. 

     

    즉, 티비 전원의 상태에 (리모컨의 상태) 따라 행동이 바뀌는 것이다.

     

     

     

     

    상태 패턴 적용 전


    온라인 강의가 있고 이것을 학생들이 수강할 수 있다고 하자. 

    온라인 강의에 학생을 추가하거나 리뷰를 달 수 있는데 이 때 온라인 강의 상태에 따라 학생 추가와 리뷰 등록이 다르게 작동한다. 

     

     

    Client

    public class Client {
        public static void main(String[] args) {
            Student student = new Student("whiteship");
            OnlineCourse onlineCourse = new OnlineCourse();
    
            Student keesun = new Student("keesun");
            keesun.addPrivateCourse(onlineCourse);
    
            onlineCourse.addStudent(student);
            onlineCourse.changeState(OnlineCourse.State.PRIVATE);
    
            onlineCourse.addStudent(keesun);
    
            onlineCourse.addReview("hello", student);
    
            System.out.println(onlineCourse.getState());
            System.out.println(onlineCourse.getStudents());
            System.out.println(onlineCourse.getReviews());
        }
    }

     

     

    OnlineCourse

    public class OnlineCourse {
    
      public enum State {
        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) {
        // PUBLISHED 상태라면 누구나 리뷰 작성 가능
        if (this.state == State.PUBLISHED) {
          this.reviews.add(review);
        // PRIVATE 상태라면 해당 강의를 수강하고 있는 학생만 리뷰작성 가능
        } else if (this.state == State.PRIVATE && this.students.contains(student)) {
          this.reviews.add(review);
        // DRAFT 상태라면 아무도 리뷰를 달 수 없음
        } else {
          throw new UnsupportedOperationException("리뷰를 작성할 수 없습니다.");
        }
      }
    
      public void addStudent(Student student) {
        // DRAFT, PUBLISHED 상태에서는 학생을 추가 가능
        if (this.state == State.DRAFT || this.state == State.PUBLISHED) {
          this.students.add(student);
        // PRIVATE 상태에서는 해당 강의를 수강중인 학생들만 추가 가능
        } 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;
      }
    
      public State getState() {
        return state;
      }
    
      public List<String> getReviews() {
        return reviews;
      }
    
      public List<Student> getStudents() {
        return students;
      }
    
      private boolean availableTo(Student student) {
        return student.isEnabledForPrivateClass(this);
      }
    
    }

     

     

    Student

    public class Student {
    
        private String name;
    
        public Student(String name) {
            this.name = name;
        }
    
        private List<OnlineCourse> privateCourses = new ArrayList<>();
    
        public boolean isEnabledForPrivateClass(OnlineCourse onlineCourse) {
            return privateCourses.contains(onlineCourse);
        }
    
        public void addPrivateCourse(OnlineCourse onlineCourse) {
            this.privateCourses.add(onlineCourse);
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }

     

     

     

    문제점

    OnlineCourse 의 addReview 메소드와 addStudent 메소드를 보면 if 문으로 인해 로직들이 꽤나 복잡하고 쉽게 읽히지 않는다. 여기에 상태패턴을 적용하여 보자.

     

     

     

    상태 패턴 적용 후


    먼저 State 인터페이스를 정의해야 한다. 

    이 전에는 addStudent 와 addReview 메소드 때문에 복잡한 코드가 되었다. 이러한 메소드들을 인터페이스에 정의한다.

     

     

    State

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

     

    이 전 코드의 OnlineCourse 의 State 는 총 3가지가 있었다. 

    그렇기 때문에 동일하게 PRIVATE, PUBLISHED, DRAFT 를 구현해주자. 

     

    예로, addStudent 메소드에서 if (state == State.DRAFT || state == State.PUBLISHED) 라면 바로 학생을 추가한다. 

    하여 DRAFT 를 구현할 시에는 addStudent 메소드에서 바로 list 에 student 를 추가한다. 

     

    이제 각 State 클래스들을 보자.

     

     

    Draft

    public class Draft implements State {
        private OnlineCourse onlineCourse; // Context 를 반드시 가지고 있어야 한다.
    
        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 (this.onlineCourse.getStudents().size() > 1) {
                this.onlineCourse.changeState(new Private(this.onlineCourse));
            }
        }
    }

     

     

    Private

    public class Private implements State {
      private OnlineCourse onlineCourse; // Context 를 반드시 가지고 있어야 한다.
    
      public Private(OnlineCourse onlineCourse) {
        this.onlineCourse = onlineCourse;
      }
    
      @Override
      public void addReview(String review, Student student) {
        if (this.onlineCourse.getStudents().contains(student)) {
          this.onlineCourse.getReviews().add(review);
        } else {
          throw new UnsupportedOperationException("프라이빗 코스를 수강하는 학생만 리뷰를 남길 수 있습니다.");
        }
      }
    
      @Override
      public void addStudent(Student student) {
        if (student.isAvailable(this.onlineCourse)) {
          this.onlineCourse.getStudents().add(student);
        } else {
          throw new UnsupportedOperationException("프라이빛 코스를 수강할 수 없습니다.");
        }
      }
    }

     

     

    Published

    public class Published implements State {
        private OnlineCourse onlineCourse; // Context 를 반드시 가지고 있어야 한다.
    
        public Published(OnlineCourse onlineCourse) {
            this.onlineCourse = onlineCourse;
        }
    
        @Override
        public void addReview(String review, Student student) {
            this.onlineCourse.getReviews().add(review);
        }
    
        @Override
        public void addStudent(Student student) {
            this.onlineCourse.getStudents().add(student);
        }
    }

     

     

    OnlineCourse

    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);
        }
    
        public State getState() {
            return state;
        }
    
        public List<Student> getStudents() {
            return students;
        }
    
        public List<String> getReviews() {
            return reviews;
        }
    
        public void changeState(State state) {
            this.state = state;
        }
    }

    이 때 특히 OnlineCourse 는 상태에 따라 다른 동작을 취하기 위해 State 를 참조해야 한다.

     

     

    Student

    public class Student {
        private String name;
    
        public Student(String name) {
            this.name = name;
        }
    
        private Set<OnlineCourse> onlineCourses = new HashSet<>();
    
        public boolean isAvailable(OnlineCourse onlineCourse) {
            return onlineCourses.contains(onlineCourse);
        }
    
        public void addPrivate(OnlineCourse onlineCourse) {
            this.onlineCourses.add(onlineCourse);
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }

     

     

    Client

    public class Client {
        public static void main(String[] args) {
            OnlineCourse onlineCourse = new OnlineCourse();
            Student student = new Student("whiteship");
            Student keesun = new Student("keesun");
            keesun.addPrivate(onlineCourse);
    
            onlineCourse.addStudent(student);
    
            onlineCourse.changeState(new Private(onlineCourse));
    
            onlineCourse.addReview("hello", student);
    
            onlineCourse.addStudent(keesun);
    
            System.out.println(onlineCourse.getState());
            System.out.println(onlineCourse.getReviews());
            System.out.println(onlineCourse.getStudents());
        }
    }

     

    실행 시 changeState() 메소드 파라미터에 private 과 published 값을 주면 리뷰 등록이 되지만 Draft 값을 주면 익셉션이 발생하는 것을 확인 할 수 있다.

     

     

     

    장점과 단점


    장점

    • 상태에 따른 동작을 개별 클래스로 옮겨서 관리 할 수 있다. (가장 큰 장점이다.)
    • 기존의 특정 상태에 따른 동작을 변경하지 않고 새로운 상태에 다른 동작을 추가 할 수 있다.
    • 코드 복잡도를 줄일 수 있다.

     

    단점

    • 클래스가 많아지면서 복잡도가 증가한다.

     

     

     

    참고


     

    코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

    디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

    www.inflearn.com

Designed by Tistory.