Skip to content

Conversation

@jeongmallro
Copy link
Contributor

⭐ Summary

#184


📌 Tasks

  1. BmScrap, SpLike 동시성

    • 테이블에 Unique 제약 조건 추가: BmScrap의 경우 {bm_id, member_id}, SpLike의 경우 {sp_id, member_id}
    • 같은 스레드에서 여러 번 호출하여 동시성 문제 발생 시 Unique 제약 조건 위반 DataIntegrityViolationException 발생, 그대로 API 응답으로 반환
  2. Sp views (조회수) 동시성

    • 캐시에 먼저 데이터 저장해놓고 추후에 DB로 업데이트 하는 write-back 전략 선택
    • 플로우: 사용자 Sp 조회 -> Redis에 해당 Sp 조회수 +1 증가 -> 1분마다 Redis에서 DB로 Sp 조회수 업데이트 스케줄링 작업 수행 (Redis에서 Sp 조회수 데이터 조회 후 Redis에서 해당 데이터 삭제)
    • Redis에서 Sp 조회수 리스트 데이터 읽고 삭제하는 작업 중간에 사용자가 Sp를 조회하면 데이터 정합성이 깨지는 문제가 발생 -> Lua script를 통해 데이터를 읽고 삭제하는 작업을 원자성있게 하여 해결
  3. Sp views Long 타입으로 변경하였습니다.

  4. 동시성 테스트 코드 작성


@jeongmallro jeongmallro requested a review from lsh2613 July 15, 2025 04:38
@jeongmallro jeongmallro self-assigned this Jul 15, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jul 15, 2025

📝 Walkthrough

"""

Walkthrough

이 변경사항은 BM 스크랩 및 SP 좋아요 엔티티에 대한 유니크 제약 추가, SP 조회수 집계 및 업데이트를 위한 Redis 및 Lua 스크립트 도입, SP 조회수 관련 서비스 및 저장소 클래스 신설, 조회수 타입을 Long으로 확장, 예외 처리 및 테스트 코드 추가를 포함합니다.

Changes

파일/경로 요약 변경 내용 요약
build.gradle Awaitility 4.3.0 테스트 의존성 추가
src/main/java/com/pitchain/bmscrap/domain/BmScrap.java,
src/main/java/com/pitchain/splike/domain/SpLike.java
member_idbm_id/sp_id 조합에 대한 유니크 제약 추가
src/main/java/com/pitchain/common/config/RedisConfig.java Lua 스크립트 로딩용 RedisScript 빈 추가
src/main/java/com/pitchain/common/exception/GlobalExceptionHandler.java DataIntegrityViolationException 핸들러 및 @Slf4j 추가
src/main/java/com/pitchain/common/redis/RedisHashRepository.java Redis 해시 조작용 저장소 클래스 신설: 증가, 전체 조회, Lua 스크립트 실행 메서드 포함
src/main/java/com/pitchain/sp/application/SpService.java SpViewsService 의존성 주입 및 SP 상세 조회 시 조회수 증가 로직 추가, 트랜잭션 어노테이션 제거
src/main/java/com/pitchain/sp/application/SpViewsService.java 조회수 Redis 적재, DB 반영, 스케줄러 및 파싱 로직 포함 서비스 클래스 신설
src/main/java/com/pitchain/sp/application/res/SpDetailRes.java,
src/main/java/com/pitchain/sp/domain/Sp.java
조회수 필드 타입 Integer/int → Long으로 변경
src/main/java/com/pitchain/sp/infrastucture/SpRepositoryCustom.java SP의 조회수 증가용 updateSpView(Long spId, Long views) 메서드 추가
src/main/java/com/pitchain/sp/infrastucture/dto/SpViewsDto.java SP ID와 조회수(Long)를 담는 레코드 추가
src/main/resources/META-INF/scripts/getanddeleteall.lua Redis 해시 전체 조회 후 삭제하는 Lua 스크립트 추가
src/test/java/com/pitchain/service/SpViewsServiceTest.java SP 조회수 Redis 적재 및 DB 반영 동시성/스케줄링 테스트 클래스 신설
src/main/java/com/pitchain/common/config/AsyncConfig.java 비동기 처리용 스레드풀 및 MDC 전파, 예외 핸들러 설정 클래스 추가

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SpService
    participant SpViewsService
    participant RedisHashRepository
    participant Redis
    participant DB

    Client->>SpService: getSpDetail(memberDetails, bmId, spId)
    SpService->>SpViewsService: updateSpView(spId)
    SpViewsService->>RedisHashRepository: increment("spViews", spId, 1)
    RedisHashRepository->>Redis: HINCRBY "spViews" spId 1
    SpService->>DB: getSpDetail(...)
    DB-->>SpService: SpDetailRes
    SpService-->>Client: SpDetailRes

    Note over SpViewsService,DB: (스케줄러)
    SpViewsService->>RedisHashRepository: getAndDeleteAll("spViews")
    RedisHashRepository->>Redis: EVAL getanddeleteall.lua
    Redis-->>RedisHashRepository: [spId1, views1, spId2, views2, ...]
    RedisHashRepository-->>SpViewsService: List<String>
    SpViewsService->>DB: updateSpView(spId, views) (여러 건)
Loading

Possibly related PRs

  • Young-Flow/server#146: BM 스크랩 엔티티의 유니크 제약 추가 및 관련 기능 분리/리네이밍 등, 본 PR과 BmScrap 엔티티 및 유니크 제약 적용에서 코드 레벨로 직접적으로 연관됨.
    """

📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be58d45 and 6e27397.

📒 Files selected for processing (1)
  • src/test/java/com/pitchain/service/SpViewsServiceTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/java/com/pitchain/service/SpViewsServiceTest.java
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/main/java/com/pitchain/sp/application/SpViewsService.java (1)

49-56: 스케줄링 작업의 안정성 개선 제안

예외 처리는 잘 되어 있지만, 장시간 실행되는 작업의 경우 중복 실행을 방지하는 메커니즘을 고려해볼 수 있습니다.

-@Scheduled(cron = "0 */1 * * * *")
+@Scheduled(cron = "0 */1 * * * *")
+@SchedulerLock(name = "runUpdateSpViews", lockAtMostFor = "50s", lockAtLeastFor = "10s")
 public void runUpdateSpViews() {

ShedLock 라이브러리를 사용하여 중복 실행을 방지할 수 있습니다.

src/test/java/com/pitchain/service/SpViewsServiceTest.java (1)

160-166: ExecutorService 종료 처리 개선 필요

ExecutorService의 안전한 종료를 위해 awaitTermination을 추가하는 것을 권장합니다.

 latch.await();  //updateSpView 모든 스레드 작업 끝날 때까지 대기
 executorService.shutdown();
+executorService.awaitTermination(10, TimeUnit.SECONDS);

 isRunning.set(false);  //updateSpViews 종료 유도

 updateExecutorService.shutdown();
+updateExecutorService.awaitTermination(10, TimeUnit.SECONDS);
 updateFuture.get();  //updateSpViews 완전히 종료될 때까지 대기
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe4954e and ba634f5.

📒 Files selected for processing (14)
  • build.gradle (1 hunks)
  • src/main/java/com/pitchain/bmscrap/domain/BmScrap.java (1 hunks)
  • src/main/java/com/pitchain/common/config/RedisConfig.java (2 hunks)
  • src/main/java/com/pitchain/common/exception/GlobalExceptionHandler.java (2 hunks)
  • src/main/java/com/pitchain/common/redis/RedisHashRepository.java (1 hunks)
  • src/main/java/com/pitchain/sp/application/SpService.java (2 hunks)
  • src/main/java/com/pitchain/sp/application/SpViewsService.java (1 hunks)
  • src/main/java/com/pitchain/sp/application/res/SpDetailRes.java (1 hunks)
  • src/main/java/com/pitchain/sp/domain/Sp.java (2 hunks)
  • src/main/java/com/pitchain/sp/infrastucture/SpRepositoryCustom.java (2 hunks)
  • src/main/java/com/pitchain/sp/infrastucture/dto/SpViewsDto.java (1 hunks)
  • src/main/java/com/pitchain/splike/domain/SpLike.java (1 hunks)
  • src/main/resources/META-INF/scripts/getanddeleteall.lua (1 hunks)
  • src/test/java/com/pitchain/service/SpViewsServiceTest.java (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/main/java/com/pitchain/sp/application/SpViewsService.java (2)
src/main/java/com/pitchain/sp/infrastucture/SpRepositoryCustom.java (1)
  • RequiredArgsConstructor (18-59)
src/main/java/com/pitchain/sp/application/SpService.java (1)
  • RequiredArgsConstructor (16-67)
🔇 Additional comments (19)
src/main/java/com/pitchain/bmscrap/domain/BmScrap.java (1)

13-13: 동시성 문제 해결을 위한 적절한 유니크 제약조건 추가

member_idbm_id 조합에 대한 유니크 제약조건이 올바르게 추가되었습니다. 이는 같은 회원이 같은 BM을 중복으로 스크랩하는 것을 데이터베이스 레벨에서 방지하여 동시성 이슈를 효과적으로 해결합니다.

src/main/java/com/pitchain/common/exception/GlobalExceptionHandler.java (3)

9-9: 로깅을 위한 @slf4j 어노테이션 추가

로깅 기능을 위해 @slf4j 어노테이션이 적절하게 추가되었습니다.

Also applies to: 19-19


11-11: DataIntegrityViolationException import 추가

새로운 예외 핸들러를 위한 필요한 import가 추가되었습니다.


79-90: 데이터 무결성 위반 예외에 대한 적절한 처리

DataIntegrityViolationException 핸들러가 올바르게 구현되었습니다. 특히 다음 사항들이 우수합니다:

  • log.error()를 사용한 구조화된 로깅 (다른 핸들러의 printStackTrace()보다 개선됨)
  • 적절한 HTTP 400 상태코드 반환
  • 유니크 제약조건 위반 시 일관된 에러 응답 제공

이는 새로 추가된 BmScrap과 SpLike의 유니크 제약조건 위반을 처리하는데 적합합니다.

src/main/java/com/pitchain/splike/domain/SpLike.java (1)

13-13: 동시성 문제 해결을 위한 적절한 유니크 제약조건 추가

member_idsp_id 조합에 대한 유니크 제약조건이 올바르게 추가되었습니다. 이는 같은 회원이 같은 SP를 중복으로 좋아요하는 것을 데이터베이스 레벨에서 방지하여 동시성 이슈를 효과적으로 해결합니다. BmScrap과 일관된 패턴으로 구현되어 있어 유지보수성도 좋습니다.

build.gradle (1)

58-59: 동시성 테스트를 위한 Awaitility 의존성 추가 승인

Awaitility 라이브러리는 동시성 및 스케줄링 테스트에 적합한 선택입니다. 새로 도입된 SpViewsService의 동시성 테스트에 유용할 것입니다.

src/main/java/com/pitchain/sp/domain/Sp.java (2)

31-31: 조회수 필드의 Long 타입 변경 승인

조회수가 많은 SP의 경우 int 범위를 초과할 수 있으므로 Long 타입으로 변경하는 것이 적절합니다. 이는 새로운 Redis 기반 조회수 관리 전략과 일치합니다.


45-45: 초기화 값의 일관성 유지 확인

Long 타입 변경에 맞춰 초기화 값을 0L로 변경한 것이 타입 일관성을 잘 유지합니다.

src/main/java/com/pitchain/sp/application/res/SpDetailRes.java (1)

34-34: 응답 DTO의 타입 일관성 유지 승인

Sp 엔티티의 views 필드가 Long으로 변경됨에 따라 응답 DTO도 일치하도록 변경한 것이 올바릅니다. 계층 간 타입 일관성을 잘 유지합니다.

src/main/java/com/pitchain/sp/infrastucture/dto/SpViewsDto.java (1)

3-3: 간단하고 적절한 DTO 설계 승인

spId와 views 필드를 Long 타입으로 정의한 불변 레코드 설계가 깔끔합니다. Redis 데이터와 데이터베이스 업데이트 간의 데이터 전달에 적합합니다.

src/main/resources/META-INF/scripts/getanddeleteall.lua (1)

1-3: 원자적 읽기-삭제 연산을 위한 Lua 스크립트 승인

Redis 해시에서 모든 데이터를 읽고 즉시 삭제하는 원자적 연산을 제공합니다. PR 목표에서 언급한 "사용자가 Sp를 조회하는 중에 Redis 읽기-삭제 작업으로 인한 데이터 불일치 방지"를 위한 핵심 구현입니다.

스크립트 로직:

  1. HGETALL로 모든 필드와 값 조회
  2. DEL로 키 삭제
  3. 조회된 데이터 반환

간단하면서도 동시성 문제를 효과적으로 해결하는 구현입니다.

src/main/java/com/pitchain/common/config/RedisConfig.java (1)

56-62: Lua 스크립트 빈 설정이 올바르게 구현되었습니다

Redis 스크립트를 Spring 빈으로 등록하는 방식이 적절하며, 원자성이 보장되는 getAndDeleteAll 연산을 위한 설정이 잘 되어 있습니다.

src/main/java/com/pitchain/sp/infrastucture/SpRepositoryCustom.java (1)

51-58: 조회수 업데이트 메서드가 올바르게 구현되었습니다

QueryDSL을 사용한 증분 업데이트 로직이 정확하며, 트랜잭션 처리도 적절합니다.

src/main/java/com/pitchain/sp/application/SpService.java (2)

21-21: SpViewsService 의존성 추가가 적절합니다

조회수 관리를 위한 새로운 서비스 의존성이 올바르게 추가되었습니다.


38-41: readOnly 트랜잭션 어노테이션 제거에 대한 확인 필요

spViewsService.updateSpView(spId) 호출로 인해 @Transactional(readOnly = true) 어노테이션이 제거되었는데, Redis 업데이트는 별도의 연산이므로 이 메서드는 여전히 readOnly로 유지할 수 있을 것 같습니다. 제거한 특별한 이유가 있나요?

src/main/java/com/pitchain/common/redis/RedisHashRepository.java (1)

13-33: Redis 해시 연산 래퍼 클래스가 잘 구현되었습니다

Redis 해시 연산을 캡슐화한 깔끔한 구현입니다. Lua 스크립트를 통한 원자성 보장이 적절하며, 메서드명도 직관적입니다.

src/main/java/com/pitchain/sp/application/SpViewsService.java (2)

29-31: Redis 조회수 증분 로직이 올바르게 구현되었습니다

Redis 해시에 조회수를 증분하는 로직이 간단명료하게 구현되어 있습니다.


36-44: DB 업데이트 로직이 적절하게 구현되었습니다

Redis에서 원자적으로 데이터를 가져와 DB에 업데이트하는 로직이 잘 구현되어 있으며, 트랜잭션 처리도 적절합니다.

src/test/java/com/pitchain/service/SpViewsServiceTest.java (1)

75-100: @transactional 추가 불필요
SpViewsServiceTest.javaSP_조회_동시성 테스트 메서드에는 이미

@Transactional(propagation = Propagation.NEVER)

이 선언되어 있어 의도된 트랜잭션 비활성화가 적용되고 있습니다. 별도의 @Transactional 추가는 필요하지 않습니다.

Likely an incorrect or invalid review comment.

@@ -0,0 +1,4 @@
package com.pitchain.sp.infrastucture.dto;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

패키지명 오타 확인 요청

패키지명에 "infrastucture"가 "infrastructure"의 오타로 보입니다. 프로젝트 전체에서 일관된 명명 규칙을 사용하고 있는지 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# 프로젝트 내 infrastructure/infrastucture 패키지명 사용 현황 확인
fd -t d "infrastucture"
fd -t d "infrastructure"

Length of output: 771


패키지명 철자 통일 필요

프로젝트에 com.pitchain.* 아래에 "infrastucture"(오타)와 "infrastructure"(올바름)가 혼용되어 있어, 모든 패키지를 "infrastructure"로 일관되게 리팩토링해야 합니다. IDE 리팩터링 기능 또는 스크립트를 활용해 디렉토리명과 package 선언, import 구문을 일괄 수정해 주세요.

영향 받는 오타 패키지 경로:

  • src/main/java/com/pitchain/bm/infrastucture/
  • src/main/java/com/pitchain/bmscrap/infrastucture/
  • src/main/java/com/pitchain/comment/infrastucture/
  • src/main/java/com/pitchain/investment/infrastucture/
  • src/main/java/com/pitchain/member/infrastucture/
  • src/main/java/com/pitchain/mysphistory/infrastucture/
  • src/main/java/com/pitchain/notificationhistory/infrastucture/
  • src/main/java/com/pitchain/sp/infrastucture/
  • src/main/java/com/pitchain/splike/infrastucture/
🤖 Prompt for AI Agents
In src/main/java/com/pitchain/sp/infrastucture/dto/SpViewsDto.java at line 1,
the package name "infrastucture" is misspelled and should be corrected to
"infrastructure". Refactor all occurrences of the misspelled package name in the
project, including directory names, package declarations, and import statements,
to use the correct spelling "infrastructure". Use your IDE's refactoring tools
or scripts to perform this change consistently across all affected packages
listed.

Comment on lines +63 to +79
private List<SpViewsDto> parseResult(List<String> spViewsStringList) {
if (spViewsStringList.size() % 2 != 0){
log.error("spViewsStringList 개수가 올바르지 않습니다.");
throw new IllegalArgumentException("spViewsStringList 개수가 올바르지 않습니다.");
}

List<SpViewsDto> spViewsDtoList = new ArrayList<>();
for (int i = 0; i < spViewsStringList.size(); i += 2) {
Long spId = Long.parseLong(spViewsStringList.get(i));
Long views = Long.parseLong(spViewsStringList.get(i + 1));

SpViewsDto spViewsDto = new SpViewsDto(spId, views);
spViewsDtoList.add(spViewsDto);
}

return spViewsDtoList;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

파싱 로직에 예외 처리 개선 필요

기본적인 검증은 있지만 Long.parseLong() 실행 시 발생할 수 있는 NumberFormatException에 대한 처리가 필요합니다.

 private List<SpViewsDto> parseResult(List<String> spViewsStringList) {
     if (spViewsStringList.size() % 2 != 0){
         log.error("spViewsStringList 개수가 올바르지 않습니다.");
         throw new IllegalArgumentException("spViewsStringList 개수가 올바르지 않습니다.");
     }

     List<SpViewsDto> spViewsDtoList = new ArrayList<>();
     for (int i = 0; i < spViewsStringList.size(); i += 2) {
-        Long spId = Long.parseLong(spViewsStringList.get(i));
-        Long views = Long.parseLong(spViewsStringList.get(i + 1));
+        try {
+            Long spId = Long.parseLong(spViewsStringList.get(i));
+            Long views = Long.parseLong(spViewsStringList.get(i + 1));
+            
+            SpViewsDto spViewsDto = new SpViewsDto(spId, views);
+            spViewsDtoList.add(spViewsDto);
+        } catch (NumberFormatException e) {
+            log.error("Invalid number format in Redis data: spId={}, views={}", 
+                     spViewsStringList.get(i), spViewsStringList.get(i + 1), e);
+            // 잘못된 데이터는 건너뛰고 계속 처리
+        }

-        SpViewsDto spViewsDto = new SpViewsDto(spId, views);
-        spViewsDtoList.add(spViewsDto);
     }

     return spViewsDtoList;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private List<SpViewsDto> parseResult(List<String> spViewsStringList) {
if (spViewsStringList.size() % 2 != 0){
log.error("spViewsStringList 개수가 올바르지 않습니다.");
throw new IllegalArgumentException("spViewsStringList 개수가 올바르지 않습니다.");
}
List<SpViewsDto> spViewsDtoList = new ArrayList<>();
for (int i = 0; i < spViewsStringList.size(); i += 2) {
Long spId = Long.parseLong(spViewsStringList.get(i));
Long views = Long.parseLong(spViewsStringList.get(i + 1));
SpViewsDto spViewsDto = new SpViewsDto(spId, views);
spViewsDtoList.add(spViewsDto);
}
return spViewsDtoList;
}
private List<SpViewsDto> parseResult(List<String> spViewsStringList) {
if (spViewsStringList.size() % 2 != 0){
log.error("spViewsStringList 개수가 올바르지 않습니다.");
throw new IllegalArgumentException("spViewsStringList 개수가 올바르지 않습니다.");
}
List<SpViewsDto> spViewsDtoList = new ArrayList<>();
for (int i = 0; i < spViewsStringList.size(); i += 2) {
try {
Long spId = Long.parseLong(spViewsStringList.get(i));
Long views = Long.parseLong(spViewsStringList.get(i + 1));
SpViewsDto spViewsDto = new SpViewsDto(spId, views);
spViewsDtoList.add(spViewsDto);
} catch (NumberFormatException e) {
log.error(
"Invalid number format in Redis data: spId={}, views={}",
spViewsStringList.get(i),
spViewsStringList.get(i + 1),
e
);
// 잘못된 데이터는 건너뛰고 계속 처리
}
}
return spViewsDtoList;
}
🤖 Prompt for AI Agents
In src/main/java/com/pitchain/sp/application/SpViewsService.java around lines 63
to 79, the parseResult method lacks handling for NumberFormatException that can
be thrown by Long.parseLong. Add try-catch blocks around the parsing statements
to catch NumberFormatException, log an appropriate error message, and throw a
meaningful exception to handle invalid number formats gracefully.

//조회수 DB에 업데이트
while (isRunning.get()) {
spViewsService.updateSpViews();
Thread.sleep(60000);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

테스트 실행 시간이 너무 깁니다

Thread.sleep(60000)은 60초 대기를 의미하며, 이는 단위 테스트에 적합하지 않습니다. CI/CD 파이프라인 속도를 고려하여 더 짧은 시간(예: 100ms)으로 변경하는 것을 권장합니다.

-                    Thread.sleep(60000);
+                    Thread.sleep(100);  // 60초 -> 100ms로 단축
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Thread.sleep(60000);
Thread.sleep(100); // 60초 -> 100ms로 단축
🤖 Prompt for AI Agents
In src/test/java/com/pitchain/service/SpViewsServiceTest.java at line 149, the
test uses Thread.sleep(60000), causing a 60-second delay which is too long for
unit tests. Reduce the sleep duration to a shorter time like 100 milliseconds to
speed up test execution and improve CI/CD pipeline efficiency.


//then
Awaitility.await()
.atMost(Durations.ONE_MINUTE)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

스케줄링 테스트의 대기 시간 최적화 필요

Durations.ONE_MINUTE는 테스트 실행 시간을 길게 만듭니다. 테스트 프로파일에서 스케줄링 주기를 더 짧게 설정하고, 대기 시간도 그에 맞춰 조정하는 것을 고려해보세요.


🏁 Script executed:

#!/bin/bash
# SpViewsService의 스케줄링 주기 설정을 확인합니다.
ast-grep --pattern '@Scheduled($$$)' src/main/java/com/pitchain/sp/application/SpViewsService.java

# 테스트용 프로퍼티 파일에서 스케줄링 관련 설정을 확인합니다.
fd -e properties -e yml -e yaml --exec grep -l "schedule\|cron" {} \; | xargs grep -A 2 -B 2 "schedule\|cron" | grep -v "^--$"

Length of output: 336


스케줄링 주기 외부 속성화 및 테스트 대기 시간 단축

  • src/main/java/com/pitchain/sp/application/SpViewsService.java (49행)
    @Scheduled(cron = "0 */1 * * * *")
    @Scheduled(cron = "${sp.views.cron:0 */1 * * * * *}") 형태로 변경
  • src/main/resources/application-test.yml 또는 application-test.properties
    테스트 프로필에
    sp.views.cron: "0/1 * * * * *" (1초 단위) 로 오버라이드
  • src/test/java/com/pitchain/service/SpViewsServiceTest.java (186행)
    .atMost(Durations.ONE_MINUTE)
    .atMost(Duration.ofSeconds(5)) 등 실제 스케줄 주기에 맞춰 단축

위 리팩토링을 통해 기본 운영 환경에선 1분 단위를 유지하면서, 테스트 환경에서만 짧은 주기로 스케줄을 실행하고 대기 시간을 최적화할 수 있습니다.

🤖 Prompt for AI Agents
In src/test/java/com/pitchain/service/SpViewsServiceTest.java at line 186, the
test wait time is set to atMost(Durations.ONE_MINUTE), which is too long for the
optimized test schedule. Change this to a shorter duration like
atMost(Duration.ofSeconds(5)) to match the faster scheduling interval configured
for tests. Also ensure the scheduling cron expression in
src/main/java/com/pitchain/sp/application/SpViewsService.java is externalized to
a property with a default, and override this property in the test profile to a
1-second interval to enable quicker test execution.

@lsh2613
Copy link
Contributor

lsh2613 commented Jul 15, 2025

궁금한 점 몇 개만 질문드리겠습니다

  • 'Redis에서 Sp 조회수 리스트 데이터 읽고 삭제하는 작업 중간에 사용자가 Sp를 조회하면 데이터 정합성이 깨지는 문제가 발생'
    • 데이터 정합성이 깨지는 이유가 동시성 문제가 아닌 sp view를 redis에 저장해서 생기는 문제 아닌가요?
  • 동시성 문제를 Lock이 아닌 lua로 처리한 이유가 따로 있나요? lua 스크립트를 처음 들어봐서 특별한 차이점이 존재하는지 궁금하네요

@jeongmallro
Copy link
Contributor Author

  • 'Redis에서 Sp 조회수 리스트 데이터 읽고 삭제하는 작업 중간에 사용자가 Sp를 조회하면 데이터 정합성이 깨지는 문제가 발생'
    • 데이터 정합성이 깨지는 이유가 동시성 문제가 아닌 sp view를 redis에 저장해서 생기는 문제 아닌가요?

사용자가 Sp를 조회할 때 조회수 데이터를 증가하는 것과, Redis에서 Sp 조회수 데이터를 읽고 삭제하는 게 같은 조회수 데이터를 대상으로 동시에 변경 작업을 한다는 점에서 동시성 문제라고 볼 수 있지 않을까요?

  • 동시성 문제를 Lock이 아닌 lua로 처리한 이유가 따로 있나요? lua 스크립트를 처음 들어봐서 특별한 차이점이 존재하는지 궁금하네요

Redis의 Hash increment 명령어는 원자적으로 처리돼서 동시성 이슈를 고려할 필요가 없더라고요. 그러면 Redis에서 값을 가져오고 삭제하는 부분만 원자적으로 수행하면 Lock을 걸지 않아도 되니까 Lua 스크립트를 통해 원자성을 보장하는 방법을 선택했습니다. 어차피 스크립트 부분이 그렇게 길지도 않으니 이 방법을 선택하는 게 효율적일 거라고 생각했습니다. 호출 수가 늘어나면 Lock 설정도 신경을 써줘야 하니까요

@lsh2613
Copy link
Contributor

lsh2613 commented Jul 15, 2025

그렇다면 Lua script를 적용한 이유는 데이터의 정합성이 아닌, 동시성 문제를 해결하기 위한 방법이신 거죠?
pr에는 데이터의 정합성 문제를 해결하기 위해 적용하셨다고 하셔서요

@jeongmallro
Copy link
Contributor Author

근데 데이터 정합성 문제가 곧 동시성 문제 아닌가요? 두 개 이상의 스레드가 공유 자원에 동시에 접근해서 발생할 수 있는 문제 = 데이터 정합성이 깨짐 이라고 생각했습니다!

@lsh2613
Copy link
Contributor

lsh2613 commented Jul 15, 2025

제가 알고 있는 지식이 틀릴 수 있습니다.

용어 정리

  • 데이터 무결성 - 데이터의 정확성, 완정성, 일관성, 유효성 등 큰 범위에서 전반적인 데이터의 신뢰성을 모두 포함
  • 데이터 정합성 - 데이터가 논리적으로 모순되지 않은 일관된 상태인가, 무결성의 하위 개념
  • 동시성 이슈 - 데이터 무결성 문제를 야기할 수 있는 문제

따라서 동시성 이슈는 데이터 정합성 문제를 야기할 수 있는 것이지, 정확히 같다고 보진 않습니다. (개인적인 의견, 틀릴 수 있음)

현재 상황의 동시성 및 정합성 내용 정리

처음에 작성해주신 조회수 동시성 이슈부터 lua 도입까지의 과정을 다시 정리할 필요가 있어보입니다.

PR 내용 - Redis에서 Sp 조회수 리스트 데이터 읽고 삭제하는 작업 중간에 사용자가 Sp를 조회하면 데이터 정합성이 깨지는 문제가 발생

저는 여기서 sp조회와 정합성 키워드로 인해 Redis의 spView읽기삭제-spView조회 작업 간의 데이터 정합성이 깨진다라고 이해했습니다.
사실 이 부분은 레디스가 싱글스레드로 명령어가 원자적으로 처리되기 때문에, 삭제-조회가 같이 일어나지 않아, 동시성 및 정합성 문제가 발생하지 않을 것으로 보입니다.

좀전에 남겨주신 코맨트를 확인하니 sp 조회란 유저의 sp 조회 로직에 포함된 redis에 spView 증가를 의미하시는 것 같습니다.
따라서 spVeiw읽기삭제-spView증가 작업으로 살펴보면, 읽기-증가-삭제 순으로 명령어가 실행될 때 동시성 문제를 야기할 수 있을 것 같아요.
다만, 증가 명령어에 대한 데이터 손실이 논리적으로 모순된 상태의 정합성이 깨졌다라고 보기에는 다소 애매한 부분이 있어 보입니다.
오히려 데이터 완전성이 깨졌다고 보는 것이 좀 더 명확하지 않을까 싶네요

@jeongmallro
Copy link
Contributor Author

아 그러네요 표현에 대한 이해 차이가 있었습니다.

PR 내용 - Redis에서 Sp 조회수 리스트 데이터 읽고 삭제하는 작업 중간에 사용자가 Sp를 조회하면 데이터 정합성이 깨지는 문제가 발생

사용자가 Sp를 조회한다고 말씀드린 게 유저의 sp 조회 로직에 포함된 redis에 spView 증가를 의미하는 게 맞습니다.
그래서 제가 계속 언급한 동시성 문제는 말씀하신 읽기-증가-삭제 순으로 명령어가 실행될 때 발생할 수 있는 동시성 문제가 맞습니다.

'사용자' 키워드를 그래서 넣은 거였는데 '사용자'도 그렇고 '조회'도 그렇고 좀 모호한 의미로 전달될 수 있겠네요ㅋㅋ
좀 더 명확하게 Redis 관점에서 spView 증가라고 말씀드렸으면 오해의 소지가 없었겠어요

데이터 정합성은 Redis에 저장된 조회수 데이터 값과 MySQL에 저장된 조회수 데이터 값의 불일치라고 생각해서 그렇게 표현했습니다.

아래 작업을 Lua Script로 처리 하지 않는다(조회와 삭제가 원자적으로 수행되지 않음)면

local data = redis.call('HGETALL', KEYS[1])   //Redis에서 데이터 읽음 -> MySQL에 업데이트되는 값  --(1)

//다른 스레드에서 Sp 조회수 증가 -> 현재 Redis에는 증가된 Sp 조회수 값  --(2) 

redis.call('DEL', KEYS[1])   //Redis에서 (2) 데이터 삭제  --(3) 
return data

(1)과 (2)의 불일치로 데이터 정합성이 깨진다고 생각했습니다.
승헌 님은 (3)에서 손실되는, 중간에 증가된 Sp 조회수 값에 대한 관점으로 데이터 무결성이 깨진다고 보신 것이죠?

데이터 무결성(영어: data integrity)은 데이터가 검색, 처리, 저장되는 모든 과정에서 변경되거나 손상되지 않도록 보장하는 특성이다.

승헌 님 말씀이 어느 맥락인지 이해가 가요. 데이터 정합성이라는 개념이 RDBMS로 생각하면 잘 와닿는데, 캐시로 생각을 하니까 좀 머리가 아프네요 ㅋㅋ 바로 Redis 데이터 삭제가 이루어지기 때문에 MySQL에 있는 조회수 값과 Redis에 있는 조회수 값이 동시에 존재하지 않는다는 점에서도 좀 헷갈리네유..

@lsh2613
Copy link
Contributor

lsh2613 commented Jul 16, 2025

맞습니다. 어떻게 보느냐에 따라서 정합성 이슈라고 볼 수도 있을 것 같아서 애매하다고 표현했습니다 ㅋㅋㅋ

사실 프로젝트 및 운영 초기 단계의 조회수의 크기를 생각해보면 바로 mysql에 반영해줘도 무방하다고 생각합니다.
다만, 유튜브 처럼 조회수처럼 값의 크기가 커서 천/만 단위로 표시를 해야 한다면 스케쥴러를 통한 처리 방법이 충분히 성능 향상 이점을 가져올 수 있을 것 같아요!

추가적으로 Redis에 읽기삭제 작업을 원자적 명령으로 인해 다른 스레드의 Redis 명령이 블로킹된다면, SP 조회 시 Redis-SpView를 증가하는 로직을 비동기로 처리하는 게 좋지 않을까 싶네요

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/main/java/com/pitchain/common/config/AsyncConfig.java (1)

36-44: AsyncConfigurer 구현을 최적화할 수 있습니다.

getAsyncExecutor() 메서드에서 빈을 직접 주입받아 사용하는 것이 더 효율적입니다.

+    private final Executor asyncTaskExecutor;
+    
+    public AsyncConfig() {
+        this.asyncTaskExecutor = asyncTaskExecutor();
+    }
+
     @Override
     public Executor getAsyncExecutor() {
-        return asyncTaskExecutor();
+        return asyncTaskExecutor;
     }

또는 @Autowired를 사용하여 빈을 주입받을 수 있습니다.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bff600 and be58d45.

📒 Files selected for processing (3)
  • src/main/java/com/pitchain/common/config/AsyncConfig.java (1 hunks)
  • src/main/java/com/pitchain/sp/application/SpService.java (2 hunks)
  • src/main/java/com/pitchain/sp/application/SpViewsService.java (1 hunks)
🧬 Code Graph Analysis (1)
src/main/java/com/pitchain/common/config/AsyncConfig.java (1)
src/main/java/com/pitchain/sp/application/SpViewsService.java (1)
  • Slf4j (16-82)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/pitchain/sp/application/SpService.java
  • src/main/java/com/pitchain/sp/application/SpViewsService.java
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/main/java/com/pitchain/common/config/AsyncConfig.java (1)
src/main/java/com/pitchain/sp/application/SpViewsService.java (1)
  • Slf4j (16-82)
🔇 Additional comments (3)
src/main/java/com/pitchain/common/config/AsyncConfig.java (3)

17-19: 비동기 설정 기본 구성이 올바릅니다.

Spring의 비동기 실행을 위한 기본적인 어노테이션과 인터페이스 구현이 적절하게 되어 있습니다.


46-53: 예외 처리기 구현이 적절합니다.

비동기 실행 중 발생하는 예외를 적절히 로깅하고 있으며, 한국어 메시지도 일관성이 있습니다.


55-71: MDC 컨텍스트 전파 구현이 우수합니다.

비동기 스레드에서 로깅 컨텍스트를 유지하기 위한 구현이 모범 사례를 잘 따르고 있습니다:

  • 현재 스레드의 MDC 컨텍스트를 적절히 캡처
  • 비동기 스레드에서 컨텍스트를 복원
  • finally 블록에서 정리 작업 수행
  • null 체크로 안전성 확보

이는 Redis 조회수 업데이트와 같은 비동기 작업에서 로그 추적성을 유지하는 데 중요합니다.

Comment on lines +21 to +34
@Bean
public Executor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//TODO - corePoolSize, maxPoolSize, queueCapacity는 서비스에 맞게 테스트를 통해 설정
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors());
// executor.setQueueCapacity();
executor.setTaskDecorator(new MdcTaskDecorator());
executor.setThreadNamePrefix("async");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.initialize();

return executor;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Redis 조회수 업데이트 용도로 스레드 풀 설정을 검토해주세요.

현재 설정은 SpViewsService@Async 메서드를 지원하기 위한 것으로 보입니다. Redis I/O 작업의 특성을 고려하면:

  1. 처리량 최적화: Redis 호출은 I/O 대기가 발생하므로 CPU 코어 수보다 많은 스레드가 도움이 될 수 있습니다
  2. 메모리 관리: 큐 용량 제한으로 과도한 메모리 사용을 방지해야 합니다
  3. 성능 테스트: TODO 주석에서 언급한 대로 실제 부하 테스트를 통한 최적화가 필요합니다

다음 스크립트로 현재 설정이 Redis 작업에 적합한지 확인해보세요:


🏁 Script executed:

#!/bin/bash
# Redis 연결 및 응답 시간 테스트를 위한 설정 확인

# SpViewsService에서 사용하는 Redis 작업 패턴 확인
rg -A 10 -B 2 "redisHashRepository\.increment"

# 현재 시스템의 프로세서 수 확인
echo "Available processors: $(nproc)"

# 동시 Redis 연결 테스트 스크립트 생성 제안
echo "동시성 테스트를 위해 Redis 연결 풀과 스레드 풀 설정을 비교 검토하세요."

Length of output: 2822


Redis I/O 특화 스레드 풀 설정 개선 필요

AsyncConfig.java(21–34)에서 현재 CPU 코어 수만큼 스레드를 생성하도록 되어 있는데, Redis 호출은 I/O 대기 시간이 크므로 아래와 같이 수정하세요.

  • corePoolSize, maxPoolSize를 하드코딩 대신 프로퍼티로 외부화
  • queueCapacity를 명시적으로 설정해 과도한 요청 폭주 방지
  • application.yml에 기본값을 두고, 운영 환경 부하 테스트를 통해 최적값 확인

예시 변경안:

 src/main/java/com/pitchain/common/config/AsyncConfig.java

     @Bean
     public Executor asyncTaskExecutor(
-        /* 하드코딩된 값 */
-        ) {
-        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
-        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
-        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors());
-        // executor.setQueueCapacity();
+        @Value("${async.core-pool-size}") int corePoolSize,
+        @Value("${async.max-pool-size}")  int maxPoolSize,
+        @Value("${async.queue-capacity}") int queueCapacity
+    ) {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(corePoolSize);
+        executor.setMaxPoolSize(maxPoolSize);
+        executor.setQueueCapacity(queueCapacity);
         executor.setTaskDecorator(new MdcTaskDecorator());
         executor.setThreadNamePrefix("async");
         executor.setWaitForTasksToCompleteOnShutdown(true);
         executor.initialize();

         return executor;
     }

application.yml 예시:

async:
  core-pool-size: 8
  max-pool-size: 16
  queue-capacity: 500

• 위 설정으로 Redis I/O 처리량과 메모리 사용을 균형 있게 관리하세요.
• 실제 부하(특히 Redis 업데이트 빈도)에 맞춰 부하 테스트 후 프로퍼티 값을 조정해 주세요.

🤖 Prompt for AI Agents
In src/main/java/com/pitchain/common/config/AsyncConfig.java lines 21 to 34, the
thread pool configuration uses hardcoded CPU core counts which is suboptimal for
Redis I/O operations. Modify the code to externalize corePoolSize, maxPoolSize,
and queueCapacity as configurable properties loaded from application.yml. Set
explicit queueCapacity to prevent request overload. Add corresponding properties
in application.yml with default values (e.g., core-pool-size: 8, max-pool-size:
16, queue-capacity: 500) and update the AsyncConfig class to read these values
via @Value or @ConfigurationProperties annotations. This allows tuning thread
pool sizes based on load testing results.

Comment on lines +24 to +27
//TODO - corePoolSize, maxPoolSize, queueCapacity는 서비스에 맞게 테스트를 통해 설정
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors());
// executor.setQueueCapacity();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

스레드 풀 설정을 재검토해주세요.

현재 설정에 몇 가지 개선이 필요합니다:

  1. corePoolSizemaxPoolSize를 동일하게 설정하면 스레드 풀의 탄력성이 제한됩니다
  2. Redis 조회수 업데이트와 같은 I/O 집약적 작업의 경우, CPU 코어 수보다 더 많은 스레드가 필요할 수 있습니다
  3. queueCapacity 설정이 없으면 기본 무제한 큐가 사용되어 메모리 문제가 발생할 수 있습니다

다음과 같은 설정을 고려해보세요:

-        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
-        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors());
-//        executor.setQueueCapacity();
+        int processors = Runtime.getRuntime().availableProcessors();
+        executor.setCorePoolSize(processors);
+        executor.setMaxPoolSize(processors * 2); // I/O 집약적 작업 고려
+        executor.setQueueCapacity(100); // 적절한 큐 용량 설정
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//TODO - corePoolSize, maxPoolSize, queueCapacity는 서비스에 맞게 테스트를 통해 설정
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors());
// executor.setQueueCapacity();
//TODO - corePoolSize, maxPoolSize, queueCapacity는 서비스에 맞게 테스트를 통해 설정
int processors = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(processors);
executor.setMaxPoolSize(processors * 2); // I/O 집약적 작업 고려
executor.setQueueCapacity(100); // 적절한 큐 용량 설정
🤖 Prompt for AI Agents
In src/main/java/com/pitchain/common/config/AsyncConfig.java around lines 24 to
27, the thread pool configuration needs improvement. Adjust corePoolSize to a
value less than maxPoolSize to allow elasticity, increase maxPoolSize beyond the
number of CPU cores to better handle I/O intensive tasks like Redis updates, and
set a finite queueCapacity to prevent potential memory issues from an unbounded
queue. Update the executor configuration accordingly to reflect these changes.

Copy link
Contributor

@lsh2613 lsh2613 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다!

@lsh2613 lsh2613 merged commit c995758 into dev Jul 23, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants