-
Notifications
You must be signed in to change notification settings - Fork 0
CS 문제에 대한 번호를 선택을 하여 7개의 데이터 정합성 체크
김무건 edited this page Aug 28, 2023
·
4 revisions
- 문제를 풀었을 때 많은 데이터의 정합성을 신경을 써야됩니다.
제일 많이 고민한 부분
-
문제를 풀었을 때 해결한 문제를 다시는 해결하지 못하게 설정을 해야되는지 다시 문제를 해결할 수 있는지
-
프론트와 협의를 하였을 때 다시 문제를 푸는게 좋다고 판단하여 해결한 문제도 계속 접근이 가능하게 설정을 하였다.
-
이때 성공한 문제를 실패하면 기존에 성공 데이터를 삭제하고 실패 데이터를 넣고 실패한 데이터에 성공을 할때 성공 데이터를 작성을 해야된다.
-
이때 고려해야 되는 상황은 다음과 같습니다.
- 회원의 랭킹 점수
- MySQL의 memberQuesiotn의 성공 데이터 정합성
- MySQL의 memberQuesiotn의 실패 데이터 정합성
- MongoDB의 ReviewUser의 SuccessQuestion 데이터 정합성
- MongoDB의 ReviewUser의 FailQuestion 데이터 정합성
- MongoDB의 ReviewNote의 SuccessQuestion 데이터 정합성
- MongoDB의 ReviewNote의 FailQuestion 데이터 정합성
@Override
@Transactional
public void choiceQuestion(LoginUserDto loginUserDto, Long questionId, ChoiceAnswerRequestDto choiceNumber) {
Integer choiceAnswerNumber = questionRepository.findQuestionWithChoicesAndCategoryById(questionId)
.orElseThrow(() -> new NotFoundQuestionId(questionId)).getChoices().stream()
.filter(Choice::isAnswer)
.map(Choice::getNumber)
.findFirst().orElseThrow();
log.warn("정답 info {}",choiceAnswerNumber);
questionRepository.findQuestionWithChoicesAndCategoryById(questionId)
.orElseThrow(() -> new NotFoundQuestionId(questionId)).getChoices().stream()
.filter(Choice::isAnswer)
.forEach(choice -> {
if (choice.getNumber() == choiceNumber.getChoiceNumber()) {
memberQuestionService.findMemberAndMemberQuestionSuccess(
loginUserDto.getMemberId(),
questionId,
choiceNumber
);
} else {
memberQuestionService.findMemberAndMemberQuestionFail(
loginUserDto.getMemberId(),
questionId,
choiceNumber
);
}
});
questionRepository.findQuestionWithChoicesAndCategoryById(questionId)
.orElseThrow(() -> new NotFoundQuestionId(questionId)).getChoices().stream()
.filter(Choice::isAnswer)
.forEach(choice -> {
if (choice.getNumber() == choiceNumber.getChoiceNumber()) {
reviewService.solveQuestionWithValid(
questionId,
choiceNumber.getChoiceNumber(),
true,
loginUserDto,
choiceAnswerNumber);
} else {
reviewService.solveQuestionWithValid(
questionId,
choiceNumber.getChoiceNumber(),
false,
loginUserDto,
choiceAnswerNumber
);
}
});
redisPublisher.publish(ChannelTopic.of(RANKING_INVALIDATION), RANKING);
}
@Override
@Transactional
public void findMemberAndMemberQuestionSuccess(Long memberId, Long questionId, ChoiceAnswerRequestDto choiceAnswerRequestDto) {
findByQuestionAboutMemberIdAndQuestionIdSuccess(memberId, questionId);
findByQuestionAboutMemberIdAndQuestionIdFail(memberId, questionId);
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundMemberId(memberId));
Question question = questionRepository.findById(questionId)
.orElseThrow();
if (memberQuestionRepository.existsByMemberAndQuestionAndSuccess(memberId, questionId, choiceAnswerRequestDto.getChoiceNumber())) {
throw new existByMemberQuestionDataException(memberId, questionId, choiceAnswerRequestDto.getChoiceNumber());
}
member.addRankingPoint(choiceAnswerRequestDto);
memberQuestionRepository.save(MemberQuestion.builder()
.member(member)
.question(question)
.success(choiceAnswerRequestDto.getChoiceNumber())
.solveTime(choiceAnswerRequestDto.getTime())
.build());
}
@Override
@Transactional
public void findMemberAndMemberQuestionFail(Long memberId, Long questionId, ChoiceAnswerRequestDto choiceAnswerRequestDto) {
findByQuestionAboutMemberIdAndQuestionIdSuccess(memberId, questionId);
findByQuestionAboutMemberIdAndQuestionIdFail(memberId, questionId);
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundMemberId(memberId));
Question question = questionRepository.findById(questionId)
.orElseThrow();
if (memberQuestionRepository.existsByMemberAndQuestionAndFail(memberId, questionId, choiceAnswerRequestDto.getChoiceNumber())) {
throw new existByMemberQuestionDataException(memberId, questionId, choiceAnswerRequestDto.getChoiceNumber());
}
member.minusRankingPoint(member.getRankingPoint());
memberQuestionRepository.save(MemberQuestion.builder()
.member(member)
.question(question)
.fail(choiceAnswerRequestDto.getChoiceNumber())
.solveTime(choiceAnswerRequestDto.getTime())
.build());
}
@Override
@Transactional
public void findByQuestionAboutMemberIdAndQuestionIdFail(Long memberId, Long questionId) {
long count = memberQuestionRepository.countByMemberIdAndQuestionIdAndFailZero(memberId, questionId);
if (count != 0) {
Optional<MemberQuestion> questionOptional = memberQuestionRepository.findByQuestionAboutMemberIdAndQuestionId(memberId, questionId);
questionOptional.ifPresent(question -> memberQuestionRepository.deleteById(question.getId()));
questionOptional.orElseThrow(() -> new RuntimeException("MemberQuestion not found"));
}
}
@Override
public QuestionAnswerDto isCorrectAnswer(Long memberId, Long questionId, ChoiceAnswerRequestDto requestDto) {
boolean answer = memberQuestionRepository.existsByMemberAndQuestionAndSuccess(memberId, questionId, requestDto.getChoiceNumber());
return QuestionAnswerDto.builder()
.answer(answer)
.build();
}
@Query("SELECT COUNT(MQ) FROM MemberQuestion MQ " +
"WHERE MQ.member.id = :memberId " +
"AND MQ.question.id = :questionId " +
"AND MQ.success = 0")
long countByMemberIdAndQuestionIdAndSuccessZero(@Param("memberId") Long memberId,
@Param("questionId") Long questionId);
@Query("SELECT COUNT(MQ) FROM MemberQuestion MQ " +
"WHERE MQ.member.id = :memberId " +
"AND MQ.question.id = :questionId " +
"AND MQ.fail = 0")
long countByMemberIdAndQuestionIdAndFailZero(@Param("memberId") Long memberId,
@Param("questionId") Long questionId);
- 데이터의 정합성을 판단하기 위해서 처음에는 Count를 통해서 기존의 데이터의 존재 유무를 판단하고 만약에 데이터가 있다면 Delete를 하여 데이터를 삭제를 합니다.
- 이후
member.addRankingPoint(choiceAnswerRequestDto);
를 통해서 더티 체킹으로 회원의 데이터를 변경하는 로직을 추가를 하였다. - 이를 통하여 회원의 랭킹 데이터는 일관성을 유지하고 memberQuestion테이블은 성공과 실패의 데이터를 넣기 이전에 같은 questionId가 있는 데이터가 있다면 이를 판단하여 데이터를 처리를 한다.
- 오답노트에서 MongoDB를 적용을 시켰다. 이에 대한 도입 근거는 다음 블로그에 정리를 하였습니다.
- MongoDB를 Spring에서 이용하며 아직 미숙한 부분이 있어 많은 리펙토링이 필요성을 느낌
- 사진을 보면 ReviewUser와 ReviewNote의 중복적인 데이터의 접근을 방지를 해야됩니다.
boolean questionExistsInFailList = byName.getFailQuestion().stream()
.anyMatch(failQuestionId -> failQuestionId.equals(String.valueOf(questionId)));
boolean questionExistsInSuccessList = byName.getSuccessQuestion().stream()
.anyMatch(successQuestionId -> successQuestionId.equals(String.valueOf(questionId)));
- 다음과 같은 검증을 통해서 boolean 타입을 반환을 받아 중복적인 데이터를 검증을 합니다. 이후 다음과 같은 코드로 기존의 데이터를 삭제를 합니다.
if (questionExistsInSuccessList) {
List<String> successQuestion = byName.getSuccessQuestion();
successQuestion.removeIf(successQuestionId -> successQuestionId.equals(String.valueOf(questionId)));
} else if (questionExistsInFailList) {
List<String> failQuestion = byName.getFailQuestion();
failQuestion.removeIf(successQuestionId -> successQuestionId.equals(String.valueOf(questionId)));
}
- 위에 코드를 통하여 ReviewUser의 데이터 정합성을 맞출 수 있습니다.
if (questionExistsInFailList || questionExistsInSuccessList) {
ReviewNote match = byName.getReviewNotes().stream()
.filter(reviewNote -> reviewNote.getQuestionId() == questionId)
.findFirst().orElseThrow(() -> new RuntimeException("fds"));
ObjectId objectId = new ObjectId(match.getId());
log.info("id : {}", objectId);
reviewNoteRepository.deleteById(objectId.toString());
}
- 해당 코드를 통하여 ReviewNote의 데이터의 정합성을 맞출 수 있었습니다.
- 하지만 처음에 deleteById를 하였을 때 MongoDB의 데이터가 삭제가 되지 않는 문제가 발생을 하였습니다.
- 오류의 원인은 MongoDB의 ID는 Object로 구성이 되어져 있습니다.
- 기본적으로 Long 또는 String의 id를 삭제를 하였을 때 정확한 Id가 아니기 때문에 데이터의 삭제가 불가능 합니다.
- 이를 해결하기 위해
ObjectId
를 이용을 하였습니다. 왜냐하면 MongoDB에 데이터는 json처럼 보이지만 실제로 저장이 되는 방식은 bson으로 압축이 되어 저장이 됩니다. - 이를 통하여 id를 Object로 BSON 형식으로 변환하여 삭제를 진행을 하였습니다.
@Override
@Transactional
public void solveQuestionWithValid(
long questionId,
int choiceNumber,
boolean isAnswer,
LoginUserDto loginUserDto,
Integer choiceAnswerNumber
) {
LocalDateTime now = LocalDateTime.now();
Member member = memberRepository.findById(loginUserDto.getMemberId())
.orElseThrow(() -> new NotFoundMemberId(loginUserDto.getMemberId()));
ReviewUser byName = userRepository.findByUserName(member.getName())
.orElseThrow(RuntimeException::new);
boolean questionExistsInFailList = byName.getFailQuestion().stream()
.anyMatch(failQuestionId -> failQuestionId.equals(String.valueOf(questionId)));
boolean questionExistsInSuccessList = byName.getSuccessQuestion().stream()
.anyMatch(successQuestionId -> successQuestionId.equals(String.valueOf(questionId)));
log.info("questionExistsInFailList : {}", questionExistsInFailList);
log.info("questionExistsInSuccessList : {}", questionExistsInSuccessList);
if (questionExistsInSuccessList) {
List<String> successQuestion = byName.getSuccessQuestion();
successQuestion.removeIf(successQuestionId -> successQuestionId.equals(String.valueOf(questionId)));
} else if (questionExistsInFailList) {
List<String> failQuestion = byName.getFailQuestion();
failQuestion.removeIf(successQuestionId -> successQuestionId.equals(String.valueOf(questionId)));
}
if (questionExistsInFailList || questionExistsInSuccessList) {
ReviewNote match = byName.getReviewNotes().stream()
.filter(reviewNote -> reviewNote.getQuestionId() == questionId)
.findFirst().orElseThrow(() -> new RuntimeException("fds"));
ObjectId objectId = new ObjectId(match.getId());
log.info("id : {}", objectId);
reviewNoteRepository.deleteById(objectId.toString());
}
log.info("reviewUser_Name : {}", byName.getUserName());
if (isAnswer) {
byName.getSuccessQuestion().add(String.valueOf(questionId));
} else {
byName.getFailQuestion().add(String.valueOf(questionId));
}
userRepository.save(byName);
if (isAnswer) {
ReviewNote successNote = ReviewNote.builder()
.questionId(questionId)
.successChoiceNumber(choiceNumber)
.createdDate(now)
.isAnswer(true)
.build();
reviewNoteRepository.save(successNote);
byName.getReviewNotes().add(successNote);
} else {
ReviewNote failNote = ReviewNote.builder()
.questionId(questionId)
.successChoiceNumber(choiceAnswerNumber)
.failChoiceNumber(choiceNumber)
.createdDate(now)
.isAnswer(false)
.build();
reviewNoteRepository.save(failNote);
byName.getReviewNotes().add(failNote);
}
userRepository.save(byName);
}