Skip to content
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

[BE] refactor: 서비스 리팩터링 #540

Merged
merged 45 commits into from
Sep 7, 2024
Merged
Changes from 1 commit
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
1e6204b
feat: 리뷰를 검증하는 서비스 구현
Kimprodp Aug 28, 2024
d62a842
refactor: 리뷰 등록 서비스가 리뷰를 생성하고 검증 서비스를 통해서 검증하도록 변경
Kimprodp Aug 28, 2024
de03539
refactor: 답변 검증 서비스가 요청이 아닌 객체를 검증하도록 변경
Kimprodp Aug 28, 2024
2fc94e1
feat: 리뷰에서 제공하는 기능 추가
Kimprodp Aug 28, 2024
7561c72
refactor: dto명 변경 및 사용하지 않는 메서드 삭제
Kimprodp Aug 28, 2024
d3842d2
refactor: 예외명 및 메세지 변경
Kimprodp Aug 28, 2024
3cfe514
refactor: 리뷰 목록 조회 서비스명 변경 및 응답 객체 mapper를 활용하도록 변경
Kimprodp Aug 28, 2024
daecbdc
feat: 리뷰 목록 응답 객체 매핑 서비스 추가
Kimprodp Aug 28, 2024
9cc53a9
refactor: dto명 변경으로 인한 수정
Kimprodp Aug 28, 2024
b60807f
refactor: ReviewMapper를 통해 리뷰를 생성하도록 변경
Kimprodp Aug 28, 2024
ebc281c
refactor: 리뷰 상세 응답 mapper를 활용하도록 변경 및 일부 로직 리팩터링
Kimprodp Aug 28, 2024
e42293c
refactor: 템플릿을 찾는 로직을 mapper에서 하도록 변경
Kimprodp Aug 28, 2024
357e9ef
refactor: 리팩터링에 따른 컨트롤러 변경사항
Kimprodp Aug 28, 2024
bb153e5
Merge remote-tracking branch 'refs/remotes/origin/develop' into be/re…
Kimprodp Aug 28, 2024
646d8b7
test: 변경사항 반영
Kimprodp Aug 28, 2024
20d6ffc
refactor: 질문이 없는 섹션은 응답 생성하지 않도록 변경
Kimprodp Aug 29, 2024
bd4b455
refactor: 답변이 null인 경우에 대한 예외 추가
Kimprodp Aug 29, 2024
fd2f730
refactor: CheckBoxAnswerValidator null 검증 제외 및 질문 유효 검증 추가
Kimprodp Aug 29, 2024
14e1819
refactor: TextAnswerValidator 질문 유효 검증 추가
Kimprodp Aug 29, 2024
76a01c7
test: Validator 테스트 작성
Kimprodp Aug 29, 2024
08e5707
test: Mapper 테스트 작성
Kimprodp Aug 29, 2024
7e15fa3
test: ReviewValidator 테스트 작성
Kimprodp Aug 29, 2024
8726e0c
test: ReviewRegisterService 테스트 간소화
Kimprodp Aug 29, 2024
cfe4fd9
test: 리팩터링 사항 반영
Kimprodp Aug 29, 2024
c351eb2
test: Review 테스트 작성
Kimprodp Aug 29, 2024
4ff1bb4
chore: secret 변경사항 반영
Kimprodp Aug 29, 2024
f494164
refactor: 답변 생성 시, null 이 들어올 수 없도록 변경
Kimprodp Sep 4, 2024
b0e20bc
refactor: ReviewMapper 에서 답변에 대한 검증 없이 엔티티 매핑만 하도록 변경
Kimprodp Sep 4, 2024
0d63f18
refactor: ReviewValidator 에서 답변에 대한 검증도 진행하도록 변경
Kimprodp Sep 4, 2024
d93f943
refactor: 메서드명 변경 (getAllQuestionIdsFromAnswers -> getAnsweredQuestio…
Kimprodp Sep 4, 2024
6363b82
refactor: @Transactional 어노테이션을 클래스 범위에만 적용하도록 변경
Kimprodp Sep 4, 2024
0062fb4
refactor: 메서드 분리 변경
Kimprodp Sep 4, 2024
1359b10
refactor: 메서드 시그니쳐 변경
Kimprodp Sep 4, 2024
8caa2b0
style : 개행 수정
Kimprodp Sep 4, 2024
b90aa47
refactor: 존재하는 메서드 활용하도록 변경
Kimprodp Sep 4, 2024
1f18e77
refactor: 리뷰에 특적 질문에 대한 답변이 있는지 확인하는 기능 추가
Kimprodp Sep 4, 2024
0071013
style: 개행 수정
Kimprodp Sep 4, 2024
7022bcb
refactor: 객체 메서드를 활용하도록 변경
Kimprodp Sep 5, 2024
c280809
test: AnswerMapperTest 작성
Kimprodp Sep 5, 2024
d9b4519
refactor: 답변 검증 시, 답변이 빈 경우에 대한 검증도 객체 생성자에서 진행 및 QuestionNotAnswered…
Kimprodp Sep 7, 2024
caa0716
refactor: 메서드명 변경 (hasQuestions -> hasAnsweredQuestion)
Kimprodp Sep 7, 2024
eca9491
style: 개행 변경
Kimprodp Sep 7, 2024
591f8cb
refactor: ReviewValidator 파라미터를 나눠보 보낼 수 있는 부분 적용
Kimprodp Sep 7, 2024
d3a615c
Merge remote-tracking branch 'refs/remotes/origin/develop' into be/re…
Kimprodp Sep 7, 2024
610b785
test: develop 브랜치 머치 후, 미변경 사항 적용
Kimprodp Sep 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package reviewme.review.service.module;

import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import reviewme.question.domain.Question;
import reviewme.question.domain.QuestionType;
import reviewme.question.repository.QuestionRepository;
import reviewme.review.domain.CheckboxAnswer;
import reviewme.review.domain.Review;
import reviewme.review.domain.TextAnswer;
import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException;
import reviewme.review.service.dto.request.ReviewAnswerRequest;
import reviewme.review.service.dto.request.ReviewRegisterRequest;
import reviewme.review.service.exception.SubmittedQuestionNotFoundException;
import reviewme.reviewgroup.domain.ReviewGroup;
import reviewme.reviewgroup.repository.ReviewGroupRepository;
import reviewme.template.domain.Template;
import reviewme.template.domain.exception.TemplateNotFoundByReviewGroupException;
import reviewme.template.repository.TemplateRepository;

@Component
@RequiredArgsConstructor
public class ReviewMapper {

private final ReviewGroupRepository reviewGroupRepository;
private final TemplateRepository templateRepository;
private final QuestionRepository questionRepository;

public Review mapToReview(ReviewRegisterRequest request,
TextAnswerValidator textAnswerValidator,
CheckBoxAnswerValidator checkBoxAnswerValidator
) {
ReviewGroup reviewGroup = findReviewGroupByRequestCodeOrThrow(request.reviewRequestCode());
Template template = findTemplateByReviewGroupOrThrow(reviewGroup);


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개행 두 줄이네요 👀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하핫.. 반영 완료👍

List<TextAnswer> textAnswers = new ArrayList<>();
List<CheckboxAnswer> checkboxAnswers = new ArrayList<>();
for (ReviewAnswerRequest answerRequest : request.answers()) {
Question question = questionRepository.findById(answerRequest.questionId())
.orElseThrow(() -> new SubmittedQuestionNotFoundException(answerRequest.questionId()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question에 대해서 저장소를 찔러보는 건 꽤나 비용이 크다고 생각해요. 요청의 Id로 모든 Question을 가져와보면 어떨까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 같은 생각입니다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영 완료👍


if (question.getQuestionType() == QuestionType.TEXT) {
TextAnswer textAnswer = mapToTextAnswer(answerRequest);
textAnswerValidator.validate(textAnswer);
textAnswers.add(textAnswer);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

textAnswer를 mapper로 생성함(null 검증은 mapper안에서 함) → 그 다음에 validator로 보내서 나머지 검증함

현재 검증이 1. mapper 2. validator 두 위치에 나눠져있는 부분이 고민됩니다.

현재는 textAnswer 객체를 생성하기 전에 해야하는 검증이 null만 있지만,
이전 로직을 비교해보면 아래와 같은 검증들도 textAnswer 객체를 생성하기 전에 mapper에서 이뤄져야하기에
그렇다면 검증이 더더욱 1. mapper 2. validator 이 두 부분으로 나눠지게 된다고 생각해요.

private void validateNotIncludingOptions(CreateReviewAnswerRequest request) {
        if (request.selectedOptionIds() != null) {
            throw new TextAnswerIncludedOptionItemException();
        }
    }

    private void validateQuestionRequired(Question question, CreateReviewAnswerRequest request) {
        if (question.isRequired() && request.text() == null) {
            throw new RequiredQuestionNotAnsweredException(question.getId());
        }
    }

객체 생성 전에 한 곳에서 검증을 싹 하고, 후에 객체를 생성하는 방식으로 풀어갈 수 있지 않을까요?(아직 안해봄.. 생각대로 될지는 해봐야합니다😂)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존의 방식은 DTO 단계에서 모든 검증이 수행되기 때문에 단순화될 수 있지만, DTO에 너무 의존적인 서비스가 되어버리는 것 같았습니다. DTO가 변경되면 DTO를 검증하는 로직을 변경해야 하므로 서비스의 유연성이 떨어지게 됩니다.
변경된 방식은 변환, 검증과 같은 로직이 각각 책임에 따라 분리되어 있어 수정이 발생하는 부분만 수정할 수 있습니다.
따라서 DTO가 변경된다면 변환 부분의 로직만 수정되고 엔티티는 변경되지 않으니 검증하는 로직은 수정이 필요 없게 됩니다.

추가로 이 부분을 읽어보았는데요.
요청 DTO 변경되어서 DTO를 검증하는 로직을 변경해야하는 경우가 떠오르지 않아서 위와 같은 코멘트를 남겼어요!
(객체 생성 전에 한 곳에서 검증을 싹 하고, 후에 객체를 생성하는 방식 = dto로 검증을 하자)

  1. 저 장점을 가져가기엔 떠오르는 변경으로 인해 검증 로직에 변경이 생기는 것이 떠오르는 게 현재 시점에서 없고(발생한 적이 없고)
  2. 하지만 객체 생성후 검증 방식으로 한다면 검증 위치가 여러곳으로 분리되고 유지보수시 파악이 어렵겠다는 이유라고 정리할 수 있겠네요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

같이 대화 나눈 내용 코멘트로 남겨놓겠습니다.

  • Mapper와 Validator는 각각의 책임을 지님
  • Mapper는 요청을 받아 객체를 매핑하는 책임 -> 이 과정에서 요청이 올바르게 들어왔는지에 대한 검증을 같이 진행
  • Validator는 생성된 객체를 비즈니스 정책에 따라 검증

수정이 필요한 부분이 요청과 관련된 부분인지, 비즈니스 정책에 관련된 부분인지에 따라서 개발자가 봐야 하는 부분을 명확하게 해보자


if (question.getQuestionType() == QuestionType.CHECKBOX) {
CheckboxAnswer checkboxAnswer = mapToCheckboxAnswer(answerRequest);
checkBoxAnswerValidator.validate(checkboxAnswer);
checkboxAnswers.add(checkboxAnswer);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 더 우아한 방법이 있을 것 같은데.. 고민해보겠습니다 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(개인 메모를 아래에 적어두겠습니다, 무시해 주세요)

  • QuestionType마다 서로 다른 Validator 등이 필요하나 궁극적으로 검증한 뒤에 담아두는 것은 같음
  • 각각의 Type에 Validator가 포함된 컨테이너가 있고, 그 컨테이너를 관리하는 친구가 있다면..?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

왜 자바는 하나의 타입만 반환이 가능할까요😢
추후 리팩토링 사항으로 남겨두면 되겠네요~

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

크크 이거 저랑 테드도 이야기했던 부분이네요.
추후 리팩터링 사항으로 남기는 것 동의합니다!!

Copy link
Contributor

@donghoony donghoony Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnswerValidator 인터페이스 -> TextAnswerValidator, CheckboxAnswerValiator 정도 있으면 좋겠는데요 ㅋㅋㅋ

}

return new Review(template.getId(), reviewGroup.getId(), textAnswers, checkboxAnswers);
}

private ReviewGroup findReviewGroupByRequestCodeOrThrow(String reviewRequestCode) {
return reviewGroupRepository.findByReviewRequestCode(reviewRequestCode)
.orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode));
}

private Template findTemplateByReviewGroupOrThrow(ReviewGroup reviewGroup) {
return templateRepository.findById(reviewGroup.getTemplateId())
.orElseThrow(() -> new TemplateNotFoundByReviewGroupException(
reviewGroup.getId(), reviewGroup.getTemplateId()));
}

private TextAnswer mapToTextAnswer(ReviewAnswerRequest answerRequest) {
return new TextAnswer(answerRequest.questionId(), answerRequest.text());
}

private CheckboxAnswer mapToCheckboxAnswer(ReviewAnswerRequest answerRequest) {
return new CheckboxAnswer(answerRequest.questionId(), answerRequest.selectedOptionIds());
}
}