[hotfix] 전체 피드 조회 화면 상단의, 내 띱 목록 조회 api 로직 수정#248
Conversation
- 무한 스크롤 기능이 없으므로, QueryDSL이 아니라 service 에서 비즈니스 로직을 수행하도록 수정
Walkthrough팔로잉 기반 피드 상단 노출 로직을 전면 교체. 기존 “최근 글쓴 팔로잉” 흐름을 제거하고, 팔로잉 목록을 팔로우 시점 역순으로 가져온 뒤, 최근 공개 피드 작성자를 우선하여 최대 10명 정렬해 반환. 이를 위해 새로운 포트/리포지토리/DTO/서비스/컨트롤러/테스트를 추가·변경. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Controller as UserQueryController
participant Service as UserShowFollowingsInFeedViewService
participant FollowRepo as FollowingQueryPort
participant FeedRepo as FeedQueryPort
participant Mapper as UserQueryMapper
User->>Controller: GET /users/my-followings/recent-feeds
Controller->>Service: showMyFollowingsInFeedView(userId)
Service->>FollowRepo: findAllFollowingUsersOrderByFollowedAtDesc(userId)
alt followings.isEmpty()
Service-->>Controller: UserShowFollowingsInFeedViewResponse(empty)
else
Service->>FeedRepo: findLatestPublicFeedCreatorsIn(followingUserIds, SIZE)
Service->>Service: assemble ordered IDs (recent public first, then recent follows)
Service->>Mapper: toFollowingFeedViewDtos(selectedDtos)
Mapper-->>Service: List<FeedViewDto>
Service-->>Controller: UserShowFollowingsInFeedViewResponse(dtos)
end
Controller-->>User: 200 OK (data.myFollowingUsers)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
Test Results407 tests 407 ✅ 30s ⏱️ Results for commit fa0a681. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (20)
src/main/java/konkuk/thip/user/application/port/out/dto/FollowingQueryDto.java (1)
7-16: DTO 필수값 검증 추가 제안(레코드 compact ctor 활용)QueryDSL @QueryProjection을 유지한 채로 필수 필드(userId/followingTargetUserId/followedAt 등)에 대한 방어적 검증을 추가하는 것을 권장합니다. 조기 실패로 데이터 무결성을 확보할 수 있습니다.
다음 패치를 고려해 주세요:
package konkuk.thip.user.application.port.out.dto; import com.querydsl.core.annotations.QueryProjection; import java.time.LocalDateTime; +import org.springframework.util.Assert; public record FollowingQueryDto( Long userId, Long followingTargetUserId, String followingUserNickname, String followingUserProfileImageUrl, LocalDateTime followedAt ) { @QueryProjection - public FollowingQueryDto {} + public FollowingQueryDto { + Assert.notNull(userId, "userId(follower) must not be null"); + Assert.notNull(followingTargetUserId, "followingTargetUserId must not be null"); + Assert.notNull(followedAt, "followedAt must not be null"); + // 닉네임/프로필 이미지의 null 허용 여부는 도메인 정책에 맞게 선택: + // Assert.hasText(followingUserNickname, "followingUserNickname must not be blank"); + // Assert.notNull(followingUserProfileImageUrl, "followingUserProfileImageUrl must not be null"); + } }추가로, 필드 의미가 혼동될 수 있어 Javadoc으로 follower(=userId) vs following target(=followingTargetUserId)를 명확히 표기하는 것을 권장합니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java (1)
27-27: 새 메서드의 반환 정렬/중복/빈 입력 처리 계약을 명시하세요계약을 명확히 하면 구현/테스트 일관성이 좋아집니다. 특히 userIds가 비었을 때 빈 리스트를 반환하는지, createdAt desc 정렬을 보장하는지, 중복 제거 여부를 문서화해 주세요.
권장 Javadoc 추가:
- List<Long> findLatestPublicFeedCreatorsIn(Set<Long> userIds, int size); + /** + * 주어진 userIds 중 공개(비공개 제외) & 활성 피드를 가진 사용자들 중 + * 가장 최근에 피드를 작성한 사용자 ID를 createdAt 내림차순으로 최대 {@code size}명 반환합니다. + * - 반환 목록은 중복이 없습니다. + * - {@code userIds}가 비어 있으면 빈 리스트를 반환해야 합니다. + */ + List<Long> findLatestPublicFeedCreatorsIn(Set<Long> userIds, int size);구현 측면에서는 IN 절에 빈 Set이 전달될 때 불필요한 쿼리를 피하고 즉시 Collections.emptyList()를 반환하도록 방어 로직을 권장합니다.
src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java (1)
19-22: Optional: 팔로잉 유저 전체 페치 대신 상위 N명만 조회하도록 오버로드 및 DB-limit 적용 고려현재
UserShowFollowingsInFeedViewService#showMyFollowingsInFeedView에서List<FollowingQueryDto> followingQueryDtos = followingQueryPort.findAllFollowingUsersOrderByFollowedAtDesc(userId);를 호출해 모든 팔로잉을 가져온 뒤, 화면 상단에 노출할 최대 10명(SIZE)에 대한 잘라내기를 하지 않고 있습니다. 팔로잉 수가 많을 경우 불필요한 I/O/메모리 비용이 발생할 수 있습니다.
제안:
- FollowingQueryPort에 기본 구현 오버로드 메서드 추가
default List<FollowingQueryDto> findTopFollowingUsersOrderByFollowedAtDesc(Long userId, int size) { List<FollowingQueryDto> all = findAllFollowingUsersOrderByFollowedAtDesc(userId); return all.size() <= size ? all : all.subList(0, size); }- 서비스 레이어에서
findAll…대신findTop…(userId, SIZE)호출로 교체- persistence 어댑터 및 QueryDSL 구현(RepositoryImpl)에
.limit(size)로 DB-레벨 최적화 추가 검토위 단계로 점진 적용하면, 상단 노출 용도일 때 불필요한 전체 조회 비용을 줄일 수 있습니다.
src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java (1)
51-53: 계약 문서화로 정렬/중복/엣지케이스 명확화Repository/Adapter/Service 전층에서 동일한 기대를 공유할 수 있도록 Javadoc으로 계약을 명확히 해 주세요.
권장 패치:
- List<Long> findLatestPublicFeedCreatorsIn(Set<Long> userIds, int size); + /** + * 주어진 userIds 중에서 '최근 공개 피드' 작성자 ID를 createdAt 내림차순으로 최대 {@code size}명 반환합니다. + * - 반환 리스트는 중복이 없습니다. + * - userIds가 비어 있으면 빈 리스트를 반환합니다. + */ + List<Long> findLatestPublicFeedCreatorsIn(Set<Long> userIds, int size);추가로, 상위 N 추출 후 나머지를 팔로우 시점 역순으로 채우는 서비스 조합 단계에서
LinkedHashSet으로 우선순위 집합(최근 공개 피드 작성자) → 잔여 팔로잉 순 삽입을 추천합니다.
이 방식은 중복 제거와 순서 보장을 동시에 달성합니다(기억된 선호도 반영).src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java (1)
19-19: 상단 노출 목적이라면 DB 레벨에서 제한/페이지네이션을 지원하세요.현재 “전체”를 모두 반환하면 팔로잉 수가 많은 사용자의 경우 메모리/네트워크 비용이 큽니다. 상단 노출은 보통 최대 N(예: 10)명만 필요하므로, DB에서 정렬+limit를 적용하는 API를 함께 노출하는 편이 성능상 유리합니다. 또한 followedAt이 동일할 때 결정적 순서를 위해 2차 정렬 키(예: targetUserId ASC)를 추가하는 것을 권장합니다.
적용 예시(오버로드 추가):
public interface FollowingQueryRepository { @@ - List<FollowingQueryDto> findAllFollowingUsersOrderByFollowedAtDesc(Long userId); + List<FollowingQueryDto> findAllFollowingUsersOrderByFollowedAtDesc(Long userId); + List<FollowingQueryDto> findFollowingUsersOrderByFollowedAtDesc(Long userId, int size); }src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java (1)
184-187: 빈 userIds 입력 시 불필요한 쿼리를 방지하는 early return 권장.QueryDSL의 in(emptySet) 처리가 DB별로 다를 수 있어, 어댑터 단에서 안전하게 빈 리스트를 즉시 반환하면 낭비를 줄일 수 있습니다. size <= 0인 케이스도 함께 방어해두면 좋습니다.
@Override public List<Long> findLatestPublicFeedCreatorsIn(Set<Long> userIds, int size) { - return feedJpaRepository.findLatestPublicFeedCreatorsIn(userIds, size); + if (userIds == null || userIds.isEmpty() || size <= 0) { + return List.of(); + } + return feedJpaRepository.findLatestPublicFeedCreatorsIn(userIds, size); }src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (1)
61-63: 전체 조회는 비용 큼 — 상단 노출 N명만 필요하면 size 인자를 추가해 DB에서 제한하세요.상단 “내 구독” 영역은 보통 소수만 노출됩니다. 현재 어댑터는 전체를 모두 조회하므로, JPA/QueryDSL 쿼리 단계에서 정렬+limit 적용이 성능상 유리합니다. 또한 userId null/size <= 0 방어 로직도 고려하세요.
- public List<FollowingQueryDto> findAllFollowingUsersOrderByFollowedAtDesc(Long userId) { - return followingJpaRepository.findAllFollowingUsersOrderByFollowedAtDesc(userId); + public List<FollowingQueryDto> findAllFollowingUsersOrderByFollowedAtDesc(Long userId, int size) { + if (userId == null || size <= 0) { + return List.of(); + } + return followingJpaRepository.findAllFollowingUsersOrderByFollowedAtDesc(userId, size); }관련 Repository/Port 시그니처도 함께 확장 필요합니다. 원하시면 전체 변경 분에 대한 일괄 패치 제안 드리겠습니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (1)
387-401: 정렬 결정성 보강(+tie-breaker)과 빈 Set 가드, 인덱스 권고.
- 동일한 max(createdAt)을 가진 사용자가 다수일 때 결과 순서가 비결정적일 수 있습니다. 2차 정렬키로 userId ASC 등을 추가해 안정성을 높이세요.
- userIds가 비어 있으면 즉시 반환하는 것이 안전/효율적입니다.
- 성능을 위해 feed(user_id, is_public, status, created_at) 또는 (user_id, created_at) 복합 인덱스를 고려하세요. 그룹핑+max에 큰 이점이 있습니다.
@Override public List<Long> findLatestPublicFeedCreatorsIn(Set<Long> userIds, int size) { - return jpaQueryFactory + if (userIds == null || userIds.isEmpty() || size <= 0) { + return List.of(); + } + return jpaQueryFactory .select(feed.userJpaEntity.userId) .from(feed) .where( feed.userJpaEntity.userId.in(userIds), feed.isPublic.isTrue(), feed.status.eq(StatusType.ACTIVE) ) .groupBy(feed.userJpaEntity.userId) - .orderBy(feed.createdAt.max().desc()) + .orderBy( + feed.createdAt.max().desc(), + feed.userJpaEntity.userId.asc() // tie-breaker for deterministic ordering + ) .limit(size) .fetch(); }src/main/java/konkuk/thip/user/application/port/in/UserShowFollowingsInFeedViewUseCase.java (1)
5-8: 서비스 트랜잭션 및 API 계약 명확화 제안
UserShowFollowingsInFeedViewUseCase의userId파라미터는 null을 허용하지 않을 경우Long→long으로 변경하거나@NotNull애노테이션 추가로 null 경로 제거- UseCase 인터페이스 및 메서드에 JavaDoc으로 “상단 노출 최대 10명” 정책(
SIZE = 10) 명시UserShowFollowingsInFeedViewService클래스 또는showMyFollowingsInFeedView메서드에@Transactional(readOnly = true)애노테이션 적용 검토- 이미
LinkedHashSet을 활용해 중복 제거 및 순서 보존 로직이 적용되어 있으므로, 해당 부분은 현재 구조를 유지해도 무방src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java (3)
119-126: 중복 팔로잉 행(데이터 품질 이슈) 대비: distinct 추가 권장히스토리나 데이터 오염으로 (user_id, following_user_id) 중복 행이 존재할 경우 결과가 중복될 수 있습니다. 소비 계층에서 LinkedHashSet으로 걸러도 되지만, 조회 단계에서
distinct를 추가하면 I/O 비용과 매핑 비용을 줄일 수 있습니다.- return jpaQueryFactory.select(new QFollowingQueryDto( + return jpaQueryFactory.select(new QFollowingQueryDto( following.userJpaEntity.userId, followingTargetUser.userId, followingTargetUser.nickname, alias.imageUrl, following.createdAt - )) + )) + .distinct()
114-115: 메서드 이름이 의도와 다소 상이해 보입니다 (네이밍 니트픽)서비스 레벨에서 “최근 피드 작성자 우선 정렬”이 추가로 적용된다면, 이 메서드는 “팔로우 시점 역순의 베이스 셋 제공” 성격입니다. 혼동 방지를 위해
findFollowingsOrderByFollowedAtDesc또는findFollowingsBaseOrderForFeedView같은 이름을 고려해볼 수 있습니다.
113-136: 대규모 팔로잉 사용자를 위한 성능 최적화 제안요구사항(최근 피드 작성자 우선 + 부족분은 팔로우 시점 역순으로 보충, 최대 10명)을 효율적으로 달성하려면 “전체 팔로잉 전량을 메모리로 불러와 재정렬” 대신 2-Phase 접근을 고려해볼 수 있습니다.
- Phase 1: 내 팔로잉 중 “최근 공개 피드 작성자”의 최신 피드 시각 max(created_at) 기준 상위 N명 조회
- Phase 2: 남은 슬롯(<= 10-N)은 팔로우 시점 역순으로 보충(자기 자신 제외/중복 제거)
이렇게 하면 불필요한 전량 스캔을 피할 수 있습니다. 인덱스가 받쳐주면 DB 정렬 비용도 훨씬 낮습니다. 구현은 Repository 또는 PersistenceAdapter에서 서브쿼리/그룹핑(QueryDSL의 groupBy/서브쿼리)로 가능하고, 서비스에서 두 결과를 합쳐 최대 10명만 반환하면 됩니다.
src/main/java/konkuk/thip/user/adapter/in/web/response/UserShowFollowingsInFeedViewResponse.java (1)
9-22: Swagger 가독성 보완(예시 값 추가) 제안API 소비자 입장에서 미리보기 품질을 높이려면
@Schema(example = "...")를 필드/레코드 단위로 일부 추가하는 것을 권장합니다. 서드파티/프론트 팀 협업에 도움이 됩니다. 적용은 선택사항입니다.src/main/java/konkuk/thip/user/application/mapper/UserQueryMapper.java (1)
23-25: 리스트 매핑 메서드는 생략 가능(MapStruct 자동 지원)MapStruct는 단일 매핑(
toFollowingFeedViewDto)이 있으면 동일 시그니처의 리스트 매핑을 자동 생성합니다. 명시 메서드를 제거해도 동작 동일하며, 인터페이스 표면적을 줄여 유지보수성이 좋아집니다. 선택사항입니다.- List<UserShowFollowingsInFeedViewResponse.UserShowFollowingsInFeedViewDto> toFollowingFeedViewDtos( - List<FollowingQueryDto> dtos - ); + // 선택: 생략 가능(MapStruct가 toFollowingFeedViewDto 기반으로 자동 생성)src/main/java/konkuk/thip/user/application/service/UserShowFollowingsInFeedViewService.java (3)
31-37: 대량 팔로잉 사용자의 성능 병목 가능성: 전체 팔로잉 전량 로딩 회피를 검토하세요현재는 전체 팔로잉을 전부 메모리로 읽은 뒤(Set 변환 포함) 처리합니다. 팔로잉 수가 매우 큰 사용자에서:
- DB IN 절이 과도하게 커지고(네트워크/플랜 비용 상승),
- 애플리케이션 메모리/GC 부하가 커질 수 있습니다.
핵심 개선 아이디어(선호 순서):
- DB로 로직을 푸시: “내 팔로잉 중 최신 공개 피드 작성자 TOP N”을 팔로잉 테이블과 포스트를 조인 + MAX(created_at)로 그룹/정렬/LIMIT 하여 한 번에 구합니다.
- 부족분은 “내 팔로잉 중(제외 목록 제외) 최근 팔로잉 맺은 순 TOP K”만 추가 조회해 채웁니다. 즉, 2개의 제한 쿼리로 전체 팔로잉 전량 로딩을 피합니다.
이렇게 하면 항상 최대 2회, 각 N(=10) 크기의 소량 결과만 다뤄 성능이 안정적입니다.
원하시면 포트/리포지토리 시그니처와 JPQL/SQL 샘플을 제안드리겠습니다.
46-53: 잠재적 중복 키로 인한 IllegalStateException 방지toMap 수집 시 중복 키가 발생하면 IllegalStateException이 납니다. 정상이라면 중복이 없어야 하지만, 방어적 차원에서 merge 함수를 지정하는 편이 안전합니다.
다음과 같이 변경을 제안합니다:
- Map<Long, FollowingQueryDto> followingMap = followingQueryDtos.stream() - .collect(Collectors.toMap( - FollowingQueryDto::followingTargetUserId, - dto -> dto - )); + Map<Long, FollowingQueryDto> followingMap = followingQueryDtos.stream() + .collect(Collectors.toMap( + FollowingQueryDto::followingTargetUserId, + dto -> dto, + (existing, ignored) -> existing + ));
28-56: (선택) 전체 로직을 2회 제한 쿼리로 축약하는 방안 제안핫픽스 이후 개선 아이디어입니다. 전체 팔로잉 로딩 없이도 동일 결과를 만들 수 있습니다.
포트 제안:
- feedQueryPort.findTopLatestPublicFeedCreatorsAmongMyFollowings(userId, size)
- followingQueryPort.findTopFollowingsOrderByFollowedAtDescExcluding(userId, excludedUserIds, size)
서비스 로직 스케치:
- latest = feedQueryPort.findTopLatestPublicFeedCreatorsAmongMyFollowings(userId, SIZE)
- 부족분이 있으면 fill = followingQueryPort.findTopFollowingsOrderByFollowedAtDescExcluding(userId, latest, SIZE - latest.size)
- orderedIds = latest ∪ fill
- 필요한 DTO만 조회(또는 기존 DTO 리턴 형태로 포트를 설계) 후 매핑
장점: 항상 소량 결과만 다뤄 안정적인 성능/메모리 사용을 보장합니다.
src/test/java/konkuk/thip/user/adapter/in/web/UserShowFollowingsInFeedViewApiTest.java (3)
37-39: 네이밍 정합성: 메서드명에 남아있는 recent_writers 접미사 정리 권장클래스/DisplayName은 새 요구사항을 반영하지만, 테스트 메서드명에는 여전히 recent_writers가 남아 있습니다. 검색/가독성 측면에서 메서드명을 feed_view/followings 중심으로 정리하면 더 명확합니다.
248-266: 직접 SQL로 created_at 조정: 안정성/유지보수 관점의 보완 제안
- 장점: 정렬 시나리오를 결정적으로 통제 가능.
- 고려사항: 테이블/컬럼명 변경에 취약하고, DB 방언 차이에 민감합니다.
대안:
- TestEntityFactory에서 createdAt을 주입 가능한 팩토리/빌더 제공(가능하다면 Auditing 우회).
- 또는 EntityManager를 통한 update 쿼리를 모듈화해 재사용성/가독성을 높임.
현 구현도 충분히 실용적이므로, 구조 변경 시점에만 고려해도 됩니다.
Also applies to: 267-304
329-373: 두 가지 보강 테스트 제안: (1) 팔로잉 0명, (2) 비공개 피드 배제
- 팔로잉이 없는 경우: 빈 배열([]) 반환을 보장하는 단위/통합 테스트를 추가하면 경계조건 신뢰도가 올라갑니다.
- 비공개 피드만 가진 팔로잉: 해당 사용자가 “최신 공개 피드 작성자” 목록에 포함되지 않음을 명시적으로 검증하면 가시성이 좋아집니다.
원하시면 해당 테스트 케이스 코드를 초안으로 드리겠습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (24)
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java(2 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowingRecentWritersResponse.java(0 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/UserShowFollowingsInFeedViewResponse.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/UserQueryPersistenceAdapter.java(0 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepository.java(0 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java(0 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/UserQueryMapper.java(2 hunks)src/main/java/konkuk/thip/user/application/port/in/UserShowFollowingRecentWritersUseCase.java(0 hunks)src/main/java/konkuk/thip/user/application/port/in/UserShowFollowingsInFeedViewUseCase.java(1 hunks)src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java(2 hunks)src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java(0 hunks)src/main/java/konkuk/thip/user/application/port/out/dto/FollowingQueryDto.java(1 hunks)src/main/java/konkuk/thip/user/application/port/out/dto/UserQueryDto.java(1 hunks)src/main/java/konkuk/thip/user/application/service/UserShowFollowingRecentWritersService.java(0 hunks)src/main/java/konkuk/thip/user/application/service/UserShowFollowingsInFeedViewService.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserShowFollowingsInFeedViewApiTest.java(6 hunks)
💤 Files with no reviewable changes (7)
- src/main/java/konkuk/thip/user/application/port/in/UserShowFollowingRecentWritersUseCase.java
- src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java
- src/main/java/konkuk/thip/user/application/service/UserShowFollowingRecentWritersService.java
- src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepository.java
- src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java
- src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowingRecentWritersResponse.java
- src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.332Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
⏰ 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 (14)
src/main/java/konkuk/thip/user/application/port/out/dto/UserQueryDto.java (1)
21-25: aliasName·aliasColor·createdAt non-null 보장 검증 완료
- QUserQueryDto를 생성하는 모든 프로젝션(UserQueryRepositoryImpl, FollowingQueryRepositoryImpl)에서 alias.value, alias.color, user.createdAt을 명시적으로 넘겨줌
- UserJpaEntity.aliasForUserJpaEntity 매핑은 DB 제약(nullable = false)로 모든 User에 반드시 Alias가 존재
- BaseJpaEntity.createdAt에 @column(nullable = false) 적용으로 생성 시 null 불가
따라서 Assert.notNull에 의한 런타임 예외 발생 가능성은 없습니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java (1)
4-4: LGTM: 새 DTO 의존성 추가가 포트 계층과 일관적입니다.기존 UserQueryDto 기반 질의들과 결을 맞추며, 읽기용 DTO 반환 방향성도 명확합니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (1)
5-5: LGTM: DTO import 추가 적절.레이어 경계를 넘어서는 반환 타입 일관성 유지에 도움됩니다.
src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java (1)
21-26: FK 컬럼 NOT NULL 전환 전 마이그레이션·데이터 정합성 검증 필수
두 FK 컬럼(@joincolumn name="user_id", nullable=false / name="following_user_id", nullable=false)을 NOT NULL로 변경하면, 기존에 NULL 값이 있는 레코드가 있거나 스키마가 아직 NOT NULL으로 설정되지 않은 경우 런타임에서 예외가 발생합니다. 운영 DB에 배포하기 전에 반드시 다음을 확인해주세요:
- 데이터 백필 및 NULL 값 정리
- DDL 마이그레이션 적용 여부
ALTER TABLE followings ALTER COLUMN user_id SET NOT NULL;ALTER TABLE followings ALTER COLUMN following_user_id SET NOT NULL;- FK 제약조건(
ADD CONSTRAINT … FOREIGN KEY)- UNIQUE 제약(
UNIQUE(user_id, following_user_id))- 필요한 인덱스 생성
현재 저장소에서 자동 검색된 마이그레이션 파일이 없습니다. 아래 경로 및 확장자(.sql, .yaml, .yml 등)에 직접 마이그레이션 스크립트가 있는지 검토 부탁드립니다:
- src/main/resources/db/migration
- src/main/resources/db/changelog
- 기타 별도 스크립트 디렉터리
추가로, JPA 엔티티 레벨에서 스키마 동기화 및 문서화를 위해 @table에 UNIQUE·INDEX를 선언하는 것도 고려해 보세요(선택 사항).
@Table( name = "followings", uniqueConstraints = { @UniqueConstraint(name = "uk_followings_user_target", columnNames = {"user_id", "following_user_id"}) }, indexes = { @Index(name = "idx_followings_user_created_at", columnList = "user_id, created_at"), @Index(name = "idx_followings_following_user_id", columnList = "following_user_id") } )src/main/java/konkuk/thip/user/adapter/in/web/response/UserShowFollowingsInFeedViewResponse.java (1)
24-26: LGTM – 빈 목록 팩토리 적절합니다
Collections.emptyList()로 null 방지/불변 리스트를 반환하는 선택 좋습니다.src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java (2)
46-46: LGTM – 신규 UseCase 의존성 교체 적절합니다컨트롤러 의존성이 새로운 요구사항에 맞게 잘 치환되었습니다.
162-170: 정렬 및 최대 10명 제한 로직 확인 완료
서비스 레이어SIZE = 10상수를 사용하고,
feedQueryPort.findLatestPublicFeedCreatorsIn(followingUserIds, SIZE)로 최근 공개 피드 작성자 우선 추출assembleOrderedIds에서 부족한 인원은 최근 팔로우 순으로 채워 최대 10명으로 제한요구사항(“최근 공개 피드 작성자 우선 + 최대 10명”)을 충족함을 확인했습니다.
src/main/java/konkuk/thip/user/application/mapper/UserQueryMapper.java (1)
18-21: FollowingQueryDto 필드 검증 완료
FollowingQueryDto레코드에 다음 필드가 모두 존재함을 확인했습니다:
followingTargetUserIdfollowingUserNicknamefollowingUserProfileImageUrlMapStruct 매핑 시점에 컴파일 오류가 발생하지 않습니다.
src/main/java/konkuk/thip/user/application/service/UserShowFollowingsInFeedViewService.java (3)
28-56: 요구사항 정합성 OK — 정렬/중복 제거/상한(10) 로직이 명확합니다
- “최근 공개 피드 작성자 우선 → 최근 팔로잉 순” 조합을 LinkedHashSet으로 구현해 중복 제거와 순서 보존을 동시에 만족합니다.
- 팔로잉이 없을 때 빈 리스트 반환 분기 처리도 적절합니다.
58-68: LinkedHashSet을 활용한 순서 보존 + 중복 제거가 목적에 부합합니다요구사항(중복 제거 및 정렬 우선순위 보존)에 딱 맞는 선택입니다. 상한(SIZE) 체크로 불필요한 삽입도 방지하고 있습니다.
39-41: 확인:findLatestPublicFeedCreatorsIn구현이 요구사항에 부합합니다
groupBy(feed.userJpaEntity.userId)로 작성자별로 중복 없이 그룹핑orderBy(feed.createdAt.max().desc())로 각 작성자의 최신 공개 피드 작성일 기준 DESC 정렬limit(size)로 상단에서 SIZE 제한 적용추가 수정 필요 없습니다.
src/test/java/konkuk/thip/user/adapter/in/web/UserShowFollowingsInFeedViewApiTest.java (3)
71-75: 팔로우 외 사용자 배제 케이스 커버리지 LGTMuser3는 비팔로우 상태에서 공개 피드를 작성하지만 응답에 포함되지 않음을 검증하고 있어, 필터링 요건을 잘 커버합니다.
187-204: 상한 10명 및 최신 공개 피드 우선 정렬 검증이 명료합니다created_at를 직접 고정시각으로 업데이트하여 명확한 정렬 기대값을 만들고, 12명 중 상위 10명만 반환됨을 확인합니다. 요구사항을 정확히 테스트하고 있습니다.
206-327: 혼합 정렬(최신 공개 피드 → 최근 팔로우) + 중복 제거 시나리오 커버리지 우수
- 공개 피드 작성자가 5명뿐인 상황에서 부족분을 “최근 팔로우”로 채우고,
- 이미 포함된 사용자(중복)는 제외하는 규칙을 정확히 검증합니다.
핵심 동작을 현실적인 데이터로 촘촘히 점검하고 있어 신뢰도가 높습니다.
| .join(following.followingUserJpaEntity, followingTargetUser) | ||
| .join(followingTargetUser.aliasForUserJpaEntity, alias) | ||
| .where( |
There was a problem hiding this comment.
🛠️ Refactor suggestion
alias 조인은 leftJoin이 더 안전합니다
동일 클래스 내 기존 메서드들은 alias에 leftJoin을 사용합니다(별칭/프로필이 없는 사용자 탈락 방지). 여기서는 join(inner join)이라 alias 없는 사용자가 결과에서 빠질 수 있습니다. 일관성과 누락 방지 차원에서 leftJoin 권장합니다.
- .join(followingTargetUser.aliasForUserJpaEntity, alias)
+ .leftJoin(followingTargetUser.aliasForUserJpaEntity, alias)📝 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.
| .join(following.followingUserJpaEntity, followingTargetUser) | |
| .join(followingTargetUser.aliasForUserJpaEntity, alias) | |
| .where( | |
| .join(following.followingUserJpaEntity, followingTargetUser) | |
| .leftJoin(followingTargetUser.aliasForUserJpaEntity, alias) | |
| .where( |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java
around lines 127 to 129, the join on alias uses an inner join which can drop
users without an alias; change the alias join from
.join(followingTargetUser.aliasForUserJpaEntity, alias) to
.leftJoin(followingTargetUser.aliasForUserJpaEntity, alias) so it matches other
methods and preserves users without aliases.
buzz0331
left a comment
There was a problem hiding this comment.
굿굿 수고하셨습니다~ 엄청 고민하신게 보이네요 👍🏻 👍🏻
| @Override | ||
| @Transactional(readOnly = true) | ||
| public UserShowFollowingsInFeedViewResponse showMyFollowingsInFeedView(Long userId) { | ||
| // 1. 유저가 팔로잉하는 사람들을 팔로잉을 최근에 맺은 순으로 조회 | ||
| // TODO : 유저가 팔로잉하는 사람들이 너무 많으면??? -> 고민해봐야 함 | ||
| List<FollowingQueryDto> followingQueryDtos = followingQueryPort.findAllFollowingUsersOrderByFollowedAtDesc(userId); | ||
|
|
||
| if (followingQueryDtos.isEmpty()) { | ||
| return UserShowFollowingsInFeedViewResponse.returnEmptyList(); | ||
| } | ||
|
|
||
| // 2. 유저가 팔로잉하는 사람들 중, 가장 최근에 공개 피드를 작성한 사람들을 조회 | ||
| List<Long> latestPublicFeedCreators = fetchLatestPublicFeedCreators(followingQueryDtos); | ||
|
|
||
| // 3. 결과 조합 : 팔로잉 유저들 중, 최신 공개 피드 작성자 우선 -> 최근 팔로우 맺은 순 | ||
| LinkedHashSet<Long> orderedIds = assembleOrderedIds(latestPublicFeedCreators, followingQueryDtos); | ||
|
|
||
| // 4. ID 순서대로 DTO 매핑하여 response 반환 | ||
| Map<Long, FollowingQueryDto> followingMap = followingQueryDtos.stream() | ||
| .collect(Collectors.toMap( | ||
| FollowingQueryDto::followingTargetUserId, | ||
| dto -> dto | ||
| )); | ||
| List<FollowingQueryDto> result = orderedIds.stream() | ||
| .map(followingMap::get) | ||
| .toList(); | ||
|
|
||
| return new UserShowFollowingsInFeedViewResponse(userQueryMapper.toFollowingFeedViewDtos(result)); | ||
| } | ||
|
|
There was a problem hiding this comment.
쿼리 성능을 엄청 신경쓰신게 느껴지네요. 다만, 적어 놓으신대로 사용자가 팔로우하는 수가 많아질수록 메모리 적재량 증가 + findLatestPublicFeedCreatorsIn 메서드의 in 절의 오버헤드 증가로 인해 서버의 부담이 커질 것 같긴하네요. 이 부분은 그냥 User 테이블에 최근에 Feed 작성한 시간을 나타내는 컬럼을 추가하거나 Redis에 각 사용자의 최신 피드 작성 시간을 저장해두고 사용하는 것도 하나의 방법일 것 같아욥. 이렇게 되면, 한번의 쿼리로 조회할 수 있을 것 같네요! 추후에 한번 고려해보죠
There was a problem hiding this comment.
넵 좋습니다! redis를 적극적으로 활용하는 것도 좋을 거 같습니다!
| public record FollowingQueryDto( | ||
| Long userId, | ||
| Long followingTargetUserId, | ||
| String followingUserNickname, | ||
| String followingUserProfileImageUrl, | ||
| LocalDateTime followedAt | ||
| ) { | ||
| @QueryProjection | ||
| public FollowingQueryDto {} | ||
| } |
There was a problem hiding this comment.
흠 이걸 보니까 UserQueryDto를 조금 고쳐야 될 것 같네요. 저는 팔로우 관련 조회도 모두 UserQueryDto로 수행했는데 UserQueryDto에서 createdAt이 사실 followedAt으로 쓰이고 있는 것 같네요. 나중에 수정해보겠습니다
There was a problem hiding this comment.
UserQueryDto 를 그대로 활용할까 하다가, 뭔가 팔로잉하는 다른 사람들을 조회하는 모델이 하나더 있으면 좋지않나? 싶어서 추가해본거긴 합니다!
하나의 dto 내부에 QueryProjection을 위한 생성자가 많은것도 별로지 않나 싶은 생각도 있었습니다
추후에 관련해서 얘기나눠보면 좋을 것 같습니다!
#️⃣ 연관된 이슈
📝 작업 내용
이슈 참고해 주시면 됩니다.
기존 코드가 요구사항을 잘못 구현하고 있어서 로직을 수정하였습니다
그런데 해당 api의 요구사항은
@heeeeyong 참고해주시면 됩니다!
이므로,
수정된 api 로직
위 과정을 service 에서 수행하도록 수정하였습니다
무한 스크롤 기능이 없는 조회이므로, QueryDSL 에서 복잡한 비즈니스 로직 + 피드를 작성하지 않은 팔로잉한 유저의 존재때문에 발생하는 feed와 user의 left join 문제를 해결하기 위해 service 로직을 대폭 수정하였습니다
📸 스크린샷
💬 리뷰 요구사항
📌 PR 진행 시 이러한 점들을 참고해 주세요