[모두의 랜덤 디펜스] Spring @Scheduled를 이용해서 오늘의 문제 출제 로직 개발하기

2024. 3. 27. 15:34Backend/Spring

https://morandi.co.kr 

 

모두의 랜덤 디펜스

모두의 랜덤 디펜스: 코딩테스트를 준비하는 최고의 솔루션!

morandi.co.kr

 

모두의 랜덤 디펜스는 기업별 코딩 테스트 실전 모의고사 서비스입니다.

현재 서비스를 피봇팅하여 시즌2를 준비중에 있습니다.

 

 

개요

모두의 랜덤 디펜스에는 오늘의 문제 (DailyDefense)라는 기능이 있습니다.

'오늘의 문제'는 매일 5개의 문제를 제공해주고, 사용자들이 자연스럽게 풀어볼 수 있게 하는 기능입니다.

 

오늘의 문제는 매일 12시에 새로 바뀌어야하고, 매일매일 바뀌는 문제를 구현하기 위해 Scheduler를 도입했습니다.

 

ERD에서 오늘의 문제가 저장되는 테이블은 위와 같습니다.

 

여기에는 항상 오늘 날짜에 해당하는 DailyDefense가 존재해야하고, 그 DailyDefense에 포함된 문제 목록인 DailyDefenseProblem이 5개식 존재해야합니다.

 

이걸 이제 정해진 시간에 문제가 뽑혀서 저장되도록 구현하면 되는데, 혹시 문제가 생겨 Scheduler가 돌지 못 했다고 가정하면, 오늘의 문제 서비스에 장애가 생깁니다.

 

이런 문제를 해결하기 위해 만약 3월 26일의 문제는 3월 25일에 미리 뽑아두고, 사용자가 요청할 때는 현재 시간으로 오늘의 문제 세트를 찾는 방식으로 구현하고자 했습니다.

 

이렇게 하면 날짜가 바뀌는 시점이라도 미리 문제가 뽑혀있기에 이상 없이 오늘의 문제를 제공할 수 있을 것으로 판단했습니다.

 

문제 출제 서비스 만들기

 

오늘의 문제 (DailyDefense)는 

 

solved.ac 난이도 기준

 

1번 B3-B1

2번 S5-S3

3번 S3-S1

4번 G5-G3

5번 G3-G1

 

위와 같은 난이도 형태로 출제될 수 있게 구상했고, 

위 기능을 구현하기 위해 DB에서 랜덤으로 문제를 가져올 수 있게 구현하려고 합니다.

 

가장 먼저 성능을 전혀 고려하지 않고 기능 구현에만 집중하겠습니다.

 

원하는 범위의 아직 출제되지 않은 문제 리스트 검색하기

가장 먼저 JPQL를 통해 

1. 문제 상태가 출제될 수 있는 'ACTIVE'상태인지

2. startTier(Enum)와 , endTier(Enum) 사이에 위치하는지

3. 기존에 DailyDefenseProblem에서 뽑힌적 없는 문제인지

조건을 통해 구현했습니다.

 

JPQL에서 limit절을 사용할 수 없는 이유로 Pageable을 통해 구현해보았습니다. 

 

이후 테스트 코드를 작성하여 Problem을 가져올 수 있는지 확인해 보았습니다.

 

현재 DailyDefenseProblem은 비어있습니다.

 

 

그런데 문제를 가져오지 못 하는 상황이 발생했습니다.

 

ProblemTier가 B5인 문제 하나만 ACTIVE해둔 상황인데

위처럼 찾지 못하는 상황이 발생했습니다.

 

하지만 이상하게

이렇게 endTier를 S5로 설정하면 

정상적으로 조회가 됐습니다....

 

 

그 이유는 ProblemTier가 int값을 가지고 있지만, DB예 저장될 떄는 String으로 저장되어, 비교 연산에서 문자열 비교를 하기 때문이었습니다.

 

따라서 시작과 끝 지점을 주는 것이 아니라, 원하는 범위의 리스트를 주어 IN쿼리로 검색하게 구현했습니다.

테스트가 성공하는 것을 확인할 수 있습니다.

 

여기에서 비즈니스 로직 하나를 더 추가해서

푼 사람 수의 범위도 맞게 쿼리를 수정했습니다.

 

 

 

DailyDefensePorblemAdapter 구현

Repository 메소드를 만든 뒤, 

각 문제별로 조건에 맞는 문제를 찾도록 구현할 계획입니다.(이 부분은 페이징을 이용한 구조로 다음 포스팅에서 개선 예정)

위와 같이 Map을 이용하여 문제번호 Long별로 정의된 RandomCriteria에 따라 문제를 가져올 수 있도록 Adapter를 구성했습니다.

 

구현한 Adpater에 대해서는 다음의 테스트코드로 검증하고자 했습니다.

현재는 단순 기능 구현만 목적으로 하고 있어 중복된 문제가 출제되는 경우 예외처리는 고려하지 않았습니다.

 

DailyDefenseGenerationService

Service에서는, 정의된 대로 난이도를 설정하고, 문제를 출제하는 로직을 수행합니다.

설계 당시 오늘의 문제 정보에 난이도 정보를 지정해두고 있지 않아 일단은 난이도 정보를 하드코딩 하는 방식으로 구현했습니다.

 

난이도 정보를 현재는 하드코딩 해뒀지만, DB에서 읽어서 반영하는 방식으로 수정할 계획이며, 

난이도 정보에 따라 request를 만들어 문제 리스트인 'dailyDefenseProblem'을 가져와서 DailyDefense.create를 진행합니다.

 

위 로직에 대한 테스트코드는

여러 문제 중 5개의 문제가 잘 생성되었는지를 검색해서 확인하는 방식으로 구현했습니다. 

(랜덤으로 문제가 저장되는 점으로, 어떤 문제가 출제됐는지 보증할 순 없으며, 난이도 범위에 속하는지 정도는 확인할 수 있을 것이지만, repository 테스트에서 이미 검증된 사항입니다.)

 

 

@Scheduled를 통한 스케줄링 

scheduling을 진행하는 클래스는 infrastrcture-scheduling하위에 위치해두고, 'DailyDefenseGenerationService'의 메소드를 주기적으로 호출하는 방식으로 구현했습니다.