Skip to content

[FEATURE] 게시글 하트 기능 구현#112

Merged
goalSetter09 merged 21 commits intodevelopfrom
modic_backend-16
Jul 18, 2025
Merged

[FEATURE] 게시글 하트 기능 구현#112
goalSetter09 merged 21 commits intodevelopfrom
modic_backend-16

Conversation

@goalSetter09
Copy link
Collaborator

@goalSetter09 goalSetter09 commented Jul 12, 2025

개요

close #16, #113

작업사항

  • 게시글 좋아요 토글기능 구현
  • 게시글 상세 조회시 좋아요 개수 필드 추가
  • 게시글 상세 조회시 자신이 좋아요한 게시글인지 확인하는 필드 추가

Summary by CodeRabbit

Summary by CodeRabbit

  • 신규 기능

    • 게시글에 '좋아요(하트)' 기능이 추가되었습니다. 사용자는 게시글에 하트를 누르거나 취소할 수 있으며, 각 게시글의 하트 개수와 내가 누른 상태를 확인할 수 있습니다.
    • 게시글 상세 및 목록 조회 시 하트 개수와 내가 하트를 눌렀는지 여부가 표시됩니다.
    • 게시글 통계가 비동기로 관리되어 성능이 개선되었습니다.
    • 게시글 이미지 조회가 최적화되어 다수 게시글의 이미지가 일괄 조회됩니다.
    • 하트 기능 관련 REST API가 새로 추가되었습니다.
  • 버그 수정

    • 사용자가 자신의 게시글에 하트를 누를 수 없도록 제한되었습니다.
  • 테스트

    • 하트 기능 및 게시글 조회 응답에 대한 단위 및 통합 테스트가 추가 및 개선되었습니다.
    • 동시성 환경에서 하트 토글과 통계 일관성 검증을 위한 통합 테스트가 추가되었습니다.
  • 문서화

    • API 응답 및 에러 코드에 하트 관련 설명이 추가되었습니다.

@goalSetter09 goalSetter09 requested a review from Copilot July 12, 2025 06:39
@coderabbitai
Copy link

coderabbitai bot commented Jul 12, 2025

"""

Walkthrough

게시글 하트(좋아요) 기능이 전체적으로 도입되었습니다. 하트 개수 및 사용자의 하트 여부가 게시글 상세 및 목록 조회 응답에 포함되고, 하트 토글을 위한 API가 추가되었습니다. 하트 데이터 저장, 통계 관리, 비동기 처리, 예외 처리, 테스트 코드가 함께 구현되었습니다.

Changes

파일/경로 요약 변경 내용 요약
.../common/error/ErrorCode.java 자신의 게시글 하트 금지 에러코드 추가
.../domain/post/service/PostService.java 하트 통계/상태 연동, like 관련 의존성 및 로직 추가, getPost/getPosts 시 하트 정보 포함
.../domain/postLike/entity/PostLikeEntity.java
.../domain/postLike/entity/PostStatisticsEntity.java
하트 엔티티 및 하트 통계 엔티티 신설
.../domain/postLike/repository/PostLikeEntityRepository.java
.../domain/postLike/repository/PostStatisticsEntityRepository.java
하트 및 하트 통계 JPA 리포지토리 신설
.../domain/postLike/service/AsyncPostStatisticsService.java
.../domain/postLike/service/PostLikeService.java
하트 토글/조회/통계 비즈니스 서비스 신설 및 비동기 통계처리
.../web/post/controller/PostController.java getPost에 사용자 정보 추가, 하트 정보 응답 연동, 어노테이션 및 파라미터 인라인화
.../web/post/dto/response/GetPostResponse.java
.../web/post/dto/response/GetPostsResponse.java
하트 개수, 하트 여부 필드 및 생성 메서드 추가
.../web/postLike/controller/PostLikeController.java 하트 토글 API 컨트롤러 신설
.../domain/post/service/PostServiceTest.java
.../web/post/controller/PostControllerTest.java
하트 정보 포함 단일/목록 조회 테스트, 하트 여부 테스트, 실패 케이스 테스트 추가/수정
.../domain/post/repository/PostImageEntityRepository.java 다중 게시글 이미지 조회용 메서드 추가
.../common/redis/distributedLock/LockManager.java 게시글 하트용 분산 락 메서드 추가
.../domain/postLike/service/PostLikeServiceIntegrationTest.java 하트 토글 동시성 및 통계 일관성 통합 테스트 추가

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant PostLikeController
    participant PostLikeService
    participant PostLikeEntityRepository
    participant AsyncPostStatisticsService
    participant PostStatisticsEntityRepository

    User->>PostLikeController: POST /api/posts/{postId}/like
    PostLikeController->>PostLikeService: toggleLike(userId, postId)
    PostLikeService->>PostLikeEntityRepository: existsByUserIdAndPostId
    alt 이미 하트함
        PostLikeService->>PostLikeEntityRepository: deleteByUserIdAndPostId
        PostLikeService->>AsyncPostStatisticsService: decrementLikeCount(postId)
    else 하트 안함
        PostLikeService->>PostLikeEntityRepository: save(PostLikeEntity)
        PostLikeService->>AsyncPostStatisticsService: incrementLikeCount(postId)
    end
Loading
sequenceDiagram
    participant User
    participant PostController
    participant PostService
    participant PostLikeService

    User->>PostController: GET /api/posts/{id}
    PostController->>PostService: getPost(id, userId)
    PostService->>PostLikeService: getLikeCount(postId)
    PostService->>PostLikeService: isLikedByUser(userId, postId)
    PostService->>PostController: GetPostResponse(likeCount, isLikedByCurrentUser)
    PostController->>User: 응답 반환
Loading

Assessment against linked issues

Objective Addressed Explanation
게시글 하트(좋아요) 기능 도입 (#16)
하트 토글 API 제공 및 예외처리 (#16)
게시글 상세/목록 조회 시 하트 개수, 하트 여부 포함 (#16)
하트 데이터 저장 및 통계 관리, 비동기 처리 (#16)

Poem

토끼가 뛰노는 들판에,
하트가 뿅뿅, 게시글에!
내 글엔 하트 못 눌러요,
남의 글엔 맘껏 눌러요.
통계도 똑똑, 비동기로 척척,
테스트까지 완벽하게!
🐰💖
"""


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db1c18f and 58a3403.

📒 Files selected for processing (1)
  • .gitignore (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .gitignore

🪧 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.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

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
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @goalSetter09, 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!

This pull request introduces a new 'heart' (like) feature for posts, enabling users to express their appreciation for content. The implementation includes a robust data model for tracking likes and their aggregated counts, an asynchronous service to update these statistics efficiently, and updates to existing post viewing functionalities to display like information. A new API endpoint is provided for users to toggle their like status on posts, with a specific check to prevent self-liking.

Highlights

  • Post Like Functionality: Implemented a new 'heart' (like) feature for posts, allowing users to toggle their like status on a given post.
  • Dedicated Data Models: Introduced two new entities: PostLikeEntity to record individual user-post like relationships and PostStatisticsEntity to store aggregated like counts for posts, optimized for performance and utilizing optimistic locking.
  • Asynchronous Statistics Updates: Developed AsyncPostStatisticsService to handle the incrementing, decrementing, and initialization of post like counts in the background, ensuring that user interactions remain responsive.
  • Enhanced Post Views: Modified PostService and related DTOs (GetPostResponse, GetPostsResponse) to include the total like count and a boolean indicating whether the current authenticated user has liked a post, visible in both single post detail and post list views.
  • New API Endpoint: Added a dedicated POST /api/posts/{postId}/like endpoint in PostLikeController for users to easily toggle their like status on a specific post.
  • Self-Like Prevention: Introduced a new error code (CANNOT_LIKE_OWN_POST_EXCEPTION) and corresponding logic to prevent users from liking their own posts.
Using Gemini Code Assist

The 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 in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist is currently in preview and 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 to provide feedback.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Adds a post “heart” (like) feature, including toggling likes, tracking counts, and indicating if the current user has liked a post.

  • Introduces a new PostLikeController and PostLikeService with asynchronous statistics updates
  • Extends GetPostResponse and GetPostsResponse DTOs with likeCount (and isLikedByCurrentUser for single-post)
  • Updates PostService and PostController to integrate like counts and user-specific like status
  • Adds repositories and entities for persisting like relations and aggregated statistics

Reviewed Changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/test/java/hanium/modic/backend/web/post/controller/PostControllerTest.java Verifies new likeCount and isLikedByCurrentUser fields in single-post API
src/test/java/hanium/modic/backend/domain/post/service/PostServiceTest.java Tests service’s getPost overloads for liked/not-liked cases
src/main/java/hanium/modic/backend/web/postLike/controller/PostLikeController.java New REST endpoint for toggling a post’s like
src/main/java/hanium/modic/backend/web/post/dto/response/GetPostsResponse.java Add likeCount to posts-list response
src/main/java/hanium/modic/backend/web/post/dto/response/GetPostResponse.java Add likeCount and isLikedByCurrentUser
src/main/java/hanium/modic/backend/web/post/controller/PostController.java Wire user ID into getPost and getPosts methods
src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java Business logic for toggling likes and querying counts
src/main/java/hanium/modic/backend/domain/postLike/service/AsyncPostStatisticsService.java Async updates for like statistics
src/main/java/hanium/modic/backend/domain/postLike/repository/PostStatisticsEntityRepository.java CRUD for aggregated like statistics
src/main/java/hanium/modic/backend/domain/postLike/repository/PostLikeEntityRepository.java CRUD for individual user-post likes
src/main/java/hanium/modic/backend/domain/postLike/entity/PostStatisticsEntity.java Entity for per-post like counts with optimistic locking
src/main/java/hanium/modic/backend/domain/postLike/entity/PostLikeEntity.java Entity for user-post like relationship
src/main/java/hanium/modic/backend/domain/post/service/PostService.java Fetch and inject like data into post APIs; init stats on create
src/main/java/hanium/modic/backend/common/error/ErrorCode.java New error code PL-001 for liking own post
Comments suppressed due to low confidence (3)

src/main/java/hanium/modic/backend/web/post/controller/PostController.java:59

  • Requiring a non-null @currentuser for the getPost endpoint prevents unauthenticated reads and breaks backward compatibility. Consider accepting an optional user or overloading the endpoint so that unauthenticated clients can still retrieve posts (with isLikedByCurrentUser == null).
	public ResponseEntity<AppResponse<GetPostResponse>> getPost(@PathVariable Long id, @CurrentUser UserEntity user) {

src/main/java/hanium/modic/backend/web/postLike/controller/PostLikeController.java:31

  • The new POST /api/posts/{postId}/like endpoint is not covered by any controller tests; add tests for successful toggles and error cases (liking own post, non-existent post, unauthenticated).
	@PostMapping("/{postId}/like")

src/main/java/hanium/modic/backend/domain/post/service/PostService.java:76

  • This asynchronous initialization of post statistics on createPost is new behavior but lacks unit tests; consider verifying that initializeStatistics is invoked.
		asyncPostStatisticsService.initializeStatistics(post.getId());

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

The code introduces a new 'like' feature. However, there are critical concurrency issues in AsyncPostStatisticsService.java and PostLikeService.java that must be addressed to prevent data inconsistencies. The deleteByUserIdAndPostId method in PostLikeEntityRepository.java should return the number of deleted rows.

Comment on lines +39 to +42
} catch (Exception e) {
log.error("하트 수 증가 실패: postId={}", postId, e);
// 비동기 처리이므로 예외를 던지지 않고 로깅만 수행
}
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The generic catch (Exception e) block can hide ObjectOptimisticLockingFailureException exceptions, leading to silent data loss during concurrent updates. Consider handling this specific exception explicitly to implement a retry mechanism or log a more specific warning.

} catch (ObjectOptimisticLockingFailureException e) {
			log.warn("Like count increment failed due to concurrency conflict for postId={}. An update was lost. A retry mechanism should be implemented.", postId, e);
		} catch (Exception e) {
			log.error("하트 수 증가 실패: postId={}", postId, e);
		}

Comment on lines +62 to +65
} catch (Exception e) {
log.error("하트 수 감소 실패: postId={}", postId, e);
// 비동기 처리이므로 예외를 던지지 않고 로깅만 수행
}
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

Similar to incrementLikeCount, the generic catch (Exception e) block can hide ObjectOptimisticLockingFailureException exceptions, leading to silent data loss during concurrent unlike actions. Handle the optimistic locking exception specifically.

} catch (ObjectOptimisticLockingFailureException e) {
			log.warn("Like count decrement failed due to concurrency conflict for postId={}. An update was lost. A retry mechanism should be implemented.", postId, e);
		} catch (Exception e) {
			log.error("하트 수 감소 실패: postId={}", postId, e);
		}

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: 1

🧹 Nitpick comments (7)
src/main/java/hanium/modic/backend/domain/postLike/repository/PostLikeEntityRepository.java (1)

22-24: 커스텀 삭제 쿼리에서 반환값 고려해보세요.

@Modifying 어노테이션을 사용한 커스텀 삭제 쿼리가 적절하게 구현되었습니다. 다만 삭제된 행의 수를 반환하도록 하면 삭제 성공 여부를 확인할 수 있어 더 좋을 것 같습니다.

-void deleteByUserIdAndPostId(@Param("userId") Long userId, @Param("postId") Long postId);
+int deleteByUserIdAndPostId(@Param("userId") Long userId, @Param("postId") Long postId);
src/main/java/hanium/modic/backend/domain/postLike/entity/PostLikeEntity.java (1)

33-37: 필드 validation 추가를 고려해보세요.

현재 nullable = false로 데이터베이스 제약은 있지만, 애플리케이션 레벨에서의 검증도 추가하면 더 안전합니다.

+@NotNull(message = "사용자 ID는 필수입니다")
+@Positive(message = "사용자 ID는 양수여야 합니다")
 @Column(name = "user_id", nullable = false)
 private Long userId;

+@NotNull(message = "게시글 ID는 필수입니다")  
+@Positive(message = "게시글 ID는 양수여야 합니다")
 @Column(name = "post_id", nullable = false)
 private Long postId;
src/main/java/hanium/modic/backend/web/postLike/controller/PostLikeController.java (1)

31-31: 토글 API의 명확성을 향상시키는 것을 고려해보세요.

현재 POST 방식의 토글은 RESTful하지만, API 소비자 입장에서는 현재 상태를 알기 어렵습니다. 응답에 토글 후 상태를 포함하는 것을 고려해보세요.

-public ResponseEntity<AppResponse<Void>> togglePostLike(
+public ResponseEntity<AppResponse<PostLikeToggleResponse>> togglePostLike(
     @PathVariable @Positive(message = "게시글 ID는 양수여야 합니다.") Long postId,
     @CurrentUser UserEntity user) {
-    postLikeService.toggleLike(user.getId(), postId);
-    return ResponseEntity.ok(AppResponse.ok(null));
+    boolean isLiked = postLikeService.toggleLike(user.getId(), postId);
+    return ResponseEntity.ok(AppResponse.ok(new PostLikeToggleResponse(isLiked)));
 }
src/main/java/hanium/modic/backend/web/post/controller/PostController.java (1)

59-61: 사용자 파라미터 추가로 좋아요 기능 통합이 잘 되었습니다.

현재 사용자의 좋아요 상태를 포함한 게시글 조회가 가능해졌습니다. 하지만 비인증 사용자도 게시글을 조회할 수 있어야 한다면, 사용자 파라미터를 Optional로 처리하는 것을 고려해보세요.

-public ResponseEntity<AppResponse<GetPostResponse>> getPost(@PathVariable Long id, @CurrentUser UserEntity user) {
-    GetPostResponse response = postService.getPost(id, user.getId());
+public ResponseEntity<AppResponse<GetPostResponse>> getPost(@PathVariable Long id, @CurrentUser(required = false) UserEntity user) {
+    Long userId = user != null ? user.getId() : null;
+    GetPostResponse response = postService.getPost(id, userId);
     return ResponseEntity.ok(AppResponse.ok(response));
 }
src/main/java/hanium/modic/backend/domain/postLike/entity/PostStatisticsEntity.java (2)

34-35: likeCount 데이터 타입을 재검토해보세요.

현재 Long 타입을 사용하고 있는데, 게시글 좋아요 수가 Integer 범위를 넘을 가능성이 낮다면 Integer 사용을 고려해보세요. 이는 인덱스 크기와 메모리 사용량을 줄일 수 있습니다.

-@Column(name = "like_count", nullable = false)
-private Long likeCount;
+@Column(name = "like_count", nullable = false)
+private Integer likeCount;

단, 향후 확장성을 고려한다면 현재 Long 타입도 적절합니다.


37-38: version 필드 초기화를 명시적으로 처리하세요.

JPA에서 @Version 필드는 보통 0으로 초기화되지만, 명시적으로 처리하면 더 명확합니다.

 @Version
-private Long version; // 낙관적 락으로 동시성 제어
+private Long version = 0L; // 낙관적 락으로 동시성 제어
src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java (1)

95-105: 효율적인 배치 조회 구현입니다.

빈 리스트 체크와 단일 쿼리로 여러 게시글의 통계를 조회하는 점이 좋습니다.

다만 통계 데이터가 없는 게시글의 경우 Map에 포함되지 않으므로, 호출하는 쪽에서 기본값 처리가 필요할 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0802c8 and 3a2f522.

📒 Files selected for processing (14)
  • src/main/java/hanium/modic/backend/common/error/ErrorCode.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/post/service/PostService.java (9 hunks)
  • src/main/java/hanium/modic/backend/domain/postLike/entity/PostLikeEntity.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/postLike/entity/PostStatisticsEntity.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/postLike/repository/PostLikeEntityRepository.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/postLike/repository/PostStatisticsEntityRepository.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/postLike/service/AsyncPostStatisticsService.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java (1 hunks)
  • src/main/java/hanium/modic/backend/web/post/controller/PostController.java (1 hunks)
  • src/main/java/hanium/modic/backend/web/post/dto/response/GetPostResponse.java (2 hunks)
  • src/main/java/hanium/modic/backend/web/post/dto/response/GetPostsResponse.java (2 hunks)
  • src/main/java/hanium/modic/backend/web/postLike/controller/PostLikeController.java (1 hunks)
  • src/test/java/hanium/modic/backend/domain/post/service/PostServiceTest.java (5 hunks)
  • src/test/java/hanium/modic/backend/web/post/controller/PostControllerTest.java (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (32)
src/main/java/hanium/modic/backend/common/error/ErrorCode.java (1)

45-47: 새로운 에러 코드 추가가 적절합니다.

CANNOT_LIKE_OWN_POST_EXCEPTION 에러 코드가 기존 패턴을 잘 따르고 있습니다. HTTP 상태 코드, 에러 코드 네이밍, 한국어 메시지 모두 적절하게 구현되었습니다.

src/main/java/hanium/modic/backend/domain/postLike/repository/PostLikeEntityRepository.java (1)

12-35: 리포지토리 인터페이스가 잘 설계되었습니다.

하트 기능에 필요한 모든 메서드가 적절히 정의되어 있고, 각 메서드의 용도가 주석으로 명확히 설명되어 있습니다. 미래 기능용과 백업용 메서드도 고려하여 설계한 점이 좋습니다.

src/main/java/hanium/modic/backend/web/post/dto/response/GetPostsResponse.java (1)

18-46: 하위 호환성을 고려한 DTO 확장이 훌륭합니다.

likeCount 필드 추가와 함께 기존 코드와의 호환성을 유지하기 위한 오버로드 메서드를 제공한 것이 좋습니다. 기본값으로 0L을 사용하는 것도 적절합니다.

src/test/java/hanium/modic/backend/domain/post/service/PostServiceTest.java (3)

56-59: 새로운 의존성 모킹이 적절합니다.

하트 기능과 관련된 PostLikeServiceAsyncPostStatisticsService 모킹이 올바르게 추가되었습니다.


111-183: 하트 기능 테스트 케이스가 포괄적입니다.

사용자가 좋아요를 한 경우와 하지 않은 경우를 모두 테스트하고, 각각의 응답값과 서비스 호출을 검증하고 있어 테스트 커버리지가 우수합니다. 테스트 메서드명도 명확하고 한국어로 잘 작성되었습니다.


185-200: 예외 상황 테스트가 잘 구현되었습니다.

존재하지 않는 게시글에 대한 예외 처리 테스트가 currentUserId 파라미터를 포함하여 올바르게 업데이트되었습니다.

src/main/java/hanium/modic/backend/domain/post/service/PostService.java (5)

42-44: 새로운 의존성 주입이 적절합니다.

하트 기능 관련 서비스들이 명확한 주석과 함께 올바르게 주입되었습니다.


75-77: 비동기 통계 초기화가 좋은 설계입니다.

게시글 생성 후 통계를 비동기로 초기화하는 것은 성능상 좋은 접근입니다. 게시글 생성 작업이 블로킹되지 않아 사용자 경험이 향상됩니다.


116-126: 배치 쿼리 최적화가 우수합니다.

여러 게시글의 하트 수를 한 번의 쿼리로 조회하는 최적화가 잘 구현되었습니다. N+1 쿼리 문제를 방지하고 성능을 크게 향상시킬 것입니다.


94-101: 하트 기능 통합이 잘 구현되었습니다.

하트 수 조회와 사용자의 좋아요 여부 확인 로직이 깔끔하게 통합되었고, 응답 DTO에 필요한 정보를 모두 전달하고 있습니다.


82-82: getPost 호출부 일관성 검증 완료

메서드 시그니처 변경 이후 PostController와 모든 관련 테스트(PostControllerTest, PostServiceTest)에서 getPost(id, userId) 형태로 정상적으로 두 개의 파라미터를 전달하고 있음을 확인했습니다. 추가 수정은 필요 없습니다.

src/main/java/hanium/modic/backend/domain/postLike/repository/PostStatisticsEntityRepository.java (1)

13-41: 전반적인 리포지토리 설계가 우수합니다.

JPA 표준을 잘 따르고 있으며, 배치 조회와 개별 조회를 적절히 분리하여 성능을 고려한 설계입니다.

src/main/java/hanium/modic/backend/domain/postLike/entity/PostLikeEntity.java (2)

22-24: 유니크 제약조건 설계가 적절합니다.

사용자가 동일한 게시글에 중복으로 좋아요를 누르는 것을 데이터베이스 레벨에서 방지하는 좋은 설계입니다.


48-53: 정적 팩토리 메서드가 잘 구현되었습니다.

빌더 패턴을 활용한 깔끔한 객체 생성 방법입니다.

src/main/java/hanium/modic/backend/web/postLike/controller/PostLikeController.java (2)

31-43: 컨트롤러 설계가 REST 원칙을 잘 따르고 있습니다.

적절한 HTTP 메서드 사용, 파라미터 검증, 인증 처리가 잘 구현되어 있습니다.


32-37: API 문서화가 상세하고 명확합니다.

Swagger 어노테이션을 통해 API 사용법과 예상 응답 코드가 잘 설명되어 있습니다.

src/main/java/hanium/modic/backend/web/post/controller/PostController.java (1)

44-46: 어노테이션 및 메서드 시그니처 포맷팅이 깔끔해졌습니다.

가독성을 해치지 않으면서 코드가 더 간결해졌습니다.

Also applies to: 65-66, 76-78, 85-87

src/main/java/hanium/modic/backend/domain/postLike/entity/PostStatisticsEntity.java (3)

37-38: 낙관적 락을 통한 동시성 제어가 잘 구현되었습니다.

@Version 어노테이션을 사용해 동시 수정 시 충돌을 방지하는 좋은 설계입니다.


56-58: 하트 수 감소 시 음수 방지가 잘 구현되었습니다.

Math.max를 사용해 안전하게 음수 방지 처리가 되어 있습니다.


63-68: 정적 팩토리 메서드가 명확하고 사용하기 쉽습니다.

새 게시글용 통계 엔티티 생성이 직관적이고 초기값 설정이 적절합니다.

src/test/java/hanium/modic/backend/web/post/controller/PostControllerTest.java (3)

138-159: 테스트 케이스가 좋아요 기능을 적절히 검증합니다.

인증된 사용자의 게시글 조회 테스트가 새로운 좋아요 관련 필드(likeCount, isLikedByCurrentUser)를 올바르게 검증하고 있습니다.


168-169: 실패 테스트도 일관성 있게 업데이트되었습니다.

서비스 메서드 시그니처 변경에 맞춰 적절히 수정되었습니다.


183-185: 게시물 목록 응답에 좋아요 수가 적절히 포함되었습니다.

목록 조회 시에도 각 게시물의 좋아요 수를 확인할 수 있도록 테스트가 업데이트되었습니다.

Also applies to: 212-212

src/main/java/hanium/modic/backend/web/post/dto/response/GetPostResponse.java (2)

27-30: 좋아요 관련 필드가 적절히 추가되었습니다.

isLikedByCurrentUser를 nullable로 처리하여 비로그인 사용자를 고려한 점이 좋습니다.


32-71: 하위 호환성을 유지한 훌륭한 설계입니다.

기존 API를 사용하는 코드에 영향을 주지 않으면서 새로운 기능을 추가했습니다.

src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java (4)

46-74: 좋아요 토글 로직이 잘 구현되었습니다.

게시글 존재 확인과 자기 게시글 좋아요 방지 검증이 적절합니다. PR 설명대로 동시성 처리는 별도 PR에서 다루실 예정이니 현재 구현은 적절합니다.


82-86: 안전한 좋아요 수 조회 구현입니다.

통계 데이터가 없을 때 0을 반환하는 처리가 적절합니다.


124-129: 향후 기능을 위한 적절한 준비입니다.

사용자가 좋아요한 게시글 목록 조회 기능이 잘 구현되었습니다.


138-160: 통계 정합성 검증 로직이 안전하게 구현되었습니다.

불일치 발견 시 로깅과 함께 통계를 재생성하는 방식이 데이터 정합성 보장에 적절합니다.

src/main/java/hanium/modic/backend/domain/postLike/service/AsyncPostStatisticsService.java (3)

28-43: 비동기 좋아요 증가 로직이 안정적으로 구현되었습니다.

통계 데이터가 없을 때 새로 생성하는 처리와 예외 발생 시 로깅만 하고 전파하지 않는 점이 적절합니다.


50-66: 좋아요 감소 로직이 안전하게 구현되었습니다.

ifPresentOrElse를 사용한 깔끔한 처리가 좋습니다. 통계 데이터가 없는 경우 경고 로그를 남기는 것도 적절합니다.


73-85: 통계 초기화 로직이 적절합니다.

이미 존재하는지 확인 후 생성하는 방어적 프로그래밍이 좋습니다.

Comment on lines +28 to +30
@Modifying
@Query("UPDATE PostStatisticsEntity p SET p.likeCount = p.likeCount + :delta WHERE p.postId = :postId")
void updateLikeCount(@Param("postId") Long postId, @Param("delta") int delta);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

updateLikeCount 메서드의 안전성을 향상시키세요.

현재 구현에서 delta 파라미터에 대한 제약이 없어 잘못된 값으로 인한 데이터 오염 가능성이 있습니다.

 @Modifying
+@Query("UPDATE PostStatisticsEntity p SET p.likeCount = CASE WHEN (p.likeCount + :delta) < 0 THEN 0 ELSE (p.likeCount + :delta) END WHERE p.postId = :postId")
-@Query("UPDATE PostStatisticsEntity p SET p.likeCount = p.likeCount + :delta WHERE p.postId = :postId")
 void updateLikeCount(@Param("postId") Long postId, @Param("delta") int delta);

또한 이 메서드를 호출하는 서비스 계층에서 @transactional 어노테이션이 있는지 확인하세요.

📝 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
@Modifying
@Query("UPDATE PostStatisticsEntity p SET p.likeCount = p.likeCount + :delta WHERE p.postId = :postId")
void updateLikeCount(@Param("postId") Long postId, @Param("delta") int delta);
@Modifying
@Query("UPDATE PostStatisticsEntity p SET p.likeCount = CASE WHEN (p.likeCount + :delta) < 0 THEN 0 ELSE (p.likeCount + :delta) END WHERE p.postId = :postId")
void updateLikeCount(@Param("postId") Long postId, @Param("delta") int delta);
🤖 Prompt for AI Agents
In
src/main/java/hanium/modic/backend/domain/postLike/repository/PostStatisticsEntityRepository.java
around lines 28 to 30, the updateLikeCount method lacks validation on the delta
parameter, risking data corruption from invalid values. Add validation to ensure
delta is only positive or negative as intended, preventing unintended increments
or decrements. Also, verify that the service layer method calling
updateLikeCount is annotated with @Transactional to maintain transaction
integrity during the update.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a 'like' feature for posts, utilizing a separate statistics table and asynchronous updates for performance. There's a critical race condition in PostLikeService.toggleLike that needs to be addressed to prevent data corruption, and the error handling in AsyncPostStatisticsService could be improved to prevent silent failures.

@goalSetter09
Copy link
Collaborator Author

#113 해결 필요

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: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8623e8 and 850bb0c.

📒 Files selected for processing (1)
  • src/test/java/hanium/modic/backend/domain/post/service/PostServiceTest.java (8 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (9)
src/test/java/hanium/modic/backend/domain/post/service/PostServiceTest.java (9)

12-12: 새로운 의존성 import가 적절하게 추가되었습니다.

좋아요 기능과 관련된 서비스들과 DTO가 올바르게 import되었습니다.

Also applies to: 38-39, 45-45


58-61: Mock 객체 설정이 적절합니다.

PostLikeService와 AsyncPostStatisticsService에 대한 Mock 객체가 올바르게 설정되었습니다.


113-154: 좋아요 기능이 포함된 단일 게시글 조회 테스트가 잘 구현되었습니다.

테스트 로직이 명확하고 모든 필요한 검증이 포함되어 있습니다. currentUserId 파라미터가 추가되고 좋아요 상태 검증이 적절히 구현되었습니다.


156-185: 좋아요하지 않은 사용자에 대한 테스트 케이스가 적절히 추가되었습니다.

사용자가 좋아요하지 않은 경우의 시나리오를 잘 커버하고 있습니다.


187-202: 예외 처리 테스트가 업데이트되었습니다.

currentUserId 파라미터가 추가되어 실제 서비스 메서드 시그니처와 일치합니다.


204-255: 배치 조회 최적화 테스트가 잘 구현되었습니다.

좋아요 수와 이미지를 배치로 조회하는 최적화 로직이 올바르게 테스트되고 있습니다. 성능 개선을 위한 중요한 테스트 케이스입니다.


268-268: 기존 테스트가 배치 조회 방식으로 업데이트되었습니다.

개별 조회에서 배치 조회로 변경된 것이 올바르게 테스트에 반영되었습니다.

Also applies to: 276-284, 297-298


316-316: 빈 결과에 대한 테스트가 배치 조회 방식으로 업데이트되었습니다.

배치 조회 메서드들이 호출되지 않아야 하는 경우에 대한 검증이 적절히 추가되었습니다.

Also applies to: 320-322


324-426: 단순 게시글 조회 기능에 대한 포괄적인 테스트가 추가되었습니다.

다양한 시나리오(게시글 존재, 게시글 없음, 이미지 없는 게시글)를 모두 커버하는 테스트가 잘 구현되었습니다. 특히 이미지가 없는 경우 null 처리에 대한 테스트도 포함되어 있어 견고합니다.

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: 3

🧹 Nitpick comments (2)
src/main/java/hanium/modic/backend/common/redis/distributedLock/LockManager.java (1)

101-101: 키 생성 방식 개선을 고려해보세요.

현재 문자열 연결을 사용하고 있는데, 가독성과 일관성을 위해 String.format()이나 formatted string을 사용하는 것이 좋을 것 같습니다.

-String key = REDISSON_POST_LIKE_LOCK_PREFIX + userId + ":" + postId;
+String key = String.format("%s%d:%d", REDISSON_POST_LIKE_LOCK_PREFIX, userId, postId);
src/test/java/hanium/modic/backend/domain/postLike/service/PostLikeServiceIntegrationTest.java (1)

125-129: Awaitility 타임아웃 설정을 확인하세요.

현재 3초로 설정되어 있는데, CI/CD 환경에서 간헐적으로 실패할 가능성이 있습니다. 필요시 더 긴 타임아웃을 고려해보세요.

-await().atMost(Duration.ofSeconds(3))
+await().atMost(Duration.ofSeconds(5))
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 850bb0c and e0a8c94.

📒 Files selected for processing (6)
  • src/main/java/hanium/modic/backend/common/error/ErrorCode.java (1 hunks)
  • src/main/java/hanium/modic/backend/common/redis/distributedLock/LockManager.java (2 hunks)
  • src/main/java/hanium/modic/backend/domain/postLike/repository/PostLikeEntityRepository.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java (1 hunks)
  • src/main/java/hanium/modic/backend/web/postLike/controller/PostLikeController.java (1 hunks)
  • src/test/java/hanium/modic/backend/domain/postLike/service/PostLikeServiceIntegrationTest.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/hanium/modic/backend/domain/postLike/repository/PostLikeEntityRepository.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/hanium/modic/backend/common/error/ErrorCode.java
  • src/main/java/hanium/modic/backend/web/postLike/controller/PostLikeController.java
  • src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/test/java/hanium/modic/backend/domain/postLike/service/PostLikeServiceIntegrationTest.java (2)
src/test/java/hanium/modic/backend/domain/post/entityfactory/PostFactory.java (1)
  • PostFactory (10-36)
src/test/java/hanium/modic/backend/domain/user/factory/UserFactory.java (1)
  • UserFactory (9-29)
🔇 Additional comments (3)
src/main/java/hanium/modic/backend/common/redis/distributedLock/LockManager.java (2)

29-30: 락 타임아웃 설정이 적절합니다.

좋아요 토글은 빠른 작업이므로 더 짧은 대기 시간(2초)과 유지 시간(1초)을 설정한 것이 합리적입니다.


95-120: 분산 락 구현이 잘 되어 있습니다.

기존 패턴을 잘 따르고 있으며, 예외 처리와 트랜잭션 관리가 적절히 구현되어 있습니다. 동시성 제어를 위한 분산 락 사용이 PR 목표에 부합합니다.

src/test/java/hanium/modic/backend/domain/postLike/service/PostLikeServiceIntegrationTest.java (1)

26-187: 통합 테스트가 전반적으로 잘 작성되었습니다.

동시성 시나리오를 포괄적으로 다루고 있으며, 결과적 일관성(eventual consistency)을 적절히 검증하고 있습니다. 분산 락과 비동기 통계 업데이트가 올바르게 동작하는지 확인하는 좋은 테스트입니다.

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: 0

♻️ Duplicate comments (1)
src/test/java/hanium/modic/backend/domain/postLike/service/PostLikeServiceIntegrationTest.java (1)

91-131: 다중 사용자 동시성 테스트가 잘 구현되어 있지만 자기 게시글 좋아요 방지 검증이 누락되었습니다.

다중 사용자 동시 좋아요 테스트 로직이 적절하고, 결과적 일관성을 위한 Awaitility 사용도 올바릅니다. 하지만 주석에서 "user1은 자신의 게시글이므로 제외"라고 명시했음에도 불구하고, 실제로 자기 게시글에 좋아요를 할 수 없는지 검증하는 별도 테스트가 없습니다.

다음과 같은 테스트 메서드 추가를 고려해보세요:

@Test
@DisplayName("자신의 게시글에는 좋아요를 할 수 없다")
void cannotLikeOwnPostTest() {
    // 예외가 발생하거나 좋아요가 추가되지 않음을 검증
    assertThatThrownBy(() -> postLikeService.toggleLike(user1.getId(), post.getId()))
        .isInstanceOf(IllegalArgumentException.class);
}
🧹 Nitpick comments (1)
src/test/java/hanium/modic/backend/domain/postLike/service/PostLikeServiceIntegrationTest.java (1)

26-188: 테스트 메서드명에 일관성을 위한 개선을 제안합니다.

테스트 메서드명과 DisplayName이 일관성 있게 작성되었지만, 더 명확한 네이밍을 위해 개선을 제안드립니다.

메서드명을 더 명확하게 개선할 수 있습니다:

-void sameUserConcurrentToggleLikeTest() throws InterruptedException {
+void sameUser_ConcurrentToggleLike_ShouldBeProcessedSequentially() throws InterruptedException {

-void multipleUsersConcurrentLikeTest() throws InterruptedException {
+void multipleUsers_ConcurrentLike_ShouldAllBeProcessedSuccessfully() throws InterruptedException {

-void concurrentLikeToggleStatisticsConsistencyTest() throws InterruptedException {
+void concurrentLikeToggle_ShouldMaintainStatisticsConsistency() throws InterruptedException {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0a8c94 and 51d2018.

📒 Files selected for processing (1)
  • src/test/java/hanium/modic/backend/domain/postLike/service/PostLikeServiceIntegrationTest.java (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/test/java/hanium/modic/backend/domain/postLike/service/PostLikeServiceIntegrationTest.java (2)
src/test/java/hanium/modic/backend/domain/post/entityfactory/PostFactory.java (1)
  • PostFactory (10-36)
src/test/java/hanium/modic/backend/domain/user/factory/UserFactory.java (1)
  • UserFactory (9-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
src/test/java/hanium/modic/backend/domain/postLike/service/PostLikeServiceIntegrationTest.java (4)

1-25: 패키지 구조와 임포트가 적절합니다.

테스트 클래스의 패키지 구조와 필요한 의존성 임포트가 올바르게 설정되어 있습니다. 동시성 테스트를 위한 ExecutorService, CountDownLatch, Awaitility 등의 도구들이 적절히 임포트되었습니다.


26-59: 테스트 셋업이 잘 구성되어 있습니다.

BaseIntegrationTest를 상속받아 통합 테스트 환경을 구성하고, 필요한 리포지토리와 서비스를 주입받았습니다. @beforeeach에서 데이터 초기화 순서도 올바르게 설정되어 있습니다.


61-89: 동시성 테스트 로직이 정확하고 ExecutorService 종료 처리가 개선되었습니다.

단일 사용자의 동시 좋아요 토글 테스트가 올바르게 구현되어 있습니다. 홀수 개의 요청(5번)으로 최종 상태를 예측 가능하게 만들었고, ExecutorService 종료 처리도 이전 리뷰 피드백을 반영하여 추가되었습니다.


133-188: 복잡한 동시성 시나리오 테스트가 우수합니다.

사전 설정 후 동시 토글 작업을 통한 통계 일관성 검증이 잘 구현되어 있습니다. 실제 데이터와 통계 데이터의 일치성을 확인하는 로직이 적절하며, ExecutorService 종료 처리도 포함되어 있습니다. 최종 상태 검증 로직도 명확합니다.

@goalSetter09 goalSetter09 merged commit f2cc83a into develop Jul 18, 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.

게시글 하트 기능

2 participants