행동 관련 : State 패턴
- 디자인 패턴
- 2023. 11. 30.
들어가기 전
이 글은 인프런 백기선님의 GOF 강의를 수강하며 작성한 글입니다.
State 패턴
- GOF : 객체 내부 상태 변경에 따라 객체의 행동이 달라지는 패턴.
- 특정한 상태에 따라 행동이 달라지는 객체들에게 적용할 수 있는 패턴임.
- 예를 들면 리모컨은 TV가 켜져있는지, 꺼져있는지에 따라서 버튼을 눌렀을 때 다르게 동작함.
- 이런 경우 If ~ Else로 떡칠되게 됨. 이런 코드들을 읽기 어렵기 때문에 State에 따른 If - Else 문을 각 클래스로 분리하는 것임.
- 디자인 패턴 Component
- Context
- 상태를 관리하는 클래스임. 이를 위해 상태 변경 메서드도 추가되어야 함.
- 컨텍스트 객체가 원래 담고 있어야 하는 정보들을 가지고 있는 클래스.
- State
- 공통 인터페이스로 만들어 둔 것임. 이 인터페이스의 구현체는 상태에 따라 달라지는 행위들을 작성함.
- Context는 공통 인터페이스에 의존함.
- ConcreteState
- 상태에 따라 달라지는 행위들을 작성함.
- Context
- Context 클래스는 상태에 따라 달라지는 Operation을 모두 State에게 위임한다. 즉, Operation은 ConcreteState가 모두 수행하도록 한다.
디자인 패턴이 필요한 경우
public class OnlineCourse {
public enum State {
DRAFT, PUBLISHED, PRIVATE
}
...
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;
}
}
...
}
위의 OnlineCourse는 State 패턴을 적용하기 좋은 코드다. 왜냐하면 특정 상태에 의존적으로 Operation이 많이 변경되기 때문이다.
- 상태는 Draft, Published, Private이 있음.
- addReview(), addStudent()에서는 Draft, Published, Private 상태에 따라 서로 다르게 동작함.
현재 이 코드의 문제점은 메서드 내에서 State에 따라 서로 다른 동작을 하는 것이 If ~ Else 구문으로 구별되어있고, 이런 코드들 때문에 '읽기 어려운 코드'가 된다는 것이다. 따라서 상태에 따른 If ~ Else 분기를 State 클래스로 분리하고, State 클래스에게 Deligation 하는 방식으로 코드를 개선해 볼 수 있다.
State 패턴을 적용한 코드
State 패턴을 적용하기 위해서 Context, State를 각각 정의해야한다. 어떻게 분리해 볼 수 있을까?
- Context
- OnlineCourse 클래스가 Context가 됨.
- OnlineCourse는 상태를 가지고 있으며, State가 작업을 하는데 필요한 전반적인 정보들 (students, review)를 가지고 있기 때문임.
- State
- Draft, Published, Private으로 분리함.
- 인터페이스는 addReview(), addStudent()를 선언한다. 기존 코드에서 상태에 따라 다르게 동작하는 코드는 addReview(), addStudent()에만 있었기 때문임.
이런 것들을 고려해서 아래에 State, Context 코드를 하나씩 작성해보자.
public class Draft implements State {
private final 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);
}
}
- Draft는 State 인터페이스를 구현한다.
- 기존 OnlineCourse에서 if (this.state == DRAFT) 분기로 처리되던 코드들을 모두 Draft로 옮긴다.
- 이 때, 각 addReview(), addStudent()는 자신의 상태에 대한 내용만 정의되어있다. 이렇게 되기 때문에 State 패턴을 적용하면 더욱 읽기 쉬운 코드가 되는 것이다.
State를 분리하게 되면서 UnsupportedOperationException도 더 명확해진다. 각 State에 맞는 에러 문구를 출력해 줄 수 있께 된다. 예를 들어 기존 코드에서는 Private에서는 '리뷰를 작성할 수 없습니다'라고 공통으로 처리되었겠지만, State 클래스로 분리된 후에 Private State 클래스에서는 '비공개 회원은 리뷰를 작성할 수 없습니다'라는 식으로 따로 명시해 줄 수 있기 때문이다.
// Context Class
public class OnlineCourse {
private State state;
private List<String> reviews = new ArrayList<>();
private List<Student> students = new ArrayList<>();
public OnlineCourse() {
this.state = new Draft(this);
}
public void addReview(String review, Student student) {
this.state.addReview(review, student);
}
public void addStudent(Student student) {
this.state.addStudent(student);
if (this.students.size() > 1) {
changeState(new Private(this));
}
}
public void changeState(State newState) {
this.state = newState;
}
...
}
- OnlineCourse의 addReview(), addStudent()에 있던 State에 의존적인 분기는 모두 State 클래스로 넘겨졌다.
- 따라서 OnlineCourse의 코드도 읽기 편해졌고, 각 State별로 필요한 내용만 가지고 있기 때문에 전체적으로 읽기 쉬운 코드가 된다.
이 때 OnlineCourse라는 Context는 State가 바뀔 수 있기 때문에 다음 기능을 추가하는 작업을 해야했다.
- 자신이 어떤 상태인지 State를 내부 참조로 가지고 있음.
- 자신의 상태를 외부에서 변경할 수 있도록 public API를 작성함.
State 패턴의 장단점
- 장점
- State에 따라 다른 동작을 각 Concrete State 클래스에 옮겨담아서 가독성을 올림.
- 개별적인 클래스로 State와 Operation을 분리했기 때문에 Unit Test가 더 쉬워짐.
- 새로운 State, Operation을 추가하기 쉬워짐.
- 단점
- 상태를 나누는 경우가 불필요함에도 이렇게 나누는 경우가 있을 수 있음. (ON / OFF 정도의 상태라면... 오버엔지니어링..?)
State 패턴을 적용할 수 있으면 적용하는 것이 좋을 듯 하다.
개별적인 클래스로 State와 Operation을 분리했기 때문에 단위 테스트 하기가 더 쉬워진다. 기존에는 거대한 클래스에 If ~ Else 문으로 구현이 되어있어 테스트 해야하는 범위가 넓어서 테스트 코드를 작성하기가 어려웠었다. 그러나 이런 부분이 State 단위로 Operation이 분리되었기 때문에 Operation을 테스트 하기 매우 쉬워진 편이다.
또한, Context - State는 '인터페이스'를 기반으로 느슨한 결합을 하고 있다. 따라서 새로운 State + Operation이 정의되더라도 기존에 존재하던 Context, State 클래스에는 어떠한 영향도 주지 않는다. 즉, OCP 원칙을 지키게 된다.
만약 State 패턴을 적용하지 않았다면 기존 OnlineCourse 클래스에 If ~ Else 문으로 새로운 상태에 대한 정의를 추가해줘야만 한다. 이런 경우, 기존의 코드를 수정하는 작업이 이루어지기 때문에 확장에 용이한 구조는 아니다.
State 패턴과 Strategy 패턴의 차이점은?
처리해야 할 알고리즘을 Strategy, State로 캡슐화해서 사용하는 것은 비슷하지만 두 가지가 다른 점이 있다.
- Strategy 패턴은 객체의 내부 상태와는 관련없이 자신이 실행할 알고리즘을 선택해서 동작한다.
- State 패턴은 객체의 내부 상태에 맞추어서 정해진 알고리즘대로 동작한다.
즉, 둘다 캡슐화된 알고리즘 인터페이스에 위임하는 것은 맞지만 Strategy 패턴은 객체의 내부 상태를 고려하지 않는다.
'디자인 패턴' 카테고리의 다른 글
구조 관련 : Composite 패턴 (0) | 2023.11.30 |
---|---|
행동 관련 : Strategy(전략) 패턴 (0) | 2023.11.30 |
행동 관련 : 템플릿 메서드 패턴 (0) | 2023.11.30 |
구조 관련 : 프록시 패턴 (0) | 2023.11.26 |
구조 관련 : 파사드 패턴 (0) | 2023.11.25 |