diff --git "a/\355\226\211\353\217\231/11\354\243\274\354\260\250-\354\203\201\355\203\234/summary/code.md" "b/\355\226\211\353\217\231/11\354\243\274\354\260\250-\354\203\201\355\203\234/summary/code.md" new file mode 100644 index 0000000..20766b6 --- /dev/null +++ "b/\355\226\211\353\217\231/11\354\243\274\354\260\250-\354\203\201\355\203\234/summary/code.md" @@ -0,0 +1,400 @@ + + +## 1. 상태 패턴(State Pattern)이란? + +객체 내부 상태 변경에 따라 객체의 행동이 달라지는 패턴 + +상태에 특화된 행동들을 분리해낼 수 있으며 새로운 행동을 추가하더라도 다른 행동에 영향을 주지 않는다. + +[![스크린샷 2022-02-09 오후 8.35.34.png](https://github.com/Nelmm/DesignPattern/raw/main/%ED%96%89%EB%8F%99/11%EC%A3%BC%EC%B0%A8-%EC%83%81%ED%83%9C/img/chang1.png)](https://github.com/Nelmm/DesignPattern/blob/main/행동/11주차-상태/img/chang1.png) + +- `Context` : 상태가 변경되는 객체, 상태를 변경하는 메서드를 가지고 있음 +- `State` : Context가 변경될 수 있는 여러 상태들에 대한 공통된 인터페이스 +- `ConcreteState` : State를 구현하는 클래스, 각 상태에 따라 실질적으로 달라지는 행동들을 구현함 + + + + + +## 2. 코드로 알아보는 상태 패턴 + +### 2-1. 요구사항 +- 유저는 채용 공고에 지원할 수 있다. +- 채용 공고가 진행중일 때 입사 지원을 할 수 있다. +- 공고에 2명이 지원하면 해당 공고에는 더이상 다른 유저는 지원할 수 없다. +- 채용 공고가 대기중이거나 끝난 상태일 때 유저는 해당 공고에 지원을 취소할 수 없다. + + + +### 2-2. 상태패턴 적용 전 + +위 요구사항을 만족하는 코드를 작성하면 다음과 같다. +```java +@Getter +@RequiredArgsConstructor +public class User { + private final String name; +} + +@Getter +public class Recruit { + public enum State { + WAIT, PROGRESS, END, FULL + } + + private static final int MAX_NUM = 2; + private final Set users = new HashSet<>(); + private State state; + private final String name; + + public Recruit(String name) { + this.name = name; + System.out.println("========공고 게시(모집전)====="); + this.state = State.WAIT; + } + + public void applyRecruit(User user) { + System.out.print(user.getName() + " 님의 공고 지원 결과 : "); + if (state == State.WAIT || state == State.END || state == State.FULL) { + System.out.println("해당 채용공고에 지원할 수 없습니다."); + return; + } + + this.users.add(user); + if (users.size() == MAX_NUM) { + this.state = State.FULL; + } + System.out.println("지원 성공하였습니다."); + } + + public void cancelRecruit(User user) { + System.out.print(user.getName() + " 님의 지원 취소 결과 : "); + if (state == State.WAIT) { + System.out.println("아직 공고 모집이 시작되지 않았습니다."); + return; + } else if (state == State.END) { + System.out.println("이미 종료된 채용 공고입니다."); + return; + }else if(!this.users.remove(user)){ + System.out.println("지원한 공고가 아닙니다."); + return; + } + + if (state == State.FULL) { + state = State.PROGRESS; + } + System.out.println("지원 취소 되었습니다."); + } + + public void closeRecruit() { + System.out.println("========공고 모집 종료====="); + this.state = State.END; + } + + public void startRecruit() { + System.out.println("========공고 모집 시작====="); + this.state = State.PROGRESS; + } + + public void printUsers() { + System.out.println("====입사 지원 유저 목록===="); + System.out.println(users.stream().map(User::getName).toList().toString()); + } +} + +public class Client { + public static void main(String[] args) { + User user1 = new User("고길동"); + User user2 = new User("고둘리"); + User user3 = new User("또치"); + + //공고 게시(모집전) + Recruit recruit = new Recruit("사람인 공고"); + recruit.applyRecruit(user1); + System.out.println(); + + //공고 모집 시작 + recruit.startRecruit(); + recruit.applyRecruit(user1); + recruit.applyRecruit(user2); + recruit.applyRecruit(user3); + System.out.println(); + + recruit.printUsers(); + System.out.println(); + + //공고 한명 취소후 다른사람이 지원 + recruit.cancelRecruit(user1); + recruit.applyRecruit(user3); + + System.out.println(); + recruit.printUsers(); + } +} + +결과: +========공고 게시(모집전)===== +고길동 님의 공고 지원 결과 : 해당 채용공고에 지원할 수 없습니다. + +========공고 모집 시작===== +고길동 님의 공고 지원 결과 : 지원 성공하였습니다. +고둘리 님의 공고 지원 결과 : 지원 성공하였습니다. +또치 님의 공고 지원 결과 : 해당 채용공고에 지원할 수 없습니다. + +====입사 지원 유저 목록==== +[고길동, 고둘리] + +고길동 님의 지원 취소 결과 : 지원 취소 되었습니다. +또치 님의 공고 지원 결과 : 지원 성공하였습니다. + +====입사 지원 유저 목록==== +[고둘리, 또치] +``` + +채용공고에 상태를 Enum으로 정의해놓고 해당 상태에 따라 입사지원, 취소가 분기처리 되고 있는 것을 볼 수 있음 :arrow_right: 분기 처리 코드 인해 코드가 한눈에 들어오지 않고 만약 상태값들이 더 늘어나게 된다면 그만큼 분기처리도 더욱 많아져 가독성이 악화 됨 + +이러한 문제를 해결하기 위해 상태 패턴을 사용할 수 있다. + + + +### 2-3. 상태패턴 적용 후 + +아래는 상태 패턴을 적용한 코드이다. + +```java +@Getter +@RequiredArgsConstructor +public class User { + private final String name; +} + +//Context +@Getter +public class Recruit{ + private final String name; //공고 이름 + private final Set users = new HashSet<>(); //공고 지원자들 + private RecruitState recruitState; //공고 상태 + + public Recruit(String name) { + this.name = name; + System.out.println("========" + name +" 공고 게시(모집전)====="); + this.recruitState = new Wait(this); + } + + //공고 지원 + public void applyRecruit(User user) { + System.out.print(user.getName() + " 님의 지원 결과 : "); + recruitState.addUser(user); //공고 지원에 대한 세부 로직은 상태에게 위임 + } + + //공고 취소 + public void cancelRecruit(User user) { + System.out.print(user.getName() + " 님의 지원 취소 결과 : "); + recruitState.removeUser(user); //공고 취소에 대한 세부 로직은 상태에게 위임 + } + + public void changeState(RecruitState recruitState) { + this.recruitState = recruitState; //상태 변경 + } + + public void printUsers() { + System.out.println("====입사 지원 유저 목록===="); + System.out.println(users.stream().map(User::getName).toList().toString()); + } +} + + +//채용 공고에 지원할 유저 클래스 +@Getter +public class User { + private String name; + + public User(String name) { + this.name = name; + } +} + +//State Interface +public interface RecruitState { + void setRecruit(Recruit recruit); + void addUser(User user); //공고 지원 + void removeUser(User user); //공고 취소 +} + + +//Concrete State +public class Wait implements RecruitState{ + private Recruit recruit; + + public Wait(Recruit recruit) {this.recruit = recruit;} + + @Override + public void setRecruit(Recruit recruit) { + this.recruit = recruit; + } + + @Override + public void addUser(User user) { + System.out.println("아직 공고 모집이 시작되지 않았습니다."); + } + + @Override + public void removeUser(User user) { + System.out.println("아직 공고 모집이 시작되지 않았습니다."); + } +} + +//Concrete State +public class Progress implements RecruitState{ + private Recruit recruit; + private static final int MAX_NUM = 2; + + public Progress(Recruit recruit) {this.recruit = recruit;} + + @Override + public void setRecruit(Recruit recruit) { + this.recruit = recruit; + } + + @Override + public void addUser(User user) { + recruit.getUsers().add(user); + System.out.println(user.getName() + "의 입사지원이 완료됐습니다."); + if(recruit.getUsers().size() == MAX_NUM) { + this.recruit.changeState(new Full(recruit)); //가득 찼다면 마감상태로 변경 + } + } + + @Override + public void removeUser(User user) { + recruit.getUsers().remove(user); + System.out.println(user.getName() + "의 입사지원이 취소됐습니다."); + } +} + + +//Concrete State +public class End implements RecruitState { + private Recruit recruit; + + public End(Recruit recruit) {this.recruit = recruit;} + + @Override + public void setRecruit(Recruit recruit) { + this.recruit = recruit; + } + @Override + public void addUser(User user) { + System.out.println("이미 종료된 채용 공고입니다."); + } + @Override + public void removeUser(User user) { + System.out.println("이미 종료된 채용 공고입니다."); + } +} + +//Concrete State +public class Full implements RecruitState{ + Recruit recruit; + + public Full(Recruit recruit) {this.recruit = recruit;} + + @Override + public void setRecruit(Recruit recruit) { + this.recruit = recruit; + } + + @Override + public void addUser(User user) { + System.out.println("채용 공고 지원수가 꽉 찼습니다."); + } + + @Override + public void removeUser(User user) { + recruit.getUsers().remove(user); + System.out.println(user.getName() + "의 입사지원이 취소됐습니다."); + this.recruit.changeState(new Progress(recruit)); //모집중 상태로 변경 + } +} + +public class Client { + public static void main(String[] args) { + User user1 = new User("고길동"); + User user2 = new User("고둘리"); + User user3 = new User("또치"); + + //공고 게시(모집전) + Recruit recruit = new Recruit("사람인"); + recruit.applyRecruit(user1); + System.out.println(); + + //공고 모집 시작 + System.out.println("==== 공고 모집 시작 ===="); + recruit.changeState(new Progress(recruit)); //모집중 상태로 변경 + recruit.applyRecruit(user1); + recruit.applyRecruit(user2); + recruit.applyRecruit(user3); + System.out.println(); + + recruit.printUsers(); + System.out.println(); + + //공고 한명 취소후 다른사람이 지원 + recruit.cancelRecruit(user1); + recruit.applyRecruit(user3); + + System.out.println(); + recruit.printUsers(); + } +} + +결과 : +========사람인 공고 게시(모집전)===== +고길동 님의 지원 결과 : 아직 공고 모집이 시작되지 않았습니다. + +==== 공고 모집 시작 ==== +고길동 님의 지원 결과 : 고길동의 입사지원이 완료됐습니다. +고둘리 님의 지원 결과 : 고둘리의 입사지원이 완료됐습니다. +또치 님의 지원 결과 : 채용 공고 지원수가 꽉 찼습니다. + +====입사 지원 유저 목록==== +[고둘리, 고길동] + +고길동 님의 지원 취소 결과 : 고길동의 입사지원이 취소됐습니다. +또치 님의 지원 결과 : 또치의 입사지원이 완료됐습니다. + +====입사 지원 유저 목록==== +[또치, 고둘리] +``` + + + +### 2-4. UML + +UML 다이어그램대로 상태 패턴을 적용하여 **Recruit이라는 객체 내에 존재하던 비즈니스 로직을 각 State에게 위임하여 확장에 유연하도록 개선**할 수 있었다. + +![Untitled](https://user-images.githubusercontent.com/32676275/153326418-6fe4c82d-f8c3-430f-a2ec-ae22fedaed48.png) + + + + + +## 3. 장점과 단점 + +### 3-1. 장점 + +- 상태에 따른 동작을 개별 클래스로 옮겨서 관리할 수 있음 +- 기존의 특정 상태에 따른 동작을 변경하지 않고 새로운 상태에 다른 동작을 추가할 수 있음 +- 코드 복잡도가 줄어듦 :arrow_right: 가독성 증가 + +### 3-2. 단점 + +- 복잡도가 증가한다. (on, off 같이 단 두가지 정도의 상태만 있을 때, 상태 패턴을 적용하면 상태 개수에 비해 복잡해질 수 있음) + + + + + +## 4. 마치며 + +웹 페이지뿐만 아니라 프로그래밍 전반적으로 상태에 따라 객체가 달라지는 형태는 자주 볼 수 있다. 때문에 상태 패턴은 다방면으로 사용할 수 있어 유용한 패턴이라고 생각된다. \ No newline at end of file