[feat] 내 팔로잉 리스트 조회 api 개발#94
Conversation
|
""" Walkthrough본 변경사항은 팔로워/팔로잉 목록 조회 기능에 커서 기반 페이징을 도입하고, 팔로잉(내가 팔로우한 유저) 리스트 API를 신규 추가합니다. 이를 위해 공통 유틸, DTO, 매퍼, 서비스, 포트, 어댑터, 컨트롤러 등 전반적인 구조가 리팩토링 및 확장되었으며, 관련 테스트 코드도 추가 및 수정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant UserQueryController
participant UserGetFollowService
participant FollowingQueryPort
participant FollowDtoMapper
Client->>UserQueryController: GET /users/{id}/followers?cursor&size
UserQueryController->>UserGetFollowService: getUserFollowers(userId, cursor, size)
UserGetFollowService->>FollowingQueryPort: getFollowersByUserId(userId, cursor, size)
FollowingQueryPort-->>UserGetFollowService: CursorBasedList<FollowQueryDto>
UserGetFollowService->>FollowDtoMapper: toFollowerList(FollowQueryDto)
FollowDtoMapper-->>UserGetFollowService: UserFollowersResponse.Follower
UserGetFollowService-->>UserQueryController: UserFollowersResponse
UserQueryController-->>Client: BaseResponse<UserFollowersResponse>
Client->>UserQueryController: GET /users/my/following?cursor&size
UserQueryController->>UserGetFollowService: getMyFollowing(userId, cursor, size)
UserGetFollowService->>FollowingQueryPort: getFollowingByUserId(userId, cursor, size)
FollowingQueryPort-->>UserGetFollowService: CursorBasedList<FollowQueryDto>
UserGetFollowService->>FollowDtoMapper: toFollowingList(FollowQueryDto)
FollowDtoMapper-->>UserGetFollowService: UserFollowingResponse.Following
UserGetFollowService-->>UserQueryController: UserFollowingResponse
UserQueryController-->>Client: BaseResponse<UserFollowingResponse>
Estimated code review effort4 (60–120 minutes) Possibly related PRs
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ 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)
✨ Finishing Touches
🪧 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: 3
🧹 Nitpick comments (4)
src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java (1)
21-21: followerCount null 처리를 명시적으로 개선해주세요.주석으로만 처리하는 대신, 명시적으로 null 허용 여부를 나타내는 것이 좋겠습니다.
다음과 같이 개선할 수 있습니다:
-// Assert.notNull(followerCount, "followerCount must not be null"); // 내 팔로잉 목록 조회에서는 필요 x + // followerCount는 팔로잉 목록 조회 시 null 허용또는
@Nullable어노테이션을 필드에 추가하여 명시적으로 표현할 수도 있습니다.src/main/java/konkuk/thip/common/util/CursorBasedList.java (1)
10-15: 효율적인 커서 기반 페이지네이션 유틸리티입니다.페이지네이션 로직이 정확하고 재사용 가능한 구조로 잘 설계되어 있습니다. 다만 엣지 케이스 처리를 고려해보세요.
빈 리스트나 size가 0인 경우에 대한 방어 로직을 추가할 수 있습니다:
public static <T> CursorBasedList<T> of(List<T> queryList, int size, CursorExtractor<T> extractor) { + if (queryList == null || queryList.isEmpty() || size <= 0) { + return new CursorBasedList<>(List.of(), null, false); + } boolean hasNext = queryList.size() > size; List<T> contents = hasNext ? queryList.subList(0, size) : queryList; String nextCursor = hasNext ? extractor.extractCursor(contents.get(size - 1)) : null; return new CursorBasedList<>(contents, nextCursor, hasNext); }src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java (1)
47-50: 메서드명과 파라미터 불일치메서드명
getMyFollowing은 현재 인증된 사용자의 팔로잉 목록을 조회하는 것을 암시하지만, 실제로는userId파라미터를 받아 해당 사용자의 팔로잉을 조회합니다. PR 목표에 따르면 "내 팔로잉 리스트 조회"가 목적이므로, 이것이 의도된 설계인지 확인이 필요합니다.만약 실제로 인증된 사용자만을 위한 메서드라면, 메서드명을
getUserFollowing으로 변경하거나, 컨트롤러 레벨에서 인증된 사용자의 ID만 전달되도록 보장하는 것이 좋습니다.src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (1)
21-42: 중복 코드 리팩토링 제안두 메서드가 거의 동일한 로직을 가지고 있습니다. 커서 파싱과 CursorBasedList 생성 로직을 공통 private 메서드로 추출하면 유지보수성이 향상될 것입니다.
+ private LocalDateTime parseCursor(String cursor) { + return cursor != null && !cursor.isBlank() ? DateUtil.parseDateTime(cursor) : null; + } + @Override public CursorBasedList<FollowQueryDto> getFollowersByUserId(Long userId, String cursor, int size) { - LocalDateTime cursorVal = cursor != null && !cursor.isBlank() ? DateUtil.parseDateTime(cursor) : null; + LocalDateTime cursorVal = parseCursor(cursor); List<FollowQueryDto> followerDtos = followingJpaRepository.findFollowerDtosByUserIdBeforeCreatedAt( userId, cursorVal, size ); return CursorBasedList.of(followerDtos, size, followerDto -> followerDto.createdAt().toString()); } @Override public CursorBasedList<FollowQueryDto> getFollowingByUserId(Long userId, String cursor, int size) { - LocalDateTime cursorVal = cursor != null && !cursor.isBlank() ? DateUtil.parseDateTime(cursor) : null; + LocalDateTime cursorVal = parseCursor(cursor); List<FollowQueryDto> followingDtos = followingJpaRepository.findFollowingDtosByUserIdBeforeCreatedAt( userId, cursorVal, size ); return CursorBasedList.of(followingDtos, size, followingDto -> followingDto.createdAt().toString()); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (20)
build.gradle(1 hunks)src/main/java/konkuk/thip/common/util/CursorBasedList.java(1 hunks)src/main/java/konkuk/thip/common/util/CursorExtractor.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java(2 hunks)src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java(2 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java(2 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowingResponse.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java(2 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java(2 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java(2 hunks)src/main/java/konkuk/thip/user/application/mapper/FollowDtoMapper.java(1 hunks)src/main/java/konkuk/thip/user/application/port/in/UserGetFollowUsecase.java(1 hunks)src/main/java/konkuk/thip/user/application/port/in/UserGetFollowersUsecase.java(0 hunks)src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java(1 hunks)src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java(1 hunks)src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java(1 hunks)src/main/java/konkuk/thip/user/application/service/following/UserGetFollowersService.java(0 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java(5 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowingApiTest.java(1 hunks)
💤 Files with no reviewable changes (2)
- src/main/java/konkuk/thip/user/application/port/in/UserGetFollowersUsecase.java
- src/main/java/konkuk/thip/user/application/service/following/UserGetFollowersService.java
🧰 Additional context used
🧠 Learnings (8)
src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java (2)
Learnt from: buzz0331
PR: #75
File: src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryRepositoryImpl.java:50-83
Timestamp: 2025-07-14T14:19:38.796Z
Learning: Vote와 VoteItem 엔티티는 자주 함께 사용되므로, N+1 문제를 방지하기 위해 양방향 매핑과 fetch join을 고려하는 것이 좋습니다. 특히 기록장 조회 API 등에서도 함께 사용될 가능성이 높습니다.
Learnt from: seongjunnoh
PR: #36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.
src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java (1)
Learnt from: seongjunnoh
PR: #93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java:369-388
Timestamp: 2025-07-21T08:18:15.744Z
Learning: THIP 프로젝트의 커서 페이지네이션에서는 클라이언트가 "현재 조회할 페이지의 첫 번째 레코드 정보"를 cursor로 전달하며, 서버는 해당 커서 이상(inclusive)의 데이터를 조회하도록 goe, loe를 사용하여 구현되어 있다.
src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java (1)
Learnt from: seongjunnoh
PR: #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로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/common/util/CursorBasedList.java (1)
Learnt from: seongjunnoh
PR: #93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java:369-388
Timestamp: 2025-07-21T08:18:15.744Z
Learning: THIP 프로젝트의 커서 페이지네이션에서는 클라이언트가 "현재 조회할 페이지의 첫 번째 레코드 정보"를 cursor로 전달하며, 서버는 해당 커서 이상(inclusive)의 데이터를 조회하도록 goe, loe를 사용하여 구현되어 있다.
src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java (1)
Learnt from: seongjunnoh
PR: #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로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java (3)
Learnt from: seongjunnoh
PR: #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로부터 조회하는 메서드를 추가함.
Learnt from: buzz0331
PR: #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: seongjunnoh
PR: #36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (1)
Learnt from: seongjunnoh
PR: #93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java:369-388
Timestamp: 2025-07-21T08:18:15.744Z
Learning: THIP 프로젝트의 커서 페이지네이션에서는 클라이언트가 "현재 조회할 페이지의 첫 번째 레코드 정보"를 cursor로 전달하며, 서버는 해당 커서 이상(inclusive)의 데이터를 조회하도록 goe, loe를 사용하여 구현되어 있다.
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java (1)
Learnt from: seongjunnoh
PR: #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로부터 조회하는 메서드를 추가함.
🧬 Code Graph Analysis (2)
src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowingApiTest.java (2)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(21-172)src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java (1)
SpringBootTest(29-101)
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (1)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
DateUtil(12-62)
⏰ 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 (23)
src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java (1)
24-24: 관계 필드의 의미를 명확히 하는 주석 추가 잘하셨습니다팔로우 관계에서 혼동하기 쉬운 필드들의 역할을 명확히 하는 주석이 추가되어 코드 가독성이 향상되었습니다.
Also applies to: 28-28
build.gradle (1)
68-70: MapStruct 의존성 추가 적절합니다DTO 매핑 책임 분리를 위한 MapStruct 라이브러리 추가가 올바르게 구성되었습니다. 런타임 의존성과 애노테이션 프로세서가 모두 포함되어 있어 정상적으로 동작할 것입니다.
src/main/java/konkuk/thip/common/util/CursorExtractor.java (1)
1-6: 커서 기반 페이지네이션을 위한 함수형 인터페이스 설계 좋습니다제네릭을 활용한 재사용 가능한 커서 추출 인터페이스가 잘 설계되었습니다. 함수형 인터페이스 패턴을 올바르게 적용하고 단일 책임 원칙을 준수하고 있습니다.
src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java (1)
36-38: API 메서드 문서화 잘하셨습니다각 컨트롤러 메서드에 대한 명확하고 간결한 JavaDoc 주석이 추가되어 코드의 가독성과 유지보수성이 향상되었습니다.
Also applies to: 49-51, 59-61
src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java (1)
9-12: 커서 기반 페이지네이션에 맞는 응답 구조 개선필드명을
followers로 간소화하고,nextCursor를 String 타입으로 변경하여 새로운 커서 기반 페이지네이션 구조와 일치시킨 변경사항이 적절합니다. 불필요한 필드들을 제거하여 응답 구조가 더 간결해졌습니다.src/main/java/konkuk/thip/user/application/mapper/FollowDtoMapper.java (1)
1-14: 깔끔한 MapStruct 매퍼 구현입니다.MapStruct를 활용한 DTO 매핑 로직이 잘 구현되어 있습니다. Spring 컴포넌트 모델 사용과 메서드 네이밍이 적절합니다.
src/main/java/konkuk/thip/user/application/port/in/UserGetFollowUsecase.java (1)
6-10: 잘 설계된 유스케이스 인터페이스입니다.헥사고날 아키텍처 원칙을 잘 따르고 있으며, 커서 기반 페이지네이션 파라미터가 일관되게 적용되어 있습니다.
src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java (3)
33-33: @transactional 어노테이션 추가로 테스트 정리가 개선되었습니다.수동 데이터베이스 정리를 대체하여 테스트 코드가 더 깔끔해졌습니다.
79-79: API 응답 구조 변경이 테스트에 올바르게 반영되었습니다.
"followerList"에서"followers"로의 필드명 변경이 테스트에 정확히 적용되어 있습니다.Also applies to: 97-97
72-100: 커서 기반 페이지네이션 테스트가 포괄적으로 구현되었습니다.첫 페이지와 후속 페이지 모두에 대한 테스트 커버리지가 우수하며, 커서 추출 및 사용 로직이 정확히 검증되고 있습니다.
src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java (1)
3-8: CQRS 컨벤션을 잘 따른 깔끔한 Query Port 인터페이스입니다.커서 기반 페이징을 위한 일관된 메서드 시그니처와
CursorBasedList<FollowQueryDto>반환 타입으로 표준화된 조회 로직을 제공합니다. 메서드 명도 팔로워/팔로잉을 명확히 구분하여 가독성이 좋습니다.src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowingResponse.java (1)
7-21: 모던 Java 패턴을 잘 활용한 Response DTO입니다.Record 문법과 중첩 구조를 통해 깔끔하게 설계되었으며, 커서 기반 페이징에 필요한
nextCursor와isLast필드가 적절히 포함되어 있습니다.@Builder어노테이션으로 객체 생성의 편의성도 제공합니다.src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowingApiTest.java (3)
29-52: 표준적인 Spring Boot 통합 테스트 설정입니다.기존 팔로워 API 테스트와 일관된 구조로 작성되어 있으며, 필요한 의존성들이 적절히 주입되어 있습니다.
54-68: 페이징 테스트를 위한 적절한 테스트 데이터 설정입니다.12명의 팔로잉 사용자로 2페이지에 걸친 페이징을 검증할 수 있도록 잘 구성되어 있으며,
TestEntityFactory를 활용하여 일관된 엔티티 생성을 보장합니다.
70-102: 커서 기반 페이징을 철저히 검증하는 테스트입니다.2페이지에 걸친 요청을 통해 페이징 로직을 체계적으로 검증하고 있으며, 응답 구조와 페이징 메타데이터를 모두 확인합니다.
requestAttr("userId")를 통한 인증 컨텍스트 시뮬레이션도 적절합니다.src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java (1)
4-4: 쿼리 성능 최적화를 위한 좋은 아키텍처 개선입니다.JPA 엔티티 대신
FollowQueryDto를 반환하여 필요한 필드만 선택적으로 조회할 수 있게 되었고, 커서 기반 페이징을 위한 일관된 메서드 시그니처를 제공합니다. 메서드명도 팔로워/팔로잉 쿼리를 명확히 구분하여 가독성이 좋습니다.Also applies to: 13-14
src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java (3)
3-4: 팔로우 관련 기능을 통합한 좋은 아키텍처 개선입니다.
UserGetFollowUsecase로 통합하여 코드 응집도를 높였고, 유효성 검증과 새로운 응답 타입을 위한 임포트가 적절히 추가되었습니다.Also applies to: 6-6, 8-8, 10-10, 23-23
35-43: 페이징 크기 제어와 문서화가 개선된 팔로워 조회 엔드포인트입니다.
@Max(10) @Min(1)검증으로 적절한 페이지 크기를 보장하고, JavaDoc 주석으로 문서화를 개선했습니다. 통합된 유스케이스 사용도 깔끔합니다.
45-53: 기존 패턴을 일관되게 따른 새로운 팔로잉 조회 엔드포인트입니다.
@UserId어노테이션으로 인증 처리를 깔끔하게 하고, 팔로워 엔드포인트와 동일한 유효성 검증 패턴을 적용하여 일관성을 유지했습니다.src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java (1)
29-31: 사용자 존재 여부 검증 로직 확인 및 예외 처리 보장 필요
UserCommandPort.findById(userId)호출 시, 존재하지 않는userId에 대해 예외(예:EntityNotFoundException)를 던지는지 확인해주세요.- 현재 구현체(
UserCommandPersistenceAdapter)가userJpaRepository.findById(id).orElseThrow(...)형태로 예외를 처리하고 있지 않다면,orElseThrow등을 통해 반드시 예외가 발생하도록 수정해야 합니다.- 서비스 레이어(
UserGetFollowService)에서 예외 발생 시 적절한 HTTP 상태 코드(404 등)로 매핑되는지 함께 검토 바랍니다.src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (1)
22-29: 커서 형식 일관성 확인 완료DateUtil.parseDateTime이 ISO_LOCAL_DATE_TIME 포맷을 사용하고, LocalDateTime.toString()도 동일한 ISO_LOCAL_DATE_TIME 형식(소수점 이하 초 단위 포함 가능)으로 출력을 보장하므로 커서 생성 및 파싱 간 형식 불일치는 없습니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java (2)
83-84: 조인 alias 매개변수 확인 필요
leftJoin(targetUser, user)에서 두 번째 인자user는 이미 사용 중인 엔티티 alias입니다. QueryDSL의leftJoin(Path<?>, Path<?>)메서드 두 번째 파라미터는 새로운 별칭(alias)이어야 하므로, 별도 alias 변수(예:targetUserAlias)를 생성·사용했는지 검토해주세요.발견 위치:
- src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java (Lines 83–84)
74-81: DTO 프로젝션 @QueryProjection 적용 확인 완료FollowQueryDto 생성자에
@QueryProjection어노테이션이 올바르게 적용되어 있어QFollowQueryDto프로젝션을 안전하게 사용할 수 있습니다. 별도 수정이 불필요합니다.
- 확인 위치: src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java:15
| List<FollowQueryDto> followerDtos = followingJpaRepository.findFollowerDtosByUserIdBeforeCreatedAt( | ||
| userId, | ||
| cursorVal, | ||
| size | ||
| ); |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
커서 기반 페이지네이션 구현 확인
메서드명 findFollowerDtosByUserIdBeforeCreatedAt는 커서 이전(exclusive) 데이터를 조회하는 것을 암시하지만, 학습된 내용에 따르면 THIP 프로젝트는 커서 이상(inclusive)의 데이터를 조회해야 합니다.
🏁 Script executed:
#!/bin/bash
# Repository 구현에서 실제 쿼리 조건 확인
rg -B 5 -A 10 "findFollowerDtosByUserIdBeforeCreatedAt.*cursor.*size" src/main/java/konkuk/thip/user/adapter/out/persistence/repository/Length of output: 3627
🏁 Script executed:
#!/bin/bash
# find the implementation of findFollowDtos to check cursor comparison operator
rg -n "private List<FollowQueryDto> findFollowDtos" -A 20 src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.javaLength of output: 1269
커서 이상(inclusive) 조회로 조건 수정 필요
THIP 프로젝트의 커서 페이지네이션 컨벤션에 따라, 전달받은 커서 이상의 데이터를 조회해야 합니다. 현재 findFollowDtos 구현에서 following.createdAt.lt(cursor)로 strict하게(exclusive) 필터링하고 있으므로, loe(cursor)로 수정해주세요.
- 대상 파일:
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java - 대상 라인:
// 변경 전 (exclusive) if (cursor != null) { condition.and(following.createdAt.lt(cursor)); }
- 변경 제안:
if (cursor != null) { - condition.and(following.createdAt.lt(cursor)); + condition.and(following.createdAt.loe(cursor)); }
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java
around the lines where the cursor condition is applied, change the filtering
condition from exclusive (following.createdAt.lt(cursor)) to inclusive
(following.createdAt.loe(cursor)) to align with the THIP project's cursor
pagination convention that requires fetching data with createdAt values greater
than or equal to the cursor.
| if (cursor != null) { | ||
| condition.and(following.createdAt.lt(cursor)); | ||
| } |
There was a problem hiding this comment.
커서 조건이 프로젝트 컨벤션과 불일치
학습된 내용에 따르면 THIP 프로젝트의 커서 페이지네이션은 "커서 이상(inclusive)"의 데이터를 조회해야 하므로 goe/loe를 사용해야 합니다. 하지만 현재 구현은 lt()를 사용하여 exclusive 조건으로 되어 있습니다.
if (cursor != null) {
- condition.and(following.createdAt.lt(cursor));
+ condition.and(following.createdAt.loe(cursor));
}🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java
at lines 67 to 69, the cursor pagination condition uses an exclusive less-than
(lt) comparison, which conflicts with the THIP project convention requiring
inclusive cursor pagination. Change the condition to use a greater-than-or-equal
(goe) or less-than-or-equal (loe) comparison as appropriate to include the
cursor value in the query results.
hd0rable
left a comment
There was a problem hiding this comment.
수고하셨어요~~
확실히 복잡한 쿼리를 QueryDSL로 다룰 때, 공통 DTO로 추출해두면 코드가 훨씬 가벼워지네요
다만, 이전 회의에서 이야기했던 것처럼 도메인이 달라지면 공통 DTO를 재사용하지 못하고 계속 새로운 DTO가 늘어날 수 있다는 점은 고민이 필요한 부분인 것 같아요.
그래서 나중에 리펙토링할 때는
"어떤 조회에서 다른 도메인의 데이터까지 함께 사용하는가?"
"그게 진짜 필요한 설계인가?"
이런 기준을 먼저 세우고, 필요한 곳만 공통화하거나 분리하면 좋을 것 같습니다!!👍🏻
| private final UserGetFollowersUsecase userGetFollowersUsecase; | ||
| private final UserGetFollowUsecase userGetFollowUsecase; | ||
|
|
||
| /** |
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "following_user_id") | ||
| private UserJpaEntity followingUserJpaEntity; | ||
| private UserJpaEntity followingUserJpaEntity; // 팔로우 당하는 유저 |
There was a problem hiding this comment.
ㅋㅋㅋㅋ 팔로잉 팔로워 헷갈리긴 하네여
There was a problem hiding this comment.
필드 이름이 너무 애매해서 그냥 주석 남겼습니다 ㅎ
seongjunnoh
left a comment
There was a problem hiding this comment.
고생하셨습니다!! 처음 보는 기술들이 있어서 공부하면서 리뷰할 수 있었습니다!
논의하고 싶은 부분이 있어서 리뷰달았는데 확인해주시면 감사하겠습니다!
| UserFollowersResponse getUserFollowers(Long userId, String cursor, int size); | ||
|
|
||
| UserFollowingResponse getMyFollowing(Long userId, String cursor, int size); |
There was a problem hiding this comment.
LGTM
메서드 네이밍이 직관적이어서 보기 편하네요!!
| public record CursorBasedList<T>( | ||
| List<T> contents, | ||
| String nextCursor, | ||
| boolean hasNext | ||
| ) { |
There was a problem hiding this comment.
클라이언트와 주고받는 cursor 값을 String 타입으로 명시하고, 영속성 adapter 내부에서 클라이언트에게 전달받은 커서의 타입을 LocalDateTime 으로 수정하셨군요!!
전 "내 모임방 목록 조회 api" 에서
- controller 의 cursor request parameter 의 타입을 LocalDate, Long 으로 설정
- 따라서 스프링부트가 자동으로 String -> LocalDate, String -> Long 으로 변환해줌
- LocalDate, Long 타입의 커서를 사용하여 페이징 처리
- 다시 클라이언트에게 nextCursor 값을 전달하기 위해 response에 LocalDate, Long 타입의 nextCursor 를 정의
- 클라이언트는 JSON 형식의 response를 받는데, 내부 nextCursor의 값은 LocalDate의 String 형식(ex "2020-00-00") 과 JS의 Number 타입 으로 변환
- 따라서 nextCursor 의 값을 이용해 다시 요청을 보낼 경우, response 로 응답받은 커서값을 형변환없이 그대로 사용 가능
위와 같은 플로우를 생각하여 controller 에서 받는 cursor request parameter, response 로 전달하는 nextCursor 의 타입을 모두 저희가 페이징 처리시 사용하는 타입 그대로 정의하였습니다.
@buzz0331 @hd0rable 혹시 이 부분에 대해서 어떻게 생각하시나요??
위 플로우처럼 스프링부트가 제공해주는 Jackson 라이브러리를 활용하면 클라이언트와 주고받는 커서의 타입을 신경쓰지 않아도 되고, 명시한 커서 타입이 아니라 다른 타입이 요청 파라미터로 들어올 경우 스프링이 내부적으로 "파라미터 바인딩 실패 -> 400 error throw" 해주므로 저희가 추가로 예외처리를 하지 않아도 되는 장점이 있을 것 같습니다!
There was a problem hiding this comment.
저는 개인적으로 커서를 하나의 문자열로 받아 처리하는 방식이 응답 구조의 일관성이나 확장성 측면에서 더 적합하다고 생각하고 있습니다. 앞으로 저희가 커서 페이징에서 공통적으로 사용할 CursorBasedList 구조에서도 nextCursor는 단일 String 타입으로 정의되어 있기 때문에, 커서 역시 하나의 문자열로 통합하여 사용하는 것이 다양한 조회 API에 일관되게 적용할 수 있고, 응답 구조 또한 통일된 형태로 유지할 수 있다는 점에서 장점이 있다고 생각합니다.
반대로, 커서를 여러 파라미터로 나누게 되면 각 API마다 커서 필드를 개별적으로 정의해야 하며, 공통 응답 객체를 사용하는 데에도 제한이 생기게 됩니다. 또한 이 경우 응답 DTO뿐만 아니라 요청 파라미터도 각각의 이름을 따로 지정해야 하기 때문에, 예를 들어 cursorDate, cursorRoomId처럼 네이밍이 API마다 달라질 가능성이 높고, 이는 클라이언트 측에서도 사용 시 혼란을 줄 수 있다고 생각합니다.
반면 커서를 cursor={String} 형태로 일관되게 주고받게 되면, 클라이언트는 커서 값을 단순히 저장하고 그대로 다음 요청에 넘겨주는 방식으로 처리할 수 있기 때문에, 구현 부담도 줄고 인터페이스도 단순해진다는 장점이 있습니다.
무엇보다 커서 구조를 외부에 드러내지 않고, 서버 내부 로직에 따라 파싱하여 처리하는 방식은 캡슐화 측면에서도 바람직하며, 추후 커서 구조가 변경되거나 인코딩/보안 처리 등을 추가해야 할 때도 서버 단에서만 수정하면 되므로 유지보수에도 유리하다고 생각합니다.
이런 이유로 성준님 PR에서 사용하신 LocalDate와 roomId 두 개의 커서 값을 "2024-07-22|102"와 같은 형식으로 하나의 문자열 커서로 묶어서 전달하고, 이를 서버 내부에서 분리·파싱하여 사용하는 방식도 한번 고려해보면 좋을 것 같습니다!
There was a problem hiding this comment.
아아 CursorBasedList에 정의해두신 String nextCursor 가 커서 하나의 String 형변환값이 아니라, 그냥 저희가 페이징처리시에 사용할 커서 자체를 의미하신 거군요!
코멘트로 남겨주신 내용처럼 복합 커서가 필요할 경우, 저희가 내부적으로 복합 커서를 String 으로 변환하고, 받은 cursor 값을 다시 복합 커서로 파싱하는 방식 좋은 것 같습니다!!
현준님 pr 머지되면 복합 커서로 작성된 코드 수정해보겠습니다!!
| @RequestParam(required = false) final String cursor, | ||
| @RequestParam(defaultValue = "10") @Max(value = 10) @Min(value = 1) final int size) { | ||
| return BaseResponse.ok(userGetFollowUsecase.getUserFollowers(userId, cursor, size)); | ||
| } | ||
|
|
||
| /** | ||
| * 내 팔로잉 리스트 조회 | ||
| */ | ||
| @GetMapping("/users/my/following") | ||
| public BaseResponse<UserFollowingResponse> showMyFollowing(@UserId final Long userId, | ||
| @RequestParam(required = false) final String cursor, | ||
| @RequestParam(defaultValue = "10") @Max(value = 10) @Min(value = 1) final int size) { |
There was a problem hiding this comment.
p3 : 저희 페이지 사이즈 BE 내부에서 정의하는게 아니라 요청 파라미터로 받는 건가요??
There was a problem hiding this comment.
넵, 프론트 쪽에서 단일 페이지 크기를 요청 파라미터로 전달할 수도 있고, 전달하지 않는 경우에는 최대 페이지 수 10을 기본값(default) 으로 설정해두었습니다
이렇게 하면 API의 유연성이 좀 더 높아지지 않을까 생각했습니다!
| @Mapper(componentModel = "spring") | ||
| public interface FollowDtoMapper { | ||
|
|
||
| UserFollowersResponse.Follower toFollowerList(FollowQueryDto dto); | ||
|
|
||
| UserFollowingResponse.Following toFollowingList(FollowQueryDto dto); | ||
| } No newline at end of file |
| size | ||
| ); | ||
|
|
||
| return CursorBasedList.of(followingDtos, size, followingDto -> followingDto.createdAt().toString()); |
넵 좋습니다! 회의에서는 제 조회 흐름을 공유드리면서 일종의 컨벤션처럼 설명드리긴 했지만, 의도했던 바는 ‘조회 DTO를 무조건 사용하자’가 아니라 자주 사용되는 컴포넌트는 공통 조회 DTO로 추출해서 재사용하자는 것이었습니다. |
#️⃣ 연관된 이슈
📝 작업 내용
이번 PR은 “팔로워 리스트 조회 API #77”과 로직 구조가 매우 유사하여, 리팩토링 중심으로 작업을 진행하였습니다. 주요 변경 사항은 다음과 같습니다:
대부분의 조회 기능이 무한 스크롤 기반으로 동작하기 때문에, 커서 기반 페이징 방식이 적절하다고 판단하였습니다. 이에 따라 커서 기반 페이징 처리를 표준화하고자 CursorBasedList를 도입하였으며, 관련 상세 내용은 노션에 정리해두었으니 참고 부탁드립니다.
Querydsl의 @QueryProjection을 활용하면 공통 응답 DTO를 생성하는 데 유용하다는 점을 확인하여, 이번에 유사한 조회 로직들에 적용해보았습니다.
FollowQueryDto는 팔로워 및 팔로잉 사용자 정보를 모두 담을 수 있는 구조로 설계되어 있으며, 응답 형식의 차이는 주로 followerCount 포함 여부입니다.
따라서, 쿼리 결과는 FollowQueryDto로 통일하여 받아오고, 서비스 계층에서 필요한 응답 DTO로 변환하는 방식으로 구성해 조회 로직의 재사용성과 응집도를 높였습니다.
서비스 계층에서 FollowQueryDto를 실제 응답 DTO로 변환하는 작업을 명확하게 분리하고자 MapStruct를 도입하였습니다.
기존에는 조회 DTO → 응답 DTO 변환 책임이 응답 DTO나 서비스 클래스 중 한 곳에 맡아 혼용되고 있었고, 이는 의존성 침해 및 서비스 로직 복잡도 증가라는 단점이 있었습니다.
이러한 문제를 해소하고자 명확한 매핑 책임을 가진 Mapper 클래스를 분리하여 도입하였고, 이를 통해 매핑 로직을 간결하고 명시적으로 관리할 수 있게 되었습니다.
앞으로 다른 조회 API 개발 시에도, 공통 필드는 QueryDto로 분리하고, 매핑에는 MapStruct를 활용하는 구조를 적용하면 유지보수성과 일관성을 높일 수 있을 것으로 기대합니다. 감사합니다.
📸 스크린샷
💬 리뷰 요구사항
현재 조회 dto(
FollowQueryDto) -> 응답 dto(UserFollowingResponse)를 파싱하는 매퍼 (FollowMapper)를 application하위에 mapper 패키지를 만들어서 두었는데 이 위치에 대한 부분도 한번 확인 부탁드릴게요!📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit