Home Quiz 정답 체크
Post
Cancel

Quiz 정답 체크

Quiz 정답 체크

이번 글에서는 사용자가 퀴즈에서 정답을 맞췄을 경우 db에 정답 상태(correct=true)를 저장하는 과정을 작성했다.


설계

구현할 기능을 아래와 같이 나누어 생각했다.

단계FrontBack주요 처리
1퀴즈 정답 체크-정답 시 quiz_id 저장
2모달 닫힘 이벤트 감지-quiz_id 목록 서버 전송
3서버로 데이터 전송PUT 요청성공 시 quiz_id 초기화
4DB 업데이트quiz_id 일괄 저장correct=true로 상태 변경





front

1. 정답 체크

사용자가 퀴즈를 한 번에 맞춘 경우 해당 quiz_id를 리스트에 추가하고 localStorage에 저장한다.

정답을 맞춘 퀴즈는 퀴즈 목록인 quizList에서 삭제하고 다음 퀴즈로 넘어간다.

1
2
3
4
5
6
7
8
9
10
11
let chance = 1; // 정답 기회

// 사용자가 정답을 한 번에 맞췄을 경우 db에 저장할 목록들을 담아둔다. 
if(chance===1){
    if(!quizId_List.includes(quizList[quizCurrentPage]["quizId"])){ // 중복 체크
        quizId_List.push(quizList[quizCurrentPage]["quizId"]); // 맞춘 quiz_id 저장
        localStorage.setItem("quizId_List",JSON.stringify(quizId_List)); // localstorage 저장
        quizList.splice(quizCurrentPage,1); // quizList에서 정답인 quiz 삭제
        quizCurrentPage-=1; // page index
    }
}




2. Modal event

사용자가 퀴즈 모달창을 닫으면 quiz_id 목록이 있을 때 서버로 데이터를 전송한다.

1
2
3
4
5
quizModal.addEventListener("hidden.bs.modal", () => {
    if(quizId_List.length>0){
        changeCorrect();
    }
});




3. AJAX : 정답 처리

데이터를 서버에 전송한 후 성공 시 quizId_List를 초기화한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function changeCorrect(){
    $.ajax({
        type: "PUT",
        url: `/quiz/${username}`,
        headers: {},
        data: JSON.stringify({quizId_List}),
        contentType: 'application/json',
        processData: false,
        success: function (response) {
            quizId_List=[]; // 초기화 
            localStorage.removeItem("quizId_List")
        }
    })
}

JSON.stringify({quizId_List}) : {“quizId_List”:[255,253,259,260]}

JSON.stringify(quizId_List) : [255,253,259,260]


DTO와 매핑 시: JSON.stringify({quizId_List}) 형태로 객체를 전송해야 함

그렇지 않을 경우 JSON parse error: Cannot deserialize value of type ~ from Array value (token ‘JsonToken.START_ARRAY’)] .


배열(List<Long>)과 매핑 시: JSON.stringify(quizId_List) 형태로 배열을 전송해야 함

그렇지 않을 경우 JSON parse error: Cannot deserialize value of type java.util.ArrayList from Object value




push()concat()

push(): 기존 배열에 단일 값을 추가하고 배열의 길이를 반환한다. (a.push(b);)

concat(): 기존 배열을 복사한 후 새로운 배열을 반환한다. (a = a.concat(b);)


기존에 push를 사용해서 id값(단일 값)을 추가했다.

그러나 퀴즈 리스트는 단일 값이 아닌 배열이기 때문에 push로 사용할 수 없다.

quizList.push(...response); 또는 quizList = quizList.concat(response);를 사용하면 합칠 수 있다.



Spread Operator

JavaScript의 스프레드 연산자(...)를 사용하면

기존 배열이나 객체의 전체 또는 일부를 다른 배열이나 객체로 빠르게 복사할 수 있다.

1
2
3
4
5
const numbersOne = [1, 2, 3];
const numbersTwo = [4, 5, 6];
const numbersCombined = [...numbersOne, ...numbersTwo];

document.write(numbersCombined); // 1,2,3,4,5,6

Spread Operator는 배열의 원소들을 분해하여 개별 요소로 만드는 기능을 한다.

push는 배열의 값을 넣기 위해 사용된다. (배열로 추가하면 그대로 값이 들어가기 때문에 배열 자체가 push 된다.)

하지만 spread operator를 사용할 경우, spread operator는 배열의 요소 각각으로 분해하기 때문에 요소 하나하나가 push 된다.


React ES6 Spread Operator





Back

Controller

@RequestBody List<Long> 형식으로 데이터를 받으면 JSON이 배열이어야 한다. (ex. [258])

{"quizId_List":[258]} 형태로 전송했기 때문에 객체로 데이터를 처리하기 위해 DTO를 사용했다.

1
2
3
4
@PutMapping("/quiz/{username}")
public void quiz_correct(@PathVariable String username, @RequestBody QuizRequestDto dto) {
    quizService.quiz_correct(dto);
}

만약 DTO가 아닌 List<Long>으로 작성했다면

JSON parse error: Cannot deserialize value of type java.util.ArrayList from Object value 라는 오류가 뜬다.




성능 최적화 : 개별 처리 vs 일괄 처리

DB 업데이트 성능을 최적화하기 위해 하나씩 처리하는 로직과 일괄로 처리하는 로직을 작성 후

어느정도의 db 개수까지 개별로 저장할지 test 했다.

개별은 true로 변경 일괄은 false로 설정해 5, 10, 15 순으로 5개씩 증가하면서 test했다.



Repository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final JdbcTemplate template;

// 개별 update (한 번에 하나씩 SQL을 실행) 
public void individualUpdates(QuizRequestDto quizIdList) {
    String sql = "UPDATE quiz SET correct = true WHERE id = ?";
    for (Long quizId : quizIdList.getQuizId_List()) {
        template.update(sql, quizId);
    }
}

// 일괄 update (batchUpdate를 사용하여 SQL을 일괄 실행)       
public void updateCorrect(QuizRequestDto quizIdList) {
    String sql = "UPDATE quiz SET correct = false WHERE quiz.id=?";
    template.batchUpdate(sql, quizIdList.getQuizId_List(), quizIdList.getQuizId_List().size(), (qz, quizId) ->{
        qz.setLong(1, quizId);
            });
}




성능 비교

1
2
3
4
5
6
7
8
9
10
11
log.info("correct=true 총 개수 : {} ",quizIds.getQuizId_List().size());

long start = System.currentTimeMillis();
jdbcRepository.individualUpdates(quizIds);
long end = System.currentTimeMillis();
log.info("개별 걸린 시간 : {}", end - start);

long start1 = System.currentTimeMillis();
jdbcRepository.updateCorrect(quizIds);
long end1 = System.currentTimeMillis();
log.info("일괄 걸린 시간 : {}", end1 - start1);

image

image

데이터 개수개별 처리 시간(ms)일괄 처리 시간(ms)
143
5227
205815

성능 테스트 결과 데이터 개수에 상관없이 일괄 처리를 사용하는 것이 성능적으로 더 유리했다.




추가) WHERE IN과 NamedParameterJdbcTemplate

spring framework 공식 문서에 따르면 SQL 문에서 매개변수를 표현하는 방법은 두 가지다.


1. positional parameter(위치 기반)

positional는 순서에 따라 파라미터값들을 넣어주는 것을 이야기한다.

물음표(?)로 표시하는 경우 순서에 따라 값을 설정하기 때문에 positional parameter에 해당한다.


2. named parameter(이름 기반)

“named는 이 파라미터가 어떤 파라미터 입니다.” 라고 명시해주면서 코드를 작성하는 것을 의미한다.

where in 같은 경우 콜론(:)을 사용한다.

콜론(:)을 통해 변수를 기준으로 값을 설정하는 경우 JdbcTemplate이 아닌 NamedParameterJdbcTemplate을 사용해야한다.

NamedParameterJdbcTemplate은 이름을 기준으로 값을 설정하기 때문에 파라미터의 순서를 신경 쓰지 않아도 되는 장점이 있다.


1
2
3
4
5
6
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void updateCorrectIndividually(QuizRequestDto quizIdList) {
    String sql = "UPDATE quiz SET correct = true WHERE id IN (:ids)";
    Map<String, Object> params = Map.of("ids", quizIdList.getQuizId_List());
    namedParameterJdbcTemplate.update(sql, params);
}


일반 Map을 사용하여 파라미터를 정의하는 방법은

NamedParameterJdbcTemplate의 메소드에 파라미터로 Map<String, ?>을 직접 전달하는 것이다.

Java 9 이상부터 of()를 활용해 map을 간단하게 작성할 수 있다.

image



of()를 확인해보면 인자의 개수에 맞춰 오버로딩하고 있는 것을 확인할 수 있다.

그러나 인자의 개수는 10개 이하로 제한되어 있으며 그 이상의 경우엔 ofEntries()를 사용하면 된다.

1
2
3
4
5
// Map.java
static <K, V> Map<K, V> of(K k1, V v1) {...}
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5) {...}
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5,
                               K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9, K k10, V v10) {...}



image


where과 where in의 큰 차이는 없어보인다.





기타 수정사항)

학습할 때 학습한 단어와 퀴즈 목록들을 저장하는데 이 과정을 실패하면

redis cache update 과정도 실패로 돌아가게끔 @Transactional을 추가했다.


퀴즈 목록에서 저장 실패 후 redis cache가 update 되지 않고 그대로 값이 있는 것을 확인했다.

처음 학습하기 실행 후 모달을 닫았을 때 cache에는 10개의 학습 리스트가 저장되며,

학습한 단어는 cache에서 삭제되는 로직을 작성했었다.

따라서 update가 되었다면 10개의 데이터가 아닌 10개 미만의 데이터를 가지고 있어야 한다.

image






REFERENCE

js - push & concat

NamedParameterJdbcTemplate

Map.of()

This post is licensed under CC BY 4.0 by the author.