-
Notifications
You must be signed in to change notification settings - Fork 432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Step1 #844
base: blossun
Are you sure you want to change the base?
Step1 #844
Changes from all commits
0f8192d
a5bebe2
fa2eb37
0e252f7
244c351
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
package qna; | ||
|
||
public class CannotDeleteException extends Exception { | ||
public class CannotDeleteException extends RuntimeException { | ||
private static final long serialVersionUID = 1L; | ||
|
||
public CannotDeleteException(String message) { | ||
super(message); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
package qna.domain; | ||
|
||
import qna.CannotDeleteException; | ||
import qna.NotFoundException; | ||
import qna.UnAuthorizedException; | ||
|
||
|
@@ -43,9 +44,11 @@ public Answer(Long id, User writer, Question question, String contents) { | |
this.contents = contents; | ||
} | ||
|
||
public Answer setDeleted(boolean deleted) { | ||
this.deleted = deleted; | ||
return this; | ||
public void delete(final User loginUser) { | ||
if (!this.writer.equals(loginUser)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 기존에 존재하는 |
||
throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); | ||
} | ||
this.deleted = true; | ||
} | ||
|
||
public boolean isDeleted() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package qna.domain; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
import javax.persistence.CascadeType; | ||
import javax.persistence.Embeddable; | ||
import javax.persistence.OneToMany; | ||
import javax.persistence.OrderBy; | ||
|
||
import org.hibernate.annotations.Where; | ||
|
||
@Embeddable | ||
public class AnswerList { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL) | ||
@Where(clause = "deleted = false") | ||
@OrderBy("id ASC") | ||
private final List<Answer> answers = new ArrayList<>(); | ||
|
||
public boolean isEmpty() { | ||
return answers.isEmpty(); | ||
} | ||
|
||
public void add(final Answer answer) { | ||
answers.add(answer); | ||
} | ||
|
||
public void deleteAll(final User loginUser) { | ||
answers.forEach(answer -> answer.delete(loginUser)); | ||
} | ||
|
||
public List<Answer> getAnswers() { | ||
return answers; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package qna.domain; | ||
|
||
import java.time.LocalDateTime; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
import javax.annotation.Resource; | ||
|
||
import org.springframework.context.event.EventListener; | ||
import org.springframework.stereotype.Component; | ||
|
||
import qna.service.DeleteHistoryService; | ||
|
||
@Component | ||
public class DeleteEventHandler { | ||
|
||
@Resource(name = "deleteHistoryService") | ||
private DeleteHistoryService deleteHistoryService; | ||
|
||
@EventListener | ||
public void saveHistory(DeleteQuestionEvent deleteQuestionEvent) { | ||
System.out.println("Subscribe deleteQuestion event!!!!!"); | ||
final Question question = deleteQuestionEvent.getQuestion(); | ||
|
||
List<DeleteHistory> deleteHistories = new ArrayList<>(); | ||
deleteHistories.add(new DeleteHistory(ContentType.QUESTION, question.getId(), question.getWriter(), LocalDateTime.now())); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이벤트로 처리하려다 보니 게터를 쓰게되었습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
for (Answer answer : question.getAnswers().getAnswers()) { | ||
deleteHistories.add(new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now())); | ||
} | ||
deleteHistoryService.saveAll(deleteHistories); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package qna.domain; | ||
|
||
public class DeleteQuestionEvent { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
1단계 미션의 주요 핵심은, 비즈니스 로직에서 테스트하기 어려운 코드와 가능한 코드를 분리하고 물론 스프링 컨테이너를 띄우고 필요한 스프링 빈들을 주입 받아서 테스트를 수행할 수는 있지만, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 리뷰 감사합니다. :) 저는 "삭제 이력 기록"이 Question과 Answer의 핵심 비즈니스 로직이 아니라고 생각을 했습니다. 이벤트 발행/구독 구조가 학습 요구사항을 벗어나는 것 같지만 이런 구조가 OOP를 벗어나는 것은 아니라고 생각이 드는데 이 부분에 대한 리뷰어님의 의견은 어떠실지 궁금합니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 추가로 현재 코드처럼 이벤트 발행을 서비스에서 한다는 것은 잘못됐네요. 이벤트를 발행하더라고 그 주체는 서비스가 아니라 도메인이 되어야할 것 같습니다. ㅠ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
넵, OOP의 특성을 잘 살려서 구현하는 거라면 저도 말씀하신 의견에 동의합니다. |
||
private final Question question; | ||
|
||
public DeleteQuestionEvent(final Question question) { | ||
this.question = question; | ||
} | ||
|
||
public Question getQuestion() { | ||
return question; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,14 @@ | ||
package qna.domain; | ||
|
||
import org.hibernate.annotations.Where; | ||
import javax.persistence.Column; | ||
import javax.persistence.Embedded; | ||
import javax.persistence.Entity; | ||
import javax.persistence.ForeignKey; | ||
import javax.persistence.JoinColumn; | ||
import javax.persistence.Lob; | ||
import javax.persistence.ManyToOne; | ||
|
||
import javax.persistence.*; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import qna.CannotDeleteException; | ||
|
||
@Entity | ||
public class Question extends AbstractEntity { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
서로 관련이 있는 필드들을 묶어서 새로운 객체를 정의하는 것도 하나의 방법이라고 생각합니다. |
||
|
@@ -18,10 +22,8 @@ public class Question extends AbstractEntity { | |
@JoinColumn(foreignKey = @ForeignKey(name = "fk_question_writer")) | ||
private User writer; | ||
|
||
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL) | ||
@Where(clause = "deleted = false") | ||
@OrderBy("id ASC") | ||
private List<Answer> answers = new ArrayList<>(); | ||
@Embedded | ||
private final AnswerList answers = new AnswerList(); | ||
|
||
private boolean deleted = false; | ||
|
||
|
@@ -80,11 +82,26 @@ public Question setDeleted(boolean deleted) { | |
return this; | ||
} | ||
|
||
public void delete(final User loginUser) throws CannotDeleteException { | ||
if (!this.writer.equals(loginUser)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분도 기존의 |
||
throw new CannotDeleteException("질문자 본인만 삭제할 수 있습니다."); | ||
} | ||
|
||
if (answers.isEmpty()) { | ||
this.deleted = true; | ||
return; | ||
} | ||
|
||
answers.deleteAll(loginUser); | ||
|
||
this.deleted = true; | ||
} | ||
|
||
public boolean isDeleted() { | ||
return deleted; | ||
} | ||
|
||
public List<Answer> getAnswers() { | ||
public AnswerList getAnswers() { | ||
return answers; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,45 @@ | ||
package qna.domain; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import qna.CannotDeleteException; | ||
|
||
public class AnswerTest { | ||
public static final Answer A1 = new Answer(UserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); | ||
public static final Answer A2 = new Answer(UserTest.SANJIGI, QuestionTest.Q1, "Answers Contents2"); | ||
|
||
private Answer answer; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
answer = new Answer(11L, UserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); | ||
} | ||
|
||
@DisplayName("작성자가 본인이면 답변을 지울 수 있다.") | ||
@Test | ||
public void delete_success_owner() { | ||
assertThat(answer.isDeleted()).isFalse(); | ||
|
||
answer.delete(UserTest.JAVAJIGI); | ||
|
||
assertThat(answer.isDeleted()).isTrue(); | ||
} | ||
|
||
@DisplayName("작성자가 아니면 답변을 지울 수 없다.") | ||
@Test | ||
public void delete_fail_no_owner() { | ||
assertThat(answer.isDeleted()).isFalse(); | ||
|
||
assertThatThrownBy(() -> answer.delete(UserTest.SANJIGI)) | ||
.isInstanceOf(CannotDeleteException.class) | ||
.hasMessage("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); | ||
|
||
assertThat(answer.isDeleted()).isFalse(); | ||
} | ||
Comment on lines
+23
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 테스트 케이스를 잘 정리해서 테스트해주셨네요 👍 |
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,88 @@ | ||
package qna.domain; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
import static org.junit.jupiter.api.Assertions.assertAll; | ||
|
||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import qna.CannotDeleteException; | ||
|
||
public class QuestionTest { | ||
public static final Question Q1 = new Question("title1", "contents1").writeBy(UserTest.JAVAJIGI); | ||
public static final Question Q2 = new Question("title2", "contents2").writeBy(UserTest.SANJIGI); | ||
|
||
private Question question; | ||
private Answer answer; | ||
private Answer answer2; | ||
|
||
@BeforeEach | ||
public void setUp() { | ||
question = new Question(1L, "title1", "contents1").writeBy(UserTest.JAVAJIGI); | ||
answer = new Answer(11L, UserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); | ||
answer2 = new Answer(UserTest.JAVAJIGI, question, "answer contents2"); | ||
question.addAnswer(answer); | ||
} | ||
|
||
|
||
@DisplayName("질문자가 본인이 아니면 질문을 삭제할 수 없다.") | ||
@Test | ||
public void delete_fail_not_owner() { | ||
assertThatThrownBy(() -> question.delete(UserTest.SANJIGI)) | ||
.isInstanceOf(CannotDeleteException.class); | ||
} | ||
|
||
@DisplayName("질문자 본인이고, 답변이 없으면 삭제가 가능하다.") | ||
@Test | ||
public void delete_success_owner_and_no_answer() { | ||
assertThat(question.isDeleted()).isFalse(); | ||
|
||
question.delete(UserTest.JAVAJIGI); | ||
|
||
assertThat(question.isDeleted()).isTrue(); | ||
} | ||
Comment on lines
+37
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
@DisplayName("질문자가 본인이고, 답변이 있는 경우, 모든 답변자가 본인이면 삭제가 가능하다.") | ||
@Test | ||
public void delete_success_question_and_answer_owner() { | ||
question.addAnswer(answer); | ||
assertThat(question.isDeleted()).isFalse(); | ||
|
||
question.delete(UserTest.JAVAJIGI); | ||
|
||
assertThat(question.isDeleted()).isTrue(); | ||
} | ||
|
||
@DisplayName("질문자가 본인이고, 답변이 있는 경우, 모든 답변자가 본인이 아니면 삭제가 불가능하다.") | ||
@Test | ||
public void delete_fail_answer_other() { | ||
question.addAnswer(AnswerTest.A1); | ||
question.addAnswer(AnswerTest.A2); | ||
|
||
assertThatThrownBy(() -> question.delete(UserTest.JAVAJIGI)) | ||
.isInstanceOf(CannotDeleteException.class) | ||
.hasMessage("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); | ||
} | ||
|
||
@DisplayName("질문이 삭제되면 답변도 모두 삭제된다.") | ||
@Test | ||
public void delete_success_all_answer_deleted() { | ||
question.addAnswer(answer2); | ||
assertAll( | ||
() -> assertThat(question.isDeleted()).isFalse(), | ||
() -> assertThat(answer.isDeleted()).isFalse(), | ||
() -> assertThat(answer2.isDeleted()).isFalse() | ||
); | ||
|
||
question.delete(UserTest.JAVAJIGI); | ||
|
||
assertAll( | ||
() -> assertThat(question.isDeleted()).isTrue(), | ||
() -> assertThat(answer.isDeleted()).isTrue(), | ||
() -> assertThat(answer2.isDeleted()).isTrue() | ||
); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
본 과정은 스프링을 학습하는 과정이 아니기 때문에 현재로썬 불필요한 의존성 추가로 보여요.
스프링 웹 의존성 없이도 충분히 가능한 미션이기 때문에, 우선은 순수 자바로 구현해보시면 좋을 것 같습니다.