Conversation
|
""" Walkthrough사용자 팔로우 및 언팔로우 기능, 팔로워 조회 API가 신규 도입되었습니다. 도메인, 서비스, 어댑터, 컨트롤러, 예외, DTO, 매퍼, JPA 엔티티 등 계층별로 관련 코드가 추가 및 수정되었으며, 통합 및 단위 테스트도 함께 작성되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant UserCommandController
participant UserFollowService
participant FollowingCommandPort
participant DB
Client->>UserCommandController: POST /users/following/{followingUserId} (follow/unfollow)
UserCommandController->>UserFollowService: changeFollowingState(command)
UserFollowService->>FollowingCommandPort: findByUserIdAndTargetUserId
FollowingCommandPort->>DB: 쿼리
FollowingCommandPort-->>UserFollowService: Following or Optional.empty
UserFollowService->>FollowingCommandPort: save/updateStatus (필요시)
FollowingCommandPort->>DB: 저장/업데이트
UserFollowService-->>UserCommandController: Boolean isFollowing
UserCommandController-->>Client: 응답 (isFollowing)
sequenceDiagram
participant Client
participant UserQueryController
participant UserGetFollowersService
participant UserCommandPort
participant FollowingQueryPort
participant DB
Client->>UserQueryController: GET /users/{userId}/followers?cursor=
UserQueryController->>UserGetFollowersService: getUserFollowers(userId, cursor)
UserGetFollowersService->>UserCommandPort: findById(userId)
UserCommandPort->>DB: 쿼리
UserCommandPort-->>UserGetFollowersService: User
UserGetFollowersService->>FollowingQueryPort: getFollowersByUserId(userId, cursor, size)
FollowingQueryPort->>DB: 쿼리 (팔로워 목록, 페이징)
FollowingQueryPort-->>UserGetFollowersService: UserFollowersResponse
UserGetFollowersService-->>UserQueryController: UserFollowersResponse
UserQueryController-->>Client: 응답 (followers, nextCursor 등)
Assessment against linked issues
Suggested labels
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: 4
🧹 Nitpick comments (11)
src/main/java/konkuk/thip/user/application/port/in/dto/UserFollowCommand.java (3)
3-7: 필드 검증 로직 추가 고려Command 객체에 기본적인 검증 로직이 없습니다. userId와 targetUserId가 null이거나 음수일 경우에 대한 검증을 추가하는 것을 고려해보세요.
public record UserFollowCommand( + @NotNull @Positive Long userId, + @NotNull @Positive Long targetUserId, + @NotNull Boolean type // true -> 팔로우, false -> 언팔로우 ) {
6-6: Boolean 타입 대신 enum 사용 고려type 필드에 Boolean 대신 enum을 사용하면 코드의 가독성과 타입 안전성을 높일 수 있습니다.
+public enum FollowType { + FOLLOW, UNFOLLOW +} public record UserFollowCommand( Long userId, Long targetUserId, - Boolean type // true -> 팔로우, false -> 언팔로우 + FollowType type ) {
8-10: 정적 팩토리 메서드 간소화record의 생성자가 이미 간단하므로, 별도의 정적 팩토리 메서드가 불필요할 수 있습니다. 특별한 검증 로직이 없다면 직접 생성자를 사용하는 것을 고려해보세요.
src/main/java/konkuk/thip/user/adapter/in/web/request/UserFollowRequest.java (2)
5-8: Boolean 타입 대신 enum 사용 고려UserFollowCommand와 마찬가지로 Boolean 타입 대신 enum을 사용하면 타입 안전성과 가독성을 향상시킬 수 있습니다.
public record UserFollowRequest( - @NotNull(message = "type은 필수 파라미터입니다.") - Boolean type // true -> 팔로우, false -> 언팔로우 + @NotNull(message = "type은 필수 파라미터입니다.") + FollowType type ) {
6-6: 검증 메시지 개선검증 메시지에 허용되는 값에 대한 정보를 추가하면 API 사용자에게 더 도움이 됩니다.
- @NotNull(message = "type은 필수 파라미터입니다.") + @NotNull(message = "type은 필수 파라미터입니다. (true: 팔로우, false: 언팔로우)")src/main/java/konkuk/thip/common/util/DateUtil.java (1)
54-61: 예외 처리 로직을 개선할 수 있습니다.메서드 구현 자체는 올바르지만, 예외 처리에서 개선할 점이 있습니다:
- 예외 메시지에 원본 예외 객체를 직접 포함하면 메시지가 복잡해질 수 있습니다.
IllegalArgumentException을 cause로 사용하는 것보다 원본DateTimeParseException을 cause로 사용하는 것이 더 적절합니다.다음과 같이 개선할 수 있습니다:
-throw new InvalidStateException(ErrorCode.API_INVALID_TYPE, new IllegalArgumentException( - dateTimeStr + "는 LocalDateTime으로 변환할 수 없는 문자열입니다. 예외 메시지: " + e)); +throw new InvalidStateException(ErrorCode.API_INVALID_TYPE, e);또는 더 자세한 메시지가 필요하다면:
-throw new InvalidStateException(ErrorCode.API_INVALID_TYPE, new IllegalArgumentException( - dateTimeStr + "는 LocalDateTime으로 변환할 수 없는 문자열입니다. 예외 메시지: " + e)); +throw new InvalidStateException(ErrorCode.API_INVALID_TYPE, + new IllegalArgumentException(dateTimeStr + "는 LocalDateTime으로 변환할 수 없는 문자열입니다.", e));src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java (1)
54-62: 팔로우/언팔로우 API 구현이 적절하지만 엔드포인트 경로 개선을 제안합니다.API 설계와 구현이 잘 되어 있습니다. 다만
/users/following/{followingUserId}경로에서followingUserId가 대상 사용자를 의미하는지 명확하지 않을 수 있습니다.더 명확한 경로를 제안합니다:
-@PostMapping("/users/following/{followingUserId}") +@PostMapping("/users/follow/{targetUserId}")또는 파라미터명을 더 명확하게:
-public BaseResponse<UserFollowResponse> followUser(@UserId final Long userId, - @PathVariable final Long followingUserId, - @RequestBody @Valid final UserFollowRequest request) { +public BaseResponse<UserFollowResponse> followUser(@UserId final Long userId, + @PathVariable final Long targetUserId, + @RequestBody @Valid final UserFollowRequest request) {src/main/java/konkuk/thip/user/application/service/UserGetFollowersService.java (1)
19-19: 페이지 크기 설정 개선 제안하드코딩된
DEFAULT_PAGE_SIZE상수보다는 설정 파일에서 관리하는 것이 유연성 측면에서 더 좋을 것 같습니다.- private static final int DEFAULT_PAGE_SIZE = 10; + @Value("${app.pagination.default-page-size:10}") + private int defaultPageSize;그리고 사용 부분:
- return followingQueryPort.getFollowersByUserId(user.getId(), cursor, DEFAULT_PAGE_SIZE); + return followingQueryPort.getFollowersByUserId(user.getId(), cursor, defaultPageSize);src/main/java/konkuk/thip/user/application/service/UserFollowService.java (1)
24-47: 팔로우 상태 변경 로직이 올바르게 구현되었습니다.비즈니스 로직이 명확하고 예외 처리가 적절합니다. 다만 몇 가지 개선 사항이 있습니다:
type변수명이 의미가 불명확합니다.isFollowing또는followAction등으로 명명하는 것이 좋겠습니다.- 주석이 한국어로 되어 있어 영어 주석으로 통일하는 것을 고려해보세요.
- Boolean type = followCommand.type(); + Boolean isFollowing = followCommand.type();- if (optionalFollowing.isPresent()) { // 이미 팔로우 관계가 존재하는 경우 + if (optionalFollowing.isPresent()) { // Follow relationship already existssrc/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java (1)
16-32: 중첩된 Follower 레코드 구조가 적절합니다.팔로워 정보를 나타내는 필드들이 완전하고, 정적 팩토리 메서드
of()를 통해 인스턴스 생성이 명확합니다. 다만followingCount를Integer로 선언했는데, 음수가 될 수 없으므로int또는 범위 검증을 고려해보세요.- Integer followingCount + int followingCount또는 validation 추가:
public static Follower of(Long userId, String nickname, String profileImageUrl, String aliasName, Integer followingCount) { + if (followingCount != null && followingCount < 0) { + throw new IllegalArgumentException("Following count cannot be negative"); + } return new Follower(src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java (1)
32-41: save 메서드에서 사용자 존재 검증이 적절합니다.팔로우 관계 저장 전에 양쪽 사용자의 존재를 검증하는 것이 데이터 무결성을 보장합니다. 다만 두 번의 개별 조회가 발생하는데, 성능 최적화를 고려해볼 수 있습니다.
성능 최적화를 위해 두 사용자를 한 번에 조회하는 방법을 고려해보세요:
- UserJpaEntity userJpaEntity = userJpaRepository.findById(following.getFollowerUserId()).orElseThrow( - () -> new EntityNotFoundException(USER_NOT_FOUND)); - - UserJpaEntity targetUser = userJpaRepository.findById(following.getFollowingUserId()).orElseThrow( - () -> new EntityNotFoundException(USER_NOT_FOUND)); + List<Long> userIds = List.of(following.getFollowerUserId(), following.getFollowingUserId()); + List<UserJpaEntity> users = userJpaRepository.findAllById(userIds); + + if (users.size() != 2) { + throw new EntityNotFoundException(USER_NOT_FOUND); + } + + UserJpaEntity userJpaEntity = users.stream() + .filter(u -> u.getUserId().equals(following.getFollowerUserId())) + .findFirst() + .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); + + UserJpaEntity targetUser = users.stream() + .filter(u -> u.getUserId().equals(following.getFollowingUserId())) + .findFirst() + .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND));
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (34)
src/main/java/konkuk/thip/common/entity/BaseDomainEntity.java(1 hunks)src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(1 hunks)src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java(1 hunks)src/main/java/konkuk/thip/common/util/DateUtil.java(2 hunks)src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java(3 hunks)src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/request/UserFollowRequest.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowResponse.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java(2 hunks)src/main/java/konkuk/thip/user/adapter/out/mapper/FollowingMapper.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingJpaRepository.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/FollowingQueryRepository.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java(2 hunks)src/main/java/konkuk/thip/user/application/port/in/UserFollowUsecase.java(1 hunks)src/main/java/konkuk/thip/user/application/port/in/UserGetFollowersUsecase.java(1 hunks)src/main/java/konkuk/thip/user/application/port/in/dto/UserFollowCommand.java(1 hunks)src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java(1 hunks)src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java(1 hunks)src/main/java/konkuk/thip/user/application/service/UserFollowService.java(1 hunks)src/main/java/konkuk/thip/user/application/service/UserGetFollowersService.java(1 hunks)src/main/java/konkuk/thip/user/domain/Following.java(1 hunks)src/test/java/konkuk/thip/common/util/TestEntityFactory.java(1 hunks)src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserFollowControllerTest.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java(1 hunks)src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java(1 hunks)src/test/java/konkuk/thip/user/domain/FollowingTest.java(1 hunks)
🧰 Additional context used
🧠 Learnings (7)
src/main/java/konkuk/thip/user/application/port/in/dto/UserFollowCommand.java (1)
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로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java (1)
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로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java (1)
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로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepository.java (1)
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로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java (2)
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로부터 조회하는 메서드를 추가함.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#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<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java (1)
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로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (1)
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로부터 조회하는 메서드를 추가함.
🧬 Code Graph Analysis (6)
src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java (2)
src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java (1)
Entity(8-38)src/main/java/konkuk/thip/user/domain/Following.java (1)
Getter(12-47)
src/test/java/konkuk/thip/user/domain/FollowingTest.java (1)
src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java (2)
Nested(33-86)Nested(88-133)
src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java (1)
src/test/java/konkuk/thip/user/domain/FollowingTest.java (2)
Nested(16-48)Nested(50-82)
src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(14-145)
src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(14-145)
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (2)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
DateUtil(12-62)src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java (1)
Repository(20-82)
🔇 Additional comments (43)
src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java (1)
33-33: 테스트 설명 개선이 잘 되었습니다.테스트가 검증하는 기능을 더 명확하게 설명하는 이름으로 변경되어 가독성이 향상되었습니다.
src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java (1)
34-34: 테스트 설명 개선이 잘 되었습니다.컨트롤러 이름보다는 실제 기능을 명시한 이름으로 변경되어 테스트 목적이 더 명확해졌습니다.
src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java (1)
33-33: 테스트 설명 개선이 잘 되었습니다.기술적 컴포넌트 이름보다는 사용자 관점에서의 기능을 명시하여 테스트 목적이 더 명확해졌습니다.
src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java (1)
102-102: 배치 삭제로 성능 최적화가 잘 되었습니다.
deleteAllInBatch()는 단일 SQL 문으로 대량 삭제를 수행하여 테스트 정리 시 성능이 향상됩니다. 팔로우 기능 추가로 인한 데이터 증가를 고려한 좋은 최적화입니다.src/main/java/konkuk/thip/common/entity/BaseDomainEntity.java (1)
18-24: 상태 토글 로직은 잘 구현되었으나 null 안전성 개선이 필요합니다.팔로우/언팔로우 상태 전환을 위한 메서드가 잘 구현되었습니다. 하지만
status필드가 null인 경우 NPE가 발생할 수 있습니다.다음과 같이 null 체크를 추가하는 것을 권장합니다:
protected void changeStatus() { + if (this.status == null) { + throw new IllegalStateException("Status cannot be null"); + } if (this.status == StatusType.ACTIVE) { this.status = StatusType.INACTIVE; } else { this.status = StatusType.ACTIVE; } }Likely an incorrect or invalid review comment.
src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java (1)
108-108: 스택트레이스 로깅 개선 승인RuntimeException 핸들러에 스택트레이스 로깅을 추가한 것은 디버깅과 문제 진단에 도움이 되는 좋은 개선사항입니다.
src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowResponse.java (1)
3-9: 간단하고 명확한 응답 DTO 구현isFollowing 필드와 정적 팩토리 메서드를 포함한 간단하고 명확한 구현입니다.
src/main/java/konkuk/thip/user/application/port/in/UserFollowUsecase.java (1)
6-8: Clean Architecture 원칙을 잘 따른 usecase 인터페이스단일 책임 원칙을 잘 따르고 있으며, 명확한 인터페이스 정의입니다.
src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java (2)
6-6: 소프트 삭제 구현이 적절합니다.
@SQLDelete어노테이션을 통한 소프트 삭제 구현이 데이터 보존과 감사 추적에 유용합니다. SQL 구문도 정확하게 작성되었습니다.Also applies to: 11-11
24-24: 필드명 변경으로 가독성이 개선되었습니다.
userJpaEntity에서followerUserJpaEntity로 변경하여 팔로우 관계에서 팔로워를 명확히 표현했습니다. 코드의 의도가 더 명확해졌습니다.src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java (1)
28-32: 팔로워 조회 엔드포인트가 올바르게 구현되었습니다.RESTful API 설계 원칙을 따르고 있으며, 커서 기반 페이지네이션이 적절히 구현되었습니다. 초기 요청 시 cursor가 null이고, 다음 페이지가 있을 때 nextCursor를 반환하는 설계가 합리적입니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingJpaRepository.java (1)
5-11: @repository 애너테이션 추가와 포맷팅 개선이 적절합니다.Spring 컴포넌트 스캔을 위한 @repository 애너테이션 추가와 인터페이스 선언 포맷팅이 코드 일관성을 향상시킵니다.
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
139-144: 파라미터명 변경으로 도메인 의미가 명확해졌습니다.
user에서followerUser로 파라미터명을 변경하여 팔로우 관계에서 각 사용자의 역할을 더 명확하게 표현합니다. 도메인 모델 변경과 일치하는 좋은 개선입니다.src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java (1)
1-14: CQRS 패턴과 프로젝트 컨벤션을 잘 따르는 인터페이스 설계입니다.CommandPort 인터페이스가 프로젝트의 CQRS 컨벤션에 맞게 설계되었습니다. findByUserIdAndTargetUserId 메서드를 통한 도메인 엔티티 조회와 save/updateStatus를 통한 상태 변경 작업이 적절히 분리되어 있습니다.
src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java (1)
38-40: 파라미터에 final 키워드 추가로 불변성이 개선되었습니다.메서드 파라미터에 final 키워드를 추가하여 코드의 안정성과 가독성을 향상시켰습니다.
Also applies to: 48-48
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)
40-48: 팔로우 기능에 대한 에러 코드가 체계적으로 정의되었습니다.새로운 팔로우 관련 에러 코드들이 적절한 HTTP 상태 코드와 함께 잘 정의되어 있습니다. 특히 자기 자신을 팔로우하는 경우(
USER_CANNOT_FOLLOW_SELF)에 대한 검증도 포함되어 있어 도메인 규칙을 잘 반영했습니다.에러 코드 범위 분리(70000: user error, 75000: follow error)도 일관성 있게 적용되었습니다.
src/main/java/konkuk/thip/user/application/service/UserGetFollowersService.java (2)
23-26: 서비스 로직 구현 승인메서드 구현이 깔끔하고 트랜잭션 설정도 적절합니다. 읽기 전용 트랜잭션을 사용한 점과 커서 기반 페이징 처리가 잘 구현되어 있습니다.
16-17: 아키텍처 관점에서 포트 사용 검토 필요사용자 조회 작업에
UserCommandPort를 사용하고 있습니다. 명명 규칙상 Command는 데이터 변경 작업을 의미하므로, 조회 작업에는UserQueryPort를 사용하는 것이 더 적절해 보입니다.- private final UserCommandPort userCommandPort; + private final UserQueryPort userQueryPort;그리고 사용 부분도 변경:
- User user = userCommandPort.findById(userId); + User user = userQueryPort.findById(userId);⛔ Skipped due to learnings
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로부터 조회하는 메서드를 추가함.src/main/java/konkuk/thip/user/adapter/out/mapper/FollowingMapper.java (2)
11-16: 매핑 로직 개선 승인매개변수명을
followerUserJpaEntity로 변경하여 팔로워와 팔로잉 사용자를 명확하게 구분한 것이 좋습니다. 코드 가독성과 유지보수성이 향상되었습니다.
18-27: 도메인 엔티티 변환 로직 승인
followerUserJpaEntity에서 사용자 ID를 추출하는 로직이 올바르게 구현되었습니다. 필드명 변경에 따른 일관성 있는 매핑 처리가 잘 되어 있습니다.src/test/java/konkuk/thip/user/domain/FollowingTest.java (3)
16-48: 팔로우 요청 테스트 케이스 승인팔로우 기능에 대한 테스트 케이스가 잘 구성되어 있습니다:
- 정상적인 상태 전환 테스트 (INACTIVE → ACTIVE)
- 중복 팔로우 시도에 대한 예외 처리 테스트
- 예외 메시지 검증까지 포함하여 완전한 테스트 커버리지 제공
50-82: 언팔로우 요청 테스트 케이스 승인언팔로우 기능에 대한 테스트 케이스도 매우 잘 구성되어 있습니다:
- 정상적인 상태 전환 테스트 (ACTIVE → INACTIVE)
- 중복 언팔로우 시도에 대한 예외 처리 테스트
- 적절한 예외 타입과 메시지 검증
84-92: 팩토리 메서드 테스트 승인
withoutId팩토리 메서드에 대한 테스트가 잘 구현되어 있습니다. 새로 생성된 팔로우 관계의 기본 상태가 ACTIVE로 설정되는 것을 올바르게 검증하고 있습니다.src/test/java/konkuk/thip/user/adapter/in/web/UserFollowControllerTest.java (1)
34-48: 헬퍼 메서드 구현 승인테스트 코드의 중복을 줄이기 위한 헬퍼 메서드들이 잘 구현되어 있습니다:
buildValidRequest(): 유효한 요청 생성assertBad(): 잘못된 요청에 대한 응답 검증코드 재사용성과 가독성이 향상되었습니다.
src/main/java/konkuk/thip/user/domain/Following.java (4)
18-20: 필드명 변경 승인
userId를followerUserId로 변경하여 팔로워와 팔로잉 사용자를 명확하게 구분한 것이 좋습니다. 도메인 모델의 의미가 더 명확해졌습니다.
22-28: 팩토리 메서드 구현 승인
withoutId팩토리 메서드가 잘 구현되었습니다:
- 새로운 팔로우 관계 생성 시 기본적으로 ACTIVE 상태로 설정
- 명확한 메서드명과 매개변수 구조
- 빌더 패턴을 활용한 객체 생성
30-36: 상태 변경 로직 승인팔로우/언팔로우 상태 변경 로직이 잘 구현되었습니다:
- 요청 타입에 따른 적절한 검증 수행
- 부모 클래스의
changeStatus()메서드를 활용한 상태 전환- 요청 타입에 따른 boolean 반환값 제공
38-46: 상태 검증 로직 승인상태 검증 로직이 매우 잘 구현되었습니다:
- 이미 팔로우 중인 상태에서 팔로우 요청 시 예외 발생
- 이미 언팔로우 중인 상태에서 언팔로우 요청 시 예외 발생
- 적절한 예외 타입과 에러 코드 사용
- 명확한 주석으로 로직 설명
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepository.java (1)
12-14: 메서드 시그니처가 적절하게 설계되었습니다.QueryRepository 인터페이스에 추가된 두 메서드가 CQRS 컨벤션을 잘 따르고 있습니다:
findByUserAndTargetUser: Optional 반환으로 null 안전성 확보findFollowersByUserIdBeforeCreatedAt: 커서 기반 페이지네이션을 위한 적절한 파라미터 구성src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (1)
26-59: 커서 기반 페이지네이션이 잘 구현되었습니다.N+1 문제를 방지하기 위한 최적화가 적절히 적용되었습니다:
countByFollowingUserIds를 통한 일괄 카운트 조회- 커서 파싱과 페이지네이션 로직이 올바르게 구현됨
UserFollowersResponse구조가 API 응답에 적합함src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java (1)
22-145: 포괄적이고 잘 구조화된 단위 테스트입니다.테스트가 다음과 같은 장점을 가지고 있습니다:
- 완전한 시나리오 커버리지: 팔로우/언팔로우의 모든 경우를 테스트
- 예외 상황 처리: 자기 자신 팔로우, 존재하지 않는 관계 언팔로우 등
- 적절한 검증: ArgumentCaptor를 통한 저장 객체 검증
- 명확한 구조: Nested 클래스로 테스트 케이스 그룹화
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java (2)
46-57: 팔로우 관계 조회 메서드가 올바르게 구현되었습니다.QueryDSL을 사용한 간단하고 명확한 구현입니다. Optional 반환으로 null 안전성을 확보했습니다.
59-81: N+1 문제를 효과적으로 방지한 팔로워 조회 구현입니다.성능 최적화가 잘 적용되었습니다:
- FetchJoin 사용:
followerUserJpaEntity와aliasForUserJpaEntity에 대한 fetchJoin으로 N+1 문제 방지- 동적 쿼리: BooleanBuilder를 통한 커서 조건 처리
- 적절한 필터링: ACTIVE 상태만 조회하는 비즈니스 로직 반영
- 정렬과 페이지네이션: 생성일시 내림차순 정렬과 limit 적용
src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java (2)
27-53: 통합 테스트 설정이 적절하게 구성되었습니다.SpringBootTest와 MockMvc를 사용한 통합 테스트 설정이 올바르며, 테스트 후 데이터 정리도 적절합니다.
55-100: 팔로우/언팔로우 통합 테스트 및 메서드 상속 확인 완료
- API 응답(
isFollowing) 검증- DB 상태(
ACTIVE→INACTIVE) 검증findByUserAndTargetUser메서드는FollowingQueryRepository에서 선언되어FollowingJpaRepository가 상속 제공src/main/java/konkuk/thip/user/application/service/UserFollowService.java (1)
49-53: 파라미터 검증 로직이 적절합니다.자기 자신을 팔로우할 수 없도록 하는 검증이 올바르게 구현되었습니다.
Objects.equals()사용으로 null 안전성도 확보되었습니다.src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java (1)
8-15: 커서 기반 페이징을 위한 응답 구조가 잘 설계되었습니다.페이징에 필요한 모든 정보(
nextCursor,isFirst,isLast)가 포함되어 있어 클라이언트가 페이징을 쉽게 구현할 수 있습니다.src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java (3)
49-63: 테스트 설정이 적절합니다.12명의 팔로워를 생성하여 2페이지 시나리오를 테스트하는 것이 적절합니다.
TestEntityFactory를 사용하여 일관된 테스트 데이터를 생성하고 있습니다.
72-104: 페이징 검증이 포괄적이고 올바릅니다.첫 페이지와 두 번째 페이지 각각에 대해 페이지 크기, 페이징 플래그, 커서 존재 여부를 모두 검증하고 있어 완전한 테스트입니다.
65-70: 테스트 데이터 정리가 적절합니다.
deleteAllInBatch()와deleteAll()을 사용하여 외래키 관계를 고려한 순서로 데이터를 정리하고 있습니다.src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java (3)
26-30: 팔로우 관계 조회 로직이 올바릅니다.도메인 엔티티와 JPA 엔티티 간 매핑이 적절하고, Optional을 사용하여 null 안전성을 확보했습니다.
43-49: updateStatus 메서드 구현이 올바릅니다.팔로우 관계 존재 검증 후 상태 업데이트를 수행하는 로직이 적절합니다. JPA의 dirty checking을 활용하여 자동으로 업데이트가 반영됩니다.
17-25: 의존성 주입과 어노테이션 설정이 적절합니다.
@Repository어노테이션과@RequiredArgsConstructor를 사용하여 Spring의 의존성 주입을 올바르게 구성했습니다.
| @@ -1,9 +1,13 @@ | |||
| package konkuk.thip.user.application.port.out; | |||
|
|
|||
| import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; | |||
There was a problem hiding this comment.
클린 아키텍처 원칙 위반이 발생했습니다.
Port 인터페이스가 adapter.in.web.response 패키지의 UserFollowersResponse를 임포트하고 있습니다. 이는 내부 계층(application)이 외부 계층(adapter)을 참조하는 것으로 클린 아키텍처 원칙에 위반됩니다.
다음 해결 방안을 제안합니다:
application.port.out패키지에 별도의 응답 모델 생성- 또는 도메인 계층에 응답 모델 정의
- adapter 계층에서 변환 로직 구현
-import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse;
+import konkuk.thip.user.application.port.out.dto.UserFollowersDto;
-UserFollowersResponse getFollowersByUserId(Long userId, String cursor, int size);
+UserFollowersDto getFollowersByUserId(Long userId, String cursor, int size);Also applies to: 11-11
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java
around line 3, the interface improperly imports UserFollowersResponse from the
adapter.in.web.response package, violating clean architecture by referencing an
outer layer from the application layer. To fix this, remove the import and
define a separate response model either within the application.port.out package
or the domain layer, then implement the conversion logic in the adapter layer to
map domain or application models to the web response objects.
There was a problem hiding this comment.
우리는 Query 즉, 조회 api에 한해서는 response dto가 외부계층에서 참조되는 것을 허용하기로 했어. 이것에 대해서는 어떻게 생각해?
There was a problem hiding this comment.
@buzz0331 좋은 접근 방식이라고 생각합니다!
CQRS 아키텍처에서 Query 쪽에 대해 이런 유연한 접근을 취하는 것은 매우 합리적입니다. 몇 가지 이유로 이 결정이 타당하다고 봅니다:
-
Query는 읽기 전용: Response DTO는 비즈니스 로직을 포함하지 않고 단순히 데이터 표현을 위한 것이므로 의존성 위험이 낮습니다.
-
성능 최적화: 불필요한 매핑 계층을 제거하여 조회 성능을 향상시킬 수 있습니다.
-
CQRS 특성: Command와 Query의 관심사가 다르므로 각각에 적합한 아키텍처 제약을 적용하는 것이 실용적입니다.
-
개발 효율성: 조회 전용 API에서 추가 변환 로직을 제거하여 개발 속도를 높일 수 있습니다.
이미 FollowingQueryPort에서 UserFollowersResponse를 직접 반환하고 있는 것도 이 컨벤션에 따른 것으로 이해됩니다. 팀의 일관된 아키텍처 원칙이 있다면 그것을 따르는 것이 맞다고 생각합니다.
🐰 ✨ 새로운 아키텍처 지식을 학습했어요!
✏️ Learnings added
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.495Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
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로부터 조회하는 메서드를 추가함.
| @@ -0,0 +1,7 @@ | |||
| package konkuk.thip.user.application.port.in; | |||
|
|
|||
| import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; | |||
There was a problem hiding this comment.
클린 아키텍처 원칙 위반이 발생했습니다.
Usecase 인터페이스가 adapter.in.web.response 패키지의 UserFollowersResponse를 임포트하고 있습니다. 이는 application 계층이 adapter 계층을 참조하는 것으로 클린 아키텍처 원칙에 위반됩니다.
다음 해결 방안을 제안합니다:
application.port.in패키지에 별도의 응답 모델 생성- 또는 도메인 계층에 응답 모델 정의
- adapter 계층에서 변환 로직 구현
-import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse;
+import konkuk.thip.user.application.port.in.dto.UserFollowersResultDto;
-UserFollowersResponse getUserFollowers(Long userId, String nextCursor);
+UserFollowersResultDto getUserFollowers(Long userId, String nextCursor);Also applies to: 6-6
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/application/port/in/UserGetFollowersUsecase.java
at line 3, the import of UserFollowersResponse from the adapter.in.web.response
package violates clean architecture by making the application layer depend on
the adapter layer. To fix this, remove the import and define a separate response
model either in the application.port.in package or in the domain layer, then
implement the conversion logic in the adapter layer to map domain or application
models to the web response objects.
| @Test | ||
| @DisplayName("type이 null이면 400 에러") | ||
| void null_type() throws Exception { | ||
| Map<String, Object> req = new HashMap<>(); // type 없음 | ||
| assertBad(req, "type은 필수 파라미터입니다."); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
추가 테스트 케이스 구현 제안
현재 type 필드 누락에 대한 테스트만 있습니다. 더 포괄적인 테스트 커버리지를 위해 다음 테스트 케이스들을 추가하는 것을 제안합니다:
@Test
@DisplayName("유효한 팔로우 요청 시 성공")
void valid_follow_request() throws Exception {
// given
Map<String, Object> req = buildValidRequest();
// when & then
mockMvc.perform(post("/users/following/{followingUserId}", 2L)
.requestAttr("userId", 1L)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isOk());
}
@Test
@DisplayName("유효한 언팔로우 요청 시 성공")
void valid_unfollow_request() throws Exception {
// given
Map<String, Object> req = new HashMap<>();
req.put("type", false);
// when & then
mockMvc.perform(post("/users/following/{followingUserId}", 2L)
.requestAttr("userId", 1L)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isOk());
}🤖 Prompt for AI Agents
In src/test/java/konkuk/thip/user/adapter/in/web/UserFollowControllerTest.java
around lines 50 to 55, add additional test cases to improve coverage by
verifying successful follow and unfollow requests. Implement one test for a
valid follow request with a properly constructed request body and expect an OK
status, and another test for a valid unfollow request with the "type" field set
to false, also expecting an OK status. Use mockMvc to perform POST requests to
the follow endpoint with appropriate userId attributes and JSON content.
src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java
Show resolved
Hide resolved
| private List<UserJpaEntity> followerUsers; // 팔로워 12명 | ||
|
|
||
| @BeforeEach | ||
| void setUp() { |
The merge-base changed after approval.
|



#️⃣ 연관된 이슈
📝 작업 내용
특정 사용자의 팔로워 리스트를 조회하는 api의 흐름은 다음과 같습니다.
조회하려는 특정 사용자 (userId)로 User 조회
-> userId로 Following 테이블에서 팔로워 리스트 조회 -> 조회한 팔로워들의 팔로우 수 count 쿼리로 한번에 Map에 저장 -> dto 파싱
커서 기반 페이징 처리 방식

커서 기반 페이징을 위해 아래와 같이 WHERE 절에서 cursor를 기준으로 데이터를 필터링하고, 정렬하여 페이징이 가능하도록 구성했습니다.
동작 방식
클라이언트가 처음 데이터를 요청할 때는 cursor 값 없이 /users/{userId}/followers 엔드포인트를 호출합니다
이때, 더 불러올 데이터가 존재하는 경우 응답에 nextCursor 값을 함께 포함시켜 반환합니다
다음 페이지가 필요할 경우, 앞서 응답으로 받은 nextCursor 값을 쿼리 파라미터로 포함하여 /users/{userId}/followers?cursor={nextCursor} 형식으로 요청을 보냅니다
서버는 해당 cursor를 기준으로 이후 데이터를 조회하여 응답합니다.
📸 스크린샷
💬 리뷰 요구사항
N + 1 문제를 방지하기 위해 다음과 같은 두 가지 개선을 적용했습니다.
팔로워들의 팔로잉 수 조회 최적화
Follower 리스트를 조회한 뒤, 각 Follower의 팔로잉 수를 조회하기 위해 개별 쿼리를 날리면 N + 1 문제가 발생할 수 있습니다. 이를 방지하기 위해 groupBy를 활용한 배치 쿼리를 사용하여 모든 Follower의 팔로잉 수를 한 번의 count 쿼리로 조회하고, 결과를 Map<userId, count> 형태로 메모리에 저장한 뒤, DTO를 생성할 때 해당 Map에서 값을 꺼내 사용하는 방식으로 쿼리 수를 최소화했습니다.
팔로워 정보 조회 시 Fetch Join 적용
Following 테이블에서 Follower 정보를 조회할 때 @manytoone(fetch = FetchType.LAZY) 설정으로 인해 각 Follower에 대해 추가적인 select 쿼리가 발생해 N + 1 문제가 생깁니다. 이를 해결하기 위해 Follower 리스트를 조회할 때 User 테이블과의 fetch join을 적용하여 연관된 Follower 정보를 한 번의 쿼리로 함께 가져올 수 있도록 최적화했습니다.
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
신규 기능
버그 수정
테스트
문서화
리팩터