Skip to content

CS 문제에 대한 번호를 선택을 하여 7개의 데이터 정합성 체크

김무건 edited this page Aug 28, 2023 · 4 revisions

CS 문제에 대한 번호를 선택을 하여 7개의 데이터 정합성 체크

고려를 해야되는 상황

  • 문제를 풀었을 때 많은 데이터의 정합성을 신경을 써야됩니다.

제일 많이 고민한 부분

  • 문제를 풀었을 때 해결한 문제를 다시는 해결하지 못하게 설정을 해야되는지 다시 문제를 해결할 수 있는지

  • 프론트와 협의를 하였을 때 다시 문제를 푸는게 좋다고 판단하여 해결한 문제도 계속 접근이 가능하게 설정을 하였다.

  • 이때 성공한 문제를 실패하면 기존에 성공 데이터를 삭제하고 실패 데이터를 넣고 실패한 데이터에 성공을 할때 성공 데이터를 작성을 해야된다.

  • 이때 고려해야 되는 상황은 다음과 같습니다.

  1. 회원의 랭킹 점수
  2. MySQL의 memberQuesiotn의 성공 데이터 정합성
  3. MySQL의 memberQuesiotn의 실패 데이터 정합성
  4. MongoDB의 ReviewUser의 SuccessQuestion 데이터 정합성
  5. MongoDB의 ReviewUser의 FailQuestion 데이터 정합성
  6. MongoDB의 ReviewNote의 SuccessQuestion 데이터 정합성
  7. MongoDB의 ReviewNote의 FailQuestion 데이터 정합성

CS 문제에 대한 번호를 선택 코드

  @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);
    }

MySQL의 memberQuesiotn의 데이터 정합성

 @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를 적용을 시켰다. 이에 대한 도입 근거는 다음 블로그에 정리를 하였습니다.
  • MongoDB를 Spring에서 이용하며 아직 미숙한 부분이 있어 많은 리펙토링이 필요성을 느낌

image

  • 사진을 보면 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의 데이터 정합성을 맞출 수 있습니다.

MongoDB deleteById 오류

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 형식으로 변환하여 삭제를 진행을 하였습니다. image
@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);
    }
Clone this wiki locally