[feat] 반응 조회 api & 마이페이지 유저 정보 조회 api 구현#132
Conversation
Walkthrough이번 변경에서는 사용자 반응(좋아요, 댓글) 조회 API가 새롭게 도입되었습니다. 이를 위해 컨트롤러, 서비스, 포트, 어댑터, 매퍼, DTO, 레포지토리 등 여러 계층에 걸쳐 관련 클래스와 메서드가 추가되었으며, 커서 기반 페이징 및 통합 응답 구조가 구현되었습니다. 통합 테스트도 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant UserQueryController
participant UserMyPageService
participant UserQueryPort
participant UserQueryPersistenceAdapter
participant UserQueryRepository
Client->>UserQueryController: GET /users/reactions (userId, type, size, cursor)
UserQueryController->>UserMyPageService: getUserReaction(userId, type, size, cursor)
UserMyPageService->>UserQueryPort: find*ReactionsByUserId(userId, cursor)
UserQueryPort->>UserQueryPersistenceAdapter: find*ReactionsByUserId(userId, cursor)
UserQueryPersistenceAdapter->>UserQueryRepository: find*ByUserId(userId, cursorDateTime, size)
UserQueryRepository-->>UserQueryPersistenceAdapter: List<ReactionQueryDto>
UserQueryPersistenceAdapter-->>UserQueryPort: CursorBasedList<ReactionQueryDto>
UserQueryPort-->>UserMyPageService: CursorBasedList<ReactionQueryDto>
UserMyPageService->>UserQueryMapper: toReactionDtoList()
UserMyPageService-->>UserQueryController: UserReactionResponse
UserQueryController-->>Client: BaseResponse<UserReactionResponse>
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
🧰 Additional context used🧠 Learnings (2)📓 Common learnings📚 Learning: thip 프로젝트에서 record와 vote는 room에 속하지만 feed는 room에 속하지 않는 구조이며, 댓글 작성 시 record/vote에 대해서만 사용자가 해당 room...Applied to files:
⏰ 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)
🔇 Additional comments (1)
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (5)
src/main/java/konkuk/thip/user/application/mapper/UserQueryMapper.java (1)
15-16: 파일 끝 불필요한 공백 라인 제거 권장파일 종료 전에 연속된 두 줄의 공백이 추가되었습니다. 팀 코딩 컨벤션에서 EOF에 단일 개행만 유지하도록 하고 있다면, 중복 공백은 제거하는 편이 좋습니다.
-src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java (1)
30-32: 필드명과 주석 개선 제안구현은 올바르지만 다음 개선사항을 제안합니다:
dtype필드명을discriminatorType또는postType으로 변경하면 더 명확할 것 같습니다.- 주석을 영어로 통일하는 것을 고려해보세요 (프로젝트 컨벤션에 따라).
- // type 구분을 위한 조회용 컬럼 - @Column(name = "dtype", insertable = false, updatable = false) - private String dtype; + // Read-only column for post type discrimination + @Column(name = "dtype", insertable = false, updatable = false) + private String discriminatorType;src/main/java/konkuk/thip/user/application/port/in/dto/UserReactionType.java (1)
19-27: 성능 개선 및 예외 메시지 개선 제안현재 구현은 올바르지만 다음 개선사항을 제안합니다:
- 성능 개선: 매번 배열을 순회하는 대신 정적 Map을 사용하면 더 효율적입니다.
- 예외 메시지: 국제화를 고려하여 영어 메시지 사용을 검토해보세요.
+import java.util.Map; +import java.util.stream.Collectors; +import java.util.Arrays; + @Getter public enum UserReactionType { LIKE("LIKE"), COMMENT("COMMENT"), BOTH("BOTH"); private final String type; + + private static final Map<String, UserReactionType> TYPE_MAP = + Arrays.stream(values()) + .collect(Collectors.toMap( + reactionType -> reactionType.getType().toUpperCase(), + reactionType -> reactionType + )); UserReactionType(String type) { this.type = type; } public static UserReactionType from(String type) { - for (UserReactionType reactionType : UserReactionType.values()) { - if (reactionType.getType().equalsIgnoreCase(type)) { - return reactionType; - } - } + UserReactionType reactionType = TYPE_MAP.get(type.toUpperCase()); + if (reactionType != null) { + return reactionType; + } throw new InvalidStateException(ErrorCode.API_INVALID_PARAM, - new IllegalArgumentException("유효하지 않은 사용자 반응 타입: " + type)); + new IllegalArgumentException("Invalid user reaction type: " + type)); }src/main/java/konkuk/thip/user/application/port/out/dto/ReactionQueryDto.java (1)
8-27: 레코드 구현이 잘 되었습니다.QueryDSL 통합을 위한
@QueryProjection사용과 모든 필드에 대한 null 체크가 적절히 구현되었습니다.id필드가 feedId 또는 postId를 나타낸다는 주석도 명확합니다.필요시 타입 안전성을 더 높이기 위해
id필드를feedId와postId로 분리하는 것을 고려해볼 수 있지만, 현재 구현도 충분히 명확합니다.src/main/java/konkuk/thip/user/adapter/in/web/response/UserReactionResponse.java (1)
5-29: 응답 DTO 구조가 잘 설계되었습니다.커서 기반 페이징을 위한 필드들과 중첩된 ReactionDto 구조가 적절합니다. 정적 팩토리 메서드 사용도 좋은 패턴입니다.
postDate필드가 String 타입인데, 일관성을 위해 다른 곳에서 사용하는 날짜 형식과 동일한지 확인해보세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
src/main/java/konkuk/thip/common/util/CursorBasedList.java(1 hunks)src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java(4 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/UserProfileResponse.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/UserReactionResponse.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java(3 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/function/ReactionQueryFunction.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepository.java(2 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java(2 hunks)src/main/java/konkuk/thip/user/application/mapper/ReactionQueryMapper.java(1 hunks)src/main/java/konkuk/thip/user/application/mapper/UserQueryMapper.java(1 hunks)src/main/java/konkuk/thip/user/application/port/in/UserMyPageUseCase.java(1 hunks)src/main/java/konkuk/thip/user/application/port/in/dto/UserReactionType.java(1 hunks)src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java(2 hunks)src/main/java/konkuk/thip/user/application/port/out/dto/ReactionQueryDto.java(1 hunks)src/main/java/konkuk/thip/user/application/service/UserMyPageService.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserReactionApiTest.java(1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.
📚 Learning: thip 프로젝트에서는 query api(조회 api)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response dto를 직접 ...
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.
Applied to files:
src/main/java/konkuk/thip/user/adapter/in/web/response/UserProfileResponse.javasrc/main/java/konkuk/thip/user/application/port/out/dto/ReactionQueryDto.javasrc/main/java/konkuk/thip/user/application/port/out/UserQueryPort.javasrc/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepository.javasrc/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java
📚 Learning: thip 프로젝트에서는 cqrs port 분리 시 다음 컨벤션을 따름: commandport에는 findbyxxx를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, querypo...
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
Applied to files:
src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.javasrc/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.javasrc/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepository.java
📚 Learning: commentcontrollertest는 댓글 생성 api의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(commentcreateapi...
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.
Applied to files:
src/test/java/konkuk/thip/user/adapter/in/web/UserReactionApiTest.java
🧬 Code Graph Analysis (2)
src/main/java/konkuk/thip/user/application/mapper/ReactionQueryMapper.java (1)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
DateUtil(12-62)
src/test/java/konkuk/thip/user/adapter/in/web/UserReactionApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(33-304)
⏰ 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 (20)
src/main/java/konkuk/thip/common/util/CursorBasedList.java (1)
17-19: LGTM!
isLast()메서드의 구현이 논리적으로 올바르며, 커서 기반 페이징에서 마지막 페이지 여부를 확인하는 용도로 적절합니다.src/main/java/konkuk/thip/user/adapter/out/persistence/function/ReactionQueryFunction.java (1)
8-11: LGTM!함수형 인터페이스 설계가 적절하며, 커서 기반 페이징을 위한 메서드 시그니처가 명확합니다. 다양한 반응 쿼리 유형을 추상화하기에 좋은 설계입니다.
src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java (2)
12-12: 새로운 import 및 의존성 추가가 적절합니다.UserReactionType enum과 UserMyPageUseCase 인터페이스의 import가 올바르게 추가되었습니다.
Also applies to: 27-27
50-50: 의존성 주입 필드 추가가 적절합니다.UserMyPageUseCase 의존성이 올바르게 추가되었습니다.
src/main/java/konkuk/thip/user/application/port/in/UserMyPageUseCase.java (1)
1-19: CQRS 패턴에 맞는 인터페이스 설계가 잘 되었습니다.Query API에서 application 계층이 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것은 팀 컨벤션에 따라 허용됩니다. 메서드 시그니처가 명확하고 책임이 잘 분리되어 있습니다.
src/main/java/konkuk/thip/user/application/service/UserMyPageService.java (1)
25-42: 사용자 반응 조회 로직이 잘 구현되었습니다.Switch 표현식을 사용한 반응 타입별 분기 처리, 커서 기반 페이징, 그리고 매퍼를 통한 DTO 변환이 깔끔하게 구현되었습니다.
src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java (1)
23-27: 새로운 반응 조회 메서드들이 잘 설계되었습니다.커서 기반 페이징을 지원하는 세 가지 반응 조회 메서드가 적절히 추가되었습니다. 메서드명이 명확하고 CQRS QueryPort 패턴을 잘 따르고 있습니다.
src/test/java/konkuk/thip/user/adapter/in/web/UserReactionApiTest.java (4)
45-49: 통합 테스트 설정이 적절합니다.Spring Boot 통합 테스트를 위한 어노테이션들과 프로파일 설정이 올바르게 구성되어 있습니다.
100-113: 테스트 데이터 준비가 체계적입니다.필요한 모든 엔티티들(alias, user, category, book, room, feed, record, vote)을 TestEntityFactory를 통해 생성하고 있어 테스트 환경이 잘 구성되었습니다.
115-139: 좋아요 반응 조회 테스트가 철저합니다.API 응답 구조와 데이터 검증이 상세하게 이루어지고 있습니다. JSON 경로별 검증이 적절합니다.
180-211: 커서 기반 페이징 테스트가 우수합니다.15개 댓글을 생성하여 첫 10개 조회 후 nextCursor로 나머지 5개를 조회하는 시나리오가 실제 사용 패턴을 잘 반영합니다. isLast 플래그 검증도 포함되어 있어 완전한 페이징 동작을 확인합니다.
src/main/java/konkuk/thip/user/application/mapper/ReactionQueryMapper.java (3)
14-23: MapStruct 매퍼 구성이 우수합니다.Spring 컴포넌트 모델 사용과 named 매핑 메서드를 통한 복잡한 매핑 로직 분리가 잘 되어 있습니다.
25-33: 포스트 타입별 ID 매핑이 적절합니다.FEED 타입은 feedId로, VOTE/RECORD 타입은 postId로 조건부 매핑하는 로직이 올바릅니다. 타입에 따른 적절한 null 처리도 포함되어 있습니다.
35-38: 시간 포맷팅이 일관성 있게 처리됩니다.기존 DateUtil.formatBeforeTime을 활용하여 상대적 시간 표시를 일관되게 처리하고 있습니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java (2)
51-64: 포트 인터페이스 구현이 깔끔합니다.세 가지 반응 조회 메서드가 getReactions 헬퍼 메서드를 통해 일관되게 구현되어 중복 코드가 제거되었습니다.
66-75: 커서 기반 페이징 로직이 잘 구현되었습니다.커서를 LocalDateTime으로 변환하고, 함수형 인터페이스를 활용한 추상화, CursorBasedList 생성 로직이 모두 적절합니다. createdAt을 다음 커서로 사용하는 것도 시간순 정렬에 적합합니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepository.java (1)
15-19: 레포지토리 인터페이스 확장이 적절합니다.커서 기반 페이징을 위한 세 가지 반응 조회 메서드가 일관된 파라미터 구조로 잘 정의되었습니다. LocalDateTime 커서 파라미터 사용이 시간 기반 페이징에 적합합니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java (3)
3-18: 새로운 imports 추가 확인됨반응 조회 API 구현을 위해 필요한 QueryDSL Q클래스들과 DTO가 적절히 추가되었습니다.
23-28: 새로운 imports 적절히 추가됨Stream API와 시간 관련 클래스들이 새로운 기능에 맞게 적절히 추가되었습니다.
151-160: 병합 로직 성능 및 페이징 정확성 검증 필요현재
findLikeAndCommentByUserId메서드는 두 개의 별도 쿼리 결과를 메모리에서 병합한 뒤 페이징 처리합니다. 대량의 데이터 환경에서 성능 및 정확성 문제가 발생할 수 있으며, 해당 로직에 대한 테스트 커버리지도 확인되지 않았습니다.확인 및 조치가 필요합니다:
- src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java
findLikeAndCommentByUserId메서드 성능 최적화 검토- 각 쿼리에서
size + 1만큼만 가져올 경우 병합 후 실제 페이지 크기보다 부족한 데이터가 반환될 여지- 테스트 커버리지
src/test/java/konkuk/thip/user/adapter/in/web/UserReactionApiTest.java등에서 해당 메서드 또는 API 경로에 대한 경계 조건(좋아요만, 댓글만, 혼합 시) 테스트가 존재하는지 확인- 없는 경우, 병합 로직에 대한 단위·통합 테스트 추가
권장 개선 방안:
public List<ReactionQueryDto> findLikeAndCommentByUserId(Long userId, LocalDateTime cursor, Integer size) { - List<ReactionQueryDto> likes = findLikeByUserId(userId, cursor, size); - List<ReactionQueryDto> comments = findCommentByUserId(userId, cursor, size); - return Stream.concat(likes.stream(), comments.stream()) - .sorted(Comparator.comparing(ReactionQueryDto::createdAt).reversed()) - .limit(size + 1) - .toList(); + // UNION 쿼리로 단일 호출하여 정렬 및 페이징 처리 + // 또는 각 쿼리에서 충분한 여유분(size + 1 이상) 데이터를 조회한 뒤 정확한 병합 로직 적용 }
| @Operation( | ||
| summary = "사용자 반응 조회", | ||
| description = "사용자가 남긴 반응(좋아요, 댓글)을 조회합니다." | ||
| ) | ||
| @GetMapping("/users/reactions") | ||
| public BaseResponse<UserReactionResponse> showUserReaction( | ||
| @Parameter(hidden = true) @UserId final Long userId, | ||
| @Parameter(description = "반응 타입 (LIKE, COMMENT) / default : 둘다", example = "LIKE") | ||
| @RequestParam(required = false, defaultValue = "BOTH") final String type, | ||
| @Parameter(description = "단일 요청 페이지 크기 (1~10) / default : 10", example = "10") | ||
| @RequestParam(defaultValue = "10") @Max(value = 10) @Min(value = 1) final int size, | ||
| @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") | ||
| @RequestParam(required = false) final String cursor) { | ||
| return BaseResponse.ok(userMyPageUseCase.getUserReaction(userId, UserReactionType.from(type), size, cursor)); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
타입 파라미터 검증 로직을 추가하세요.
API 엔드포인트 구현이 대체로 잘 되었지만, UserReactionType.from(type) 호출 시 잘못된 타입이 전달되면 예외가 발생할 수 있습니다. 컨트롤러 레벨에서 명시적인 검증을 추가하는 것을 고려해보세요.
다음과 같이 @Pattern 어노테이션을 추가하여 검증을 강화할 수 있습니다:
@Parameter(description = "반응 타입 (LIKE, COMMENT) / default : 둘다", example = "LIKE")
-@RequestParam(required = false, defaultValue = "BOTH") final String type,
+@RequestParam(required = false, defaultValue = "BOTH")
+@Pattern(regexp = "LIKE|COMMENT|BOTH", message = "타입은 LIKE, COMMENT, BOTH 중 하나여야 합니다") final String type,📝 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.
| @Operation( | |
| summary = "사용자 반응 조회", | |
| description = "사용자가 남긴 반응(좋아요, 댓글)을 조회합니다." | |
| ) | |
| @GetMapping("/users/reactions") | |
| public BaseResponse<UserReactionResponse> showUserReaction( | |
| @Parameter(hidden = true) @UserId final Long userId, | |
| @Parameter(description = "반응 타입 (LIKE, COMMENT) / default : 둘다", example = "LIKE") | |
| @RequestParam(required = false, defaultValue = "BOTH") final String type, | |
| @Parameter(description = "단일 요청 페이지 크기 (1~10) / default : 10", example = "10") | |
| @RequestParam(defaultValue = "10") @Max(value = 10) @Min(value = 1) final int size, | |
| @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") | |
| @RequestParam(required = false) final String cursor) { | |
| return BaseResponse.ok(userMyPageUseCase.getUserReaction(userId, UserReactionType.from(type), size, cursor)); | |
| } | |
| @Operation( | |
| summary = "사용자 반응 조회", | |
| description = "사용자가 남긴 반응(좋아요, 댓글)을 조회합니다." | |
| ) | |
| @GetMapping("/users/reactions") | |
| public BaseResponse<UserReactionResponse> showUserReaction( | |
| @Parameter(hidden = true) @UserId final Long userId, | |
| @Parameter(description = "반응 타입 (LIKE, COMMENT) / default : 둘다", example = "LIKE") | |
| @RequestParam(required = false, defaultValue = "BOTH") | |
| @Pattern(regexp = "LIKE|COMMENT|BOTH", message = "타입은 LIKE, COMMENT, BOTH 중 하나여야 합니다") final String type, | |
| @Parameter(description = "단일 요청 페이지 크기 (1~10) / default : 10", example = "10") | |
| @RequestParam(defaultValue = "10") @Max(value = 10) @Min(value = 1) final int size, | |
| @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") | |
| @RequestParam(required = false) final String cursor) { | |
| return BaseResponse.ok(userMyPageUseCase.getUserReaction(userId, UserReactionType.from(type), size, cursor)); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java around
lines 127 to 141, the type parameter passed to UserReactionType.from(type) can
cause exceptions if invalid values are provided. To fix this, add validation on
the 'type' request parameter by using the @Pattern annotation with a regex that
only allows valid values like "LIKE", "COMMENT", or "BOTH". This will ensure
invalid inputs are rejected before reaching the service layer, preventing
runtime exceptions.
| @Override | ||
| public List<ReactionQueryDto> findLikeByUserId(Long userId, LocalDateTime cursorLocalDateTime, Integer size) { | ||
| QUserJpaEntity user = QUserJpaEntity.userJpaEntity; | ||
| QPostLikeJpaEntity postLike = QPostLikeJpaEntity.postLikeJpaEntity; | ||
| QPostJpaEntity post = QPostJpaEntity.postJpaEntity; | ||
|
|
||
| BooleanBuilder where = new BooleanBuilder(); | ||
| where.and(user.userId.eq(userId)) | ||
| .and(post.status.eq(StatusType.ACTIVE)) | ||
| .and(postLike.status.eq(StatusType.ACTIVE)); | ||
| if (cursorLocalDateTime != null) { | ||
| where.and(postLike.createdAt.lt(cursorLocalDateTime)); | ||
| } | ||
|
|
||
| return queryFactory | ||
| .select(new QReactionQueryDto( | ||
| Expressions.constant("좋아요"), | ||
| post.postId, | ||
| post.userJpaEntity.nickname, | ||
| post.userJpaEntity.userId, | ||
| post.dtype, | ||
| post.content, | ||
| postLike.createdAt | ||
| )) | ||
| .from(postLike) | ||
| .join(postLike.postJpaEntity, post) | ||
| .join(postLike.userJpaEntity, user) | ||
| .where(where) | ||
| .orderBy(postLike.createdAt.desc()) | ||
| .limit(size + 1) | ||
| .fetch(); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
좋아요 조회 메서드 구현이 적절함
커서 기반 페이징과 적절한 조인을 통해 사용자의 좋아요 반응을 조회하는 로직이 잘 구현되었습니다. 다만 다음 사항들을 확인해주세요:
limit(size + 1)사용으로 다음 페이지 존재 여부를 확인할 수 있어 좋습니다.- 활성 상태 필터링이 적절히 적용되었습니다.
성능 최적화를 위해 다음 인덱스가 존재하는지 확인해주세요:
🏁 Script executed:
#!/bin/bash
# Description: 좋아요 조회 성능을 위한 인덱스 존재 여부 확인
# Expected: post_like 테이블에 (user_id, created_at, status) 복합 인덱스가 존재해야 함
echo "좋아요 조회 성능을 위한 인덱스 확인이 필요합니다:"
echo "1. PostLikeJpaEntity에서 (userJpaEntity.userId, createdAt, status) 복합 인덱스"
echo "2. PostJpaEntity에서 status 인덱스"
# 데이터베이스 스키마 파일에서 인덱스 정의 확인
fd -e sql -e ddl | xargs grep -l "post_like\|PostLike" | head -5Length of output: 355
🏁 Script executed:
#!/bin/bash
echo "JPA 엔티티의 인덱스 정의 확인:"
# PostLikeJpaEntity에서 @Table 및 @Index 어노테이션 검색
fd -e java -g "PostLikeJpaEntity.java" | xargs grep -n "@Table\|@Index"
# PostJpaEntity에서 @Table 및 @Index 어노테이션 검색
fd -e java -g "PostJpaEntity.java" | xargs grep -n "@Table\|@Index"Length of output: 244
인덱스 정의가 누락됨 – 성능 최적화용 인덱스 추가 필요
PostLikeJpaEntity 및 PostJpaEntity에서 조회 쿼드의 성능을 보장하기 위한 인덱스(@Index 또는 DB 마이그레이션)가 정의되어 있지 않습니다. 아래 인덱스를 추가해주세요:
- PostLikeJpaEntity.java
@Table( name = "post_likes", indexes = { @Index( name = "idx_post_likes_user_created_status", columnList = "user_id, created_at, status" ) } )
- PostJpaEntity.java
@Table( name = "posts", indexes = { @Index( name = "idx_posts_status", columnList = "status" ) } )
또는 Flyway/Liquibase 마이그레이션 스크립트에 아래 SQL을 추가할 수도 있습니다:
CREATE INDEX idx_post_likes_user_created_status
ON post_likes (user_id, created_at, status);
CREATE INDEX idx_posts_status
ON posts (status);위 인덱스가 반영된 후, 실제 쿼리 성능이 개선되었는지 확인 부탁드립니다.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java
lines 85 to 116, the query performance can be improved by adding database
indexes. Add the specified @Index annotations to the PostLikeJpaEntity and
PostJpaEntity classes on the columns user_id, created_at, status for post_likes
and status for posts respectively. Alternatively, create corresponding Flyway or
Liquibase migration scripts to add these indexes at the database level. After
applying the indexes, verify that the query performance has improved.
| @Override | ||
| public List<ReactionQueryDto> findCommentByUserId(Long userId, LocalDateTime cursorLocalDateTime, Integer size) { | ||
| QUserJpaEntity user = QUserJpaEntity.userJpaEntity; | ||
| QPostJpaEntity post = QPostJpaEntity.postJpaEntity; | ||
| QCommentJpaEntity comment = QCommentJpaEntity.commentJpaEntity; | ||
|
|
||
| BooleanBuilder where = new BooleanBuilder(); | ||
| where.and(user.userId.eq(userId)) | ||
| .and(post.status.eq(StatusType.ACTIVE)) | ||
| .and(comment.status.eq(StatusType.ACTIVE)); | ||
| if (cursorLocalDateTime != null) { | ||
| where.and(comment.createdAt.lt(cursorLocalDateTime)); | ||
| } | ||
|
|
||
| return queryFactory | ||
| .select(new QReactionQueryDto( | ||
| Expressions.constant("댓글"), | ||
| post.postId, | ||
| post.userJpaEntity.nickname, | ||
| post.userJpaEntity.userId, | ||
| post.dtype, | ||
| post.content, // 일단은 post의 content를 사용 (추후에 댓글 content로 수정 가능) | ||
| comment.createdAt | ||
| )) | ||
| .from(comment) | ||
| .join(comment.userJpaEntity, user) | ||
| .join(comment.postJpaEntity, post) | ||
| .where(where) | ||
| .orderBy(comment.createdAt.desc()) | ||
| .limit(size + 1) | ||
| .fetch(); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
댓글 조회 메서드에 개선점 존재
전반적인 구현은 좋으나 다음 사항들을 개선해주세요:
- Line 139:
post.content대신comment.content를 사용해야 합니다. 주석에서 언급했듯이 추후 수정이 필요한 부분입니다. - 조인 순서를 최적화할 수 있습니다.
다음과 같이 수정을 권장합니다:
.select(new QReactionQueryDto(
Expressions.constant("댓글"),
post.postId,
post.userJpaEntity.nickname,
post.userJpaEntity.userId,
post.dtype,
- post.content, // 일단은 post의 content를 사용 (추후에 댓글 content로 수정 가능)
+ comment.content,
comment.createdAt
))📝 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.
| @Override | |
| public List<ReactionQueryDto> findCommentByUserId(Long userId, LocalDateTime cursorLocalDateTime, Integer size) { | |
| QUserJpaEntity user = QUserJpaEntity.userJpaEntity; | |
| QPostJpaEntity post = QPostJpaEntity.postJpaEntity; | |
| QCommentJpaEntity comment = QCommentJpaEntity.commentJpaEntity; | |
| BooleanBuilder where = new BooleanBuilder(); | |
| where.and(user.userId.eq(userId)) | |
| .and(post.status.eq(StatusType.ACTIVE)) | |
| .and(comment.status.eq(StatusType.ACTIVE)); | |
| if (cursorLocalDateTime != null) { | |
| where.and(comment.createdAt.lt(cursorLocalDateTime)); | |
| } | |
| return queryFactory | |
| .select(new QReactionQueryDto( | |
| Expressions.constant("댓글"), | |
| post.postId, | |
| post.userJpaEntity.nickname, | |
| post.userJpaEntity.userId, | |
| post.dtype, | |
| post.content, // 일단은 post의 content를 사용 (추후에 댓글 content로 수정 가능) | |
| comment.createdAt | |
| )) | |
| .from(comment) | |
| .join(comment.userJpaEntity, user) | |
| .join(comment.postJpaEntity, post) | |
| .where(where) | |
| .orderBy(comment.createdAt.desc()) | |
| .limit(size + 1) | |
| .fetch(); | |
| } | |
| @Override | |
| public List<ReactionQueryDto> findCommentByUserId(Long userId, LocalDateTime cursorLocalDateTime, Integer size) { | |
| QUserJpaEntity user = QUserJpaEntity.userJpaEntity; | |
| QPostJpaEntity post = QPostJpaEntity.postJpaEntity; | |
| QCommentJpaEntity comment = QCommentJpaEntity.commentJpaEntity; | |
| BooleanBuilder where = new BooleanBuilder(); | |
| where.and(user.userId.eq(userId)) | |
| .and(post.status.eq(StatusType.ACTIVE)) | |
| .and(comment.status.eq(StatusType.ACTIVE)); | |
| if (cursorLocalDateTime != null) { | |
| where.and(comment.createdAt.lt(cursorLocalDateTime)); | |
| } | |
| return queryFactory | |
| .select(new QReactionQueryDto( | |
| Expressions.constant("댓글"), | |
| post.postId, | |
| post.userJpaEntity.nickname, | |
| post.userJpaEntity.userId, | |
| post.dtype, | |
| comment.content, | |
| comment.createdAt | |
| )) | |
| .from(comment) | |
| .join(comment.userJpaEntity, user) | |
| .join(comment.postJpaEntity, post) | |
| .where(where) | |
| .orderBy(comment.createdAt.desc()) | |
| .limit(size + 1) | |
| .fetch(); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java
between lines 118 and 149, the method findCommentByUserId incorrectly selects
post.content instead of comment.content as noted in the comment; update the
select clause to use comment.content. Additionally, optimize the join order by
starting from the post entity, then joining comment and user entities to improve
query performance.
hd0rable
left a comment
There was a problem hiding this comment.
수고하셨습니닷!! 간단한 리뷰 남겼는데 확인 부탁드립니당
| ) | ||
| @ExceptionDescription(GET_USER_FOLLOW) | ||
| @GetMapping("/users/my/following") | ||
| @GetMapping("/users/my-followings") |
|
|
||
| return queryFactory | ||
| .select(new QReactionQueryDto( | ||
| Expressions.constant("좋아요"), |
There was a problem hiding this comment.
p3: 여기에 하드코딩 대신 UserReactionType에 좋아요,댓글도 추가해서 UserReactionType.getValue() 이런식으로 쓰는 것은 어떨까요?
There was a problem hiding this comment.
UserReactionType은 파라미터로 받는 상수라고 생각해서 컨트롤러 쪽에서만 사용하려규 일부러 주입을 안 시킨거긴 한데, 가져와서 사용하는게 나을까요? 근데 BOTH에는 그러면 무의미한 문자열이 들어갈 것 같아서,, 아니면 ReactionLabel이라는 enum을 하나 persistence패키지에 둘까요??
There was a problem hiding this comment.
아하 근데 그 결국 파라미터의 값이 반환값이기도 하니 의존받아도될거같 긴한데 그러면 Both에은 무의미한 문자열이 들어갈것같기도 하네여
ReactionLabel이라는 enum을 하나 persistence패키지에 둘까요??
이렇게하면 그 enum을 파라미터에서도 쓰신다는건가요?? 아니면 따로 또 생성하신다는건지..? 따로 쓸거면 그냥 파라미터 값이 결국 반환값이니 의존받아도될것같다는 생각입니닷
There was a problem hiding this comment.
전 어차피 request 로 들어오는 String label 값을 내부에서 enum 으로 매핑 (+ 유효성 검증) 하고, enum type인 reactionType 을 다시 response로 응답하는 거니, UserQueryRepositoryImpl 에서 현재 구현된 UserReactionType enum의 의존성이 있어도 괜찮다고 생각합니다!
동일한 value를 가지는 enum을 adatper/in, adapter/out 에 중복해서 유지한다면, 해당 코드가 수정될 경우 2군데를 건드려야하는 번거로움이 있을것 같습니다
adapter/out 에서 enum 의존성이 있는게 별로라면,
- service에서 reactionType에 따라 분기처리하여 queryPort의 메서드를 호출할때 String 타입의 enum value를 넘기고
- 이걸 UserQueryRepositoryImpl 까지 넘기기
- UserQueryRepositoryImpl 에서는 전달받은 String type의 reactionType 을 이용해서 dto 구성하여 반환
-> both 의 경우에는 현재 코드 구조상 service에서 "like", "comment" 2개를 모두 전달하고, UserQueryRepositoryImpl 에서는 이를 받아 "like" 를 활용해 메서드 호출, "comment" 를 활용해 메서드 호출, 반환받은 결과를 ordering 해야할 것 같네요
위와 같은 방법도 있을 것 같습니다!
There was a problem hiding this comment.
오호 좋은 것 같습니다!! 성준님 말대로 enum에서 매핑해두고 서비스 로직에서 PersistenceAdapter쪽으로 넘겨줄때 enum에서 label을 문자열로 꺼내서 넘겨주는 쪽으로 수정해보겠습니다!
| post.userJpaEntity.nickname, | ||
| post.userJpaEntity.userId, | ||
| post.dtype, | ||
| post.content, // 일단은 post의 content를 사용 (추후에 댓글 content로 수정 가능) |
There was a problem hiding this comment.
엇 이거 왜 post의 content를 사용하신건가요??
There was a problem hiding this comment.
현재 댓글에 대한 반응을 누르면 해당하는 게시물로 이동이 되는 흐름으로 이해해서 commentId가 아닌 postId를 넘기고 있습니다. 따라서 보여주는 content도 댓글이 아닌 게시글을 보여주는게 더 자연스럽지 않을까 생각했습니다!(기획적인 부분이라 한번 물어봐야될 것 같네여)
| @ActiveProfiles("test") | ||
| @AutoConfigureMockMvc(addFilters = false) | ||
| @Transactional | ||
| @DisplayName("[통합] 사용자 반응 조회 API 통합 테스트") |
| @Override | ||
| public UserReactionResponse getUserReaction(Long userId, UserReactionType userReactionType, int size, String cursorStr) { | ||
|
|
||
| Cursor cursor = Cursor.from(cursorStr, size); |
| private CursorBasedList<ReactionQueryDto> getReactions(Long userId, Cursor cursor, ReactionQueryFunction reactionQueryFunction) { | ||
| LocalDateTime cursorLocalDateTime = cursor.isFirstRequest() ? null : cursor.getLocalDateTime(0); | ||
|
|
||
| List<ReactionQueryDto> reactionQueryDtos = reactionQueryFunction.fetch(userId, cursorLocalDateTime, cursor.getPageSize()); | ||
|
|
||
| return CursorBasedList.of(reactionQueryDtos, cursor.getPageSize(), reactionQueryDto -> { | ||
| Cursor nextCursor = new Cursor(List.of(reactionQueryDto.createdAt().toString())); | ||
| return nextCursor.toEncodedString(); | ||
| }); | ||
| } |
| BooleanBuilder where = new BooleanBuilder(); | ||
| where.and(user.userId.eq(userId)) | ||
| .and(post.status.eq(StatusType.ACTIVE)) | ||
| .and(postLike.status.eq(StatusType.ACTIVE)); |
There was a problem hiding this comment.
p3 : PostLike 는 hard delete 이므로 해당 조건문은 없어도 문제없을 것 같습니다!
|
|
||
| return queryFactory | ||
| .select(new QReactionQueryDto( | ||
| Expressions.constant("좋아요"), |
There was a problem hiding this comment.
전 어차피 request 로 들어오는 String label 값을 내부에서 enum 으로 매핑 (+ 유효성 검증) 하고, enum type인 reactionType 을 다시 response로 응답하는 거니, UserQueryRepositoryImpl 에서 현재 구현된 UserReactionType enum의 의존성이 있어도 괜찮다고 생각합니다!
동일한 value를 가지는 enum을 adatper/in, adapter/out 에 중복해서 유지한다면, 해당 코드가 수정될 경우 2군데를 건드려야하는 번거로움이 있을것 같습니다
adapter/out 에서 enum 의존성이 있는게 별로라면,
- service에서 reactionType에 따라 분기처리하여 queryPort의 메서드를 호출할때 String 타입의 enum value를 넘기고
- 이걸 UserQueryRepositoryImpl 까지 넘기기
- UserQueryRepositoryImpl 에서는 전달받은 String type의 reactionType 을 이용해서 dto 구성하여 반환
-> both 의 경우에는 현재 코드 구조상 service에서 "like", "comment" 2개를 모두 전달하고, UserQueryRepositoryImpl 에서는 이를 받아 "like" 를 활용해 메서드 호출, "comment" 를 활용해 메서드 호출, 반환받은 결과를 ordering 해야할 것 같네요
위와 같은 방법도 있을 것 같습니다!
| public UserProfileResponse getUserProfile(Long userId) { | ||
| User user = userCommandPort.findById(userId); | ||
|
|
||
| return UserProfileResponse.of(user.getAlias().getImageUrl(), user.getNickname(), user.getAlias().getValue()); |
| @Test | ||
| @DisplayName("커서 기반 페이징 정상 동작 확인") |
|
@hd0rable @seongjunnoh 리뷰 반영했습니다! 확인 부탁드릴게여~
|
| case LIKE -> userQueryPort.findLikeReactionsByUserId(userId, cursor, UserReactionType.LIKE.getLabel()); | ||
| case COMMENT -> userQueryPort.findCommentReactionsByUserId(userId, cursor, UserReactionType.COMMENT.getLabel()); | ||
| case BOTH -> userQueryPort.findBothReactionsByUserId(userId, cursor, UserReactionType.LIKE.getLabel(), UserReactionType.COMMENT.getLabel()); |
| post.userJpaEntity.nickname, | ||
| post.userJpaEntity.userId, | ||
| post.dtype, | ||
| comment.content, |
#️⃣ 연관된 이슈
📝 작업 내용
반응 조회 api 흐름은 다음과 같습니다.
마이페이지 유저 정보 조회 api도 같이 구현했습니다. (너무 간단해서 굳이 테스트코드는 짜지 않았습니다.)
📸 스크린샷
💬 리뷰 요구사항
추가적으로 PostJpaEntity에 조회용 dtype 필드를 추가했습니다! insertable과 updatable을 모두 false로 둬서 오직 getter를 통해 불러오는 것만 가능한 필드입니다!
@DiscriminatorValue에 설정해둔 FEED, VOTE, RECORD 이런식으로 반환됩니다.추후에 상속 구조를 없애는 걸로 되어있었는데 찾아보니 상속 전략에 JOINED 전략과 SIGNLE_TABLE 전략이 있더라구요. 자동 join이 발생하는 것을 방지하고 싶으면 상속 구조를 아예 없애는 것보다 SINGLE_TABLE 전략을 고려해보는 것이 더 좋은 선택일 것 같다는 생각이 듭니다!
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
Summary by CodeRabbit
신규 기능
테스트