Skip to content

Commit fa5f35f

Browse files
Mo-bile모재영
andauthored
Step2 : 수강신청(도메인 모델) (#805)
* refactor : 접근제어자 변경, 컨벤션 준수 * doc : Question의 필드의 객체 분리 필요성 관련 의견 메모 * fix : 컴파일 에러 해결 * feat : 강의 시작일 종료일 관련 객체 구현 및 유효성 검증 * feat : 강의 커버 이미지 객체 구현 및 정규식 검증 * feat : 강의 상태 기능 개발 및 테스트 * feat : 무료, 유료 강의에 따라 강의 신청 및 정원 지불 관리 개발 * refactor : enum 클래스 공통된 enumerate 패키지로 이동 * refactor : 정책관련 객체 ProvidePolicy 를 추가하고, Provide 에 일부 필드 변경 * feat : session 및 관련 객체 개발 완료, 수강신청 기능 개발 완료 * refactor : apply 관련 메서드 반환 값 변경 및 관련 테스트 코드 변경 * fix : 강의 모입 중 상태에 신청 가능하게 수정 * test : 깨진 테스트 고치기 * refactor 1. amount, pay 관련 자료형 int 에서 Long으로 변경 2. Session 에 session 찾는 메서드 추가 * feat : Course 에 예외전파 및 수강신청 추가 * refactor : 수강신청 수강생 수가 아니라 신청 자 리스트를 받게끔 변경 * feat : 결제모듈 붙이고, 항상 결제된것으로 구현 * refactor : EnrolledUsers, Sessions 일급컬렉션으로 변환 * refactor : 정원초과 로직 EnrolledUsers 로 이관 * docs : 개발 과정 메모 * refactor : 1. Base를 추상 클래스로 구현하여 상속전용으로만 하고 직접 생성은 자식이 못하게 막음 (도메인 객체처럼 사용할 우려 막음) 2. Base 의 필드를 private 로 변경해 직접 수정 못하게 막음 * refactor : CoverImage 의 많은 필드의 원시값을 포장하여 관련된 것 끼리 분리 * refactor 1. type 검증은 enum 내부에서 직접 하게 변경 2. boolean 타입이 아닌데 is 로 시작한 메서드명 변경 * refactor : apply 에 ProvideType 에 따라 분기되게끔 변경 * refactor 1. 객체의 역할과 책임을 명확히 인지하기 위해 객체명 변경 2. Session 객체 필드 일부를 EnrollmentPolicy로 이관 * refactor : 각각 변수를 SessionApply로 객체 분리 * refactor : 불필요한 static 등 제거 * refactor : null object + 전략 패턴 도입으로 무료 유료강의 정책에 대한 DI 방식 구현 * refactor : 디렉토리 구조 변경 * refactor : 테스트 픽스쳐를 '오브젝트 마더 패턴' + 빌더패턴(기본 값 숨기기, 빌더 덜쓰기, 팩토리 메서드를 이용해 도메인 강조, 비슷한 객체를 이용 시 코드 줄이기위한 but 사용) 사용하여 리팩터링 * docs : 리팩터링 기준 메모 * refactor : Free, Paid로 나누어진 Builder를 하나로 합쳐서 간소화 * refactor : 놓친 필드 기본 설정 제거 * chore : 패키지 리스트럭쳐링 --------- Co-authored-by: 모재영 <mo@utransfer.com>
1 parent 650a424 commit fa5f35f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1654
-71
lines changed

README.md

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,174 @@
6767
- [x] : `Answer` 에 대한 삭제 테스트 필요하자않은가? 이유작성
6868
- 이유 : 필요하다 Answer 을 수동적으로 바라보았기에 비즈니스 로직에 대한 테스트 코드가 생략되었다.
6969
- [ ] : `Answers` 에서 한땀한땀 삭제하는건 `Answer` 에 대한 수동적으로 바라보는 느낌아닌가? 이유 작성
70-
- 이유 : 맞다 또한 검증에 대해서도 모두 수행하는것은 바람직하지 않음, 검증은 `Answer` 내부에서 해야함
70+
- 이유 : 맞다 또한 검증에 대해서도 모두 수행하는것은 바람직하지 않음, 검증은 `Answer` 내부에서 해야함
71+
72+
## 3차 피드백
73+
- [x] : `Question``title`, `contents` 필드를 별도로 `QuestionBody` 로 분리하는 것이 의미 있을까?
74+
- 생각컨데 분리는 의마가 없다고 본다.
75+
- 그 이유는 : `title`, `contents` 로 분리를 하면은 값을 보관해주는 역할에 불과한 점
76+
- 분리한 객체만의 특별한 도메인 규칙이나 행동이 없는 점
77+
- 때문이다.
78+
79+
# 2단계 : 수강신청(도메인 모델)
80+
81+
- DB 테이블 설계 없이 도메인 모델부터 구현하기
82+
- 도메인 모델은 TDD로 구현 (Service 클래스는 단위 테스트 없이)
83+
84+
## 기능목록
85+
86+
1. 과정(Course)과 강의(Session)
87+
- 과정(Course)은 '기수' 단위 운영
88+
- 과정에 여러개의 강의(Session) 를 가질 수있음
89+
2. 강의 기간
90+
- 시작일과 종료일 가짐
91+
3. 강의에 커버이미지 가짐
92+
- 이미지 크기는 1MB 이하
93+
- 이미지 타입은 gif, jpg(jpeg), png, svg
94+
- 이미지 크기 : width - 300px / height - 200px 이상
95+
- width / height 비율 3:2
96+
4. 강의 타입 : '무료 강의' '유료 강의'로 나누어진다
97+
- 무료 강의 : 최대 수강신청 인원 제한이 없다
98+
- 유료 강의 : 강의 최대 수강 인원 초과할 수 없음
99+
- 수강생이 결제한 금액과 수강료가 일치 할 시 수강 신청 가능
100+
5. 강의 상태 : '준비중', '모집중', '종료'
101+
- 모집 중 일때 강의 수강신청 가능
102+
6. 결제
103+
- 유료 강의 결제는 이미 완료된 것으로 가정 하고 이후 과정 구현함
104+
- 결제 완료 한 결제정보는 payments 모듈로 관리, 결제정보는 Payment 객체에 담겨 반환
105+
106+
## 객체 설계
107+
108+
### 객체 역할
109+
110+
- Course : 기수별 session 들의 관리관련 정보 전문가 및 행위 책임자
111+
- Session : 강의 자체의 전반적인 정보 전문가 및 행위 책임자
112+
- Duration : 강의의 시작, 종료 관련 정보 전문가 및 행위 책임자
113+
- CoverImage : 강의의 커버 이미지 관련 정보 전문가 및 행위 책임자
114+
- CoverImageType
115+
- ProvideType : 강의의 무료, 유료 강의 관련 정보 전문가 및 행위 책임자
116+
- SessionStatus : 강의의 준비중, 모집 중 같은 정보 전문가 및 행위 책임자
117+
- SessionStatusType
118+
119+
### 구현 순서 (작은 도메인 부터)
120+
121+
Duration → CoverImage → SessionStatus → ProvideType → Session → Course
122+
123+
### 중요 : 만들어진 강의에 수강신청 하는 것임
124+
125+
### 객체 별 기능 사항
126+
127+
#### Duration
128+
129+
- [x] : 종료일이 시작일 보다 뒤에 있을 수없다
130+
- [x] : 시작일과 종료일이 오늘보다 뒤에 있다
131+
- [ ] : startDate, endDate 원시 값 포장
132+
133+
#### CoverImage
134+
135+
- [x] : 커버 이미지의 크기는 1MB 이하이다
136+
- [x] : 이미지 타입은 gif, jpg(jpeg), png, svg 이다 (CoverImageType)
137+
- [x] : 이미지 크기는 width - 300px / height - 200px 이상 이다
138+
- [x] : width / height 비율 3:2 이다
139+
140+
#### SessionStatus
141+
142+
- [x] : 모집 중 일때 만 강의 수강신청 가능
143+
144+
#### Provide
145+
146+
- [x] : 강의 타입은 무료 강의, 유료 강의 2가지 이다.
147+
- [x] : 수강신청 제한인원은 타입에 존재한다 (규칙)
148+
- [x] : 강의를 수강신청 하면 수강생 수가 올라간다
149+
- [x] : 클라이언트 코드에서 수강신청 제한인원 받아서 그 수가 넘는지 확인해준다
150+
- [x] : 지불금액을 보고 금액이 같은지 검증한다
151+
- [x] : 무료인데 지불하면 안된다.
152+
153+
##### 객체 분리 필요성 고민
154+
155+
1. Provide 를 Free, Paid 로 객체를 나눌 것인가?
156+
- 기대효과 : 각각 다른 규칙에 대해서 정리 가능
157+
- 예) 무료 : 인원수 제한 X/ 금액 무료
158+
- 한계점 : 비슷한 형태의 객체가 2개인데, 어떤 강의를 신청하느냐에 따라 요청이 오거나, 불러오는 객체가 달라짐
159+
160+
2. enrolledCount, maxEnrollment / tuitionFee, pay 각각 묶을 것인가?
161+
- 기대효과 : 인원수 체크가 손쉽게 됨, 금액을 내야하는지 체크가 가능 / 서로 비슷한 규칙끼리 묶여서 검증 쉬움
162+
- 한계점 : 최초 고민인 무료인 경우 인원수 제한, 수강료에 대한 통제는 여전히 어려움
163+
- 규칙 : 그러나 만들어진 각각 객체안에서 무료인 경우 제한 검증 X, 수강료 검증 X 되지 않을까?
164+
- 생성 : 무료인경우 생성자 인자를 생략하기? -> but 결국에 인원수를 0으로 세팅해야함
165+
166+
3. enrolledCount, tuitionFee / pay, maxEnrollment 각각 묶을 것인가?
167+
- 기대 효과 : 무료, 유료인 경우에 대한 변칙 적용이 수월함
168+
- 한계점 : 묶이는 필드끼리 생명주기가 같을지 의문
169+
- 차라리 현상이 아닌 정해진 생명주기끼리 묶는게 낫지않을까?
170+
- 예) tuitionFee, maxEnrollment (세션자체의 규칙) / enrolledCount, pay (요청 때 마다 변칙)
171+
- 전자는 Session 에서, 후자는 Provide 에서
172+
173+
4. tuitionFee, maxEnrollment / pay, enrolledCount 각각 묶을 것인가?
174+
- 기대 효과 : tuitionFee, maxEnrollment 는 상위 클래스 Session 에서 통제함
175+
- 제약과 현상을 분리함
176+
- 무료인 경우 0 fee 와 무제한 수강인원에 대한 검증 가능
177+
- 그리고 억지로 0으로 만들필요 없음
178+
- 한계점 : 하지만 전자를 결국에 객체로 만들면 0으로 세팅이 필요함
179+
- 결국에 상단 2번처럼 문제가
180+
181+
- 고민
182+
- 정원을 숫자가 아닌 정책으로 보아야함
183+
- 무제한 정책은 숫자가 필요없음
184+
- 제한 정책은 숫자가 필요
185+
- 정원이 없는것은 null 로 지정 가능
186+
- DDD 관점에서 '개념 부재' 표현임 (null 보다 의미없는 값(0) 으로 도메인 개념 왜곡이 더큰 문제)
187+
- null 은 인간언어로 표현 가능함
188+
- 두갈래 고민
189+
- 2번 : 수강생 수 / 비용 으로 분리
190+
- 생성 방식에 대한 문제 해결함 -> 고려 가능
191+
- 도달 여부 쉽게 판단
192+
- 4번 : 기준 / 준수여부 로 분리
193+
- 생명주기 비슷
194+
- 최종 결정
195+
- 4번: ProvidePolicy / ProvideStatus
196+
- 걱정 : null 로 넣는게 인위적인데 전략패턴을 이용해야할까 싶으면서 오버엔지이너링일까 우려됨
197+
198+
#### Session
199+
200+
- [x] : `Provide` 에게 해당 세션의 제한인원 수 전달해서 남은 자리 조율
201+
- [x] : 무료, 유료에 따라서 수강신청 메서드 분리
202+
203+
#### Course
204+
205+
- [x] : 신청하는 강의의 id 와 가격을 넣어서 수강신청한다
206+
- [x] : 수강신청 하려는 session의 id가 없으면 예외전파
207+
208+
#### CourseService
209+
210+
- [x] : 해당 코스의 특정 강의를 조회한다
211+
- [x] : 조회한 강의를 수강신청한다
212+
- 무료강의 이면 바로 신청
213+
- 유료 강의 이면 결제 모듈 조회 후 신청
214+
- 당연 성공이므로 곧바로 amount 넣기
215+
- [x] : 조회 성공 후 결과를 저장하기
216+
217+
#### 놓치거나 마무리 부분
218+
219+
- [x] : 수강신청한 수강생의 정보 넣기
220+
- [x] : 결제 완료를 기준으로 던지기
221+
- [x] : 컬렉션을 일급 컬렉션으로 바꾸기 (EnrolledUsers, Sessions)
222+
223+
## 1차 피드백
224+
225+
- [x] : 무료 유료인 상태의 객체 구분을 null로 구현이 아닌, null을 추상화한 Null Object 로 추가
226+
- 인터페이스로 null Object 로 구분하기 / 그에 따라서 구현하는 방향 바꾸기
227+
- 무료는 싱글턴 null 객체로, 유료는 생성자로 초기화
228+
- DI 방식으로 하니 무분별하게 분기 칠 필요가 없어짐
229+
- [x] : 무료 유료 구분은 생성자 유무가 아닌 ProvideType 값에 따라 결정
230+
- apply() 분기도 마찬가지
231+
- [x] : Base 추상클래스로 구현하여 직접 생상안되게 하기
232+
- [x] : CoverImage 필드 수 줄이기
233+
- [x] : boolean 타입이 아닌데 is 로 시작하는 메서드명 validate 시작으로 변경
234+
- [x] : `ProvidePolicy` 에 수강신청 판단 하는 모든 값 가져도 될지 고민하기
235+
- SessionStatus, 현재 수강신청한 수강생 목록도 신청여부 판단에 필요한 값
236+
- -> 이름을 Enrollment의 EnrollmentPolicy로 몀명하니 필요성을 가지게 됨
237+
- [x] : 복잡한 객체의 테스트 데이터 생성 시 중복코드 많고, 생성자 인자가 변경되는 경우 변경할 부분이 많음
238+
- 테스트 데이터 생성 시 방법 탐색해서 적용 후 앞으로 활용하기
239+
- 테스트 픽스쳐를 '오브젝트 마더 패턴' + 빌더패턴'(기본 값 숨기기, 빌더 덜쓰기, 팩토리 메서드를 이용해 도메인 강조, 비슷한 객체를 이용 시 코드 줄이기위한 but 사용)
240+
- 이용해서 축소
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package nextstep.courses;
2+
3+
public class CanNotCreateException extends Exception {
4+
5+
public CanNotCreateException(String message) {
6+
super(message);
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package nextstep.courses;
2+
3+
public class CanNotJoinException extends Exception {
4+
5+
public CanNotJoinException(String message) {
6+
super(message);
7+
}
8+
}

src/main/java/nextstep/courses/domain/Course.java

Lines changed: 0 additions & 53 deletions
This file was deleted.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package nextstep.courses.domain.builder;
2+
3+
import static nextstep.courses.domain.builder.EnrollmentPolicyBuilder.aFreeEnrollmentPolicyBuilder;
4+
import static nextstep.courses.domain.builder.EnrollmentPolicyBuilder.aPaidEnrollmentPolicyBuilder;
5+
6+
import nextstep.courses.CanNotCreateException;
7+
import nextstep.courses.domain.enrollment.Enrollment;
8+
import nextstep.courses.domain.enrollment.EnrollmentPolicy;
9+
import nextstep.courses.domain.enumerate.EnrollmentType;
10+
11+
public class EnrollmentBuilder {
12+
13+
private EnrollmentType enrollmentType;
14+
private EnrollmentPolicy enrollmentPolicy;
15+
16+
public static EnrollmentBuilder aFreeEnrollmentBuilder() {
17+
return new EnrollmentBuilder()
18+
.withEnrollmentType(EnrollmentType.FREE)
19+
.withEnrollmentPolicy(aFreeEnrollmentPolicyBuilder().build());
20+
}
21+
22+
public static EnrollmentBuilder aPaidEnrollmentBuilder() {
23+
return new EnrollmentBuilder()
24+
.withEnrollmentType(EnrollmentType.PAID)
25+
.withEnrollmentPolicy(aPaidEnrollmentPolicyBuilder().build());
26+
}
27+
28+
private EnrollmentBuilder() {}
29+
30+
private EnrollmentBuilder(EnrollmentBuilder copy) {
31+
this.enrollmentType = copy.enrollmentType;
32+
this.enrollmentPolicy = copy.enrollmentPolicy;
33+
}
34+
35+
public EnrollmentBuilder withEnrollmentType(EnrollmentType enrollmentType) {
36+
this.enrollmentType = enrollmentType;
37+
return this;
38+
}
39+
40+
public EnrollmentBuilder withEnrollmentPolicy(EnrollmentPolicy enrollmentPolicy) {
41+
this.enrollmentPolicy = enrollmentPolicy;
42+
return this;
43+
}
44+
45+
public Enrollment build() throws CanNotCreateException {
46+
return new Enrollment(enrollmentType, enrollmentPolicy);
47+
}
48+
49+
public EnrollmentBuilder but() {
50+
return new EnrollmentBuilder(this);
51+
}
52+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package nextstep.courses.domain.builder;
2+
3+
import java.util.List;
4+
import nextstep.courses.domain.enrollment.EnrolledUsers;
5+
import nextstep.courses.domain.enrollment.EnrollmentPolicy;
6+
import nextstep.courses.domain.enrollment.SessionStatus;
7+
import nextstep.courses.domain.enrollment.enrollmentcondition.EnrollmentCondition;
8+
import nextstep.courses.domain.enrollment.enrollmentcondition.FreeEnrollmentCondition;
9+
import nextstep.courses.domain.enrollment.enrollmentcondition.PaidEnrollmentCondition;
10+
import nextstep.courses.domain.enumerate.SessionStatusType;
11+
12+
public class EnrollmentPolicyBuilder {
13+
14+
private EnrollmentCondition enrollmentCondition;
15+
private EnrolledUsers enrolledUsers = new EnrolledUsers(List.of(1L, 2L, 3L, 4L, 5L));
16+
private SessionStatus status = new SessionStatus(SessionStatusType.RECRUITING);
17+
18+
public static EnrollmentPolicyBuilder aFreeEnrollmentPolicyBuilder() {
19+
return new EnrollmentPolicyBuilder()
20+
.withEnrollmentCondition(FreeEnrollmentCondition.INSTANCE);
21+
}
22+
23+
public static EnrollmentPolicyBuilder aPaidEnrollmentPolicyBuilder() {
24+
return new EnrollmentPolicyBuilder()
25+
.withEnrollmentCondition(new PaidEnrollmentCondition(10L, 10));
26+
}
27+
28+
private EnrollmentPolicyBuilder() {}
29+
30+
private EnrollmentPolicyBuilder(EnrollmentCondition enrollmentCondition) {
31+
this.enrollmentCondition = enrollmentCondition;
32+
}
33+
34+
private EnrollmentPolicyBuilder(EnrollmentPolicyBuilder copy) {
35+
this.enrollmentCondition = copy.enrollmentCondition;
36+
this.enrolledUsers = copy.enrolledUsers;
37+
this.status = copy.status;
38+
}
39+
40+
public EnrollmentPolicyBuilder withEnrollmentCondition(EnrollmentCondition enrollmentCondition) {
41+
this.enrollmentCondition = enrollmentCondition;
42+
return this;
43+
}
44+
45+
public EnrollmentPolicyBuilder withEnrolledUsers(EnrolledUsers enrolledUsers) {
46+
this.enrolledUsers = enrolledUsers;
47+
return this;
48+
}
49+
50+
public EnrollmentPolicyBuilder withSessionStatus(SessionStatus sessionStatus) {
51+
this.status = sessionStatus;
52+
return this;
53+
}
54+
55+
public EnrollmentPolicy build() {
56+
return new EnrollmentPolicy(enrollmentCondition, enrolledUsers, status);
57+
}
58+
59+
public EnrollmentPolicyBuilder but() {
60+
return new EnrollmentPolicyBuilder(this);
61+
}
62+
63+
}

0 commit comments

Comments
 (0)