feat: 커뮤니티 좋아요 API#120
Conversation
Walkthrough게시물 좋아요 기능을 구현하는 새로운 서비스, 엔티티, 리포지토리, API 컨트롤러 및 응답 DTO들이 추가되었습니다. Post 엔티티에서 Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant PostLikeController
participant PostLikeService
participant PostRepository
participant UserRepository
participant PostLikeRepository
participant Database
Client->>PostLikeController: POST /api/v1/posts/{postId}/likes
PostLikeController->>PostLikeService: toggleLike(postId, userId)
PostLikeService->>PostRepository: findById(postId)
PostRepository->>Database: SELECT post WHERE id = ?
Database-->>PostRepository: post
PostRepository-->>PostLikeService: post
alt post not found
PostLikeService-->>PostLikeController: throw BusinessException(POST_NOT_FOUND)
PostLikeController-->>Client: 404 error response
else post exists
PostLikeService->>UserRepository: findById(userId)
UserRepository->>Database: SELECT user WHERE id = ?
Database-->>UserRepository: user
UserRepository-->>PostLikeService: user
alt user not found
PostLikeService-->>PostLikeController: throw EntityNotFoundException
PostLikeController-->>Client: error response
else user exists
PostLikeService->>PostLikeRepository: findByPostIdAndUserId(postId, userId)
PostLikeRepository->>Database: SELECT postLike WHERE postId = ? AND userId = ?
Database-->>PostLikeRepository: postLike (if exists)
PostLikeRepository-->>PostLikeService: Optional<PostLike>
alt like exists
PostLikeService->>PostLikeRepository: delete(postLike)
PostLikeRepository->>Database: DELETE FROM postLike
Database-->>PostLikeRepository: success
PostLikeRepository-->>PostLikeService: void
PostLikeService->>PostLikeRepository: countByPostId(postId)
else like does not exist
PostLikeService->>PostLikeRepository: save(new PostLike)
PostLikeRepository->>Database: INSERT INTO postLike
Database-->>PostLikeRepository: saved postLike
PostLikeRepository-->>PostLikeService: postLike
PostLikeService->>PostLikeRepository: countByPostId(postId)
end
PostLikeRepository->>Database: SELECT COUNT(*) FROM postLike WHERE postId = ?
Database-->>PostLikeRepository: count
PostLikeRepository-->>PostLikeService: likeCount
PostLikeService-->>PostLikeController: PostLikeToggleResponse(liked, likeCount)
PostLikeController-->>Client: 200 OK with response
end
end
sequenceDiagram
participant Client
participant PostLikeController
participant PostLikeService
participant PostRepository
participant PostLikeRepository
participant Database
Client->>PostLikeController: GET /api/v1/posts/{postId}/likes/me
PostLikeController->>PostLikeService: getMyLikeStatus(postId, userId)
PostLikeService->>PostRepository: findById(postId)
PostRepository->>Database: SELECT post WHERE id = ?
Database-->>PostRepository: post
PostRepository-->>PostLikeService: post
alt post not found
PostLikeService-->>PostLikeController: throw BusinessException(POST_NOT_FOUND)
PostLikeController-->>Client: 404 error response
else post exists
PostLikeService->>PostLikeRepository: existsByPostIdAndUserId(postId, userId)
PostLikeRepository->>Database: SELECT EXISTS(...) FROM postLike WHERE postId = ? AND userId = ?
Database-->>PostLikeRepository: boolean
PostLikeRepository-->>PostLikeService: liked (true/false)
PostLikeService-->>PostLikeController: PostLikeStatusResponse(liked)
PostLikeController-->>Client: 200 OK with response
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/main/java/kr/co/webee/application/post/service/PostService.java (1)
29-40: 목록 조회의 per-row count 쿼리를 배치 집계로 줄이는 것을 권장합니다.현재
findAll(pageable)이후 게시글마다 좋아요 수와 댓글 수를 각각 조회해 목록 크기가 N이면 대략1 + 2N개의 쿼리가 발생합니다. 새로 추가된 Line 38 때문에 목록 API의 DB 부하가 더 커질 수 있으니, 현재 Slice의 post id들을 모아GROUP BY post_id집계로 한 번에 가져오는 방식이 좋겠습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/kr/co/webee/application/post/service/PostService.java` around lines 29 - 40, getAllPosts currently calls postRepository.findAll(pageable) then for each Post invokes postLikeRepository.countByPostId(...) and postCommentRepository.countByPostId(...), causing O(N) extra queries; instead, collect the Slice<Post> ids, call batch aggregate queries (e.g., new repository methods like countByPostIdInGroup or a custom `@Query` that returns postId->count maps) to fetch like and comment counts in two grouped queries, then map those counts back to each Post when calling PostListResponse.from so you build the Slice with no per-row count queries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/java/kr/co/webee/application/post/service/PostLikeService.java`:
- Around line 33-55: Add a safe concurrency fix for toggling likes: either (A)
enforce DB uniqueness by adding `@Table`(uniqueConstraints =
`@UniqueConstraint`(columnNames = {"post_id","user_id"})) to the PostLike entity
and create the corresponding migration, then catch
DataIntegrityViolationException in PostLikeService around
postLikeRepository.save(...) to convert the race-error into a successful
"already liked" PostLikeToggleResponse (use
postLikeRepository.countByPostId(postId) for the count), or (B) make the
check-then-write atomic by adding `@Lock`(LockModeType.PESSIMISTIC_WRITE) to the
repository method used by
postLikeRepository.findByPostIdAndUserId(postId,userId) so concurrent toggles
serialize; pick one approach and update PostLikeService to handle the chosen
behavior consistently (refer to PostLike, PostLikeService,
postLikeRepository.findByPostIdAndUserId, postLikeRepository.save, and
PostLikeToggleResponse).
In `@src/main/java/kr/co/webee/domain/post/entity/PostLike.java`:
- Around line 21-33: Add a DB-level unique constraint on (post_id, user_id) and
handle races in toggleLike(): annotate the PostLike entity with
`@Table`(uniqueConstraints = `@UniqueConstraint`(columnNames =
{"post_id","user_id"})) to ensure uniqueness at the schema level and add a DB
migration (ALTER TABLE ... ADD CONSTRAINT/UNIQUE INDEX) so the constraint exists
independent of ddl-auto; then update the toggleLike() flow to avoid
find-then-act races by attempting a single save and catching
DataIntegrityViolationException (or a specific constraint violation) to treat
concurrent insert as "already liked" and re-query using
repository.findByPostIdAndUserId(...) as needed, or perform a delete via
repository.deleteByPostAndUser(...) for unlike operations; ensure methods
countByPostId() and findByPostIdAndUserId() are still used only for
read-after-write verification, not for enforcing uniqueness.
---
Nitpick comments:
In `@src/main/java/kr/co/webee/application/post/service/PostService.java`:
- Around line 29-40: getAllPosts currently calls
postRepository.findAll(pageable) then for each Post invokes
postLikeRepository.countByPostId(...) and
postCommentRepository.countByPostId(...), causing O(N) extra queries; instead,
collect the Slice<Post> ids, call batch aggregate queries (e.g., new repository
methods like countByPostIdInGroup or a custom `@Query` that returns postId->count
maps) to fetch like and comment counts in two grouped queries, then map those
counts back to each Post when calling PostListResponse.from so you build the
Slice with no per-row count queries.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 77919fdc-0c07-4b72-9ba2-4f29d18a833e
📒 Files selected for processing (11)
src/main/java/kr/co/webee/application/post/service/PostLikeService.javasrc/main/java/kr/co/webee/application/post/service/PostService.javasrc/main/java/kr/co/webee/domain/post/entity/Post.javasrc/main/java/kr/co/webee/domain/post/entity/PostLike.javasrc/main/java/kr/co/webee/domain/post/repository/PostLikeRepository.javasrc/main/java/kr/co/webee/presentation/post/api/PostLikeApi.javasrc/main/java/kr/co/webee/presentation/post/controller/PostLikeController.javasrc/main/java/kr/co/webee/presentation/post/dto/response/PostDetailResponse.javasrc/main/java/kr/co/webee/presentation/post/dto/response/PostLikeStatusResponse.javasrc/main/java/kr/co/webee/presentation/post/dto/response/PostLikeToggleResponse.javasrc/main/java/kr/co/webee/presentation/post/dto/response/PostListResponse.java
💤 Files with no reviewable changes (1)
- src/main/java/kr/co/webee/domain/post/entity/Post.java
| return postLikeRepository.findByPostIdAndUserId(postId, userId) | ||
| .map(postLike -> { | ||
| postLikeRepository.delete(postLike); | ||
|
|
||
| return PostLikeToggleResponse.of( | ||
| false, | ||
| postLikeRepository.countByPostId(postId) | ||
| ); | ||
|
|
||
| }) | ||
| .orElseGet(() -> { | ||
| postLikeRepository.save( | ||
| PostLike.builder() | ||
| .post(post) | ||
| .user(user) | ||
| .build() | ||
| ); | ||
|
|
||
| return PostLikeToggleResponse.of( | ||
| true, | ||
| postLikeRepository.countByPostId(postId) | ||
| ); | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 설명: PostLike의 유니크 제약/잠금/충돌 처리 여부를 확인합니다.
# 기대 결과: (post_id, user_id) 유니크 제약과 DataIntegrityViolationException 처리 또는 잠금 전략이 확인되어야 합니다.
fd -i 'PostLike|schema|migration|changelog' -t f | xargs -r sed -n '1,220p'
rg -n -C3 --type java 'DataIntegrityViolationException|PESSIMISTIC_WRITE|LockModeType|findByPostIdAndUserId'
rg -n -C3 'uniqueConstraints|unique\s*=\s*true|post_id.*user_id|user_id.*post_id|CREATE UNIQUE|UNIQUE INDEX'Repository: BeeKeeprs/was
Length of output: 11640
🏁 Script executed:
# PostLike 엔티티의 완전한 정의 확인 (모든 어노테이션 포함)
cat src/main/java/kr/co/webee/domain/post/entity/PostLike.javaRepository: BeeKeeprs/was
Length of output: 1236
🏁 Script executed:
# 마이그레이션/스키마 파일 찾기
fd -type f \( -name '*.sql' -o -name '*.xml' -o -name '*.yaml' -o -name '*migration*' -o -name '*changelog*' \) | head -20Repository: BeeKeeprs/was
Length of output: 227
🏁 Script executed:
# ErrorType.VIOLATION_OCCURRED의 정의 확인
rg -n 'VIOLATION_OCCURRED' --type java -B2 -A2Repository: BeeKeeprs/was
Length of output: 1951
🏁 Script executed:
# 모든 마이그레이션/스키마 파일 찾기
find . -type f \( -name '*.sql' -o -name '*.xml' -o -name '*migration*' -o -name '*changelog*' \)Repository: BeeKeeprs/was
Length of output: 39
🏁 Script executed:
# 리소스 디렉토리에서 스키마 관련 파일 찾기
find src/main/resources src/test/resources -type f 2>/dev/null | grep -i -E '(schema|migration|liquibase|flyway|sql)'Repository: BeeKeeprs/was
Length of output: 39
🏁 Script executed:
# post_id와 user_id의 조합을 언급하는 SQL/XML 파일 검색
rg -l 'post_id.*user_id|user_id.*post_id|PostLike' --type sql --type xml 2>/dev/null || echo "No matches"Repository: BeeKeeprs/was
Length of output: 67
🏁 Script executed:
# `@Table` 어노테이션이 있는지 전체 PostLike 파일 확인
cat -n src/main/java/kr/co/webee/domain/post/entity/PostLike.java | head -40Repository: BeeKeeprs/was
Length of output: 1516
🏁 Script executed:
# 혹시 다른 곳에서 unique constraint를 정의했는지 확인
rg -i 'uniqueConstraints|@UniqueConstraint' --type javaRepository: BeeKeeprs/was
Length of output: 39
🏁 Script executed:
# H2 또는 다른 embedded DB 설정 확인
find . -name 'application*.yml' -o -name 'application*.yaml' -o -name 'application*.properties' | xargs -r grep -l 'spring.jpa\|spring.datasource' 2>/dev/null | head -5Repository: BeeKeeprs/was
Length of output: 39
동시 토글 요청에서 중복 좋아요나 500 응답이 발생할 수 있습니다.
현재 PostLike 엔티티에 @UniqueConstraint가 없고, 데이터베이스 마이그레이션 파일도 확인되지 않아 (post_id, user_id) 조합에 대한 유니크 제약이 없습니다. findByPostIdAndUserId 후 save/delete하는 check-then-write 구조에서 같은 사용자/게시글에 대한 동시 요청이 둘 다 "좋아요 없음"으로 판단할 수 있습니다.
결과:
- 유니크 제약이 없으면: 동시 요청 시 중복 좋아요가 저장됩니다.
- 유니크 제약이 있어도:
DataIntegrityViolationException이 발생하면 406 상태코드로 사용자에게 노출됩니다.
다음 중 하나를 적용해 주세요:
@Table(uniqueConstraints =@UniqueConstraint(columnNames = {"post_id", "user_id"}))추가 + 예외 처리 개선- 잠금 기반 접근:
findByPostIdAndUserId(postId, userId)쿼리에@Lock(LockModeType.PESSIMISTIC_WRITE)적용
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/kr/co/webee/application/post/service/PostLikeService.java`
around lines 33 - 55, Add a safe concurrency fix for toggling likes: either (A)
enforce DB uniqueness by adding `@Table`(uniqueConstraints =
`@UniqueConstraint`(columnNames = {"post_id","user_id"})) to the PostLike entity
and create the corresponding migration, then catch
DataIntegrityViolationException in PostLikeService around
postLikeRepository.save(...) to convert the race-error into a successful
"already liked" PostLikeToggleResponse (use
postLikeRepository.countByPostId(postId) for the count), or (B) make the
check-then-write atomic by adding `@Lock`(LockModeType.PESSIMISTIC_WRITE) to the
repository method used by
postLikeRepository.findByPostIdAndUserId(postId,userId) so concurrent toggles
serialize; pick one approach and update PostLikeService to handle the chosen
behavior consistently (refer to PostLike, PostLikeService,
postLikeRepository.findByPostIdAndUserId, postLikeRepository.save, and
PostLikeToggleResponse).
| @Entity | ||
| public class PostLike extends BaseTimeEntity { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "post_id", nullable = false) | ||
| private Post post; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "user_id", nullable = false) | ||
| private User user; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: PostLike의 post_id/user_id 복합 유니크 제약이 엔티티나 DB migration에 정의되어 있는지 확인합니다.
rg -n -C3 'PostLike|post_like|post_id|user_id|UniqueConstraint|uniqueConstraints|uk_post_like|UNIQUE|unique'Repository: BeeKeeprs/was
Length of output: 29487
🏁 Script executed:
cat -n src/main/java/kr/co/webee/domain/post/entity/PostLike.javaRepository: BeeKeeprs/was
Length of output: 1516
🏁 Script executed:
find . -type f \( -name "*.sql" -o -name "*.xml" -o -name "*V*.sql" -o -name "*V*.java" \) | grep -iE "(migration|flyway|liquibase)" | head -20Repository: BeeKeeprs/was
Length of output: 39
🏁 Script executed:
rg -l "post_like|PostLike" --type-list | grep -iE "(migration|sql|xml)" | head -10Repository: BeeKeeprs/was
Length of output: 446
🏁 Script executed:
find . -type d \( -name "migration" -o -name "migrations" -o -name "flyway" -o -name "liquibase" -o -name "db" \) 2>/dev/nullRepository: BeeKeeprs/was
Length of output: 39
🏁 Script executed:
fd -t f -e sql -e xml . | grep -iE "(migration|flyway|db)" | head -20Repository: BeeKeeprs/was
Length of output: 39
🏁 Script executed:
rg "post_like|PostLike" --type sql --type xml 2>/dev/null | head -30Repository: BeeKeeprs/was
Length of output: 39
🏁 Script executed:
find . -type f -name "*.sql" 2>/dev/null | head -20Repository: BeeKeeprs/was
Length of output: 39
🏁 Script executed:
ls -la src/main/resources/ 2>/dev/null || echo "No resources directory"Repository: BeeKeeprs/was
Length of output: 454
🏁 Script executed:
find . -path ./node_modules -prune -o -type d -name "resources" -print 2>/dev/nullRepository: BeeKeeprs/was
Length of output: 98
🏁 Script executed:
rg "CREATE TABLE.*post_like|CREATE TABLE.*PostLike" --type sql 2>/dev/null || rg "post_like" --type sql 2>/dev/null || echo "No SQL files found"Repository: BeeKeeprs/was
Length of output: 75
🏁 Script executed:
cat src/main/resources/application.yml | grep -A 10 -B 2 "jpa\|hibernate\|ddl"Repository: BeeKeeprs/was
Length of output: 358
🏁 Script executed:
rg "existsByPostIdAndUserId" -A 5 -B 2 src/main/java/kr/co/webee/domain/post/repository/PostLikeRepository.javaRepository: BeeKeeprs/was
Length of output: 158
🏁 Script executed:
cat -n src/main/java/kr/co/webee/application/post/service/PostLikeService.java | head -70Repository: BeeKeeprs/was
Length of output: 3171
post_id/user_id 복합 유니크 제약을 추가해 주세요.
현재 엔티티는 @Table 어노테이션으로 제약을 정의하지 않고 있으며, 데이터베이스 마이그레이션 파일도 없습니다. ddl-auto: update 설정에서는 엔티티 정의 없이는 제약이 생성되지 않습니다.
더욱이 toggleLike() 메서드의 find-then-act 패턴(라인 33-55)에서, 동시에 여러 요청이 들어오면 같은 사용자가 같은 게시글을 여러 번 좋아요한 중복 row가 생길 수 있습니다. 이는 countByPostId()의 결과를 오염시키고, findByPostIdAndUserId()의 단건 조회 가정도 깨뜨립니다.
🔒 제안 수정
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
import kr.co.webee.domain.common.BaseTimeEntity;
import kr.co.webee.domain.user.entity.User;
`@Getter`
`@NoArgsConstructor`(access = AccessLevel.PROTECTED)
`@Entity`
+@Table(
+ uniqueConstraints = `@UniqueConstraint`(
+ name = "uk_post_like_post_id_user_id",
+ columnNames = {"post_id", "user_id"}
+ )
+)
public class PostLike extends BaseTimeEntity {📝 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.
| @Entity | |
| public class PostLike extends BaseTimeEntity { | |
| @Id | |
| @GeneratedValue(strategy = GenerationType.IDENTITY) | |
| private Long id; | |
| @ManyToOne(fetch = FetchType.LAZY) | |
| @JoinColumn(name = "post_id", nullable = false) | |
| private Post post; | |
| @ManyToOne(fetch = FetchType.LAZY) | |
| @JoinColumn(name = "user_id", nullable = false) | |
| private User user; | |
| `@Entity` | |
| `@Table`( | |
| uniqueConstraints = `@UniqueConstraint`( | |
| name = "uk_post_like_post_id_user_id", | |
| columnNames = {"post_id", "user_id"} | |
| ) | |
| ) | |
| public class PostLike extends BaseTimeEntity { | |
| `@Id` | |
| `@GeneratedValue`(strategy = GenerationType.IDENTITY) | |
| private Long id; | |
| `@ManyToOne`(fetch = FetchType.LAZY) | |
| `@JoinColumn`(name = "post_id", nullable = false) | |
| private Post post; | |
| `@ManyToOne`(fetch = FetchType.LAZY) | |
| `@JoinColumn`(name = "user_id", nullable = false) | |
| private User user; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/kr/co/webee/domain/post/entity/PostLike.java` around lines 21 -
33, Add a DB-level unique constraint on (post_id, user_id) and handle races in
toggleLike(): annotate the PostLike entity with `@Table`(uniqueConstraints =
`@UniqueConstraint`(columnNames = {"post_id","user_id"})) to ensure uniqueness at
the schema level and add a DB migration (ALTER TABLE ... ADD CONSTRAINT/UNIQUE
INDEX) so the constraint exists independent of ddl-auto; then update the
toggleLike() flow to avoid find-then-act races by attempting a single save and
catching DataIntegrityViolationException (or a specific constraint violation) to
treat concurrent insert as "already liked" and re-query using
repository.findByPostIdAndUserId(...) as needed, or perform a delete via
repository.deleteByPostAndUser(...) for unlike operations; ensure methods
countByPostId() and findByPostIdAndUserId() are still used only for
read-after-write verification, not for enforcing uniqueness.
🧷 이슈
🔨 작업 내용
👀 리뷰 요구사항
Summary by CodeRabbit
새로운 기능