Skip to content

Conversation

@chaen-ing
Copy link
Member

@chaen-ing chaen-ing commented Aug 18, 2025

📝 개요

SCRUM-364 Refactor : 편의점 응답 및 기타 수정

🛠️ 작업 사항

  • 편의점 리스트 응답에 상대시간 추가 (상대시간 유틸을 global로 이동)
  • 커뮤니티 게시글, 댓글 좋아요를 비관적 > 낙관적 락으로 변경 (성능 향상을 위해!)
  • api 프로필을 분리해서 기본 회원가입 배포환경에서는 불가능하도록 수정
  • 기타 자잘한 이름들 수정

🔗 관련 이슈 / JIRA

✅ 체크리스트

  • 코드 리뷰 반영 완료
  • 테스트 코드 작성
  • 로컬 테스트 완료
  • 문서 업데이트 필요 시 반영

🙏 기타 사항

추가적으로 리뷰어가 알아야 할 사항 작성

Summary by CodeRabbit

  • New Features
    • 편의시설 게시글 목록에 상대 생성일 표시(예: “3시간 전”).
  • Refactor
    • 커뮤니티 게시글/댓글 좋아요 토글에 낙관적 락 및 최대 3회 재시도 적용으로 동시성 상황에서 안정성과 정확성 향상.
    • 조회수 증가 및 최근 검색어 처리 로직 안정화.
  • Chores
    • 기본 회원가입 API를 로컬 환경에서만 제공하도록 조정.
    • 시간 유틸리티 공용화에 따른 참조 경로 정리.
    • 캐시 서비스 명칭 정리에 따른 내부 참조 수정.

@chaen-ing chaen-ing requested a review from erika0915 August 18, 2025 15:17
@chaen-ing chaen-ing self-assigned this Aug 18, 2025
@chaen-ing chaen-ing added the ✨ Feature 새로운 기능 추가 label Aug 18, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 18, 2025

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/SCRUM-364-convenience-reseponse-time

🪧 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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

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.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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
Contributor

@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 (17)
src/main/java/com/kkinikong/be/global/exception/handler/GlobalExceptionHandler.java (1)

15-15: @nonnull 애너테이션은 Spring 패키지 사용을 권장.

Micrometer의 NonNull 보다는 Spring의 org.springframework.lang.NonNull 사용이 일반적이며, 프레임워크 내 일관성 측면에서 유리합니다. 기능 차이는 없으나 의존성/의미상 더 적합합니다.

적용 제안(diff):

-import io.micrometer.common.lang.NonNull;
+import org.springframework.lang.NonNull;

(메서드 시그니처의 @nonnull 표기는 동일하게 유지하면 됩니다.)

Also applies to: 130-133

src/main/java/com/kkinikong/be/auth/controller/SignupController.java (3)

33-33: 메서드 명명 일관성: signUp → signup 제안

Service의 signup(...)과 맞춰 컨트롤러 메서드도 카멜케이스 단어 분리 없이 signup으로 통일하면 가독성이 좋아집니다.

다음 변경을 고려해주세요:

-  public ResponseEntity<ApiResponse<Object>> signUp(
+  public ResponseEntity<ApiResponse<Object>> signup(

37-37: 회원가입 성공 시 201 Created 반환이 REST 관례에 더 적합

리소스 생성 시 200 OK 대신 201 Created를 사용하는 것이 일반적입니다. 응답 바디는 그대로 두고 상태코드만 조정하는 방안을 제안합니다. 기존 규약(전역적으로 200 고정)을 유지해야 한다면 현행도 허용 가능합니다.

다음과 같이 수정 가능:

-    return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE);
+    return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.EMPTY_RESPONSE);

22-22: Swagger Tag 명명 통일 제안

태그를 "Auth 로컬"로 분리하면 스웨거 UI에서 "Auth"와 "Auth 로컬" 두 그룹으로 나뉘어 보입니다. 한 그룹으로 묶고 설명에 로컬 전용임을 명시하면 문서 탐색성이 좋아집니다.

예시:

-@Tag(name = "Auth 로컬", description = "인증 및 회원 관련 API")
+@Tag(name = "Auth", description = "인증 및 회원 관련 API (local 전용)")
src/main/java/com/kkinikong/be/cache/service/RedisTemplateCacheService.java (5)

14-16: 필드 주입 대신 생성자 주입으로 전환 권장.

테스트 용이성과 불변성 측면에서 생성자 주입(+ final) 사용을 권장합니다.

아래와 같이 클래스 어노테이션에 @RequiredArgsConstructor 추가를 제안합니다:

 @Service
 @Slf4j
-public class RedisTemplateCacheService {
+@RequiredArgsConstructor
+public class RedisTemplateCacheService {

10-12: 생성자 주입 전환 시 Lombok import 추가 필요.

suggest_optional_refactor 반영 시 import 추가가 필요합니다.

 import lombok.extern.slf4j.Slf4j;
 
+import lombok.RequiredArgsConstructor;
 import com.kkinikong.be.cache.type.RedisKey;

18-18: @Autowired 필드 주입을 final 필드로 변경.

스프링 5+에서는 생성자 주입이 권장됩니다. @RequiredArgsConstructor와 함께 final로 전환하세요.

-  @Autowired private RedisTemplate<String, Object> redisTemplate;
+  private final RedisTemplate<String, Object> redisTemplate;

45-53: 불필요한 Redis 왕복(hasKey) 제거 및 NPE 방어 코드 추가.

opsForList().range 결과만으로 비어있음/없음을 판단하면 네트워크 왕복을 줄일 수 있습니다. 구현체에 따라 range가 null을 반환할 여지도 있어 NPE 방어를 권장합니다.

   public List<String> getRecentSearches(long userId) {
     String key = generateRecentSearchKey(userId);
 
-    if (!redisTemplate.hasKey(key)) {
-      return List.of();
-    }
-
-    List<Object> recentSearches = redisTemplate.opsForList().range(key, 0, -1);
-    return recentSearches.stream().map(Object::toString).toList();
+    List<Object> recentSearches = redisTemplate.opsForList().range(key, 0, -1);
+    if (recentSearches == null || recentSearches.isEmpty()) {
+      return List.of();
+    }
+    return recentSearches.stream().map(Object::toString).toList();
   }

35-42: 최근 검색어 키에 TTL 설정 고려.

최근검색은 특성상 유효기간이 있습니다. 메모리/스토리지 관리 차원에서 TTL(ex. 30일)을 부여하는 것이 안전합니다. 예: redisTemplate.expire(key, Duration.ofDays(30)).

src/main/java/com/kkinikong/be/cache/service/ScheduledService.java (2)

28-42: 뷰 카운트 동기화 과정에서 레이스로 집계 손실 가능성.

현재는 HGETALL 후 DB 반영, 이후 DEL 순서라 동기화 중 발생한 추가 증가분이 DEL로 소거될 수 있습니다. 키 rename → 처리 → 삭제 패턴 또는 Lua 스크립트 기반 drain을 권장합니다.

대안:

  • RENAME 기반:
    1. RENAME store_views store_views:processing:<timestamp> (원자적)
    2. processing 키의 엔트리만 DB 반영
    3. 처리 후 processing 키 삭제
  • Lua 스크립트 기반 원자적 drain (HGETALL + DEL):
    • 단일 스크립트로 해시를 읽고 비우며, 반환된 스냅샷으로 DB 반영

55-58: 오타/가독성: post 처리 로직의 지역변수 명명 수정 제안.

커뮤니티 포스트 컨텍스트에서 storeIdpostId가 의미에 맞습니다.

-          Long storeId = Long.parseLong(k.toString());
+          Long postId = Long.parseLong(k.toString());
           Long viewCount = v == null ? 0L : Long.parseLong(v.toString());
-          communityPostRepository.incrementViews(storeId, viewCount);
+          communityPostRepository.incrementViews(postId, viewCount);
src/main/java/com/kkinikong/be/user/dto/response/MyCommentGroupByPostResponse.java (1)

34-36: shortContent NPE 가능성 — CommunityPostListResponse와 동일한 null-세이프 처리 권장

현재 구현은 content가 null이면 NPE가 발생합니다. 같은 패턴의 CommunityPostListResponse는 null을 허용하도록 방어코드가 있습니다. 도메인에서 null이 절대 나오지 않는 보장이 없다면 아래처럼 통일하는 것을 권장합니다.

-  private static String shortContent(String content, int maxLength) {
-    return content.length() > maxLength ? content.substring(0, maxLength) + "..." : content;
-  }
+  private static String shortContent(String content, int maxLength) {
+    if (content == null) {
+      return null;
+    }
+    return content.length() > maxLength ? content.substring(0, maxLength) + "..." : content;
+  }

추가로, 동일 로직이 여러 DTO에 반복되므로 전역 StringUtil(예: com.kkinikong.be.global.util.StringUtil.shorten(...))로 추출하면 DRY에 도움이 됩니다.

src/main/java/com/kkinikong/be/community/dto/response/CommunityPostListResponse.java (1)

31-36: null-세이프 짧은 콘텐츠 처리 좋음 — 공통 유틸로의 추출 제안

이 로직은 다른 DTO(MyCommentGroupByPostResponse 등)에도 반복됩니다. 전역 유틸(StringUtil.shorten 등)로 추출해 재사용하면 중복 제거 및 일관성 유지에 유리합니다.

src/main/java/com/kkinikong/be/global/util/TimeUtil.java (1)

7-12: null/미래 시각 처리 및 유틸 클래스 형태 개선 제안

  • 입력 past가 null이거나 미래 시각인 경우 음수 시간(예: “-3분 전”)이 노출될 수 있습니다. 간단히 “방금”으로 처리하는 가드 추가를 권장합니다.
  • 유틸 클래스는 인스턴스화가 불필요하므로 final + private 생성자 패턴을 제안합니다.

아래와 같이 반영 가능합니다:

-public class TimeUtil {
+public final class TimeUtil {
+
+  private TimeUtil() {}
 
   public static String relativeTimeFormatter(LocalDateTime past) {
-    LocalDateTime now = LocalDateTime.now();
+    if (past == null) {
+      return "방금";
+    }
+    LocalDateTime now = LocalDateTime.now();
 
     Duration duration = Duration.between(past, now);
+    if (duration.isNegative()) {
+      return "방금";
+    }
     long minutes = duration.toMinutes();
     long hours = duration.toHours();

Also applies to: 16-21

src/main/java/com/kkinikong/be/community/domain/Comment.java (1)

92-94: likeCount 감소 시 음수 방지 가드 권장

동시/반복 언라이크 요청이 겹치면 음수가 될 수 있습니다. 0 하한을 두세요.

-  public void decrementLikeCount() {
-    this.likeCount--;
-  }
+  public void decrementLikeCount() {
+    this.likeCount = Math.max(0, this.likeCount - 1);
+  }
src/main/java/com/kkinikong/be/community/service/CommunityService.java (2)

84-85: 재시도 상수화/설정화 제안

  • final int maxRetries = 3;는 클래스 상수로 승격하거나 설정값으로 주입하는 것이 관리에 용이합니다.
-  final int maxRetries = 3;
+  private static final int MAX_RETRIES = 3;

아래 루프의 maxRetries 참조도 MAX_RETRIES로 교체해 주세요.


231-241: 재시도 실패 시 예외 타입/에러코드 재검토

모든 시도가 낙관적 락 충돌로 실패한 경우 “게시글을 찾을 수 없음” 대신 충돌 또는 재시도 초과를 의미하는 에러코드가 적절합니다. 마지막 발생 예외를 래핑해 전달하는 방안을 고려해 주세요.

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 259bf32 and 2aac5c4.

📒 Files selected for processing (19)
  • src/main/java/com/kkinikong/be/auth/controller/AuthController.java (0 hunks)
  • src/main/java/com/kkinikong/be/auth/controller/SignupController.java (1 hunks)
  • src/main/java/com/kkinikong/be/cache/service/RedisTemplateCacheService.java (1 hunks)
  • src/main/java/com/kkinikong/be/cache/service/ScheduledService.java (3 hunks)
  • src/main/java/com/kkinikong/be/community/domain/Comment.java (2 hunks)
  • src/main/java/com/kkinikong/be/community/domain/CommunityPost.java (2 hunks)
  • src/main/java/com/kkinikong/be/community/dto/response/CommentListResponse.java (1 hunks)
  • src/main/java/com/kkinikong/be/community/dto/response/CommunityPostInfoResponse.java (1 hunks)
  • src/main/java/com/kkinikong/be/community/dto/response/CommunityPostListResponse.java (1 hunks)
  • src/main/java/com/kkinikong/be/community/repository/comment/CommentRepository.java (0 hunks)
  • src/main/java/com/kkinikong/be/community/repository/communityPost/CommunityPostRepository.java (0 hunks)
  • src/main/java/com/kkinikong/be/community/service/CommunityService.java (6 hunks)
  • src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java (1 hunks)
  • src/main/java/com/kkinikong/be/global/exception/handler/GlobalExceptionHandler.java (2 hunks)
  • src/main/java/com/kkinikong/be/global/util/TimeUtil.java (1 hunks)
  • src/main/java/com/kkinikong/be/notification/dto/response/NotificationResponse.java (1 hunks)
  • src/main/java/com/kkinikong/be/store/service/StoreService.java (3 hunks)
  • src/main/java/com/kkinikong/be/user/dto/response/MyCommentGroupByPostResponse.java (1 hunks)
  • src/main/java/com/kkinikong/be/user/dto/response/MypageReviewResponse.java (1 hunks)
💤 Files with no reviewable changes (3)
  • src/main/java/com/kkinikong/be/community/repository/comment/CommentRepository.java
  • src/main/java/com/kkinikong/be/auth/controller/AuthController.java
  • src/main/java/com/kkinikong/be/community/repository/communityPost/CommunityPostRepository.java
🧰 Additional context used
🧬 Code Graph Analysis (14)
src/main/java/com/kkinikong/be/cache/service/RedisTemplateCacheService.java (1)
src/main/java/com/kkinikong/be/cache/service/RedisTempleCacheService.java (3)
  • Service (14-67)
  • clearViewCounts (31-33)
  • deleteRecentSearches (57-61)
src/main/java/com/kkinikong/be/community/dto/response/CommunityPostListResponse.java (2)
src/main/java/com/kkinikong/be/global/util/TimeUtil.java (1)
  • TimeUtil (7-39)
src/main/java/com/kkinikong/be/community/util/TimeUtil.java (2)
  • TimeUtil (7-39)
  • relativeTimeFormatter (9-38)
src/main/java/com/kkinikong/be/community/dto/response/CommunityPostInfoResponse.java (2)
src/main/java/com/kkinikong/be/global/util/TimeUtil.java (1)
  • TimeUtil (7-39)
src/main/java/com/kkinikong/be/community/util/TimeUtil.java (2)
  • TimeUtil (7-39)
  • relativeTimeFormatter (9-38)
src/main/java/com/kkinikong/be/community/dto/response/CommentListResponse.java (2)
src/main/java/com/kkinikong/be/global/util/TimeUtil.java (1)
  • TimeUtil (7-39)
src/main/java/com/kkinikong/be/community/util/TimeUtil.java (2)
  • TimeUtil (7-39)
  • relativeTimeFormatter (9-38)
src/main/java/com/kkinikong/be/notification/dto/response/NotificationResponse.java (2)
src/main/java/com/kkinikong/be/global/util/TimeUtil.java (1)
  • TimeUtil (7-39)
src/main/java/com/kkinikong/be/community/util/TimeUtil.java (1)
  • TimeUtil (7-39)
src/main/java/com/kkinikong/be/community/domain/CommunityPost.java (1)
src/main/java/com/kkinikong/be/community/repository/communityPost/CommunityPostRepository.java (3)
  • Lock (45-47)
  • Modifying (49-51)
  • Repository (22-55)
src/main/java/com/kkinikong/be/user/dto/response/MypageReviewResponse.java (1)
src/main/java/com/kkinikong/be/global/util/TimeUtil.java (1)
  • TimeUtil (7-39)
src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java (1)
src/main/java/com/kkinikong/be/global/util/TimeUtil.java (1)
  • TimeUtil (7-39)
src/main/java/com/kkinikong/be/community/domain/Comment.java (1)
src/main/java/com/kkinikong/be/community/repository/comment/CommentRepository.java (2)
  • Repository (17-28)
  • Lock (23-25)
src/main/java/com/kkinikong/be/global/util/TimeUtil.java (1)
src/main/java/com/kkinikong/be/community/util/TimeUtil.java (2)
  • TimeUtil (7-39)
  • relativeTimeFormatter (9-38)
src/main/java/com/kkinikong/be/auth/controller/SignupController.java (2)
src/main/java/com/kkinikong/be/auth/controller/AuthController.java (4)
  • RequiredArgsConstructor (23-49)
  • Operation (39-48)
  • RequiredArgsConstructor (23-60)
  • Operation (50-59)
src/main/java/com/kkinikong/be/auth/service/AuthService.java (2)
  • signup (47-59)
  • Service (25-74)
src/main/java/com/kkinikong/be/store/service/StoreService.java (1)
src/main/java/com/kkinikong/be/cache/service/RedisTempleCacheService.java (4)
  • Service (14-67)
  • clearViewCounts (31-33)
  • increaseViewCounts (21-23)
  • getViewCounts (26-28)
src/main/java/com/kkinikong/be/cache/service/ScheduledService.java (1)
src/main/java/com/kkinikong/be/cache/service/RedisTempleCacheService.java (3)
  • Service (14-67)
  • clearViewCounts (31-33)
  • increaseViewCounts (21-23)
src/main/java/com/kkinikong/be/community/service/CommunityService.java (3)
src/main/java/com/kkinikong/be/cache/service/RedisTemplateCacheService.java (1)
  • Service (14-67)
src/test/java/com/kkinikong/be/community/service/CommunityServiceTest.java (3)
  • SpringBootTest (25-82)
  • Test (53-81)
  • communityService (65-71)
src/main/java/com/kkinikong/be/community/repository/communityPost/CommunityPostRepository.java (1)
  • Repository (22-55)
🔇 Additional comments (24)
src/main/java/com/kkinikong/be/global/exception/handler/GlobalExceptionHandler.java (2)

91-95: 메서드명 정정으로 가독성/일관성 개선 (ReviewException 핸들러).

핸들러 메서드명이 예외 타입과 일치하도록 수정되어 가독성이 좋아졌습니다. @ExceptionHandler 기반 디스패치라 동작 변화는 없고, 의미가 명확해졌습니다.


105-109: 메서드명 정정으로 가독성/일관성 개선 (FeedbackException 핸들러).

예외 타입과 메서드명이 일치하도록 정리된 점 좋습니다. 동작에는 영향 없고, 유지보수성 향상에 기여합니다.

src/main/java/com/kkinikong/be/auth/controller/SignupController.java (2)

20-25: 로컬 프로필로 격리된 컨트롤러 구성, 의도와 부합합니다

@Profile("local")로 기본 회원가입 엔드포인트를 로컬에서만 활성화한 점 좋습니다. 배포 환경에서의 노출을 확실히 차단할 수 있습니다.


32-32: 매핑 중복 없음 확인: /signup 매핑은 SignupController에만 존재합니다. 로컬 프로필에서도 충돌 없이 애플리케이션이 기동됩니다.

src/main/java/com/kkinikong/be/cache/service/RedisTemplateCacheService.java (1)

16-16: 클래스명 오탈자 수정 적절 (Temple → Template).

전역 사용처와의 일관성이 맞춰져서 더 명확합니다.

src/main/java/com/kkinikong/be/community/domain/CommunityPost.java (1)

77-78: 낙관적 락(@Version) 필드 추가 적절.

동시성 충돌 시점에서 일관성 보장에 도움이 됩니다. 서비스 레이어 재시도 로직과도 잘 맞습니다.

src/main/java/com/kkinikong/be/cache/service/ScheduledService.java (1)

21-21: RedisTemplateCacheService로 의존성 전환 OK.

클래스명 정정과 사용처 일관성 확보가 잘 되었습니다.

src/main/java/com/kkinikong/be/store/service/StoreService.java (1)

16-16: 캐시 서비스 전환 및 조회수 적재 포인트 유지 OK.

  • import 전환, 필드 타입 변경, 적재 호출부 수정이 일관되게 반영되었습니다.
  • 조회 API에서 Redis에 조회수를 적재하고 배치로 DB 반영하는 현재 설계와도 정합적입니다.

Also applies to: 46-46, 109-109

src/main/java/com/kkinikong/be/community/dto/response/CommentListResponse.java (1)

8-8: TimeUtil 글로벌 유틸로 이동 반영 OK.

타 모듈과의 중복 제거 및 공용화 측면에서 적절합니다.

src/main/java/com/kkinikong/be/user/dto/response/MypageReviewResponse.java (2)

7-7: 전역 TimeUtil로의 import 경로 변경 LGTM

전역 유틸로 이동한 의도를 잘 반영했습니다. 사용부도 동일해 리스크 없습니다.


7-7: 구 TimeUtil 클래스 및 import 중복 확인 결과: 문제 없음

  • com/kkinikong/be/community/util/TimeUtil.java 파일이 존재하지 않습니다.
  • import com.kkinikong.be.community.util.TimeUtil 혹은 FQCN 직접 사용 흔적이 0건입니다.

따라서 구 파일 삭제나 import 제거 작업은 이미 완료되어 있으며, 추가 조치가 필요하지 않습니다.

src/main/java/com/kkinikong/be/community/dto/response/CommunityPostInfoResponse.java (1)

8-8: TimeUtil 전역화 반영 정상

import 경로 변경만이며, 상대시간 포맷터 사용은 기존과 동일합니다.

src/main/java/com/kkinikong/be/user/dto/response/MyCommentGroupByPostResponse.java (1)

7-7: TimeUtil 전역화 반영 LGTM

다른 DTO들과 동일한 경로로 정리되어 일관성 좋습니다.

src/main/java/com/kkinikong/be/notification/dto/response/NotificationResponse.java (1)

3-3: TimeUtil import 경로 변경 OK

행동 변화 없고, 전역 유틸 사용으로 의존성 위치가 명확해졌습니다.

src/main/java/com/kkinikong/be/community/dto/response/CommunityPostListResponse.java (1)

4-4: 전역 TimeUtil로 일괄 정리 완료

from(...) 내부 사용도 동일하여 리스크 없습니다.

src/main/java/com/kkinikong/be/global/util/TimeUtil.java (1)

1-1: 패키지 이동 적절합니다.

공용 유틸로 이동해 재사용성과 응집도가 좋아졌습니다. 다른 모듈에서의 import 경로 정리도 일관성 있어 보입니다.

src/main/java/com/kkinikong/be/community/domain/Comment.java (2)

17-17: 낙관적 락 도입을 위한 @Version import 추가, 방향성 좋습니다.

버전 필드를 통한 동시성 제어 준비가 잘 되어 있습니다. 동일 패턴이 Post 엔티티에도 적용되었는지 확인 부탁드립니다.


72-73: 버전 컬럼 마이그레이션 확인 필요

  • @Version 필드를 추가하셨으나, comments 테이블에 해당 컬럼을 생성하는 마이그레이션이 존재하는지 확인이 필요합니다.
  • src/main/resources 내 Flyway/Liquibase 변경 로그 또는 SQL 파일에서 comments 테이블에 version 컬럼을 추가하는 스크립트를 찾을 수 없습니다.
  • 운영 DB 반영 전, 스키마 동기화 검증을 반드시 수행해 주세요.
src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java (1)

4-4: TimeUtil 도입으로 상대시간 생성 일관화된 점 좋습니다.

src/main/java/com/kkinikong/be/community/service/CommunityService.java (5)

18-18: 낙관적 락 예외 import 추가 OK

재시도 로직 도입에 필요한 의존성입니다.


26-26: RedisTemple → RedisTemplate 네이밍 정정 및 주입 교체 적절

서비스 명 오탈자 정정으로 가독성과 일관성이 향상되었습니다.

Also applies to: 81-81


293-293: 조회수 증가의 RedisTemplateCacheService 사용 전환 OK

핫패스에서의 외부 I/O 의존성도 적절히 캡슐화되어 있습니다.


347-359: 최근 검색어 저장/삭제 동작 이관 OK

키 생성/중복 제거/트리밍 로직이 서비스 내에 일관되게 유지됩니다.


221-226: 이벤트 발행 타이밍 검증 완료

NotificationEventListener에서 Spring의 @TransactionalEventListener를 사용하며(기본 phase=AFTER_COMMIT), 리스너에 @Transactional(propagation = Propagation.REQUIRES_NEW)가 적용되어 있습니다.
따라서 낙관적 락 실패로 롤백되면 이벤트는 커밋 이후에만 처리되어 중복·허위 알림이 발생하지 않습니다.

Copy link
Contributor

@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: 5

♻️ Duplicate comments (1)
src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java (1)

6-7: 필드 명명 일관성: createdAt vs relativeCreatedAt 결정 필요

다른 리스트 DTO(예: CommunityPostListResponse)는 동일한 “상대시간 문자열”을 createdAt으로 제공하고 있습니다. 이 DTO만 relativeCreatedAt을 사용하면 JSON 응답 키가 불일치합니다. 전사 규칙을 createdAt으로 유지하거나, 반대로 모든 DTO를 relativeCreatedAt으로 통일하는지 결정이 필요합니다. 현재 PR 범위에서는 createdAt으로 맞추는 쪽이 역호환 리스크가 적습니다.

통일을 선택할 경우의 제안(diff):

-public record ConveniencePostListResponse(
-    Long id, String name, boolean isAvailable, String relativeCreatedAt) {
+public record ConveniencePostListResponse(
+    Long id, String name, boolean isAvailable, String createdAt) {

검증 스크립트(다른 DTO들의 키 확인 및 사용처 점검):

#!/bin/bash
# createdAt / relativeCreatedAt 사용 현황 확인
rg -n -C2 -P 'record\s+\w+\((?>[^()]|(?R))*\)' --type=java
echo "---- createdAt 필드 사용"
rg -n -C1 -P '\bcreatedAt\b' --type=java
echo "---- relativeCreatedAt 필드 사용"
rg -n -C1 -P '\brelativeCreatedAt\b' --type=java

# 기존 API 계약 의존(프론트/문서)에서 relativeCreatedAt를 기대하는 곳이 있는지(있다면 표기):
rg -n -C2 -P 'relativeCreatedAt' -g '!**/build/**' -S
🧹 Nitpick comments (4)
src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java (1)

55-57: likeCount 하한(0) 보장 검토

동시성에 의해 중복 감소가 발생하면 likeCount가 0 미만으로 내려갈 수 있습니다. decrementLikeCount() 내부에서 최소값 0을 보장하도록 보완하는 것을 권장합니다.

외부 파일 변경 예시(CommunityPost/Comment 엔티티):

public void decrementLikeCount() {
  if (this.likeCount > 0) this.likeCount--;
}

Also applies to: 86-87

src/main/java/com/kkinikong/be/community/service/CommunityService.java (3)

79-79: 재시도 상수는 private static final로 선언해 주세요

의미상 상수이며, 인스턴스마다 달라질 필요가 없습니다.

-final int MAX_RETRIES = 3;
+private static final int MAX_RETRIES = 3;

470-474: 파라미터 명명 일관성: reviewId → postId

타 메서드와 일관되게 postId가 적절합니다.

-  private CommunityPost getCommunityPostOrThrow(Long reviewId) {
+  private CommunityPost getCommunityPostOrThrow(Long postId) {
     return communityPostRepository
-        .findById(reviewId)
+        .findById(postId)
         .orElseThrow(() -> new CommunityException(CommunityErrorCode.COMMUNITY_POST_NOT_FOUND));
   }

71-71: CommunityService에서 사용되지 않는 CommentLikeRepository 의존성 제거 제안

CommunityService 클래스 내에서 commentLikeRepository 필드와 생성자 파라미터가 실제로 사용되지 않으므로, 불필요한 의존성을 제거하는 것을 권장드립니다.

  • 파일: src/main/java/com/kkinikong/be/community/service/CommunityService.java
    • 71행: private final CommentLikeRepository commentLikeRepository; 제거
    • 생성자 시그니처에서 CommentLikeRepository commentLikeRepository 파라미터 및
      this.commentLikeRepository = commentLikeRepository; 할당 구문 삭제
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2aac5c4 and cea7adf.

📒 Files selected for processing (3)
  • src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java (1 hunks)
  • src/main/java/com/kkinikong/be/community/service/CommunityService.java (6 hunks)
  • src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java (5)
src/main/java/com/kkinikong/be/community/repository/CommunityPostLikeRepository.java (1)
  • Repository (14-33)
src/main/java/com/kkinikong/be/community/controller/CommunityController.java (2)
  • PostMapping (171-182)
  • PostMapping (184-196)
src/test/java/com/kkinikong/be/community/service/CommunityServiceTest.java (2)
  • SpringBootTest (25-82)
  • communityService (65-71)
src/main/java/com/kkinikong/be/community/domain/CommunityPostLike.java (1)
  • Table (20-46)
src/main/java/com/kkinikong/be/community/domain/CommunityPost.java (1)
  • Table (25-111)
src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java (4)
src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostDetailResponse.java (1)
  • ConveniencePostDetailResponse (11-36)
src/main/java/com/kkinikong/be/community/dto/response/CommunityPostListResponse.java (1)
  • CommunityPostListResponse (6-37)
src/main/java/com/kkinikong/be/convenience/domain/ConveniencePost.java (1)
  • Table (16-82)
src/main/java/com/kkinikong/be/community/util/TimeUtil.java (1)
  • TimeUtil (7-39)
src/main/java/com/kkinikong/be/community/service/CommunityService.java (4)
src/test/java/com/kkinikong/be/community/service/CommunityServiceTest.java (3)
  • SpringBootTest (25-82)
  • Test (53-81)
  • communityService (65-71)
src/main/java/com/kkinikong/be/community/repository/communityPost/CommunityPostRepository.java (1)
  • Repository (22-55)
src/main/java/com/kkinikong/be/community/repository/CommunityPostLikeRepository.java (1)
  • Repository (14-33)
src/main/java/com/kkinikong/be/community/domain/CommunityPost.java (1)
  • Table (25-111)
🔇 Additional comments (5)
src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java (1)

4-4: TimeUtil 중복 클래스 제거 확인 완료

  • com.kkinikong.be.community.util.TimeUtil 클래스 정의가 더 이상 존재하지 않습니다.
  • 모든 소스에서 com.kkinikong.be.global.util.TimeUtil 만 import 되고 있습니다.

추가 작업 불필요합니다.

src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java (3)

30-32: REQUIRES_NEW로 토글 로직을 분리한 결정, 매우 적절합니다

낙관적 락 재시도 외부 루프와 트랜잭션 경계를 분리한 구조가 깔끔하고, 이전 리뷰 이슈(같은 트랜잭션 내 재시도/이벤트 타이밍 문제)를 잘 해소합니다.


41-69: 이미 CommunityPost 및 Comment 엔티티에 @Version 필드가 선언되어 있습니다.

  • src/main/java/com/kkinikong/be/community/domain/Comment.java:72 (@Version private Long version;)
  • src/main/java/com/kkinikong/be/community/domain/CommunityPost.java:77 (@Version private Long version;)

따라서 추가 조치가 필요 없습니다.

Likely an incorrect or invalid review comment.


63-66: 이벤트 리스너가 이미 AFTER_COMMIT 단계로 동작하므로 추가 변경 불필요

  • NotificationEventListener#handlePostLikeEvent#handleCommentLiked에 적용된
    @TransactionalEventListener의 기본 phaseAFTER_COMMIT입니다.
  • 따라서 트랜잭션 커밋 실패 시 알림이 발행되지 않으며, 별도 명시 없이도 안전합니다.
src/main/java/com/kkinikong/be/community/service/CommunityService.java (1)

190-200: 외부 실행자 기반 재시도 구조, 방향성 좋습니다

토글 실행을 CommunityLikeToggleExecutor.REQUIRES_NEW로 위임하고, 서비스 레벨에서 낙관적 락 예외에 대해서만 선형 백오프 재시도를 수행하는 접근이 명확하고 안전합니다.

Copy link
Member

@erika0915 erika0915 left a comment

Choose a reason for hiding this comment

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

확인해쓰용~

Copy link
Contributor

@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

♻️ Duplicate comments (1)
src/main/java/com/kkinikong/be/community/service/CommunityService.java (1)

191-207: 트랜잭셔널 이벤트 리스너(AFTER_COMMIT) 적용 확인 및 수정 필요

좋아요 토글 이벤트는 Executor 내부에서 publish 후 saveAndFlush 로직이 커밋되기 전에 발생합니다. 현재 NotificationEventListener의 핸들러는 @Transactional(propagation = Propagation.REQUIRES_NEW)만 적용되어 있어, 롤백된 트랜잭션에서도 알림이 발송될 수 있습니다. 운영 중 불필요한 알림이 발송되지 않도록 아래 메소드들에 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 또는 @EventListener(phase = AFTER_COMMIT)를 적용해주세요.

  • 대상 파일: src/main/java/com/kkinikong/be/notification/event/NotificationEventListener.java
    • handlePostLikeEvent(CommunityLikeEvent) : 25–26행
    • handleReplyCreated(CommentReplyEvent) : 51–52행
    • handleCommentLiked(CommentLikeEvent) : 64–65행

예시 변경:

-  @Transactional(propagation = Propagation.REQUIRES_NEW)
-  public void handlePostLikeEvent(CommunityLikeEvent event) {
+  @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+  public void handlePostLikeEvent(CommunityLikeEvent event) {
     // ...
  }

다른 핸들러에도 동일하게 반영 부탁드립니다.

🧹 Nitpick comments (1)
src/main/java/com/kkinikong/be/community/service/CommunityService.java (1)

79-79: 상수는 private static final로 선언하세요

서비스 수준 재시도 횟수는 인스턴스마다 달라질 필요가 없어 보입니다. 정적 상수로 선언하면 의도도 명확해집니다.

아래처럼 변경 제안합니다:

-  final int MAX_RETRIES = 3;
+  private static final int MAX_RETRIES = 3;
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cea7adf and 1487bcb.

📒 Files selected for processing (2)
  • src/main/java/com/kkinikong/be/community/exception/errorcode/CommunityErrorCode.java (1 hunks)
  • src/main/java/com/kkinikong/be/community/service/CommunityService.java (6 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/main/java/com/kkinikong/be/community/exception/errorcode/CommunityErrorCode.java (9)
src/main/java/com/kkinikong/be/community/exception/CommunityException.java (1)
  • Getter (8-12)
src/main/java/com/kkinikong/be/auth/exception/errorcode/AuthErrorCode.java (1)
  • Getter (10-21)
src/main/java/com/kkinikong/be/review/exception/errorcode/ReviewErrorCode.java (1)
  • Getter (10-20)
src/main/java/com/kkinikong/be/report/exception/errorcode/ReportErrorCode.java (1)
  • Getter (10-20)
src/main/java/com/kkinikong/be/opensearch/exception/errorcode/OpenSearchErrorCode.java (1)
  • Getter (10-23)
src/main/java/com/kkinikong/be/feedback/exception/errorcode/FeedbackErrorCode.java (1)
  • Getter (10-16)
src/main/java/com/kkinikong/be/convenience/exception/errorcode/ConvenienceErrorCode.java (1)
  • Getter (10-22)
src/main/java/com/kkinikong/be/user/exception/errorcode/UserErrorCode.java (1)
  • Getter (10-24)
src/main/java/com/kkinikong/be/global/exception/errorcode/GlobalErrorCode.java (1)
  • Getter (9-20)
src/main/java/com/kkinikong/be/community/service/CommunityService.java (4)
src/main/java/com/kkinikong/be/cache/service/RedisTemplateCacheService.java (1)
  • Service (14-67)
src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java (1)
  • Service (30-105)
src/main/java/com/kkinikong/be/community/repository/communityPost/CommunityPostRepository.java (1)
  • Repository (22-55)
src/main/java/com/kkinikong/be/community/repository/CommunityPostLikeRepository.java (1)
  • Repository (14-33)
🔇 Additional comments (9)
src/main/java/com/kkinikong/be/community/exception/errorcode/CommunityErrorCode.java (2)

25-25: 에러코드 추가 적절합니다

낙관적 락 재시도 초과 시 의미가 명확한 에러코드가 생겨서 좋습니다.


25-25: RETRY_LIMIT_EXCEEDED가 실질적으로 던져지지 않습니다 (CommunityService 로직과 불일치)

현재 CommunityService의 재시도 루프에서 마지막 시도에선 ObjectOptimisticLockingFailureException을 그대로 재던지기 때문에, 메서드 말미의 RETRY_LIMIT_EXCEEDED 예외까지 도달하지 못합니다. 의도대로라면 마지막 실패 후 이 에러코드를 던지도록 서비스 로직을 조정하거나, 반대로 “마지막 예외를 그대로 전파”하는 전략으로 합의해 불필요한 에러코드를 제거해야 합니다. 아래 CommunityService 코멘트를 참고해 주세요.

src/main/java/com/kkinikong/be/community/service/CommunityService.java (7)

18-18: 낙관적 락 예외 import 적절

ObjectOptimisticLockingFailureException 직접 처리에 필요한 정확한 import입니다.


26-26: RedisTemplateCacheService 전환 LGTM

캐시 서비스 명칭/타입 정정 및 주입 전환 문제 없습니다.


75-78: 의존성 주입 추가 전반적으로 타당

  • RedisTemplateCacheService, CommunityLikeToggleExecutor 주입 추가 적절합니다.
  • Like 토글은 실행자(Executor)로 위임되어 트랜잭션 경계가 명확해졌습니다.

233-233: 조회수 증가를 Redis로만 처리 — 읽기 경로/동기화 전략 확인 필요

Redis에만 증가시키고 응답(CommunityPostInfoResponse)에는 DB의 viewCount만 사용하면 실시간 수치가 반영되지 않을 수 있습니다. 읽기 시 Redis 값을 합산하거나, 배치 동기화(Job) 존재 여부를 확인해 주세요.

원하시면 읽기 시 합산 적용 또는 동기화 배치 설계안을 제안하겠습니다.


257-257: 최근 검색어 저장 전환 LGTM

중복 제거 후 좌측 삽입, 최대 5개 트림 로직 모두 적절합니다.


288-288: 최근 검색어 조회 로직 LGTM

키 미존재 시 빈 리스트 반환으로 NPE 방지되어 있습니다.


299-299: 최근 검색어 삭제 위임 LGTM

트림/중복 제거 전략과 일관성 있습니다.

@chaen-ing chaen-ing merged commit f3b7a2c into develop Aug 19, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants