Skip to content

Commit 650a424

Browse files
Mo-bile모재영
andauthored
Step1 : 레거시 코드 리팩터링 (#795)
* refactor : List<Answer> 부분 일급 컬렉션으로 Answers 전환 및 비즈니스 로직 이관 * doc : 리팩터링 절차 및 계획안 마련 * refactor 1. DeleteHistories 로 일급컬렉션 뽑아내고, 관련 로직 도메인으로 이관 2. answer 제거 및 history 저장관련 내용 메서드 분리 * refactor 1. 메서드명 수정하여 그 의미 명확화 2. DeleteHistories 생성자 추가하여 생성 역할 다양화 3. deleteQuestion 메서드 리팩터링 4. Question의 일부 비즈니스 로직 도메인 내로 이관 * refactor 1. 불필요한 setter 제거 * refactor : 기존적인 생성, 수정일시 관련 공통된 인스턴스 필드 분리 * docs : 리팩토링 상황 메모 * refactor : 규칙 7 인스턴스 변수 줄이기 * refactor : 컨벤션 및 import 정리 * refactor : 테스트 픽스쳐 청리 * refactor : 깨진 코드 재 커밋 * refactor : 깨진 코드 재 커밋2 * refactor : 깨진 코드 해결용 커밋 * refactor 1. 도메인 규칙은 해당하는 도메인 내부에 캡슐화 시켜서 도메인 불변식을 유지한다. 그 결과 해당 도메인을 모르는 제3의 서비스에서 삭제를 할 때 삭제 규칙을 누락하는 경우를 방지한다. 2. `규칙7 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않기` 를 무리하게 이용된 부분 제거 * doc : 리팩터링 진행 상황 메모 * refactor : deleteAll, delete 메서드의 빈 리스트, 클래스 반환하여 하는 방법이 아닌, 예외 전파방식으로 무의미한 DeleteHistories 생성 방지 * refactor : 무의미한 방어코드를 제거(도메인 내부는 무조건 true / 실익은 영속성 레이어, 트랜잭셔널 범위에서 검증에서) * refactor : 1. Answer 을 수동적으로 바라보아서 Answer에 대한 검증을 Answers 에서 하게한 것을 Answer로 이관 2. 불필요한 메서드 및 테스트 코드 제거 * doc : 이유 작성 --------- Co-authored-by: 모재영 <mo@utransfer.com>
1 parent de587b9 commit 650a424

File tree

12 files changed

+394
-109
lines changed

12 files changed

+394
-109
lines changed

README.md

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,65 @@
66
* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다.
77

88
## 온라인 코드 리뷰 과정
9-
* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview)
9+
* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview)
10+
11+
12+
# 1단계 : 레거시 코드 리팩터링
13+
## 리팩터링 요구사항
14+
- nextstep.qna.service.QnaService의 `deleteQuestion()`는 앞의 질문 삭제 기능을 구현한 코드이다.
15+
- 이 메소드는 단위 테스트하기 어려운 코드와 단위 테스트 가능한 코드가 섞여 있다.
16+
- QnaService의 `deleteQuestion()` 메서드에 단위 테스트 가능한 코드(핵심 비지니스 로직)를 도메인 모델 객체에 구현한다.
17+
- QnaService의 비지니스 로직을 도메인 모델로 이동하는 리팩터링을 진행할 때 TDD로 구현한다.
18+
- QnaService의 `deleteQuestion()` 메서드에 대한 단위 테스트는 src/test/java 폴더 `nextstep.qna.service.QnaServiceTest`이다.
19+
- 도메인 모델로 로직을 이동한 후에도 QnaServiceTest의 모든 테스트는 통과해야 한다.
20+
21+
## 리팩터링 절차
22+
1. 테스트하기 쉬운 부분(비즈니스 로직)과 어려운 부분(날짜, 셔플, 랜덤, DB, API call)을 구분해보기
23+
2. 분리 시 먼저 단위테스트 추가
24+
3. 관련 비즈니스 로직을 도메인 객체로 이동
25+
26+
27+
## 리팩터링 계획
28+
1. `List<Answer>` 의 일급컬렉션으로 래핑 및 주요 로직 이동
29+
- [x] : 이동 및 테스트 완료
30+
2. `List<DeleteHistory>` 의 일급컬렉션으로 래핑 및 주요 로직 이동
31+
- [x] : 이동 및 테스트 완료
32+
3. `Question` 의 주요 로직 이동
33+
- [x] : 이동 및 테스트 완료
34+
4. 불필요한 setter 제거
35+
5. 인스턴스 변수 수 줄이기
36+
37+
## 1차 피드백
38+
39+
- [x] : 분리된 글쓴이 판단 -> 삭제 상태로 변경 한번에
40+
- [x] : 이 방식이 좋은 이유?
41+
- 반대의견 : `deleteQuestion()` 은 삭제가 행동이지만, 삭제의 큰 흐름은 알아야한다
42+
- 권한이 있는지, 연관 `Answers` 까지 아는지? 그리고 너무 과하게 코드를 줄인다.
43+
- 찬성의견 : 해당 도메인 내부에 있는 값을 굳이 노출시키거나 꺼내서 해결할 필요는 없다
44+
- `deleteQuestion()`의 관심사는 말 그대로 질문 삭제일 뿐이다
45+
- 얘는 삭제 권한이있는지, 답변이 모드 같은 사람이 쓴건지 이런것은 관심이 없다.
46+
- 그냥 삭제만 하고 싶어할 뿐이다.
47+
- 정리한 찬성 의견
48+
1. 도메인 불변식 유지 : 모델이 깨지면 안되는 규칙 -> 어떤 경로이든 참이어야 함
49+
- 규칙검증을 서비스에 두면 제3의 다른서비스는 이 규칙 검증을 건너뛸 수있게 됨
50+
- 규칙이 도메인 밖으로 새면 안됨
51+
2. 서비스 레이어 메서드는 외부(DB, I/O, 셔플)와 `@Transactional` 가 관심사임
52+
- 비즈니스 규칙 자체는 도메인 내부에서 해결해야함
53+
- [x] : 연관된 답변까지 Question 에서 한꺼번에 삭제
54+
- [x] : 불필요하게 분리된 객체의 필요성 고민
55+
- 고민 결과 도메인만의 규칙, 행동이 없으므로 롤백
56+
57+
## 2차 피드백
58+
59+
- [x] : 불필요한 setter 와 getter의 무의미한 재귀 호출 제거 (재귀호출 stack over flow 위험)
60+
- [x] : 삭제 시 삭제됐는지 확인하는 메서드 필요한지? 필요없다면 이유 작성
61+
- 이유
62+
- this.isQuestionDeleteDone(); 같은 도메인 내부에서 값의 변화를 검증하는 부분인데,
63+
1. 도메인 내부 값 변화는 확실한 점
64+
2. 그래서 도메인 내부에서 값 변화를 체크하는 방어코드는 무의미한 점
65+
3. 만약 검증하려면 db(영속성) 레이어 혹은 트랜잭셔널 범위(서비스 레이어) 등에서 검증해야하지만 금번 과제는 리팩터링 이므로 추가 할 필요가 없는점
66+
- 이므로 제거
67+
- [x] : `Answer` 에 대한 삭제 테스트 필요하자않은가? 이유작성
68+
- 이유 : 필요하다 Answer 을 수동적으로 바라보았기에 비즈니스 로직에 대한 테스트 코드가 생략되었다.
69+
- [ ] : `Answers` 에서 한땀한땀 삭제하는건 `Answer` 에 대한 수동적으로 바라보는 느낌아닌가? 이유 작성
70+
- 이유 : 맞다 또한 검증에 대해서도 모두 수행하는것은 바람직하지 않음, 검증은 `Answer` 내부에서 해야함
Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,21 @@
11
package nextstep.qna.domain;
22

3+
import nextstep.qna.CannotDeleteException;
34
import nextstep.qna.NotFoundException;
45
import nextstep.qna.UnAuthorizedException;
56
import nextstep.users.domain.NsUser;
67

7-
import java.time.LocalDateTime;
8-
9-
public class Answer {
8+
public class Answer extends Base {
109
private Long id;
11-
10+
1211
private NsUser writer;
13-
12+
1413
private Question question;
15-
14+
1615
private String contents;
17-
16+
1817
private boolean deleted = false;
1918

20-
private LocalDateTime createdDate = LocalDateTime.now();
21-
22-
private LocalDateTime updatedDate;
23-
2419
public Answer() {
2520
}
2621

@@ -37,7 +32,6 @@ public Answer(Long id, NsUser writer, Question question, String contents) {
3732
if(question == null) {
3833
throw new NotFoundException();
3934
}
40-
4135
this.writer = writer;
4236
this.question = question;
4337
this.contents = contents;
@@ -46,34 +40,50 @@ public Answer(Long id, NsUser writer, Question question, String contents) {
4640
public Long getId() {
4741
return id;
4842
}
49-
50-
public Answer setDeleted(boolean deleted) {
51-
this.deleted = deleted;
52-
return this;
43+
44+
public void delete(NsUser loginUser) throws CannotDeleteException {
45+
isHaveAuthority(loginUser);
46+
deleteAnswer();
47+
}
48+
49+
public void deleteAnswer() {
50+
this.deleted = true;
5351
}
5452

5553
public boolean isDeleted() {
5654
return deleted;
5755
}
58-
59-
public boolean isOwner(NsUser writer) {
60-
return this.writer.equals(writer);
56+
57+
private void isHaveAuthority(NsUser loginUser) throws CannotDeleteException {
58+
if(isNotOwner(loginUser)) {
59+
throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다.");
60+
}
61+
}
62+
63+
public boolean isNotOwner(NsUser writer) {
64+
return !this.writer.equals(writer);
6165
}
6266

6367
public NsUser getWriter() {
64-
return writer;
68+
return this.writer;
6569
}
6670

6771
public String getContents() {
68-
return contents;
72+
return this.contents;
6973
}
7074

7175
public void toQuestion(Question question) {
7276
this.question = question;
7377
}
74-
78+
7579
@Override
7680
public String toString() {
77-
return "Answer [id=" + getId() + ", writer=" + writer + ", contents=" + contents + "]";
81+
return "Answer{" +
82+
"id=" + id +
83+
", writer=" + writer +
84+
", question=" + question +
85+
", contents='" + contents + '\'' +
86+
", deleted=" + deleted +
87+
'}';
7888
}
7989
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package nextstep.qna.domain;
2+
3+
import java.time.LocalDateTime;
4+
import java.util.ArrayList;
5+
import java.util.Arrays;
6+
import java.util.List;
7+
import java.util.Objects;
8+
import java.util.stream.Collectors;
9+
import nextstep.qna.CannotDeleteException;
10+
import nextstep.users.domain.NsUser;
11+
12+
public class Answers {
13+
14+
private final List<Answer> answers;
15+
16+
public Answers() {
17+
this(List.of());
18+
}
19+
20+
public Answers(Answer... answers) {
21+
this(Arrays.asList(answers));
22+
}
23+
24+
public Answers(List<Answer> answers) {
25+
this.answers = new ArrayList<>(answers);
26+
}
27+
28+
public void addAnswer(Answer answer) {
29+
this.answers.add(answer);
30+
}
31+
32+
public List<DeleteHistory> deleteAll(NsUser loginUser) throws CannotDeleteException {
33+
deleteAllAnswers(loginUser);
34+
return addInDeleteHistory();
35+
}
36+
37+
private void deleteAllAnswers(NsUser loginUser) throws CannotDeleteException {
38+
for(Answer answer: answers) {
39+
answer.delete(loginUser);
40+
}
41+
}
42+
43+
public List<DeleteHistory> addInDeleteHistory() {
44+
return this.answers.stream()
45+
.map(answer -> new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now()))
46+
.collect(Collectors.toList());
47+
}
48+
49+
private boolean isNotAllMatch(NsUser loginUser) {
50+
return this.answers.stream()
51+
.anyMatch(answer -> !answer.isNotOwner(loginUser));
52+
}
53+
54+
@Override
55+
public boolean equals(Object o) {
56+
if(o == null || getClass() != o.getClass()) {
57+
return false;
58+
}
59+
Answers answers1 = (Answers) o;
60+
return Objects.equals(answers, answers1.answers);
61+
}
62+
63+
@Override
64+
public int hashCode() {
65+
return Objects.hashCode(answers);
66+
}
67+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package nextstep.qna.domain;
2+
3+
import java.time.LocalDateTime;
4+
5+
public class Base {
6+
protected LocalDateTime createdDate = LocalDateTime.now();
7+
protected LocalDateTime updatedDate;
8+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package nextstep.qna.domain;
2+
3+
import java.util.ArrayList;
4+
import java.util.Arrays;
5+
import java.util.List;
6+
import java.util.stream.Collectors;
7+
import java.util.stream.Stream;
8+
9+
public class DeleteHistories {
10+
11+
private List<DeleteHistory> deleteHistories = new ArrayList<>();
12+
13+
public DeleteHistories(DeleteHistory... deleteHistories) {
14+
this(Arrays.asList(deleteHistories));
15+
}
16+
17+
public DeleteHistories(DeleteHistory deleteHistory, List<DeleteHistory> deleteHistories) {
18+
this (Stream.concat(Stream.of(deleteHistory), deleteHistories.stream()).collect(Collectors.toList()));
19+
}
20+
21+
public DeleteHistories(List<DeleteHistory> deleteHistories) {
22+
this.deleteHistories.addAll(deleteHistories);
23+
}
24+
25+
public void addHistories(DeleteHistories deleteHistories) {
26+
addHistories(deleteHistories.deleteHistories);
27+
}
28+
29+
public void addHistories(List<DeleteHistory> deleteHistories) {
30+
this.deleteHistories.addAll(deleteHistories);
31+
}
32+
33+
public void addHistories(DeleteHistory deleteHistories) {
34+
this.deleteHistories.add(deleteHistories);
35+
}
36+
37+
public List<DeleteHistory> toList() {
38+
return this.deleteHistories;
39+
}
40+
}
Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
package nextstep.qna.domain;
22

3-
import nextstep.users.domain.NsUser;
4-
53
import java.time.LocalDateTime;
64
import java.util.Objects;
5+
import nextstep.users.domain.NsUser;
76

8-
public class DeleteHistory {
7+
public class DeleteHistory extends Base{
98
private Long id;
10-
9+
1110
private ContentType contentType;
12-
11+
1312
private Long contentId;
14-
13+
1514
private NsUser deletedBy;
1615

17-
private LocalDateTime createdDate = LocalDateTime.now();
18-
1916
public DeleteHistory() {
2017
}
2118

@@ -25,26 +22,28 @@ public DeleteHistory(ContentType contentType, Long contentId, NsUser deletedBy,
2522
this.deletedBy = deletedBy;
2623
this.createdDate = createdDate;
2724
}
28-
25+
2926
@Override
3027
public boolean equals(Object o) {
31-
if (this == o) return true;
32-
if (o == null || getClass() != o.getClass()) return false;
28+
if(o == null || getClass() != o.getClass()) {
29+
return false;
30+
}
3331
DeleteHistory that = (DeleteHistory) o;
34-
return Objects.equals(id, that.id) &&
35-
contentType == that.contentType &&
36-
Objects.equals(contentId, that.contentId) &&
37-
Objects.equals(deletedBy, that.deletedBy);
32+
return Objects.equals(id, that.id) && contentType == that.contentType && Objects.equals(contentId, that.contentId) && Objects.equals(deletedBy, that.deletedBy);
3833
}
39-
34+
4035
@Override
4136
public int hashCode() {
4237
return Objects.hash(id, contentType, contentId, deletedBy);
4338
}
44-
39+
4540
@Override
4641
public String toString() {
47-
return "DeleteHistory [id=" + id + ", contentType=" + contentType + ", contentId=" + contentId + ", deletedBy="
48-
+ deletedBy + ", createdDate=" + createdDate + "]";
42+
return "DeleteHistory{" +
43+
"id=" + id +
44+
", contentType=" + contentType +
45+
", contentId=" + contentId +
46+
", deletedBy=" + deletedBy +
47+
'}';
4948
}
5049
}

0 commit comments

Comments
 (0)