Java/Design Pattern

[디자인 패턴] 행동 패턴 - 상태 패턴 (State Pattern)

민돌v 2023. 4. 19. 17:56
728x90

 

(인프런) 코딩으로 학습하는 GoF의 디자인 패턴 - 백기선, 강의를 보고 정리한 글입니다.
코드는 GitHub 에 있습니다

 

#1. 객체 생성 관련 패턴

#2. 구조 관련 패턴

 

#3. 행동 관련 패턴

 

 

 


✔️ 상태 패턴이란 (State Pattern)

객체 내부 상태 변경에 따라 객체의 행동이 달라지는 패턴.
  • 👏🏻 상태 패턴은, 객체 내부의 현재 상태에 따라 수행하는 기능이 달라야 할 때(변경해야 할 떄)  적용할 수 있는 패턴 입니다.
  • 예를들어, TV가 "켜진 상태" 라면 볼륨 버튼을 눌러 볼륨을 조절할 수 있지만 | "커진 상태" 라면 불륨 버튼을 아무리 눌러도 조절할 수 없습니다.
  • 이렇게 특정 상태에 따른 행위를 개별적으로 관리하는 것을 상태 패턴이라 합니다.

 

📌 상태 패턴을 사용하여 Enum의 단점을 보완할 수 있다.

  • Java 에서는 주로 특정한 상수들의 집합을 Enum 클래스로 관리하곤 합니다. (저는..?)
  • 이렇게 특정한 상수들을 집합은 주로 상태가되고, 하나의 Enum 클래스에서 많은 상태를 관리하게 되죠
  • 그렇다면, Enum 클래스 내부에서 이 상태 집합을 관리하거나 판단하기위한 분기 처리 (if/else, switch) 가 늘어나게 되고 점점 무거워집니다.
  • 이러한 단점 또한 상태패턴을 사용하여 어느정도 보완할 수 있습니다.

 

 

State Pattern 구조

 

  • Context : 객체의 상태를 정의하는 데 사용되는 메소드를 정의하는 인터페이스입니다.
  • State : 상태에 따른 동작을 정의하는 인터페이스입니다.
  • ConcreteState : State에서 정의된 메소드를 구현하는 클래스입니다.

 


✔️ 상태 패턴 예시

1) 상태 패턴 적용 전

  • 단순한, 학생이 교육을 수강하는 예제입니다.
  • 교육의 상태에 따라, 학생은 수강을 듣거나/ 듣지 못할 수 있고
  • 리뷰를 남기거나/남기지 못할 수 있습니다.

 

상태 Enum 클래스

  • 교육의 상태를 관리하는 Enum Class 입니다.
  • Draft 모집중이라 리뷰를 남길 수 없지만, 수강생 추가는 가능합니다.
  • Published 는 이미 강의가 나와 리뷰 / 수강생 추가 모두 가능합니다.
  • Private 는 비밀강좌로 인증된 학생만 접근할 수 있습니다.
public enum State{
    DRAFT,
    PUBLISHED,
    PRIVATE;

    public boolean isDraft() {
        return this == State.DRAFT;
    }

    public boolean isPublished() {
        return this == State.PUBLISHED;
    }

    public boolean isPrivate() {
        return this == State.PRIVATE;
    }
}
 

 

학생 클래스
  • 학생클래스 입니다.
  • onlineCourses 에서 비밀 수강 가능 목록을 가집니다.
@Getter
@AllArgsConstructor
public class Student {

    private String name;
    private Set<OnlineCourse> onlineCourses = new HashSet<>();

    public Student(String name) {
        this.name = name;
    }

    public void addPrivateCourse(OnlineCourse onlineCourse) {
        this.onlineCourses.add(onlineCourse);
    }
}

 

온라인 교육 수강 클래스

  • 온라인 강의 수강 클래스입니다.
  • 강의 수강 및 리뷰 작성에 대한 책임을 가집니다.
  • 각 상태에 따른 행위들을 조건문으로 가져가고 있습니다.
  • 솔직히 한눈에 보기도 어렵고 코드 분석도 난잡합니다.
  • State enum class 뿐 만 아니라 가지고있는 멤버변수에 대한 상태또한 관리해주고 있기 때문에 요구사항이 늘어갈수록 점점 더 많은 분기처리가 필요집니다.
@Getter
public class OnlineCourse {

    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.isPublished()) {
            reviews.add(review);
        }
        if (this.state.isPrivate() && this.students.contains(student)) {
            reviews.add(review);
        }

        throw new UnsupportedOperationException("드래프트 상태에서는 리뷰를 작성할 수 없습니다.");
    }

    public void addStudent(Student student) {
        if (this.state.isDraft() || this.state.isPublished()) {
            students.add(student);
        }
        else if (this.state.isPrivate() && availableTo(student)) {
            students.add(student);
        }
        else{ throw new UnsupportedOperationException("학생을 해당 수업에 추가할 수 없습니다.");}

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

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

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

 

👇

2) 상태 패턴 적용

  • 이제 위의 코드를 상태패턴을 적용해 보겠습니다.
  • OnlineCourse 클래스에서 각 상태에 따른 행위를 조건 분기처리로 관리해주었습니다.
  • 이를 각각의 상태 클래스로 캡슐화하여 관리해 보겠습니다.

 

State interface

  • 이번에는, 각 상태에 따른 행위를 캡슐화하기 때문에 상태에 따라 달라지는 행위들을 추상화합니다.
public interface State {

    void addReview(String review, Student student);

    void addStudent(Student student);
}

 

Concreate State class
  • 각 상태에 맞는 행위들을 구현한 클래스 입니다.
  • 상태에 따른 책임을 분리했기 때문에 추가적인 요구사항이 생기면 기존의 코드를 변경하지않고 새로운 상태 객체를 만들어 확장내가면 됩니다.
@AllArgsConstructor
public class Draft implements State {

    private 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));
        }
    }
}


-----------------------------------------------------------------------------------

@AllArgsConstructor
public class Published implements State {

    private 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);
    }
}

-----------------------------------------------------------------------------------

@AllArgsConstructor
public class Private implements State {

    private OnlineCourse onlineCourse;

    @Override
    public void addReview(String review, Student student) {
        if (this.onlineCourse.getStudents().contains(student)) {
            this.onlineCourse.getReviews().add(review);
        }

        throw new UnsupportedOperationException("프라이빗 코스를 수강하는 학생만 리뷰를 남길 수 있습니다.");
    }

    @Override
    public void addStudent(Student student) {
        if (student.isAvailable(onlineCourse)) {
            onlineCourse.getStudents().add(student);
        }
        throw new UnsupportedOperationException("프라이빗 코스를 수강할 수 없습니다.");
    }
}

 

OnliceCourse

  • 상태에 따른 행위를 캡슐화하였기 때문에 외부에서 주입받을 수 있습니다.
  • 이제 OnlineCourse는 행위에 대한 책임만 가져가게됩니다.
@Getter
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) {
        state.addStudent(student);
    }

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

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

 

 

Client + Student

  • 클라이언트에서 상태를 주입하여 초기 상태를 지정할 수 있는 구조가 되었습니다.
  • 위에 OnlineCourse 클래스의 changeState 메소드처럼 상태는 언제든지 변경할 수 있도록 구조를 설계할 수 있습니다.
public class Client {

    public static void main(String[] args) {
        OnlineCourse onlineCourse = new OnlineCourse();
        Student student = new Student("whiteShip");

        onlineCourse.addStudent(student);
        onlineCourse.addReview("hello", student);


        System.out.println(onlineCourse.getState());
        System.out.println(onlineCourse.getReviews());
        System.out.println(onlineCourse.getStudents());
    }
}


@ToString
@AllArgsConstructor
public class Student {

    private String name;

    private Set<OnlineCourse> onlineCourses = new HashSet<>();

    public Student(String name) {
        this.name = name;
    }

    public boolean isAvailable(OnlineCourse onlineCourse) {
        return onlineCourses.contains(onlineCourse);
    }

    public void addPrivate(OnlineCourse onlineCourse) {
        this.onlineCourses.add(onlineCourse);
    }
}

 

👏🏻 특정 상태 자체를 객체로 분리하여, 특정 상태에대한 행위를 단일 책임으로 각각의 상태객체가 가져가게 되었습니다.

  • 이렇게 됨으로써 확장에 유연해졌고 - [판단 - 상태 - 행위] 로 각각의 책임이 분리되어 코드도 보기쉬어졌고
  • 런타임 시에도 유연하게 상태 변경을 관리할 수 있는 장점이 생겼다고 생각합니다.

 


 

✔️ 상태 패턴 장단점

장점

  • 상태에 따른 행동을 개별적인 클래스(상태 객체)로 캡슐화 시켜서 책임을 분리할 수 있습니다.
  • 개별적인 행위를 각각 캡슐화하였기 때문에 기존의 특정 상태에 따른 동작을 변경하지 않고 새로운 상태에 다른 동작을 추가할 수 있습니다.
  •  State 패턴을 사용하면 객체의 상태에 따른 조건문(if/else, switch)이 줄어들어 코드가 간결해지고 가독성이 올라갑니다.

 

단점

  • 클래스가 많아집니다.

 

 

 

 


참고

 

반응형