Conversation
- 사용하는 쪽에서 deteched 상태로 사용할 수 있으므로 방지
- 비동기화는 다른 스레드 사용하므로 Matcher 객체가 담긴 현재 스레드의 ThreadLocal를 인식하지 못함
Summary of ChangesHello @sunwon12, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 PR은 베스트셀러 책에 대한 퀴즈를 미리 생성하여 사용자 경험을 개선하고, 퀴즈 생성 시스템의 안정성을 높이는 기능을 도입합니다. 또한, 기존 독서 진행 상황 관리 로직의 데이터 정합성을 강화하고, 퀴즈 생성 관련 서비스의 영속성 컨텍스트 관리 방식을 개선하여 코드의 견고함을 향상시켰습니다. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
이 PR은 베스트셀러 도서에 대한 퀴즈를 미리 생성하는 스케줄러 기능을 추가하는 것을 중심으로 합니다. 전반적으로 새로운 기능의 구현과 테스트 코드 작성이 잘 이루어졌습니다. 특히 비동기 처리와 재시도 로직, 오류 알림 등 안정성을 고려한 설계가 돋보입니다. 또한, 기존 퀴즈 생성 관련 서비스에서 엔티티 대신 ID를 사용하도록 리팩토링하여 코드의 결합도를 낮춘 점도 좋은 개선입니다. 몇 가지 스케줄러 설정의 명확성, 엣지 케이스 처리, DB操作의 원자성 보장에 대한 개선점을 제안했습니다.
| if (chapters.isEmpty()) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
processBestseller 메서드에서 챕터가 없는 책(chapters.isEmpty())의 경우 아무런 처리 없이 return하고 있습니다. 이 경우 해당 BestSeller 엔티티는 DB에서 삭제되지 않고, 스케줄러에 의해 매시간 다시 처리되어 불필요한 부하를 유발할 수 있습니다. 챕터가 없어 퀴즈 생성이 불가능한 책은 BestSeller 테이블에서 삭제하여 더 이상 처리되지 않도록 하는 것이 좋겠습니다.
if (chapters.isEmpty()) {
log.warn("책에 챕터가 없어 퀴즈를 생성할 수 없습니다. 베스트셀러 목록에서 삭제합니다. bookId: {}", book.getId());
bestSellerRepository.delete(bestSeller);
return;
}| * 매주 일요일 밤 10시에 알라딘 베스트셀러를 가져와 저장 | ||
| */ | ||
| @Scheduled(cron = "0 0 2 * * MON") |
There was a problem hiding this comment.
스케줄러의 cron 표현식과 주석의 내용이 일치하지 않습니다. 주석에는 "매주 일요일 밤 10시"라고 되어 있지만, cron 표현식 0 0 2 * * MON은 "매주 월요일 새벽 2시"를 의미합니다. 의도한 스케줄 시간에 맞게 주석이나 cron 표현식을 수정하여 혼동을 줄이는 것이 좋겠습니다. 참고로 일요일 밤 10시는 0 0 22 * * SUN 입니다. 뒤따르는 퀴즈 생성 스케줄이 월요일 새벽 3시부터인 것을 감안할 때, 주석을 수정하는 것이 더 적절해 보입니다.
/**
* 매주 월요일 새벽 2시에 알라딘 베스트셀러를 가져와 저장
*/
@Scheduled(cron = "0 0 2 * * MON")| private void handleFailure(Long bestSellerId) { | ||
| try { | ||
| bestSellerRepository.findById(bestSellerId).ifPresent(bestSeller -> { | ||
| bestSeller.incrementRetryCount(); | ||
| bestSellerRepository.save(bestSeller); | ||
| }); | ||
| } catch (Exception ex) { | ||
| log.error("실패 카운트 증가 중 오류 발생", ex); | ||
| } | ||
| } |
There was a problem hiding this comment.
현재 handleFailure 메서드는 findById로 엔티티를 조회하고 save로 상태를 업데이트하는 두 단계로 이루어져 있습니다. 이 작업은 원자적이지 않아 드물게 경쟁 상태(race condition)를 유발할 수 있습니다.
더 안전하고 효율적인 방법은 BestSellerRepository에 @Modifying 어노테이션을 사용한 업데이트 쿼리 메서드를 추가하여 DB에서 직접 값을 증가시키는 것입니다. 이렇게 하면 단일 DB 호출로 작업을 완료할 수 있습니다.
BestSellerRepository.java에 추가할 내용:
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.repository.query.Param;
// ...
@Modifying
@Query("UPDATE BestSeller b SET b.retryCount = b.retryCount + 1 WHERE b.id = :id")
void incrementRetryCount(@Param("id") Long id);위 메서드를 추가한 후 handleFailure를 다음과 같이 수정하는 것을 권장합니다.
private void handleFailure(Long bestSellerId) {
try {
bestSellerRepository.incrementRetryCount(bestSellerId);
} catch (Exception ex) {
log.error("실패 카운트 증가 중 오류 발생: bestSellerId={}", bestSellerId, ex);
}
}
Test Results101 files 101 suites 18s ⏱️ Results for commit 9869ae9. |
🌻 테스트 커버리지 리포트
|
배경
해결 목표
핵심 개선점
1. 베스트셀러 퀴즈 생성 스케줄러 구현
[Feat]
BestsellerScheduler
:
수집: 매주 월요일 새벽 2시에 알라딘 베스트셀러(1~20페이지, 약 1000권)를 수집하여 DB에 저장합니다.
생성: 매주 월요일 오전 3시 ~ 오후 11시 사이에 매 시각 정각마다 수집된 베스트셀러에 대한 퀴즈 생성을 요청합니다.
[Refactor] QuizSaveService: 엔티티 대신 ID를 파라미터로 받도록 리팩토링하여 Detached Entity 문제를 방지했습니다.
사용자 경험 개선: 베스트셀러 도서에 대해 미리 퀴즈를 생성해 둠으로써, 사용자가 챌린지를 생성할 때 약 1분의 대기 시간 없이 즉시 이용할 수 있게 됩니다.
부하 분산: 퀴즈 생성 요청을 월요일 하루 동안 분산시켜 Gemini API 및 서버 부하를 조절합니다.
2. 챌린지 진행 상태 중복 생성 방지 및 처리
[Fix] ReadingProgressService.addProgress: findFirstBy...를 사용하여 이미 진행 상태가 존재할 경우 새로 생성하지 않고 기존 ID를 반환하도록 수정했습니다 (멱등성 보장).
[Fix] ReadingProgressService.completeChapter: 챕터 완료 처리 시 중복된 진행 상태가 발견되면, 하나만 남기고 나머지는 삭제(Deduplication)하도록 로직을 개선했습니다.
[Fix]
V2ChallengeResponse
: toMap 수집 시 중복 키가 있어도 에러가 나지 않도록 Merge Function을 추가했습니다.
3. 비동기 메서드 테스트 수정
[Test]
BestsellerServiceTest
: @async가 적용된
QuizGenerationAsyncService
의 Mocking 실패 문제를 해결하기 위해 AopTestUtils.getUltimateTargetObject()를 사용하여 프록시를 벗겨낸 후 Stubbing 하도록 수정했습니다.
@async 프록시로 인해 Mockito의 ThreadLocal Matcher가 인식되지 않는 근본 원인을 해결하기 위해, 프록시 타겟을 직접 꺼내서 Stubbing 하는 방식을 선택했습니다.