(인프런) 코딩으로 학습하는 GoF의 디자인 패턴 - 백기선, 강의를 보고 정리한 글입니다.
코드는 GitHub 에 있습니다
#1. 객체 생성 관련 패턴 |
#2. 구조 관련 패턴 |
#3. 행동 관련 패턴 |
||
✔️ 상태 패턴이란 (State Pattern)
객체 내부 상태 변경에 따라 객체의 행동이 달라지는 패턴.
- 👏🏻 상태 패턴은, 객체 내부의 현재 상태에 따라 수행하는 기능이 달라야 할 때(변경해야 할 떄) 적용할 수 있는 패턴 입니다.
- 예를들어, TV가 "켜진 상태" 라면 볼륨 버튼을 눌러 볼륨을 조절할 수 있지만 | "커진 상태" 라면 불륨 버튼을 아무리 눌러도 조절할 수 없습니다.
- 이렇게 특정 상태에 따른 행위를 개별적으로 관리하는 것을 상태 패턴이라 합니다.
📌 상태 패턴을 사용하여 Enum의 단점을 보완할 수 있다.
- Java 에서는 주로 특정한 상수들의 집합을 Enum 클래스로 관리하곤 합니다. (저는..?)
- 이렇게 특정한 상수들을 집합은 주로 상태가되고, 하나의 Enum 클래스에서 많은 상태를 관리하게 되죠
- 그렇다면, Enum 클래스 내부에서 이 상태 집합을 관리하거나 판단하기위한 분기 처리 (if/else, switch) 가 늘어나게 되고 점점 무거워집니다.
- 이러한 단점 또한 상태패턴을 사용하여 어느정도 보완할 수 있습니다.
- 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)이 줄어들어 코드가 간결해지고 가독성이 올라갑니다.
단점
- 클래스가 많아집니다.
참고
'Java > Design Pattern' 카테고리의 다른 글
[디자인 패턴] 행동 패턴 - 템플릿 메소드 패턴(Template Method Pattern) (0) | 2023.04.24 |
---|---|
[디자인 패턴] 행동 패턴 - 전략 패턴 (Strategy Pattern) (0) | 2023.04.21 |
[디자인 패턴] 행동 패턴 - 옵저버(관찰자) 패턴 (Observer Pattern) (0) | 2023.04.13 |
[디자인 패턴] 행동 패턴 - 메멘토 패턴 (Memento Pattern) (0) | 2023.04.12 |
[디자인 패턴] 행동 패턴 - 책임 연쇄 패턴 (Chain of Responsibility Pattern) (0) | 2023.04.11 |